//! Tessera-owned UI payloads using Aster's existing embedding interface. use std::{ any::Any, collections::HashMap, sync::{Arc, Mutex, OnceLock}, }; use anyhow::{Result, bail}; use endor_core::{EndorReal, ObjectPayload, ObjectRef, Value, Vm, VmError}; use crate::document::parse_color; pub const MAX_UI_NODES: usize = 4096; pub const MAX_UI_DEPTH: usize = 64; pub const MAX_DIMENSION: f64 = 1_000_000.0; #[derive(Debug)] pub struct UiNode { pub kind: UiKind, depth: usize, } #[derive(Debug)] pub enum UiKind { 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), } #[derive(Debug)] struct NodePayload(UiSlot); type UiSlot = Arc>>; impl ObjectPayload for NodePayload { fn as_any(&self) -> &dyn Any { self } fn kind_name(&self) -> &'static str { "tessera-ui-node" } } #[derive(Debug)] struct BuildSession { // Borrowed handles only. Candidate owns every pool root until extraction // and execution have finished. Native child edges contain Arc, // never VM handles, so payload-owned GC edges remain empty and immutable. pool: Vec<(ObjectRef, UiSlot)>, indexes: HashMap, used: usize, } impl BuildSession { fn child(&self, value: &Value) -> Result, VmError> { let Value::Object(reference) = value else { return Err(VmError::InvalidArgument("UI children must be UI nodes")); }; self.indexes .get(reference) .and_then(|index| self.pool[*index].1.get()) .cloned() .ok_or(VmError::InvalidArgument( "UI children must be initialized nodes from this build", )) } fn emit(&mut self, kind: UiKind) -> Result { let depth = match &kind { UiKind::Column { children, .. } | UiKind::Row { children, .. } => { children.iter().map(|node| node.depth).max().unwrap_or(0) + 1 } _ => 1, }; if depth > MAX_UI_DEPTH { return Err(VmError::InvalidArgument("UI tree exceeds 64 levels")); } let Some((reference, slot)) = self.pool.get(self.used) else { return Err(VmError::InvalidArgument( "UI build exceeds the 4096 node budget", )); }; slot.set(Arc::new(UiNode { kind, depth })) .map_err(|_| VmError::InvalidArgument("UI node initialized twice"))?; self.used += 1; Ok(Value::Object(*reference)) } } #[derive(Debug, Clone, Copy)] enum Constructor { Text, Rect, Column, Row, Spacer, } impl Constructor { fn name(self) -> &'static str { match self { Self::Text => "text", Self::Rect => "rect", Self::Column => "column", Self::Row => "row", Self::Spacer => "spacer", } } } #[derive(Debug)] struct UiConstructor { constructor: Constructor, session: Arc>, } impl ObjectPayload for UiConstructor { fn as_any(&self) -> &dyn Any { self } fn kind_name(&self) -> &'static str { "tessera-ui-constructor" } fn call(&self, arguments: &[Value]) -> Result { let constructor = self.constructor; let (required, maximum) = match constructor { Constructor::Text => (1, 3), Constructor::Rect => (2, 4), Constructor::Column | Constructor::Row => (2, MAX_UI_NODES + 2), Constructor::Spacer => (1, 1), }; if !(required..=maximum).contains(&arguments.len()) { return Err(VmError::ArgumentCount { name: format!("ui.{}", constructor.name()), required, maximum, provided: arguments.len(), }); } let mut session = self .session .lock() .map_err(|_| VmError::InvalidArgument("UI build session is poisoned"))?; let kind = match constructor { Constructor::Text => { let Value::Text(value) = &arguments[0] else { return Err(VmError::InvalidArgument("ui.text expects a string")); }; if value.len() > 16_384 { return Err(VmError::InvalidArgument("UI text exceeds 16 KiB")); } let size = if let Some(value) = arguments.get(1) { let Value::Number(number) = value else { return Err(VmError::InvalidArgument( "text size must be an integer from 1 to 8", )); }; number .as_i64() .filter(|size| (1..=8).contains(size)) .ok_or(VmError::InvalidArgument( "text size must be an integer from 1 to 8", ))? as u32 } else { 1 }; UiKind::Text { value: value.clone(), size, color: color(arguments.get(2), "#242422")?, } } Constructor::Rect => UiKind::Rect { width: dimension(&arguments[0])?, height: dimension(&arguments[1])?, radius: arguments.get(2).map(dimension).transpose()?.unwrap_or(0.0), color: color(arguments.get(3), "#DCE8FF")?, }, Constructor::Column | Constructor::Row => { let padding = dimension(&arguments[0])?; let gap = dimension(&arguments[1])?; let children = arguments[2..] .iter() .map(|value| session.child(value)) .collect::, _>>()?; if matches!(constructor, Constructor::Column) { UiKind::Column { padding, gap, children, } } else { UiKind::Row { padding, gap, children, } } } Constructor::Spacer => UiKind::Spacer(dimension(&arguments[0])?), }; session.emit(kind) } } fn color(value: Option<&Value>, default: &str) -> Result { let value = match value { Some(Value::Text(value)) => value.as_str(), None => default, _ => return Err(VmError::InvalidArgument("UI color must be a string")), }; parse_color(value) .map_err(|_| VmError::InvalidArgument("UI color must use #RRGGBB or #RRGGBBAA"))?; Ok(value.to_owned()) } fn dimension(value: &Value) -> Result { let invalid = || VmError::InvalidArgument("UI dimensions must be finite numbers between 0 and 1000000"); let Value::Number(number) = value else { return Err(invalid()); }; // Conversion belongs to the UI boundary. Aster's exact Number is unchanged. let value = match number.as_real() { Some(EndorReal::Integer(value)) => { value.to_string().parse::().map_err(|_| invalid())? } Some(EndorReal::Decimal(value)) => value.literal().parse::().map_err(|_| invalid())?, Some(EndorReal::BinaryFloat(bits)) => f64::from_bits(*bits), _ => return Err(invalid()), }; if !value.is_finite() || !(0.0..=MAX_DIMENSION).contains(&value) { return Err(invalid()); } Ok(value as f32) } /// All allocation roots go to the candidate, including partial installation. pub fn install(vm: &mut Vm, roots: &mut Vec) -> Result { let mut pool = Vec::with_capacity(MAX_UI_NODES); let mut indexes = HashMap::with_capacity(MAX_UI_NODES); for index in 0..MAX_UI_NODES { let slot = Arc::new(OnceLock::new()); let reference = vm.allocate(NodePayload(slot.clone()))?; roots.push(reference); indexes.insert(reference, index); pool.push((reference, slot)); } let session = Arc::new(Mutex::new(BuildSession { pool, indexes, used: 0, })); let ui = vm.new_object()?; roots.push(ui); for constructor in [ Constructor::Text, Constructor::Rect, Constructor::Column, Constructor::Row, Constructor::Spacer, ] { let function = vm.allocate(UiConstructor { constructor, session: session.clone(), })?; roots.push(function); vm.set_attr(ui, constructor.name(), Value::Object(function))?; } Ok(ui) } pub fn snapshot(vm: &Vm, result: &Value) -> Result> { let Value::Object(reference) = result else { bail!("main must return a UI node"); }; Ok(vm .payload::(*reference, "Tessera UI node")? .0 .get() .ok_or_else(|| anyhow::anyhow!("main returned an uninitialized UI node"))? .clone()) }