Files
endorphin-tessera-gui/src/ui_bridge.rs
T

3453 lines
124 KiB
Rust

//! 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<u64>,
pub interactive: bool,
pub kind: UiKind,
}
impl UiNode {
pub fn children(&self) -> &[Arc<UiNode>] {
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<Arc<UiNode>>,
},
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),
Scroll {
axis: crate::scroll::Axis,
width: f32,
height: f32,
padding: f32,
gap: f32,
children: Vec<Arc<UiNode>>,
},
}
#[derive(Debug)]
struct Session {
// Weak visual-object registry belongs to the VM, not a particular window.
nodes: Vec<WeakObjectRef>,
presentations: std::collections::BTreeMap<u64, crate::presentation::Presentation>,
current: u64,
master: ObjectRef,
next_window: u64,
next_mount: u64,
retired_commands: Vec<crate::window::Command>,
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<ObjectRef> {
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<Mutex<Session>>;
#[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<CallSignature, VmError> {
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<CallSignature, VmError> {
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<CallSignature>,
operation: Operation,
owner: Option<ObjectRef>,
session: Shared,
requested_type: Option<ObjectRef>,
}
/// 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<bool, VmError> {
let Some(function) = store
.object(function)?
.payload()
.as_any()
.downcast_ref::<ContextNativeFunction>()
else {
return Ok(false);
};
Ok(function
.implementation::<TypeMethod>()
.is_some_and(|method| matches!(method.operation, Operation::OnEvent)))
}
impl NativeCallable for TypeMethod {
fn signature(&self) -> Option<CallSignature> {
Some(self.signature.clone())
}
fn arity(&self) -> Option<endor_core::CallArity> {
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<NativeResult, VmError> {
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<CallSignature>,
options: Option<CallSignature>,
operation: Operation,
session: Shared,
}
impl NativeCallable for NativeFactory {
fn signature(&self) -> Option<CallSignature> {
self.signature.clone()
}
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::range(
self.operation.arity().required + 1,
usize::MAX,
))
}
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
let requested = object(args.first())?;
let fields = cx
.store()
.payload::<EndorTypeObject>(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<ObjectRef, VmError> {
match value {
Some(Value::Object(value)) => Ok(*value),
_ => Err(invalid("expected an object")),
}
}
fn text(value: Option<&Value>) -> Result<String, VmError> {
match value {
Some(Value::Text(value)) => Ok(value.clone()),
_ => Err(invalid("expected text")),
}
}
fn integer(value: Option<&Value>) -> Result<u64, VmError> {
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<String, VmError> {
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<f32, VmError> {
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::<f64>().ok(),
Some(EndorReal::Decimal(v)) => v.literal().parse::<f64>().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<ObjectRef, VmError> {
new_typed_block(cx, data, session, None)
}
fn new_typed_block(
cx: &mut NativeContext<'_>,
data: Data,
session: &Shared,
requested: Option<ObjectRef>,
) -> Result<ObjectRef, VmError> {
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::<block::Block>(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<ObjectRef, VmError> {
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<Option<u64>, 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<ObjectRef>,
) -> 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::<crate::window::ManagedWindow>(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<endor_core::CallArity> {
Some(endor_core::CallArity::exact(1))
}
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
let [Value::Object(root)] = args else {
return Err(invalid("Presentable.present must return a Block"));
};
let is_page = cx.store().payload::<block::Block>(*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<ObjectRef, VmError> {
if let Value::Object(block) = value
&& cx
.store()
.object(*block)?
.payload()
.as_any()
.is::<block::Block>()
{
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<CallSignature> {
self.signature.clone()
}
fn arity(&self) -> Option<endor_core::CallArity> {
Some(self.operation.arity())
}
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
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::<endor_core::EndorTraitObject>(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::<crate::mount::Mount>(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::<crate::window::ManagedWindow>(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::<block::Block>(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::<Vec<_>>()
.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::<Result<_, _>>()?,
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::<block::Block>()
&& 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", "<tessera>", 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::<block::Block>(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::<crate::mount::Mount>(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::<crate::event_dispatch::Connection>(*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<PendingLayout>),
Complete(Box<crate::document::Frame>),
}
struct LayoutContext {
root: Arc<UiNode>,
tabs: Vec<crate::tabs::Tab>,
full_viewport: Option<crate::layout::Size>,
desktop: ObjectRef,
window: u64,
hooks: Option<ObjectRef>,
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<Value> {
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<Vec<crate::tabs::Tab>> {
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<usize> {
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<usize> {
self.tasks.poll(vm, limit)
}
pub fn cancel_tasks(&self, vm: &mut Vm) -> Result<usize> {
self.tasks.cancel_all(vm)
}
pub fn window_ids(&self) -> Result<Vec<u64>> {
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<Vec<crate::window::Command>> {
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<crate::layout::Size>) -> Result<bool> {
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<bool> {
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::<crate::window::ManagedWindow>(*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::<crate::mount::Mount>(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::<crate::mount::Mount>(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<ObjectRef>) -> 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<Option<Option<Arc<UiNode>>>> {
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<Arc<UiNode>>,
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<Arc<UiNode>>,
frame: crate::document::Frame,
) -> Result<crate::source_paint::Progress> {
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::<Result<Vec<_>>>()?;
(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<crate::source_paint::Progress> {
let progress = work.resume(vm, 256)?;
self.accept_paint_progress(progress)
}
fn accept_paint_progress(
&self,
progress: crate::source_paint::Progress,
) -> Result<crate::source_paint::Progress> {
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<Arc<UiNode>>,
) -> Result<LayoutProgress> {
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<LayoutProgress> {
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<crate::source_layout::Progress>,
) -> Result<LayoutProgress> {
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<crate::document::Frame> {
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<crate::document::Frame> = (|| {
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<Option<Arc<UiNode>>> {
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<ObjectRef>) -> Result<UiBridge> {
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<ObjectRef, VmError> {
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<Arc<UiNode>> {
let Value::Object(root) = value else {
bail!("main must return a Block");
};
block::snapshot(vm.object_store(), *root).map_err(Into::into)
}