Files
endorphin-tessera-gui/src/ui_bridge.rs
T
codex-bot 8cb2bf8607
Verify Tessera GUI / verify (push) Successful in 7m38s
feat: execute Tessera UI source with embedded Aster
2026-10-20 03:35:46 +08:00

304 lines
9.3 KiB
Rust

//! 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<Arc<UiNode>>,
},
Row {
padding: f32,
gap: f32,
children: Vec<Arc<UiNode>>,
},
Spacer(f32),
}
#[derive(Debug)]
struct NodePayload(UiSlot);
type UiSlot = Arc<OnceLock<Arc<UiNode>>>;
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<UiNode>,
// never VM handles, so payload-owned GC edges remain empty and immutable.
pool: Vec<(ObjectRef, UiSlot)>,
indexes: HashMap<ObjectRef, usize>,
used: usize,
}
impl BuildSession {
fn child(&self, value: &Value) -> Result<Arc<UiNode>, 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<Value, VmError> {
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<Mutex<BuildSession>>,
}
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<Value, VmError> {
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::<Result<Vec<_>, _>>()?;
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<String, VmError> {
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<f32, VmError> {
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::<f64>().map_err(|_| invalid())?
}
Some(EndorReal::Decimal(value)) => value.literal().parse::<f64>().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<ObjectRef>) -> Result<ObjectRef> {
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<Arc<UiNode>> {
let Value::Object(reference) = result else {
bail!("main must return a UI node");
};
Ok(vm
.payload::<NodePayload>(*reference, "Tessera UI node")?
.0
.get()
.ok_or_else(|| anyhow::anyhow!("main returned an uninitialized UI node"))?
.clone())
}