//! Managed window resources and versioned, coalesced native projections. //! The VM owns Page roots; only scalar commands cross to the GUI thread. use crate::{native_visual, object_list, ui_bridge::desktop_type}; use endor_core::{ ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef, ObjectStore, Value, Vm, VmError, }; use std::sync::{Arc, Mutex}; pub const MAIN_WINDOW: u64 = 1; pub const MAX_WINDOW_DIMENSION: f64 = 4096.0; #[derive(Debug, Clone, PartialEq)] pub enum Action { Resize { width: f64, height: f64 }, SetTitle(String), MoveTo { x: i32, y: i32 }, Focus, Close, } #[derive(Debug, Clone, PartialEq)] pub struct Command { pub window: u64, pub sequence: u64, pub action: Action, } impl Command { pub fn same_kind(&self, other: &Self) -> bool { self.window == other.window && std::mem::discriminant(&self.action) == std::mem::discriminant(&other.action) } } #[derive(Debug, Clone)] pub struct Envelope { pub session: u64, pub command: Command, } #[derive(Debug, Clone)] pub struct ManagedWindow { pub id: u64, pub desktop: ObjectRef, pub closed: bool, pub tasks: Option, tabs: ObjectRef, pages: Vec, sequence: u64, // Transport state only. Draining it must not change a language object's // version or create a render-dependency feedback loop. outbox: Arc>>, } impl ObjectPayload for ManagedWindow { fn as_any(&self) -> &dyn std::any::Any { self } fn kind_name(&self) -> &'static str { "tessera-window" } fn allows_attributes(&self) -> bool { true } fn reference_assignment_only(&self) -> bool { true } fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) { visit(self.desktop); if let Some(tasks) = self.tasks { visit(tasks); } visit(self.tabs); for page in &self.pages { visit(*page); } } } fn invalid(message: &'static str) -> VmError { VmError::InvalidArgument(message) } fn screen_coordinate(value: &Value) -> Result { let Value::Number(value) = value else { return Err(invalid("screen position requires integer coordinates")); }; value .as_i64() .filter(|number| (-1_000_000..=1_000_000).contains(number)) .and_then(|number| i32::try_from(number).ok()) .ok_or(invalid("screen position exceeds limits")) } fn scalar(value: &Value) -> Result { let Value::Number(value) = value else { return Err(invalid("window size requires numbers")); }; let number = value .to_string() .parse::() .map_err(|_| invalid("invalid window size"))?; if !number.is_finite() || !(1.0..=MAX_WINDOW_DIMENSION).contains(&number) { return Err(invalid("window size exceeds limits")); } Ok(number) } #[derive(Debug, Clone, Copy)] enum Operation { Id, Viewport, Pages, Resize, MoveTo, SetTitle, Focus, } #[derive(Debug)] struct Method { signature: endor_core::CallSignature, operation: Operation, desktop: ObjectRef, } impl NativeCallable for Method { fn signature(&self) -> Option { Some(self.signature.clone()) } fn arity(&self) -> Option { Some(endor_core::CallArity::exact(match self.operation { Operation::Resize | Operation::MoveTo => 3, Operation::SetTitle => 2, _ => 1, })) } fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { let Some(Value::Object(owner)) = args.first() else { return Err(invalid("Window method requires a Window")); }; let data = cx .store() .payload::(*owner, "Window")? .clone(); if data.closed && !matches!(self.operation, Operation::Id | Operation::Pages) { return Err(invalid("native window is closed")); } let expected = match self.operation { Operation::Resize | Operation::MoveTo => 3, Operation::SetTitle => 2, _ => 1, }; if args.len() != expected { return Err(invalid("incorrect Window method argument count")); } let result = match self.operation { Operation::Id => Value::Number((data.id as i64).into()), Operation::Pages => { Value::Object(object_list::collection(cx, data.pages, self.desktop)?) } Operation::Viewport => { let Value::Object(input) = cx.store().get_attr(data.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")?; Value::Object(native_visual::record( cx, self.desktop, "MeasureResult", vec![("width", width), ("height", height)], )?) } operation => { if cx.store().is_read_only() { return Err(invalid( "window operations are forbidden during read-only rendering", )); } let action = match operation { Operation::MoveTo => Action::MoveTo { x: screen_coordinate(&args[1])?, y: screen_coordinate(&args[2])?, }, Operation::Resize => Action::Resize { width: scalar(&args[1])?, height: scalar(&args[2])?, }, Operation::SetTitle => { let Value::Text(title) = &args[1] else { return Err(invalid("window title requires text")); }; if title.len() > 4096 || title.chars().any(char::is_control) { return Err(invalid("invalid window title")); } Action::SetTitle(title.clone()) } Operation::Focus => Action::Focus, _ => unreachable!(), }; enqueue(cx, *owner, action)?; Value::Nil } }; Ok(NativeResult::Return(result)) } } pub fn enqueue( cx: &mut NativeContext<'_>, owner: ObjectRef, action: Action, ) -> Result<(), VmError> { let mut data = cx .store() .payload::(owner, "Window")? .clone(); data.sequence = data .sequence .checked_add(1) .ok_or(invalid("window command sequence exhausted"))?; let command = Command { window: data.id, sequence: data.sequence, action, }; let queue = data.outbox.clone(); let mut queue = queue .lock() .map_err(|_| invalid("window outbox poisoned"))?; // The heap barrier succeeds before any external transport state changes. cx.replace_payload(owner, data)?; queue.retain(|old| !old.same_kind(&command)); queue.push(command); Ok(()) } pub fn register_page( cx: &mut NativeContext<'_>, window: ObjectRef, page: ObjectRef, ) -> Result<(), VmError> { if cx.store().is_read_only() { return Err(invalid( "opening a Page is forbidden during read-only rendering", )); } let mut data = cx .store() .payload::(window, "Window")? .clone(); if data.closed { return Err(invalid("native window is closed")); } if !data.pages.contains(&page) { data.pages.push(page); cx.dict_set( data.tabs, Value::Text(page.to_string()), Value::Object(page), )?; cx.replace_payload(window, data)?; } Ok(()) } pub fn unregister_page( cx: &mut NativeContext<'_>, window: ObjectRef, page: ObjectRef, ) -> Result<(), VmError> { if cx.store().is_read_only() { return Err(invalid( "closing a Page is forbidden during read-only rendering", )); } let mut data = cx .store() .payload::(window, "Window")? .clone(); if !data.pages.contains(&page) { return Ok(()); } data.pages.retain(|value| *value != page); cx.dict_delete(data.tabs, &Value::Text(page.to_string()))?; cx.replace_payload(window, data)?; Ok(()) } /// Discard an unpublished Page registration without invoking guest callbacks. pub fn unregister_page_host( vm: &mut Vm, window: ObjectRef, page: ObjectRef, ) -> Result<(), VmError> { let mut data = vm.payload::(window, "Window")?.clone(); if data.pages.contains(&page) { data.pages.retain(|value| *value != page); vm.dict_delete(data.tabs, &Value::Text(page.to_string()))?; vm.replace_payload(window, data)?; } Ok(()) } pub fn move_page( cx: &mut NativeContext<'_>, window: ObjectRef, page: ObjectRef, position: usize, ) -> Result<(), VmError> { let mut data = cx .store() .payload::(window, "Window")? .clone(); if data.closed { return Err(invalid("native window is closed")); } let from = data .pages .iter() .position(|candidate| *candidate == page) .ok_or(invalid("Page is not open in this window"))?; if position >= data.pages.len() { return Err(invalid("tab position out of range")); } if cx.store().is_read_only() { return Err(invalid( "tab ordering is forbidden during read-only rendering", )); } if from != position { let page = data.pages.remove(from); data.pages.insert(position, page); cx.replace_payload(window, data)?; } Ok(()) } pub fn pages(store: &ObjectStore, window: ObjectRef) -> Result<&[ObjectRef], VmError> { Ok(&store.payload::(window, "Window")?.pages) } pub fn take_commands(vm: &Vm, window: ObjectRef) -> Result, VmError> { let data = vm.payload::(window, "Window")?; let mut queue = data .outbox .lock() .map_err(|_| invalid("window outbox poisoned"))?; let mut commands = std::mem::take(&mut *queue); commands.sort_by_key(|command| command.sequence); Ok(commands) } pub fn install( vm: &mut Vm, roots: &mut Vec, desktop: ObjectRef, tabs: ObjectRef, ) -> Result { let definition = desktop_type(vm.object_store(), "Window", desktop)?; let function_type = desktop_type(vm.object_store(), "Function", desktop)?; for (name, operation) in [ ("id", Operation::Id), ("viewport", Operation::Viewport), ("pages", Operation::Pages), ("resize", Operation::Resize), ("moveTo", Operation::MoveTo), ("setTitle", Operation::SetTitle), ("focus", Operation::Focus), ] { use endor_core::{AnnotationType as T, CallEffect, CallSignature}; let (parameters, returns, effect) = match operation { Operation::Id => (vec![], T::Number, CallEffect::ReadOnly), Operation::Pages => ( vec![], T::Nominal(desktop_type(vm.object_store(), "ObjectList", desktop)?), CallEffect::ReadOnly, ), Operation::Viewport => ( vec![], T::Nominal(desktop_type(vm.object_store(), "MeasureResult", desktop)?), CallEffect::ReadOnly, ), Operation::Resize | Operation::MoveTo => { (vec![T::Number, T::Number], T::Unit, CallEffect::Mutating) } Operation::SetTitle => (vec![T::String], T::Unit, CallEffect::Mutating), Operation::Focus => (vec![], T::Unit, CallEffect::Mutating), }; let mut arguments = vec![T::Nominal(definition)]; arguments.extend(parameters); let function = vm.allocate(ContextNativeFunction::new( name, Method { operation, desktop, signature: CallSignature::exact(arguments, returns, effect), }, vec![desktop], ))?; roots.push(function); vm.attach_type(function, function_type)?; vm.define_method(definition, name, function)?; } let window = vm.allocate(ManagedWindow { id: MAIN_WINDOW, desktop, closed: false, tasks: None, tabs, pages: Vec::new(), sequence: 0, outbox: Arc::new(Mutex::new(Vec::new())), })?; roots.push(window); vm.attach_type(window, definition)?; vm.set_attr(desktop, "window", Value::Object(window))?; let windows = vm.new_dict()?; vm.set_attr(desktop, "windows", Value::Object(windows))?; vm.dict_set( windows, Value::Text(MAIN_WINDOW.to_string()), Value::Object(window), )?; vm.release(windows)?; Ok(window) } /// Create a managed native-window resource. The GUI creates the OS resource only /// after the owning candidate/input publishes its immutable projection. pub fn create( cx: &mut NativeContext<'_>, master: ObjectRef, id: u64, definition: ObjectRef, ) -> Result<(ObjectRef, ObjectRef, ObjectRef), VmError> { if cx.store().is_read_only() { return Err(invalid( "window creation is forbidden during read-only rendering", )); } let desktop = cx.new_object()?; let tabs = cx.new_dict()?; let types = cx.store().get_attr(master, "types")?; cx.set_attr(desktop, "types", types)?; cx.set_attr(desktop, "viewport", Value::Nil)?; cx.set_attr(desktop, "tabs", Value::Object(tabs))?; let window = cx.allocate(ManagedWindow { id, desktop, closed: false, tasks: None, tabs, pages: Vec::new(), sequence: 0, outbox: Arc::new(Mutex::new(Vec::new())), })?; cx.attach_type(window, definition)?; cx.set_attr(desktop, "window", Value::Object(window))?; let Value::Object(windows) = cx.store().get_attr(master, "windows")? else { return Err(invalid("window registry missing")); }; cx.dict_set(windows, Value::Text(id.to_string()), Value::Object(window))?; Ok((window, desktop, tabs)) } pub fn close(cx: &mut NativeContext<'_>, window: ObjectRef) -> Result<(), VmError> { if cx.store().is_read_only() { return Err(invalid( "window close is forbidden during read-only rendering", )); } let mut data = cx .store() .payload::(window, "Window")? .clone(); if data.closed { return Ok(()); } crate::tasks::cancel_owner(cx, window)?; for page in data.pages.clone() { crate::tasks::cancel_owner(cx, page)?; unregister_page(cx, window, page)?; } data = cx .store() .payload::(window, "Window")? .clone(); data.closed = true; cx.replace_payload(window, data)?; enqueue(cx, window, Action::Close) } /// Host teardown for a stopped VM: no binding, dispatch or source execution. pub fn retire(vm: &mut Vm, window: ObjectRef) -> Result<(), VmError> { let mut data = vm.payload::(window, "Window")?.clone(); vm.dict_clear(data.tabs)?; data.pages.clear(); data.closed = true; vm.replace_payload(window, data) }