//! Tessera's native visual protocols over Aster's managed object graph. mod developer_pages; use std::sync::{Arc, Mutex}; use anyhow::{Result, bail}; use endor_core::{ AnnotationType, CallEffect, CallSignature, CallType, ContextNativeFunction, EndorReal, EndorTypeObject, NativeCallable, NativeContext, NativeResult, ObjectRef, TypeConstructor, Value, Vm, VmError, WeakObjectRef, }; use crate::{ block::{self, Child, Data, Kind}, document::parse_color, }; pub const MAX_UI_NODES: usize = 4096; pub const MAX_PAGES: usize = 64; pub const MAX_DIMENSION: f64 = 1_000_000.0; pub const TYPE_NAMES: &[&str] = &[ "Object", "Type", "Function", "Trait", "Visual", "EventTarget", "Children", "Presentable", "Block", "ContainerBlock", "ScrollBlock", "TextBlock", "RectBlock", "Spacer", "Row", "Column", "Button", "Page", "Window", "Mount", "Event", "Subscription", "Task", "ObjectList", "Constraints", "MeasureResult", "Bounds", "Point", "Arrangement", "DisplayList", "PaintCommand", ]; pub const QUERY_NAMES: &[&str] = &[ "isInstance", "isSubtype", "implement", "implements", "supports", ]; #[derive(Debug, Clone, PartialEq)] pub struct UiNode { pub flex: f32, pub flex_policy: crate::layout::FlexPolicy, pub main_align: crate::layout::MainAlign, pub block_id: String, pub protocol_key: String, pub slots: Vec, pub interactive: bool, pub kind: UiKind, } impl UiNode { pub fn children(&self) -> &[Arc] { match &self.kind { UiKind::Column { children, .. } | UiKind::Row { children, .. } | UiKind::Button { children, .. } | UiKind::Scroll { children, .. } => children, _ => &[], } } pub fn has_flex(&self) -> bool { self.flex > 0.0 || self.flex_policy != crate::layout::FlexPolicy::default() } pub fn shallow_eq(&self, other: &Self) -> bool { if self.flex_policy != other.flex_policy || self.flex != other.flex || self.main_align != other.main_align || self.block_id != other.block_id || self.protocol_key != other.protocol_key || self.slots != other.slots || self.interactive != other.interactive { return false; } let same_children = || { self.children().len() == other.children().len() && self .children() .iter() .zip(other.children()) .all(|(a, b)| Arc::ptr_eq(a, b)) }; match (&self.kind, &other.kind) { ( UiKind::Text { value: a, size: sa, color: ca, }, UiKind::Text { value: b, size: sb, color: cb, }, ) => a == b && sa == sb && ca == cb, ( UiKind::Rect { width: a, height: b, radius: c, color: d, }, UiKind::Rect { width: e, height: f, radius: g, color: h, }, ) => (a, b, c, d) == (e, f, g, h), (UiKind::Spacer(a), UiKind::Spacer(b)) => a == b, ( UiKind::Scroll { axis: aa, width: a, height: b, padding: pa, gap: ga, children: _, }, UiKind::Scroll { axis: ab, width: c, height: d, padding: pb, gap: gb, children: _, }, ) => aa == ab && a == c && b == d && pa == pb && ga == gb && same_children(), ( UiKind::Column { padding: a, gap: b, .. }, UiKind::Column { padding: c, gap: d, .. }, ) | ( UiKind::Row { padding: a, gap: b, .. }, UiKind::Row { padding: c, gap: d, .. }, ) | ( UiKind::Button { padding: a, gap: b, .. }, UiKind::Button { padding: c, gap: d, .. }, ) => a == c && b == d && same_children(), _ => false, } } } #[derive(Debug, Clone, PartialEq)] pub enum UiKind { Button { padding: f32, gap: f32, children: Vec>, }, Text { value: String, size: u32, color: String, }, Rect { width: f32, height: f32, radius: f32, color: String, }, Column { padding: f32, gap: f32, children: Vec>, }, Row { padding: f32, gap: f32, children: Vec>, }, Spacer(f32), Scroll { axis: crate::scroll::Axis, width: f32, height: f32, padding: f32, gap: f32, children: Vec>, }, } #[derive(Debug)] struct Session { // Weak visual-object registry belongs to the VM, not a particular window. nodes: Vec, presentations: std::collections::BTreeMap, current: u64, master: ObjectRef, next_window: u64, next_mount: u64, retired_commands: Vec, refresh_yields: usize, refresh_restarts: usize, layout_cancels: usize, paint_cancels: usize, paint_slices: usize, binding_evaluations: usize, exiting: bool, } impl Session { fn host_desktops(&self) -> std::collections::HashSet { self.presentations .values() .map(|view| view.desktop) .collect() } } impl std::ops::Deref for Session { type Target = crate::presentation::Presentation; fn deref(&self) -> &Self::Target { self.presentations .get(&self.current) .expect("selected presentation") } } impl std::ops::DerefMut for Session { fn deref_mut(&mut self) -> &mut Self::Target { self.presentations .get_mut(&self.current) .expect("selected presentation") } } type Shared = Arc>; #[derive(Debug, Clone, Copy)] enum Operation { IsInstance, Implement, Implements, Supports, IsSubtype, OpenValue, Mounts, Stats, Viewport, Window, PageWindow, NewWindow, CloseWindow, Detach, MoveWindow, MoveTab, State, StateSet, Increment, TextOf, Bind, Text, Rect, Spacer, Scroll, Row, Column, Button, Page, SetText, SetColor, Append, Insert, Remove, Replace, Move, Children, Render, Unmount, Open, Close, Activate, Navigate, Back, Forward, OnClick, On, Dispatch, OnEvent, Invalidate, OnMount, OnUnmount, Click, Present, } impl Operation { fn arity(self) -> endor_core::CallArity { use Operation::*; let (minimum, maximum) = match self { IsInstance | IsSubtype | Bind | Implement | Implements | Supports => (2, 2), Stats | Viewport | Window | PageWindow | Mounts => (0, 0), State | StateSet => (1, 1), Increment => (0, 0), TextOf => (2, 4), Scroll => (3, 3), Text => (1, 3), Rect => (2, 4), MoveTab | MoveWindow | OpenValue | Spacer | SetText | SetColor | Append | Remove | Render | Navigate | OnClick | Present => (1, 1), Row | Column => (2, MAX_UI_NODES + 2), Button => (1, 1), Insert | Replace | Move => (2, 2), Page | Unmount | Close | Activate | Back | Forward | Children | NewWindow | CloseWindow | Detach => (0, 0), Open => (0, 1), Click | Invalidate => (0, 1), On => (2, 3), Dispatch => (1, 2), OnEvent | OnMount | OnUnmount => (1, 1), }; endor_core::CallArity::range(minimum, maximum) } } fn method_signature( store: &endor_core::ObjectStore, desktop: ObjectRef, receiver: ObjectRef, operation: Operation, ) -> Result { use AnnotationType::{Any, Function, Nominal, Number, String as TextType, Unit}; use Operation::*; let nominal = |name| desktop_type(store, name, desktop).map(Nominal); let (arguments, returns, effect) = match operation { Bind => ( vec![TextType, Function], nominal("Subscription")?, CallEffect::Mutating, ), Mounts => (vec![], nominal("ObjectList")?, CallEffect::ReadOnly), Children => (vec![], Any, CallEffect::ReadOnly), Invalidate => (vec![TextType], Unit, CallEffect::Mutating), SetText | SetColor => (vec![TextType], Unit, CallEffect::Mutating), Append => (vec![nominal("Block")?], Number, CallEffect::Mutating), Insert => ( vec![Number, nominal("Block")?], Number, CallEffect::Mutating, ), Remove => (vec![Number], Unit, CallEffect::Mutating), Replace => (vec![Number, nominal("Block")?], Unit, CallEffect::Mutating), Move => (vec![Number, Number], Unit, CallEffect::Mutating), Render | Navigate => (vec![nominal("Block")?], Unit, CallEffect::Mutating), OnClick => (vec![Function], Nominal(receiver), CallEffect::Mutating), OnEvent => (vec![nominal("Event")?], Unit, CallEffect::External), OnMount | OnUnmount => (vec![nominal("Mount")?], Unit, CallEffect::External), Click => ( vec![nominal("Mount")?], nominal("Event")?, CallEffect::External, ), Dispatch => ( vec![nominal("Event")?, nominal("Mount")?], nominal("Event")?, CallEffect::External, ), MoveWindow => ( vec![nominal("Window")?], Nominal(receiver), CallEffect::Mutating, ), MoveTab => (vec![Number], Nominal(receiver), CallEffect::Mutating), Activate => (vec![], Nominal(receiver), CallEffect::Mutating), Detach => (vec![], nominal("Window")?, CallEffect::Mutating), Unmount | Close | CloseWindow | Back | Forward => (vec![], Unit, CallEffect::Mutating), Open => (vec![Any], Nominal(receiver), CallEffect::Mutating), PageWindow => (vec![], Any, CallEffect::ReadOnly), On => ( vec![TextType, Function, Any], nominal("Subscription")?, CallEffect::Mutating, ), _ => return Err(invalid("unsupported native method signature")), }; let arity = operation.arity(); let mut parameters = vec![Nominal(receiver)]; parameters.extend(arguments); let mut signature = CallSignature { arity: endor_core::CallArity::range(arity.required + 1, arity.maximum + 1), parameters: parameters.into_iter().map(CallType::from).collect(), variadic: None, returns: returns.into(), effect, }; match operation { Open => signature.parameters[1] = CallType::nullable(nominal("Window")?), PageWindow => signature.returns = CallType::nullable(nominal("Window")?), On => signature.parameters[3] = CallType::Union(vec![AnnotationType::Bool, TextType]), _ => {} } Ok(signature) } fn operation_signature( store: &endor_core::ObjectStore, desktop: ObjectRef, operation: Operation, ) -> Result { use AnnotationType::{Any, Bool, Number, String as TextType, Trait, Type, Unit}; use Operation::*; let nominal = |name| desktop_type(store, name, desktop).map(AnnotationType::Nominal); let (parameters, variadic, returns, effect) = match operation { IsInstance => (vec![Any, Type], None, Bool, CallEffect::ReadOnly), IsSubtype => (vec![Type, Type], None, Bool, CallEffect::ReadOnly), Implement => (vec![Type, Trait], None, Unit, CallEffect::Mutating), Implements => (vec![Type, Trait], None, Bool, CallEffect::ReadOnly), Supports => (vec![Any, Trait], None, Bool, CallEffect::ReadOnly), OpenValue => (vec![Any], None, nominal("Page")?, CallEffect::Mutating), Present => (vec![Any], None, nominal("Block")?, CallEffect::Delegated), State => (vec![Any], None, Any, CallEffect::ReadOnly), StateSet => (vec![Any], None, Unit, CallEffect::Mutating), Increment => (vec![], None, Unit, CallEffect::Mutating), Stats => (vec![], None, Any, CallEffect::External), Viewport => ( vec![], None, nominal("MeasureResult")?, CallEffect::ReadOnly, ), Window => (vec![], None, nominal("Window")?, CallEffect::ReadOnly), NewWindow => (vec![], None, nominal("Window")?, CallEffect::Mutating), TextOf => ( vec![Any, TextType, Number, TextType], None, nominal("TextBlock")?, CallEffect::ReadOnly, ), Text => ( vec![TextType, Number, TextType], None, nominal("TextBlock")?, CallEffect::ReadOnly, ), Rect => ( vec![Number, Number, Number, TextType], None, nominal("RectBlock")?, CallEffect::ReadOnly, ), Spacer => (vec![Number], None, nominal("Spacer")?, CallEffect::ReadOnly), Scroll => ( vec![Number, Number, nominal("Block")?], None, nominal("ScrollBlock")?, CallEffect::ReadOnly, ), Row | Column => ( vec![Number, Number], Some(nominal("Block")?), nominal(if matches!(operation, Row) { "Row" } else { "Column" })?, CallEffect::ReadOnly, ), Button => ( vec![TextType], None, nominal("Button")?, CallEffect::ReadOnly, ), Page => (vec![], None, nominal("Page")?, CallEffect::ReadOnly), _ => return Err(invalid("unsupported UI function signature")), }; Ok(CallSignature { arity: operation.arity(), parameters: parameters.into_iter().map(CallType::from).collect(), variadic: variadic.map(CallType::from), returns: returns.into(), effect, }) } #[derive(Debug)] struct Function { signature: Option, operation: Operation, owner: Option, session: Shared, requested_type: Option, } /// One shared implementation on the nominal type. The VM binds receivers; /// individual Blocks no longer allocate owner-capturing method closures. #[derive(Debug)] struct TypeMethod { signature: CallSignature, operation: Operation, expected: ObjectRef, session: Shared, } pub(crate) fn is_intrinsic_event( store: &endor_core::ObjectStore, function: ObjectRef, ) -> Result { let Some(function) = store .object(function)? .payload() .as_any() .downcast_ref::() else { return Ok(false); }; Ok(function .implementation::() .is_some_and(|method| matches!(method.operation, Operation::OnEvent))) } impl NativeCallable for TypeMethod { fn signature(&self) -> Option { Some(self.signature.clone()) } fn arity(&self) -> Option { let arity = self.operation.arity(); Some(endor_core::CallArity::range( arity.required + 1, arity.maximum + 1, )) } fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { let owner = object(args.first())?; if !cx.store().is_instance(owner, self.expected)? { return Err(invalid("incorrect receiver type for visual method")); } Function { signature: None, operation: self.operation, owner: Some(owner), session: self.session.clone(), requested_type: None, } .invoke(cx, &args[1..]) } } /// The Type dispatcher passes the requested subclass explicitly. Native /// constructor options precede the subclass's inherited/own field arguments. #[derive(Debug)] struct NativeFactory { signature: Option, options: Option, operation: Operation, session: Shared, } impl NativeCallable for NativeFactory { fn signature(&self) -> Option { self.signature.clone() } fn arity(&self) -> Option { Some(endor_core::CallArity::range( self.operation.arity().required + 1, usize::MAX, )) } fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { let requested = object(args.first())?; let fields = cx .store() .payload::(requested, "Type")? .fields() .to_vec(); let values = &args[1..]; let native_count = values .len() .checked_sub(fields.len()) .ok_or(invalid("missing subclass field arguments"))?; cx.store() .check_class_field_arguments(requested, &values[native_count..])?; self.options .as_ref() .ok_or(invalid("native factory is not initialized"))? .check_arguments( cx.store(), "native constructor options", &values[..native_count], )?; let result = Function { signature: None, operation: self.operation, owner: None, session: self.session.clone(), requested_type: Some(requested), } .invoke(cx, &values[..native_count])?; let NativeResult::Return(Value::Object(instance)) = result else { return Err(invalid("native constructor did not return an instance")); }; if cx.store().type_of(instance)? != Some(requested) { return Err(invalid( "native constructor returned an incorrect nominal type", )); } for (field, value) in fields.iter().zip(&values[native_count..]) { cx.set_attr(instance, &field.name, value.clone())?; } Ok(NativeResult::Return(Value::Object(instance))) } } fn invalid(message: &'static str) -> VmError { VmError::InvalidArgument(message) } fn object(value: Option<&Value>) -> Result { match value { Some(Value::Object(value)) => Ok(*value), _ => Err(invalid("expected an object")), } } fn text(value: Option<&Value>) -> Result { match value { Some(Value::Text(value)) => Ok(value.clone()), _ => Err(invalid("expected text")), } } fn integer(value: Option<&Value>) -> Result { match value { Some(Value::Number(value)) => value .as_i64() .and_then(|v| u64::try_from(v).ok()) .ok_or(invalid("expected a nonnegative integer")), _ => Err(invalid("expected a nonnegative integer")), } } fn color(value: Option<&Value>, default: &str) -> Result { let value = value .map(|v| text(Some(v))) .transpose()? .unwrap_or_else(|| default.into()); parse_color(&value).map_err(|_| invalid("color must use #RRGGBB or #RRGGBBAA"))?; Ok(value) } fn dimension(value: Option<&Value>) -> Result { let Some(Value::Number(number)) = value else { return Err(invalid("expected a finite nonnegative dimension")); }; let value = match number.as_real() { Some(EndorReal::Integer(v)) => v.to_string().parse::().ok(), Some(EndorReal::Decimal(v)) => v.literal().parse::().ok(), Some(EndorReal::BinaryFloat(v)) => Some(f64::from_bits(*v)), _ => None, } .filter(|v| v.is_finite() && (0.0..=MAX_DIMENSION).contains(v)) .ok_or(invalid("dimension exceeds limits"))?; Ok(value as f32) } fn bind( cx: &mut NativeContext<'_>, owner: ObjectRef, name: &'static str, operation: Operation, session: &Shared, ) -> Result<(), VmError> { let desktop = session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; let function = cx.allocate(ContextNativeFunction::new( name, Function { signature: Some(operation_signature(cx.store(), desktop, operation)?), operation, owner: Some(owner), session: session.clone(), requested_type: None, }, vec![owner, desktop], ))?; attach_desktop_type(cx, function, "Function", desktop)?; cx.set_attr(owner, name, Value::Object(function)) } fn new_block( cx: &mut NativeContext<'_>, data: Data, session: &Shared, ) -> Result { new_typed_block(cx, data, session, None) } fn new_typed_block( cx: &mut NativeContext<'_>, data: Data, session: &Shared, requested: Option, ) -> Result { let type_name = match data.kind { Kind::Text { .. } => "TextBlock", Kind::Rect { .. } => "RectBlock", Kind::Spacer(_) => "Spacer", Kind::Scroll { .. } => "ScrollBlock", Kind::Row => "Row", Kind::Column => "Column", Kind::Button => "Button", Kind::Page => "Page", }; let page = matches!(data.kind, Kind::Page); let desktop = session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; let expected = desktop_type(cx.store(), type_name, desktop)?; let type_object = requested.unwrap_or(expected); if !cx.store().is_subtype(type_object, expected)? { return Err(invalid("native constructor cannot allocate this subclass")); } let block = block::allocate(cx, data)?; cx.attach_type(block, type_object)?; { let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?; if state.nodes.len() >= MAX_UI_NODES { return Err(VmError::ExecutionLimit("4096 node budget")); } if page { let mut pages = 0; for weak in &state.nodes { if let Some(node) = cx.upgrade(*weak)? && cx.store().payload::(node, "Block")?.page { pages += 1; } } if pages >= MAX_PAGES { return Err(VmError::ExecutionLimit("64 page budget")); } } state.nodes.push(cx.store().downgrade(block)?); } Ok(block) } fn create_window( cx: &mut NativeContext<'_>, state: &mut Session, definition: ObjectRef, ) -> Result { if cx.store().is_read_only() { return Err(invalid( "window creation is forbidden during read-only rendering", )); } if state.exiting { return Err(invalid("runtime is exiting; window creation is forbidden")); } if state.presentations.len() >= 16 { return Err(VmError::ExecutionLimit("16 window budget")); } let id = state .next_window .checked_add(1) .filter(|id| *id <= i64::MAX as u64) .ok_or(invalid("window identities exhausted"))?; let (window, desktop, tabs) = crate::window::create(cx, state.master, id, definition)?; let mut presentation = crate::presentation::Presentation::new(desktop, tabs); presentation.force_frame = true; state.presentations.insert(id, presentation); state.next_window = id; Ok(window) } fn page_window( store: &endor_core::ObjectStore, state: &Session, page: ObjectRef, ) -> Result, VmError> { for (id, view) in &state.presentations { let window = object(Some(&store.get_attr(view.desktop, "window")?))?; if crate::window::pages(store, window)?.contains(&page) { return Ok(Some(*id)); } } Ok(None) } fn remove_page( cx: &mut NativeContext<'_>, view: &mut crate::presentation::Presentation, page: ObjectRef, ) -> Result<(), VmError> { let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?; crate::window::unregister_page(cx, window, page)?; if view.pending.unwrap_or(view.active) == Some(page) { let next = crate::window::pages(cx.store(), window)?.first().copied(); cx.set_attr( view.desktop, "pending", next.map(Value::Object).unwrap_or(Value::Nil), )?; view.pending = Some(next); } Ok(()) } fn show_in( cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared, target: Option, ) -> Result<(), VmError> { let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?; let old = page_window(cx.store(), &state, page)?; let id = match target { Some(window) => { let data = cx .store() .payload::(window, "Window")?; if data.closed { return Err(invalid("native window is closed")); } let id = data.id; if state .presentations .get(&id) .is_none_or(|view| view.desktop != data.desktop) { return Err(invalid("window belongs to another runtime")); } id } None => old.unwrap_or(state.current), }; if let Some(old) = old && old != id { remove_page( cx, state .presentations .get_mut(&old) .ok_or(invalid("missing source window"))?, page, )?; } let view = state .presentations .get_mut(&id) .ok_or(invalid("missing target window"))?; let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?; crate::window::register_page(cx, window, page)?; cx.set_attr(view.desktop, "pending", Value::Object(page))?; view.pending = Some(Some(page)); Ok(()) } fn show(cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared) -> Result<(), VmError> { show_in(cx, page, session, None) } #[derive(Debug)] struct PresentResult { open: bool, session: Shared, } impl NativeCallable for PresentResult { fn arity(&self) -> Option { Some(endor_core::CallArity::exact(1)) } fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { let [Value::Object(root)] = args else { return Err(invalid("Presentable.present must return a Block")); }; let is_page = cx.store().payload::(*root, "Block")?.page; let result = if self.open { let page = if is_page { *root } else { new_block( cx, Data::new(Kind::Page, vec![*root], 0.0, 0.0), &self.session, )? }; show(cx, page, &self.session)?; page } else { *root }; Ok(NativeResult::Return(Value::Object(result))) } } fn present_value( cx: &mut NativeContext<'_>, value: &Value, session: &Shared, ) -> Result { if let Value::Object(block) = value && cx .store() .object(*block)? .payload() .as_any() .is::() { return Ok(*block); } let value = match value { Value::Nil => "nil".into(), Value::Bool(v) => v.to_string(), Value::Number(v) => v.to_string(), Value::Text(v) => v.clone(), Value::Object(v) => cx.store().repr(*v)?, }; if value.len() > 16_384 { return Err(invalid("presentation exceeds 16 KiB")); } new_block( cx, Data::new( Kind::Text { value, size: 1, color: "#242422".into(), }, vec![], 0.0, 0.0, ), session, ) } impl NativeCallable for Function { fn signature(&self) -> Option { self.signature.clone() } fn arity(&self) -> Option { Some(self.operation.arity()) } fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { use Operation::*; let arity = self.operation.arity(); let (minimum, maximum) = (arity.required, arity.maximum); if !(minimum..=maximum).contains(&args.len()) { return Err(invalid("incorrect UI method argument count")); } if matches!(self.operation, Implement | Implements | Supports) { let capability = object(args.get(1))?; let value = match self.operation { Implement => { cx.implement_trait(object(args.first())?, capability)?; Value::Nil } Implements => Value::Bool( cx.store() .implements_trait(object(args.first())?, capability)?, ), Supports => { // Validate the contract even for immediate scalar values. cx.store() .payload::(capability, "Trait")?; Value::Bool(match args[0] { Value::Object(value) => cx.store().supports(value, capability)?, _ => false, }) } _ => unreachable!(), }; return Ok(NativeResult::Return(value)); } if matches!(self.operation, IsInstance | IsSubtype) { let candidate = object(args.first())?; let expected = object(args.get(1))?; let value = if matches!(self.operation, IsInstance) { cx.store().is_instance(candidate, expected)? } else { cx.store().is_subtype(candidate, expected)? }; return Ok(NativeResult::Return(Value::Bool(value))); } if matches!(self.operation, Invalidate) { if cx.store().is_read_only() { return Err(invalid( "invalidation is forbidden during read-only rendering", )); } let reason = args .first() .map(|value| text(Some(value))) .transpose()? .unwrap_or_else(|| "layout".into()); if !matches!(reason.as_str(), "paint" | "layout" | "content") { return Err(invalid( "invalidation reason must be paint, layout or content", )); } let owner = self.owner.ok_or(invalid("invalidation requires a Block"))?; let (data, _) = block::resolve(cx.store(), owner)?; let mut session = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; let mut affected = std::collections::HashSet::new(); for weak in &session.nodes { if let Some(block) = cx.upgrade(*weak)? && block::resolve(cx.store(), block)?.0 == data { affected.insert(block); } } for presentation in session.presentations.values_mut() { presentation.invalidate(cx, &affected, &reason)?; } return Ok(NativeResult::Return(Value::Nil)); } if matches!(self.operation, Mounts) { let owner = self.owner.ok_or(invalid("mount query requires a Block"))?; let session = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; let mut positions = Vec::new(); for (window, presentation) in &session.presentations { for (id, weak) in &presentation.mounts.active { if let Some(object) = cx.upgrade(*weak)? && cx .store() .payload::(object, "Mount")? .block == owner { positions.push((*window, *id, object)); } } } positions.sort_unstable_by_key(|(window, id, _)| (*window, *id)); let set = crate::mount::collection( cx, positions.into_iter().map(|(_, _, object)| object).collect(), session.desktop, )?; return Ok(NativeResult::Return(Value::Object(set))); } if matches!(self.operation, NewWindow) { let mut state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; let definition = self .requested_type .ok_or(invalid("Window constructor requires a type"))?; let window = create_window(cx, &mut state, definition)?; return Ok(NativeResult::Return(Value::Object(window))); } if matches!(self.operation, CloseWindow) { let window = self .owner .ok_or(invalid("window close requires a receiver"))?; let id = cx .store() .payload::(window, "Window")? .id; crate::window::close(cx, window)?; let mut state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; if let Some(view) = state.presentations.get_mut(&id) { cx.set_attr(view.desktop, "pending", Value::Nil)?; view.pending = Some(None); view.force_frame = true; view.closing = true; } return Ok(NativeResult::Return(Value::Nil)); } if matches!(self.operation, Window) { let desktop = self .session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; return Ok(NativeResult::Return( cx.store().get_attr(desktop, "window")?, )); } if matches!(self.operation, Viewport) { let desktop = self .session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; let Value::Object(input) = cx.store().get_attr(desktop, "viewport")? else { return Err(invalid("window viewport is unavailable")); }; let width = cx.store().get_attr(input, "width")?; let height = cx.store().get_attr(input, "height")?; return Ok(NativeResult::Return(Value::Object( crate::native_visual::record( cx, desktop, "MeasureResult", vec![("width", width), ("height", height)], )?, ))); } if matches!(self.operation, Stats) { if cx.store().is_read_only() { return Err(invalid( "runtime statistics are unavailable during read-only rendering", )); } let session = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; let mut blocks = 0_i64; let mut pages = 0_i64; for weak in &session.nodes { if let Some(object) = cx.upgrade(*weak)? { blocks += 1; if cx.store().payload::(object, "Block")?.page { pages += 1; } } } let objects = cx.store().object_count() as i64; let tabs = cx.store().dict_items(session.tabs)?.len() as i64; let stats = cx.new_object()?; for (name, value) in [ ("objects", objects), ("blocks", blocks), ("pages", pages), ("tabs", tabs), ("mounts", session.mounts.active.len() as i64), ("snapshotVisited", session.snapshot_visited as i64), ("snapshotReused", session.snapshot_reused as i64), ("measured", session.layout.measured as i64), ("sourceMeasureHits", session.measure_cache.hits as i64), ("sourceMeasureMisses", session.measure_cache.misses as i64), ("arranged", session.layout.arranged as i64), ("layoutPasses", session.layout.settle_passes as i64), ("layoutSlices", session.layout.slices as i64), ("refreshYields", session.refresh_yields as i64), ("refreshRestarts", session.refresh_restarts as i64), ("layoutCancels", session.layout_cancels as i64), ("paintCancels", session.paint_cancels as i64), ("paintSlices", session.paint_slices as i64), ("bindingEvaluations", session.binding_evaluations as i64), ( "dependencyInvalidations", session.dependency_invalidations as i64, ), ] { cx.set_attr(stats, name, Value::Number(value.into()))?; } cx.set_attr( stats, "lastInvalidDependency", Value::Text(session.last_invalid_dependency.clone()), )?; cx.set_attr( stats, "windowDependencyDiagnostics", Value::Text( session .presentations .iter() .filter(|(_, view)| !view.last_invalid_dependency.is_empty()) .map(|(id, view)| format!("{id}: {}", view.last_invalid_dependency)) .collect::>() .join("; "), ), )?; return Ok(NativeResult::Return(Value::Object(stats))); } if matches!(self.operation, State) { let state = cx.new_object()?; attach_named_type(cx, state, "Object", &self.session)?; cx.set_attr(state, "value", args[0].clone())?; bind(cx, state, "set", StateSet, &self.session)?; bind(cx, state, "increment", Increment, &self.session)?; return Ok(NativeResult::Return(Value::Object(state))); } if matches!(self.operation, StateSet | Increment) { let owner = self.owner.ok_or(invalid("state method has no owner"))?; let value = if matches!(self.operation, StateSet) { args[0].clone() } else { let Value::Number(value) = cx.store().get_attr(owner, "value")? else { return Err(invalid("increment requires an integer state")); }; let next = value .as_i64() .and_then(|n| n.checked_add(1)) .ok_or(invalid("integer state increment exceeds its range"))?; Value::Number(next.into()) }; cx.set_attr(owner, "value", value)?; return Ok(NativeResult::Return(Value::Nil)); } if matches!(self.operation, TextOf) { let source = object(args.first())?; let field = text(args.get(1))?; cx.store().get_attr(source, &field)?; let size = args .get(2) .map(|v| integer(Some(v))) .transpose()? .unwrap_or(1); if !(1..=8).contains(&size) { return Err(invalid("text size must be 1 through 8")); } let mut data = Data::new( Kind::Text { value: String::new(), size: size as u32, color: color(args.get(3), "#242422")?, }, vec![], 0.0, 0.0, ); data.text_binding = Some((source, field)); return Ok(NativeResult::Return(Value::Object(new_block( cx, data, &self.session, )?))); } let constructor = match self.operation { Scroll => Some(Data::new( Kind::Scroll { width: dimension(args.first())?, height: dimension(args.get(1))?, }, vec![object(args.get(2))?], 0.0, 0.0, )), Text => Some(Data::new( Kind::Text { value: text(args.first())?, size: args .get(1) .map(|v| integer(Some(v))) .transpose()? .unwrap_or(1) .try_into() .map_err(|_| invalid("text size exceeds limits"))?, color: color(args.get(2), "#242422")?, }, vec![], 0.0, 0.0, )), Rect => Some(Data::new( Kind::Rect { width: dimension(args.first())?, height: dimension(args.get(1))?, radius: args .get(2) .map(|v| dimension(Some(v))) .transpose()? .unwrap_or(0.0), color: color(args.get(3), "#DCE8FF")?, }, vec![], 0.0, 0.0, )), Spacer => Some(Data::new( Kind::Spacer(dimension(args.first())?), vec![], 0.0, 0.0, )), Row | Column => Some(Data::new( if matches!(self.operation, Row) { Kind::Row } else { Kind::Column }, args[2..] .iter() .map(|v| object(Some(v))) .collect::>()?, dimension(args.first())?, dimension(args.get(1))?, )), Page => Some(Data::new(Kind::Page, vec![], 0.0, 0.0)), Button => { let label = new_block( cx, Data::new( Kind::Text { value: text(args.first())?, size: 1, color: "#243047".into(), }, vec![], 0.0, 0.0, ), &self.session, )?; Some(Data::new(Kind::Button, vec![label], 10.0, 0.0)) } _ => None, }; if let Some(data) = constructor { if let Kind::Text { size, value, .. } = &data.kind && (!(1..=8).contains(size) || value.len() > 16_384) { return Err(invalid("text exceeds size limits")); } return Ok(NativeResult::Return(Value::Object(new_typed_block( cx, data, &self.session, self.requested_type, )?))); } if matches!(self.operation, OpenValue | Present) && let Value::Object(value) = args[0] { let (desktop, current) = { let state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; (state.master, state.desktop) }; let capability = desktop_type(cx.store(), "Presentable", desktop)?; if !cx .store() .object(value)? .payload() .as_any() .is::() && cx.store().supports(value, capability)? { let definition = cx .store() .type_of(value)? .ok_or(invalid("untyped Presentable"))?; let function = cx .store() .lookup_method(definition, "present")? .ok_or(invalid("missing present method"))?; let window = object(Some(&cx.store().get_attr(current, "window")?))?; let complete = cx.allocate(ContextNativeFunction::new( "validate presentation", PresentResult { open: matches!(self.operation, OpenValue), session: self.session.clone(), }, vec![desktop], ))?; let instructions = [ endor_core::Instruction::new(endor_core::Opcode::LoadConst, 0), endor_core::Instruction::simple(endor_core::Opcode::PushNull), endor_core::Instruction::new(endor_core::Opcode::LoadConst, 1), endor_core::Instruction::simple(endor_core::Opcode::PushNull), endor_core::Instruction::new(endor_core::Opcode::LoadConst, 2), endor_core::Instruction::new(endor_core::Opcode::LoadConst, 3), endor_core::Instruction::new(endor_core::Opcode::Call, 2), endor_core::Instruction::new(endor_core::Opcode::Call, 1), endor_core::Instruction::simple(endor_core::Opcode::ReturnValue), ]; let code = endor_core::EndorCode::from_instructions( &instructions, vec![ Value::Object(complete), Value::Object(function), Value::Object(value), Value::Object(window), ], endor_core::CodeLayout::default(), )? .with_location("Presentable.present", "", 1); let code = cx.allocate(endor_core::EndorCodeObject::new(code))?; let globals = cx.new_dict()?; let driver = cx.new_function("Presentable.present", code, globals)?; attach_desktop_type(cx, driver, "Function", desktop)?; return Ok(NativeResult::Invoke { function: driver, arguments: vec![], }); } } if matches!(self.operation, OpenValue) { let root = present_value(cx, &args[0], &self.session)?; let page = if cx.store().payload::(root, "Block")?.page { root } else { new_block( cx, Data::new(Kind::Page, vec![root], 0.0, 0.0), &self.session, )? }; show(cx, page, &self.session)?; return Ok(NativeResult::Return(Value::Object(page))); } if matches!(self.operation, Present) { return Ok(NativeResult::Return(Value::Object(present_value( cx, &args[0], &self.session, )?))); } let owner = self.owner.ok_or(invalid("method has no owner"))?; if matches!(self.operation, Bind) { let field = text(args.first())?; let compute = object(args.get(1))?; return Ok(NativeResult::Return(Value::Object( crate::binding::connect( cx, owner, &field, compute, self.session .lock() .map_err(|_| invalid("UI session poisoned"))? .master, )?, ))); } if matches!(self.operation, MoveTab) { let position = usize::try_from(integer(args.first())?) .map_err(|_| invalid("tab position out of range"))?; let mut state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; let id = page_window(cx.store(), &state, owner)?.ok_or(invalid("Page is not open"))?; let view = state .presentations .get_mut(&id) .ok_or(invalid("missing window"))?; let window = object(Some(&cx.store().get_attr(view.desktop, "window")?))?; crate::window::move_page(cx, window, owner, position)?; view.force_frame = true; return Ok(NativeResult::Return(Value::Object(owner))); } let (data_ref, data) = block::resolve(cx.store(), owner)?; let mut data = data.clone(); let mut result = Value::Nil; match self.operation { SetText => match &mut data.kind { Kind::Text { value, .. } => { let next = text(args.first())?; if next.len() > 16_384 { return Err(invalid("text exceeds 16 KiB")); } *value = next; data.text_binding = None; cx.set_attr(owner, "text", args[0].clone())?; } _ => return Err(invalid("setText requires a text Block")), }, SetColor => match &mut data.kind { Kind::Text { color: value, .. } | Kind::Rect { color: value, .. } => { *value = color(args.first(), "#242422")?; cx.set_attr(owner, "color", Value::Text(value.clone()))?; } _ => return Err(invalid("this Block has no color")), }, Append | Insert | Remove | Replace | Move => { if !data.is_container() { return Err(invalid("expected a container Block")); } if matches!(self.operation, Append | Insert) { let (position, argument) = if matches!(self.operation, Insert) { let position = usize::try_from(integer(args.first())?) .map_err(|_| invalid("child position out of range"))?; if position > data.children.len() { return Err(invalid("child position out of range")); } (position, args.get(1)) } else { (data.children.len(), args.first()) }; let child = object(argument)?; block::validate(cx.store(), child, Some(data_ref))?; let slot = data.next_slot; data.next_slot = slot .checked_add(1) .filter(|next| *next <= i64::MAX as u64) .ok_or(VmError::IdentityExhausted)?; data.children.insert(position, Child { slot, block: child }); result = Value::Number((slot as i64).into()); } else { let slot = integer(args.first())?; let index = data .children .iter() .position(|v| v.slot == slot) .ok_or(invalid("child slot does not exist"))?; match self.operation { Remove => { data.children.remove(index); } Replace => { let child = object(args.get(1))?; block::validate(cx.store(), child, Some(data_ref))?; data.children[index].block = child; } Move => { let to = usize::try_from(integer(args.get(1))?) .map_err(|_| invalid("child position out of range"))?; if to >= data.children.len() { return Err(invalid("child position out of range")); } let child = data.children.remove(index); data.children.insert(to, child); } _ => unreachable!(), } } } Children => { let result = cx.new_dict()?; for child in data.children { cx.dict_set( result, Value::Text(child.slot.to_string()), Value::Object(child.block), )?; } return Ok(NativeResult::Return(Value::Object(result))); } Render | Navigate => { if !matches!(data.kind, Kind::Page) { return Err(invalid("expected a Page")); } let root = object(args.first())?; block::validate(cx.store(), root, Some(data_ref))?; if matches!(self.operation, Navigate) { if data.history.is_empty() { if let Some(old) = data.children.first() { data.history.push(old.block); } } else { data.history.truncate(data.history_index + 1); } data.history.push(root); data.history_index = data.history.len() - 1; } data.children = vec![Child { slot: 0, block: root, }]; data.next_slot = data.next_slot.max(1); show(cx, owner, &self.session)?; } Back | Forward => { if data.history.is_empty() { return Ok(NativeResult::Return(Value::Nil)); } let next = if matches!(self.operation, Back) { data.history_index.saturating_sub(1) } else { (data.history_index + 1).min(data.history.len() - 1) }; data.history_index = next; data.children = vec![Child { slot: 0, block: data.history[next], }]; show(cx, owner, &self.session)?; } Detach => { let window = { let mut state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; let definition = desktop_type(cx.store(), "Window", state.master)?; create_window(cx, &mut state, definition)? }; show_in(cx, owner, &self.session, Some(window))?; return Ok(NativeResult::Return(Value::Object(window))); } PageWindow => { let state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; let value = if let Some(id) = page_window(cx.store(), &state, owner)? { cx.store() .get_attr(state.presentations[&id].desktop, "window")? } else { Value::Nil }; return Ok(NativeResult::Return(value)); } Open | Activate | MoveWindow => { if matches!(self.operation, MoveWindow) && matches!(args.first(), None | Some(Value::Nil)) { return Err(invalid("Page.moveToWindow requires a Window")); } let target = match args.first() { None | Some(Value::Nil) => None, Some(value) => Some(object(Some(value))?), }; show_in(cx, owner, &self.session, target)?; return Ok(NativeResult::Return(Value::Object(owner))); } Unmount => { data.children.clear(); } Close => { crate::tasks::cancel_owner(cx, owner)?; let mut state = self .session .lock() .map_err(|_| invalid("UI session poisoned"))?; if let Some(id) = page_window(cx.store(), &state, owner)? { // Closing the tab ends its presentation even if source // reopens it before the next layout can detach the Mounts. let page = cx.store().downgrade(owner)?; for weak in state.presentations[&id].mounts.ordered() { if let Some(mount) = cx.upgrade(weak)? && cx .store() .payload::(mount, "Mount")? .page == Some(page) { crate::tasks::cancel_owner(cx, mount)?; } } remove_page( cx, state .presentations .get_mut(&id) .ok_or(invalid("missing window"))?, owner, )?; } return Ok(NativeResult::Return(Value::Nil)); } OnClick => { let handler = object(args.first())?; crate::event_dispatch::validate_handler(cx.store(), handler)?; data.click = Some(handler); result = Value::Object(owner); } On => { let kind = text(args.first())?; if kind.is_empty() || kind.len() > 128 { return Err(invalid("invalid event kind")); } let handler = object(args.get(1))?; crate::event_dispatch::validate_handler(cx.store(), handler)?; let capture = match args.get(2) { None | Some(Value::Bool(false)) => false, Some(Value::Bool(true)) => true, Some(Value::Text(phase)) if phase == "capture" => true, Some(Value::Text(phase)) if phase == "bubble" => false, _ => return Err(invalid("phase must be capture or bubble")), }; let mut active = Vec::new(); for token in &data.subscriptions { if cx .store() .payload::(*token, "Connection")? .handler .is_some() { active.push(*token); } } if active.len() >= MAX_UI_NODES { return Err(invalid("too many event subscriptions")); } let connection = cx.allocate(crate::event_dispatch::Connection { kind, handler: Some(handler), capture, })?; let token = cx.allocate(crate::event_dispatch::Subscription { connection: cx.store().downgrade(connection)?, })?; attach_named_type(cx, token, "Subscription", &self.session)?; active.push(connection); data.subscriptions = active; result = Value::Object(token); } OnEvent | OnMount | OnUnmount => return Ok(NativeResult::Return(Value::Nil)), Click | Dispatch => { let desktop = self .session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; let (event, mount) = if matches!(self.operation, Click) { let definition = desktop_type(cx.store(), "Event", desktop)?; ( crate::event_dispatch::new_event(cx, definition, "click")?, args.first(), ) } else { (object(args.first())?, args.get(1)) }; let mount = mount.map(|value| object(Some(value))).transpose()?; return crate::event_dispatch::dispatch(cx, owner, event, mount, desktop); } _ => return Err(invalid("unsupported Block operation")), } cx.replace_payload(data_ref, data)?; Ok(NativeResult::Return(result)) } } #[must_use = "pending layout must be resumed on its owning VM"] pub enum LayoutProgress { Pending(Box), Complete(Box), } struct LayoutContext { root: Arc, tabs: Vec, full_viewport: Option, desktop: ObjectRef, window: u64, hooks: Option, checkpoint: crate::mount::Checkpoint, } pub struct PendingLayout { work: crate::source_layout::Work, context: LayoutContext, } pub struct UiBridge { pub ui: ObjectRef, session: Shared, tasks: crate::tasks::Scheduler, } fn invoke_member( vm: &mut Vm, owner: ObjectRef, method: &str, arguments: &[Value], instruction_budget: usize, ) -> Result { let bound = vm.bind_attr(owner, method)?; let result = match &bound { Value::Object(function) => vm .call_with_limits( *function, arguments, endor_core::ExecutionLimits { max_frames: 128, max_instructions: instruction_budget, }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error)), _ => Err(anyhow::anyhow!("object method is not callable")), }; vm.release_value(bound)?; result } fn project_tabs( vm: &Vm, state: &mut crate::presentation::Presentation, ) -> Result> { let window = object(Some(&vm.get_attr(state.desktop, "window")?))?; let pages = crate::window::pages(vm.object_store(), window)?; let active = state.pending.unwrap_or(state.active); let mut live = Vec::new(); let mut tabs = Vec::new(); for (index, page) in pages.iter().copied().enumerate() { let weak = vm.object_store().downgrade(page)?; let id = if let Some((_, id)) = state.tab_ids.iter().find(|(w, _)| *w == weak) { *id } else { state.next_tab = state .next_tab .checked_add(1) .ok_or_else(|| anyhow::anyhow!("tab identities exhausted"))?; state.next_tab }; live.push((weak, id)); let title = match vm.get_attr(page, "title") { Ok(Value::Text(title)) if !title.is_empty() => title .chars() .filter(|c| !c.is_control()) .take(120) .collect(), _ => format!("Page {}", index + 1), }; tabs.push(crate::tabs::Tab { id, title, developer_tool: match vm.get_attr(page, "_tesseraTool") { Ok(Value::Text(name)) => Some(name), _ => None, }, active: active == Some(page), }); } state.tab_ids = live; Ok(tabs) } impl UiBridge { /// Detach all occurrences without running layout/paint. All windows are /// already closed before source unmount hooks run, so they cannot remount. pub fn shutdown(&self, vm: &mut Vm, callbacks: bool) -> Result<(usize, usize, usize)> { let tasks = self.tasks.quiesce(vm)?; let mut drivers = Vec::new(); let mut mounts = 0; let prepared = (|| -> Result { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; state.exiting = true; let ids: Vec<_> = state.presentations.keys().copied().collect(); for id in &ids { let view = state.presentations.get_mut(id).expect("known window"); let desktop = view.desktop; mounts += view.mounts.active.len(); if callbacks { if let Some(hooks) = view.mounts.synchronize( vm, desktop, None, Vec::new(), &crate::document::Frame::default(), false, )? { drivers.push((*id, hooks)); } } else { view.mounts.retire(vm, desktop)?; } let window = object(Some(&vm.get_attr(desktop, "window")?))?; crate::window::retire(vm, window)?; vm.set_attr(desktop, "pending", Value::Nil)?; vm.set_attr(desktop, "focus", Value::Nil)?; view.pending = Some(None); view.active = None; view.closing = true; } Ok(ids.len()) })(); let mut result = prepared.map(|windows| (windows, mounts, tasks)); for (id, hooks) in drivers { if result.is_ok() { let call = (|| -> Result<()> { self.select_window(id)?; let value = vm .call_with_limits( hooks, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error))?; vm.release_value(value)?; Ok(()) })(); if let Err(error) = call { result = Err(error); } } if let Err(error) = vm.release(hooks) { result = Err(error.into()); } } self.select_window(crate::window::MAIN_WINDOW)?; result } pub fn has_cooperative_tasks(&self) -> bool { self.tasks.has_cooperative_work() } pub fn poll_tasks(&self, vm: &mut Vm, limit: usize) -> Result { self.tasks.poll(vm, limit) } pub fn cancel_tasks(&self, vm: &mut Vm) -> Result { self.tasks.cancel_all(vm) } pub fn window_ids(&self) -> Result> { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let mut ids: Vec<_> = state.presentations.keys().copied().collect(); ids.sort_by_key(|id| { let view = &state.presentations[id]; ( if view.active.is_some() && view.pending.is_some() && view.pending.flatten() != view.active { 0 } else { 1 }, *id, ) }); Ok(ids) } pub fn accepts_window(&self, id: u64) -> bool { self.session.lock().is_ok_and(|state| { state .presentations .get(&id) .is_some_and(|view| !view.closing) }) } pub fn select_window(&self, id: u64) -> Result<()> { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; if !state.presentations.contains_key(&id) { bail!("window is unavailable"); } state.current = id; Ok(()) } pub fn retire_window(&self, vm: &mut Vm, id: u64) -> Result<()> { if id == crate::window::MAIN_WINDOW { bail!("the primary shell is closed by the OS window control"); } let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let Some(view) = state.presentations.get_mut(&id) else { return Ok(()); }; let desktop = view.desktop; let window = object(Some(&vm.get_attr(desktop, "window")?))?; view.mounts.retire(vm, desktop)?; crate::window::retire(vm, window)?; vm.set_attr(desktop, "pending", Value::Nil)?; vm.set_attr(desktop, "focus", Value::Nil)?; vm.set_attr(desktop, "focusRequested", Value::Bool(false))?; // Draining is not publication: failed-domain effects are discarded. let _ = crate::window::take_commands(vm, window)?; let windows = object(Some(&vm.get_attr(state.master, "windows")?))?; vm.dict_delete(windows, &Value::Text(id.to_string()))?; state.presentations.remove(&id); state.current = crate::window::MAIN_WINDOW; Ok(()) } pub fn finish_closed(&self, vm: &mut Vm) -> Result<()> { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let ids: Vec<_> = state .presentations .iter() .filter(|(id, view)| **id != crate::window::MAIN_WINDOW && view.closing) .map(|(id, _)| *id) .collect(); let windows = object(Some(&vm.get_attr(state.master, "windows")?))?; for id in ids { let desktop = state.presentations[&id].desktop; let window = object(Some(&vm.get_attr(desktop, "window")?))?; state .retired_commands .extend(crate::window::take_commands(vm, window)?); vm.dict_delete(windows, &Value::Text(id.to_string()))?; state.presentations.remove(&id); } state.current = crate::window::MAIN_WINDOW; Ok(()) } pub fn take_window_commands(&self, vm: &Vm) -> Result> { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let mut commands = std::mem::take(&mut state.retired_commands); for view in state.presentations.values() { let window = object(Some(&vm.get_attr(view.desktop, "window")?))?; commands.extend(crate::window::take_commands(vm, window)?); } commands.sort_by_key(|command| (command.window, command.sequence)); Ok(commands) } pub fn set_viewport(&self, vm: &mut Vm, viewport: Option) -> Result { if let Some(size) = viewport && (!size.width.is_finite() || !size.height.is_finite() || !(0.0..=MAX_DIMENSION as f32).contains(&size.width) || !(0.0..=MAX_DIMENSION as f32).contains(&size.height)) { bail!("invalid window viewport"); } let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; if state.layout.viewport == viewport { return Ok(false); } if let Some(size) = viewport { let input = vm.new_object()?; let result = (|| { vm.set_attr(input, "width", Value::Number((size.width as f64).into()))?; vm.set_attr(input, "height", Value::Number((size.height as f64).into()))?; vm.set_attr(state.desktop, "viewport", Value::Object(input)) })(); vm.release(input)?; result?; } else { vm.set_attr(state.desktop, "viewport", Value::Nil)?; } state.layout.viewport = viewport; // A fresh Console domain can own window inputs without replacing the // preserved displayed page until it actually opens/renders something. state.force_frame |= state.active.is_some() || state.pending.flatten().is_some(); Ok(true) } pub fn install_globals(&self, vm: &mut Vm, globals: ObjectRef) -> Result<()> { vm.dict_set(globals, Value::Text("ui".into()), Value::Object(self.ui))?; // Compatibility spelling used by the original module examples. vm.dict_set( globals, Value::Text("View".into()), vm.get_attr(self.ui, "Block")?, )?; for name in TYPE_NAMES.iter().chain(QUERY_NAMES) { let value = vm.get_attr(self.ui, name)?; vm.dict_set(globals, Value::Text((*name).into()), value)?; } Ok(()) } pub fn has_page(&self) -> Result { Ok(self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .presentations .values() .any(|view| view.pending.flatten().is_some() || view.active.is_some())) } pub fn accepts_tab_drop(&self, vm: &Vm, input: &crate::events::Input) -> bool { let Some(target) = &input.tab_drop else { return true; }; let Ok(state) = self.session.lock() else { return false; }; for (id, version) in [ (input.window, input.layout_version), (target.window, target.layout_version), ] { let Some(view) = state.presentations.get(&id) else { return false; }; if view.closing || view.published_version != version { return false; } let Ok(Value::Object(window)) = vm.get_attr(view.desktop, "window") else { return false; }; let Ok(pages) = crate::window::pages(vm.object_store(), window) else { return false; }; if view.tab_ids.len() != pages.len() || !pages .iter() .zip(&view.tab_ids) .all(|(page, (weak, _))| vm.object_store().downgrade(*page).ok() == Some(*weak)) { return false; } if id == target.window && target.index > pages.len() { return false; } } true } pub fn open_value(&self, vm: &mut Vm, value: &Value) -> Result<()> { let function = object(Some(&vm.get_attr(self.ui, "open")?))?; let page = vm.call(function, std::slice::from_ref(value))?; vm.release_value(page)?; Ok(()) } fn dispatch_tab(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> { let page = { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; if state.published_version != input.layout_version { bail!("event targets an obsolete presentation"); } let weak = state .tab_ids .iter() .find(|(_, id)| *id == input.mount) .map(|(weak, _)| *weak) .ok_or_else(|| anyhow::anyhow!("tab has expired"))?; let window = object(Some(&vm.get_attr(state.desktop, "window")?))?; crate::window::pages(vm.object_store(), window)? .iter() .copied() .find(|page| vm.object_store().downgrade(*page).ok() == Some(weak)) .ok_or_else(|| anyhow::anyhow!("Page is no longer open"))? }; if let Some(target) = &input.tab_drop { let window = { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let view = state .presentations .get(&target.window) .ok_or_else(|| anyhow::anyhow!("drop window has closed"))?; if view.published_version != target.layout_version { bail!("drop targets an obsolete presentation"); } object(Some(&vm.get_attr(view.desktop, "window")?))? }; if target.window != input.window { let value = invoke_member(vm, page, "moveToWindow", &[Value::Object(window)], 300_000)?; vm.release_value(value)?; } // An override may deliberately decline or redirect the move. Do // not reorder an unrelated window after that source decision. let destination = { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; page_window(vm.object_store(), &state, page)? }; if destination == Some(target.window) { let pages = crate::window::pages(vm.object_store(), window)?; let from = pages .iter() .position(|candidate| *candidate == page) .ok_or_else(|| anyhow::anyhow!("Page is no longer open"))?; let position = if input.window == target.window && from < target.index { target.index - 1 } else { target.index }; if position >= pages.len() { bail!("drop destination changed during Page method"); } let value = invoke_member( vm, page, "moveTab", &[Value::Number((position as i64).into())], 300_000, )?; vm.release_value(value)?; let value = invoke_member(vm, page, "activate", &[], 300_000)?; vm.release_value(value)?; } return Ok(()); } let method = match input.kind.as_str() { "tab-close" => "close", "tab-detach" => "detachToWindow", _ => "activate", }; let value = invoke_member(vm, page, method, &[], 1_000_000)?; let placed = (|| -> Result<()> { if input.kind == "tab-detach" && let Some([x, y]) = input.screen_position && let Value::Object(window) = &value && vm .payload::(*window, "Window") .is_ok() { let result = invoke_member( vm, *window, "moveTo", &[ Value::Number((x as i64).into()), Value::Number((y as i64).into()), ], 100_000, )?; vm.release_value(result)?; } Ok(()) })(); vm.release_value(value)?; placed?; Ok(()) } pub fn dispatch(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> { input .validate() .map_err(|message| anyhow::anyhow!(message))?; if self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .current != input.window { bail!("event belongs to another window"); } if input.kind == "window-close" { let desktop = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .desktop; let window = object(Some(&vm.get_attr(desktop, "window")?))?; let bound = vm.bind_attr(window, "close")?; let result = match &bound { Value::Object(function) => vm.call_with_limits( *function, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ), _ => Err(invalid("Window.close is not callable")), }; vm.release_value(bound)?; vm.release_value(result?)?; return Ok(()); } if let Some(name) = input.kind.strip_prefix("developer-open:") { let function = object(Some(&vm.get_attr(self.ui, "developerTools")?))?; let page = vm.call(function, &[Value::Text(name.into())])?; vm.release_value(page)?; return Ok(()); } if matches!( input.kind.as_str(), "tab-activate" | "tab-close" | "tab-detach" | "tab-drop" ) { return self.dispatch_tab(vm, input); } let (weak, desktop, candidates, viewport) = { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; if state.published_version != input.layout_version { bail!("event targets an obsolete presentation"); } ( *state .mounts .active .get(&input.mount) .ok_or_else(|| anyhow::anyhow!("Mount is no longer active"))?, state.desktop, state.mounts.ordered(), state.layout.viewport.map(|size| crate::layout::Size { width: (size.width - crate::tabs::RAIL - crate::tabs::INSET).max(0.0), height: (size.height - crate::tabs::INSET - crate::tabs::height(&state.last_tabs)) .max(0.0), }), ) }; if input.kind == "scroll" { let mount = vm .upgrade(weak)? .ok_or_else(|| anyhow::anyhow!("scroll Mount expired"))?; let result = (|| { let data = vm.payload::(mount, "Mount")?; let point = input .point .ok_or_else(|| anyhow::anyhow!("scroll has no pointer"))?; if !data.mounted || data.version != input.layout_version || viewport .is_some_and(|size| point[0] >= size.width || point[1] >= size.height) || !data .geometry .contains(crate::geometry::Point::new(point[0], point[1])) { bail!("scroll targets an obsolete or clipped Mount"); } let delta = input .delta .ok_or_else(|| anyhow::anyhow!("scroll has no delta"))?; let method = vm.bind_attr(mount, "scrollBy")?; let result = match method { Value::Object(function) => vm.call_with_limits( function, &[ Value::Number((delta[0] as f64).into()), Value::Number((delta[1] as f64).into()), ], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 100_000, }, ), _ => Err(invalid("scrollBy is not callable")), }; vm.release_value(method)?; vm.release_value(result?)?; Ok(()) })(); vm.release(mount)?; return result; } let mount = if let (Some(press), Some(release)) = (input.press_point, input.point) { if viewport.is_some_and(|size| { [press, release] .iter() .any(|point| point[0] >= size.width || point[1] >= size.height) }) { return Ok(()); } let Some(mount) = crate::source_hit::resolve(vm, desktop, candidates, press, release)? else { return Ok(()); }; mount } else { vm.upgrade(weak)? .ok_or_else(|| anyhow::anyhow!("Mount has expired"))? }; let result = (|| { let data = vm.payload::(mount, "Mount")?.clone(); if !data.mounted { bail!("event targets an obsolete presentation"); } if !data.geometry.interactive { bail!("Mount does not accept input"); } vm.set_attr( desktop, "focus", Value::Number((data.geometry.id as i64).into()), )?; vm.set_attr(desktop, "focusRequested", Value::Bool(true))?; let (owner, name, args) = match input.kind.as_str() { "click" => (data.block, "click", vec![Value::Object(mount)]), "focus" => (mount, "requestFocus", vec![]), _ => bail!("unsupported native event"), }; let bound = vm.bind_attr(owner, name)?; let result = match &bound { Value::Object(function) => vm.call_with_limits( *function, &args, endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ), _ => Err(invalid("event method is not callable")), }; vm.release_value(bound)?; vm.release_value(result?)?; Ok(()) })(); vm.release(mount)?; result } pub fn reclaim(&self, vm: &mut Vm, _roots: &mut Vec) -> Result<()> { vm.collect_cycles(); let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let mut live = Vec::new(); for weak in &state.nodes { if let Some(object) = vm.upgrade(*weak)? { vm.release(object)?; live.push(*weak); } } state.nodes = live; for presentation in state.presentations.values_mut() { presentation.render_cache.prune(vm.object_store()); } Ok(()) } pub fn discard_pending(&self, vm: &mut Vm) -> Result<()> { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; for presentation in state.presentations.values_mut() { let window = object(Some(&vm.get_attr(presentation.desktop, "window")?))?; // A failed first render must not become visible on the next input // merely because show() already registered its Page with the Window. if presentation.active.is_none() && presentation.last_tabs.is_empty() { let unpublished = crate::window::pages(vm.object_store(), window)?.to_vec(); for page in unpublished { crate::window::unregister_page_host(vm, window, page)?; } } let pages = crate::window::pages(vm.object_store(), window)?; let next = if presentation.closing { None } else { presentation .active .filter(|page| pages.contains(page)) .or_else(|| pages.first().copied()) }; // Earlier successful statements in a failed REPL input remain live. // Repair ownership changes before the next publication so a closed or // moved Page cannot survive in an old context's mount dictionary. presentation.pending = if next != presentation.active || presentation.closing { Some(next) } else { None }; vm.set_attr( presentation.desktop, "pending", presentation .pending .flatten() .map(Value::Object) .unwrap_or(Value::Nil), )?; if presentation .active .is_some_and(|page| vm.object(page).is_err()) { presentation.active = None; } } Ok(()) } pub fn refresh_bindings(&self, vm: &mut Vm) -> Result<()> { let (nodes, desktops) = { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; (state.nodes.clone(), state.host_desktops()) }; let evaluated = crate::binding::refresh(vm, &nodes, &desktops)?; let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; state.binding_evaluations = state.binding_evaluations.saturating_add(evaluated); Ok(()) } pub fn prepare_frame(&self, vm: &mut Vm) -> Result>>> { let changes = vm.take_changes(); let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let active = state.pending.unwrap_or(state.active); // One VM change drain feeds every presentation before selecting a root. // Otherwise a window refreshed first could consume another window's work. for presentation in state.presentations.values_mut() { presentation.observe_changes(vm, &changes); } let root = active .map(|v| state.render_cache.snapshot(vm.object_store(), v)) .transpose()?; let same = match (&root, &state.last_snapshot) { (Some(a), Some(b)) => Arc::ptr_eq(a, b), (None, None) => true, _ => false, }; let focus_changed = matches!( vm.get_attr(state.desktop, "focusRequested"), Ok(Value::Bool(true)) ); let tabs = project_tabs(vm, &mut state)?; let changed = state.pending.is_some() || !same || focus_changed || matches!( vm.get_attr(state.desktop, "scrollRequested"), Ok(Value::Bool(true)) ) || state.force_frame || tabs != state.last_tabs; if changed { if !same { state.last_invalid_dependency = format!("snapshot {}", state.render_cache.last_invalidation); } state.snapshot_visited = state.render_cache.visited; state.snapshot_reused = state.render_cache.reused; } Ok(changed.then_some(root)) } pub fn commit_frame( &self, vm: &mut Vm, snapshot: Option>, frame: &crate::document::Frame, ) -> Result<()> { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let page = state.pending.unwrap_or(state.active); let desktop = state.desktop; let nodes = state.nodes.clone(); let hooks = state .mounts .synchronize(vm, desktop, page, nodes, frame, false)?; let result = (|| { if snapshot.is_none() { state.layout.clear_occurrences(); } if let Some(page) = state.pending.take() { state.active = page; } vm.set_attr(state.desktop, "pending", Value::Nil)?; vm.set_attr(state.desktop, "focusRequested", Value::Bool(false))?; vm.set_attr(state.desktop, "scrollRequested", Value::Bool(false))?; state.force_frame = false; state.last_snapshot = snapshot; state.last_tabs = frame.tabs.clone(); state.published_version = frame.layout_version; // Hooks may call Page/container APIs; never hold the session mutex // across user code. Publish only after the refresh loop settles. drop(state); if let Some(hooks) = hooks { let value = vm .call_with_limits( hooks, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error))?; vm.release_value(value)?; } Ok(()) })(); if let Some(hooks) = hooks { vm.release(hooks)?; } result } pub fn begin_paint_frame( &self, vm: &mut Vm, root: Option>, frame: crate::document::Frame, ) -> Result { let (desktop, mounts) = { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let mounts = frame .mounts .iter() .map(|mount| { state .mounts .active .get(&mount.id) .copied() .map(|weak| (mount.id, weak)) .ok_or_else(|| anyhow::anyhow!("paint Mount is unavailable")) }) .collect::>>()?; (state.desktop, mounts) }; let progress = crate::source_paint::start(vm, desktop, mounts, root, frame, 256)?; self.accept_paint_progress(progress) } pub fn resume_paint_frame( &self, vm: &mut Vm, work: crate::source_paint::Work, ) -> Result { let progress = work.resume(vm, 256)?; self.accept_paint_progress(progress) } fn accept_paint_progress( &self, progress: crate::source_paint::Progress, ) -> Result { if let crate::source_paint::Progress::Complete(draft) = &progress { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let desktops = state.host_desktops(); let reads = draft .reads .iter() .filter(|read| !desktops.contains(&read.object())) .cloned() .collect(); let (reads, names) = crate::presentation::split_reads(reads); state.paint_reads = reads; state.paint_names = names; state.paint_slices = draft.slices; } Ok(progress) } pub fn cancel_paint_frame( &self, vm: &mut Vm, window: u64, work: crate::source_paint::Work, ) -> Result<()> { let cancelled = work.cancel(vm); let invalidated = self.select_window(window).and_then(|()| { self.session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .force_frame = true; Ok(()) }); cancelled?; invalidated } pub fn begin_layout_frame( &self, vm: &mut Vm, root: Option>, ) -> Result { anyhow::ensure!( !vm.has_suspended_call() && !vm.object_store().is_read_only(), "cannot prepare another frame during suspended layout" ); let Some(root) = root else { self.session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .measure_cache .clear(); self.session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .layout_reads .clear(); self.session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .layout_names .clear(); let frame = { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; state.layout.version = state .layout .version .checked_add(1) .ok_or_else(|| anyhow::anyhow!("layout version exhausted"))?; crate::document::Frame { window: state.current, layout_version: state.layout.version, tabs: project_tabs(vm, &mut state)?, ..crate::document::Frame::default() } }; self.commit_frame(vm, None, &frame)?; return Ok(LayoutProgress::Complete(Box::new(frame))); }; let (mut layout, cache, desktop, page, nodes) = { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; ( state.layout.clone(), state.measure_cache.clone(), state.desktop, state.pending.unwrap_or(state.active), state.nodes.clone(), ) }; let tabs = { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; project_tabs(vm, &mut state)? }; let full_viewport = layout.viewport; if let Some(size) = &mut layout.viewport { size.width = (size.width - crate::tabs::RAIL - crate::tabs::INSET).max(0.0); size.height = (size.height - crate::tabs::height(&tabs) - crate::tabs::INSET).max(0.0); } { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; layout.reserve_identity_floor(state.next_mount); } let mut frame = layout.structure(&root)?; { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; state.next_mount = state.next_mount.max(layout.identity_high_watermark()); frame.window = state.current; } let (hooks, mounts, checkpoint) = { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; let checkpoint = state.mounts.checkpoint(vm, desktop)?; let hooks = match state .mounts .synchronize(vm, desktop, page, nodes, &frame, true) { Ok(hooks) => hooks, Err(error) => { checkpoint.restore(vm, desktop, &mut state.mounts)?; return Err(error.into()); } }; (hooks, state.mounts.ordered(), checkpoint) }; let context = LayoutContext { root: root.clone(), tabs, full_viewport, desktop, window: frame.window, hooks, checkpoint, }; let draft = crate::source_layout::Draft { frame, runtime: layout, cache, reads: Vec::new(), }; let progress = crate::source_layout::start(vm, desktop, mounts, root, draft, 256); self.accept_layout_progress(vm, context, progress) } pub fn note_refresh_yield(&self) -> Result<()> { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; state.refresh_yields = state.refresh_yields.saturating_add(1); Ok(()) } pub fn note_refresh_restart( &self, cancelled_layout: bool, cancelled_paint: bool, ) -> Result<()> { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; state.refresh_restarts = state.refresh_restarts.saturating_add(1); state.layout_cancels = state .layout_cancels .saturating_add(usize::from(cancelled_layout)); state.paint_cancels = state .paint_cancels .saturating_add(usize::from(cancelled_paint)); Ok(()) } fn discard_layout_context(&self, vm: &mut Vm, context: LayoutContext) -> Result<()> { let LayoutContext { desktop, window, hooks, checkpoint, .. } = context; let selected = self.select_window(window); let restored = if selected.is_ok() { match self.session.lock() { Ok(mut state) => { let result = checkpoint.restore(vm, desktop, &mut state.mounts); state.force_frame = true; result.map_err(anyhow::Error::from) } Err(_) => checkpoint .release(vm) .map_err(anyhow::Error::from) .and(Err(anyhow::anyhow!("UI session poisoned"))), } } else { checkpoint.release(vm).map_err(anyhow::Error::from) }; let released = hooks.map(|hooks| vm.release(hooks)).transpose(); selected?; restored?; released?; Ok(()) } pub fn cancel_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> { let PendingLayout { work, context } = pending; let cancelled = work.cancel(vm); let restored = self.discard_layout_context(vm, context); cancelled?; restored } pub fn abort_layout_frame(&self, vm: &mut Vm, pending: PendingLayout) -> Result<()> { self.cancel_layout_frame(vm, pending) } pub fn resume_layout_frame( &self, vm: &mut Vm, pending: PendingLayout, ) -> Result { let PendingLayout { work, context } = pending; if let Err(error) = self.select_window(context.window) { let cancelled = work.cancel(vm); let restored = self.discard_layout_context(vm, context); cancelled?; restored?; return Err(error); } let progress = work.resume(vm, 256); self.accept_layout_progress(vm, context, progress) } fn accept_layout_progress( &self, vm: &mut Vm, context: LayoutContext, progress: Result, ) -> Result { match progress { Ok(crate::source_layout::Progress::Pending(work)) => { Ok(LayoutProgress::Pending(Box::new(PendingLayout { work, context, }))) } Ok(crate::source_layout::Progress::Complete(draft)) => self .finish_layout_frame(vm, context, draft) .map(|frame| LayoutProgress::Complete(Box::new(frame))), Err(error) => { self.discard_layout_context(vm, context)?; Err(error) } } } fn finish_layout_frame( &self, vm: &mut Vm, context: LayoutContext, draft: crate::source_layout::Draft, ) -> Result { let LayoutContext { root, tabs, full_viewport, desktop, window, hooks, checkpoint, } = context; let crate::source_layout::Draft { mut frame, runtime: mut layout, mut cache, mut reads, } = draft; let result: Result = (|| { self.select_window(window)?; { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; crate::scroll::project(vm, &state.mounts, &root, &mut frame)?; } // Native layout may replace the structural Frame; attach chrome only // after either layout path has produced the final page geometry. frame.tabs = tabs; frame.window = window; // Capture versions before lifecycle hooks can change a dependency. let desktops = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))? .host_desktops(); // Shared intrinsic functions capture the primary desktop even when // invoked from another window. Desktop bookkeeping is private host // state; Window, viewport input and business dependencies remain. reads.retain(|read| !desktops.contains(&read.object())); cache.exclude_host_desktops(&desktops); let (reads, names) = crate::presentation::split_reads(reads); frame.focus = match vm.get_attr(desktop, "focus") { Ok(Value::Number(id)) => id.as_i64().and_then(|n| u64::try_from(n).ok()), _ => None, }; frame.focus = frame .focus .filter(|id| frame.mounts.iter().any(|m| m.id == *id && m.interactive)); { let mut state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; layout.viewport = full_viewport; state.layout = layout; state.measure_cache = cache; state.layout_reads = reads; state.layout_names = names; } self.commit_frame(vm, Some(root), &frame)?; if let Some(hooks) = hooks { let value = vm .call_with_limits( hooks, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ) .map_err(|error| crate::endor_host::runtime_error(vm, error))?; vm.release_value(value)?; } Ok(frame) })(); let released_hooks = hooks.map(|hooks| vm.release(hooks)).transpose(); let released_checkpoint = checkpoint.release(vm); let frame = result?; released_hooks?; released_checkpoint?; Ok(frame) } pub fn rendered(&self, vm: &Vm) -> Result>> { let state = self .session .lock() .map_err(|_| anyhow::anyhow!("UI session poisoned"))?; state .pending .flatten() .map(|v| block::snapshot(vm.object_store(), v)) .transpose() .map_err(Into::into) } } pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { let desktop = vm.new_object()?; roots.push(desktop); vm.set_attr(desktop, "viewport", Value::Nil)?; let tabs = vm.new_dict()?; roots.push(tabs); vm.set_attr(desktop, "tabs", Value::Object(tabs))?; let ui = vm.new_object()?; roots.push(ui); let session = Arc::new(Mutex::new(Session { nodes: Vec::new(), presentations: std::collections::BTreeMap::from([( crate::window::MAIN_WINDOW, crate::presentation::Presentation::new(desktop, tabs), )]), current: crate::window::MAIN_WINDOW, master: desktop, next_window: crate::window::MAIN_WINDOW, next_mount: 0, retired_commands: Vec::new(), refresh_yields: 0, refresh_restarts: 0, layout_cancels: 0, paint_cancels: 0, paint_slices: 0, binding_evaluations: 0, exiting: false, })); let mut public_functions = Vec::new(); let mut factories = Vec::new(); for (name, operation) in [ ("implement", Operation::Implement), ("implements", Operation::Implements), ("supports", Operation::Supports), ("isInstance", Operation::IsInstance), ("isSubtype", Operation::IsSubtype), ("open", Operation::OpenValue), ("text", Operation::Text), ("rect", Operation::Rect), ("spacer", Operation::Spacer), ("row", Operation::Row), ("column", Operation::Column), ("button", Operation::Button), ("page", Operation::Page), ("present", Operation::Present), ("state", Operation::State), ("textOf", Operation::TextOf), ("stats", Operation::Stats), ("viewport", Operation::Viewport), ("window", Operation::Window), ] { let function = vm.allocate(ContextNativeFunction::new( name, Function { signature: None, operation, owner: None, session: session.clone(), requested_type: None, }, vec![desktop], ))?; roots.push(function); vm.set_attr(ui, name, Value::Object(function))?; public_functions.push((name, function, operation)); } let root_types = vm.bootstrap_types()?; roots.extend([root_types.object, root_types.type_type]); vm.set_attr(ui, "Object", Value::Object(root_types.object))?; vm.set_attr(ui, "Type", Value::Object(root_types.type_type))?; let mut definitions = vec![ ("Object", root_types.object), ("Type", root_types.type_type), ]; for (name, base_name, constructor_name) in [ ("Trait", "Object", None), ("Function", "Object", None), ("Block", "Object", None), ("ContainerBlock", "Block", None), ("TextBlock", "Block", Some("text")), ("RectBlock", "Block", Some("rect")), ("Spacer", "Block", Some("spacer")), ("ScrollBlock", "ContainerBlock", Some("scroll")), ("Row", "ContainerBlock", Some("row")), ("Column", "ContainerBlock", Some("column")), ("Button", "ContainerBlock", Some("button")), ("Page", "ContainerBlock", Some("page")), ("Window", "Object", Some("window-create")), ("Mount", "Object", None), ("Event", "Object", Some("event")), ("Subscription", "Object", None), ("Task", "Object", None), ("ObjectList", "Object", Some("object-list")), ] { let base = object(Some(&vm.get_attr(ui, base_name)?))?; let constructor = match constructor_name { Some("object-list") => { let factory = vm.allocate(ContextNativeFunction::new( "object-list", crate::object_list::Constructor { desktop, signature: None, }, vec![desktop], ))?; roots.push(factory); let function_type = object(Some(&vm.get_attr(ui, "Function")?))?; vm.attach_type(factory, function_type)?; TypeConstructor::NativeTyped(factory) } Some("event") => { let factory = vm.allocate(ContextNativeFunction::new( "event", crate::event_dispatch::Constructor { signature: None }, vec![], ))?; roots.push(factory); let function_type = object(Some(&vm.get_attr(ui, "Function")?))?; vm.attach_type(factory, function_type)?; TypeConstructor::NativeTyped(factory) } Some(constructor_name) => { let operation = match constructor_name { "text" => Operation::Text, "rect" => Operation::Rect, "spacer" => Operation::Spacer, "scroll" => Operation::Scroll, "row" => Operation::Row, "column" => Operation::Column, "button" => Operation::Button, "page" => Operation::Page, "window-create" => Operation::NewWindow, _ => return Err(invalid("unknown native type allocator").into()), }; let factory = vm.allocate(ContextNativeFunction::new( constructor_name, NativeFactory { signature: None, options: None, operation, session: session.clone(), }, vec![desktop], ))?; roots.push(factory); let function_type = object(Some(&vm.get_attr(ui, "Function")?))?; vm.attach_type(factory, function_type)?; TypeConstructor::NativeTyped(factory) } None => TypeConstructor::Abstract, }; if let (Some(tag), TypeConstructor::NativeTyped(factory)) = (constructor_name, constructor) { factories.push((tag, factory)); } let definition = vm.new_type(name, base, constructor)?; roots.push(definition); vm.set_attr(ui, name, Value::Object(definition))?; definitions.push((name, definition)); } for (name, fields) in [ ( "Constraints", vec!["minWidth", "minHeight", "maxWidth", "maxHeight"], ), ("MeasureResult", vec!["width", "height"]), ("Bounds", vec!["x", "y", "width", "height"]), ("Point", vec!["x", "y"]), ("Arrangement", vec!["children"]), ("DisplayList", vec!["commands"]), ("PaintCommand", vec![]), ] { let fields = fields .into_iter() .map(|name| endor_core::ClassField { name: name.into(), type_name: if matches!(name, "children" | "commands") { "Object" } else { "Number" } .into(), }) .collect(); let definition = vm.new_class( name, root_types.object, fields, std::collections::BTreeMap::new(), )?; roots.push(definition); vm.set_attr(ui, name, Value::Object(definition))?; definitions.push((name, definition)); } // The desktop owns the registry through ordinary traced attributes. The // Rust session only borrows handles and adds no hidden lifetime roots. let types = vm.new_dict()?; roots.push(types); for (name, definition) in definitions { vm.dict_set(types, Value::Text(name.into()), Value::Object(definition))?; } vm.set_attr(desktop, "types", Value::Object(types))?; vm.attach_type(desktop, root_types.object)?; vm.attach_type(ui, root_types.object)?; let function_type = object(Some(&vm.get_attr(ui, "Function")?))?; vm.set_function_type(function_type)?; vm.set_source_type_checks(true)?; for (name, function, operation) in public_functions { vm.replace_payload( function, ContextNativeFunction::new( name, Function { signature: Some(operation_signature(vm.object_store(), desktop, operation)?), operation, owner: None, session: session.clone(), requested_type: None, }, vec![desktop], ), )?; } for (name, function) in factories { let operation = match name { "text" => Some(Operation::Text), "rect" => Some(Operation::Rect), "spacer" => Some(Operation::Spacer), "scroll" => Some(Operation::Scroll), "row" => Some(Operation::Row), "column" => Some(Operation::Column), "button" => Some(Operation::Button), "page" => Some(Operation::Page), "window-create" => Some(Operation::NewWindow), "event" | "object-list" => None, _ => return Err(invalid("unknown deferred factory").into()), }; if let Some(operation) = operation { let options = operation_signature(vm.object_store(), desktop, operation)?; let envelope = CallSignature { arity: endor_core::CallArity::range(options.arity.required + 1, usize::MAX), parameters: vec![AnnotationType::Type.into()], variadic: Some(AnnotationType::Any.into()), returns: options.returns.clone(), effect: options.effect, }; let mut captures = vec![desktop]; captures.extend(options.references()); vm.replace_payload( function, ContextNativeFunction::new( name, NativeFactory { signature: Some(envelope), options: Some(options), operation, session: session.clone(), }, captures, ), )?; } else { let kind = if name == "event" { "Event" } else { "ObjectList" }; let signature = CallSignature { arity: endor_core::CallArity::range( if name == "event" { 2 } else { 1 }, usize::MAX, ), parameters: if name == "event" { vec![AnnotationType::Type.into(), AnnotationType::String.into()] } else { vec![AnnotationType::Type.into()] }, variadic: Some(AnnotationType::Any.into()), returns: AnnotationType::Nominal(desktop_type(vm.object_store(), kind, desktop)?) .into(), effect: CallEffect::ReadOnly, }; if name == "event" { vm.replace_payload( function, ContextNativeFunction::new( name, crate::event_dispatch::Constructor { signature: Some(signature), }, vec![], ), )?; } else { vm.replace_payload( function, ContextNativeFunction::new( name, crate::object_list::Constructor { desktop, signature: Some(signature), }, vec![desktop], ), )?; } } } for name in [ "open", "text", "rect", "spacer", "row", "column", "button", "page", "present", "state", "textOf", "stats", "viewport", "window", "isInstance", "isSubtype", "implement", "implements", "supports", ] { let function = object(Some(&vm.get_attr(ui, name)?))?; vm.attach_type(function, function_type)?; } for (type_name, name, operation) in [ ("Block", "bind", Operation::Bind), ("Block", "mounts", Operation::Mounts), ("Block", "invalidate", Operation::Invalidate), ("Block", "onClick", Operation::OnClick), ("Block", "on", Operation::On), ("Block", "dispatch", Operation::Dispatch), ("Block", "onEvent", Operation::OnEvent), ("Block", "onMount", Operation::OnMount), ("Block", "onUnmount", Operation::OnUnmount), ("Block", "click", Operation::Click), ("TextBlock", "setText", Operation::SetText), ("TextBlock", "setColor", Operation::SetColor), ("RectBlock", "setColor", Operation::SetColor), ("ContainerBlock", "append", Operation::Append), ("ContainerBlock", "insert", Operation::Insert), ("ContainerBlock", "remove", Operation::Remove), ("ContainerBlock", "replace", Operation::Replace), ("ContainerBlock", "move", Operation::Move), ("ContainerBlock", "children", Operation::Children), ("Window", "close", Operation::CloseWindow), ("Page", "moveToWindow", Operation::MoveWindow), ("Page", "moveTab", Operation::MoveTab), ("Page", "detachToWindow", Operation::Detach), ("Page", "render", Operation::Render), ("Page", "unmount", Operation::Unmount), ("Page", "open", Operation::Open), ("Page", "window", Operation::PageWindow), ("Page", "close", Operation::Close), ("Page", "activate", Operation::Activate), ("Page", "navigate", Operation::Navigate), ("Page", "back", Operation::Back), ("Page", "forward", Operation::Forward), ] { let expected = object(Some(&vm.get_attr(ui, type_name)?))?; let method = vm.allocate(ContextNativeFunction::new( name, TypeMethod { signature: method_signature(vm.object_store(), desktop, expected, operation)?, operation, expected, session: session.clone(), }, vec![expected, desktop], ))?; roots.push(method); vm.attach_type(method, function_type)?; vm.define_method(expected, name, method)?; } crate::window::install(vm, roots, desktop, tabs)?; let block_type = object(Some(&vm.get_attr(ui, "Block")?))?; crate::native_visual::install(vm, roots, desktop, block_type, function_type)?; let event_type = object(Some(&vm.get_attr(ui, "Event")?))?; let token_type = object(Some(&vm.get_attr(ui, "Subscription")?))?; crate::event_dispatch::install(vm, roots, event_type, token_type, function_type)?; crate::protocols::install(vm, roots, desktop, ui)?; let tasks = crate::tasks::install(vm, roots, ui, desktop)?; developer_pages::install(vm, ui, &session)?; let bridge = UiBridge { ui, session, tasks }; developer_pages::install_start_page(vm, &bridge)?; Ok(bridge) } fn attach_named_type( cx: &mut NativeContext<'_>, object: ObjectRef, name: &str, session: &Shared, ) -> Result<(), VmError> { let desktop = session .lock() .map_err(|_| invalid("UI session poisoned"))? .desktop; attach_desktop_type(cx, object, name, desktop) } pub fn attach_desktop_type( cx: &mut NativeContext<'_>, instance: ObjectRef, name: &str, desktop: ObjectRef, ) -> Result<(), VmError> { let definition = desktop_type(cx.store(), name, desktop)?; cx.attach_type(instance, definition) } pub(crate) fn desktop_type( store: &endor_core::ObjectStore, name: &str, desktop: ObjectRef, ) -> Result { let types = object(Some(&store.get_attr(desktop, "types")?))?; object(store.dict_get(types, &Value::Text(name.into()))?.as_ref()) } pub fn snapshot(vm: &Vm, value: &Value) -> Result> { let Value::Object(root) = value else { bail!("main must return a Block"); }; block::snapshot(vm.object_store(), *root).map_err(Into::into) }