1370 lines
47 KiB
Rust
1370 lines
47 KiB
Rust
//! Tessera's native visual protocols over Aster's managed object graph.
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use anyhow::{Result, bail};
|
|
use endor_core::{
|
|
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",
|
|
"Block",
|
|
"ContainerBlock",
|
|
"TextBlock",
|
|
"RectBlock",
|
|
"Spacer",
|
|
"Row",
|
|
"Column",
|
|
"Button",
|
|
"Page",
|
|
"Mount",
|
|
"Event",
|
|
"Subscription",
|
|
];
|
|
pub const QUERY_NAMES: &[&str] = &["isInstance", "isSubtype"];
|
|
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct UiNode {
|
|
pub block_id: 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, .. } => children,
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
pub fn shallow_eq(&self, other: &Self) -> bool {
|
|
if self.block_id != other.block_id
|
|
|| 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::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),
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Session {
|
|
desktop: ObjectRef,
|
|
tabs: ObjectRef,
|
|
active: Option<ObjectRef>,
|
|
pending: Option<Option<ObjectRef>>,
|
|
nodes: Vec<WeakObjectRef>,
|
|
last_snapshot: Option<Arc<UiNode>>,
|
|
render_cache: crate::snapshot::RenderCache,
|
|
layout: crate::layout::LayoutRuntime,
|
|
mounts: crate::mount::MountTable,
|
|
published_version: u64,
|
|
snapshot_visited: usize,
|
|
snapshot_reused: usize,
|
|
}
|
|
type Shared = Arc<Mutex<Session>>;
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum Operation {
|
|
IsInstance,
|
|
IsSubtype,
|
|
OpenValue,
|
|
Mounts,
|
|
Stats,
|
|
State,
|
|
StateSet,
|
|
Increment,
|
|
TextOf,
|
|
Text,
|
|
Rect,
|
|
Spacer,
|
|
Row,
|
|
Column,
|
|
Button,
|
|
Page,
|
|
SetText,
|
|
SetColor,
|
|
Append,
|
|
Remove,
|
|
Replace,
|
|
Move,
|
|
Children,
|
|
Render,
|
|
Unmount,
|
|
Open,
|
|
Close,
|
|
Activate,
|
|
Navigate,
|
|
Back,
|
|
Forward,
|
|
OnClick,
|
|
On,
|
|
Dispatch,
|
|
OnEvent,
|
|
Click,
|
|
Present,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Function {
|
|
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 {
|
|
operation: Operation,
|
|
expected: ObjectRef,
|
|
session: Shared,
|
|
}
|
|
|
|
impl NativeCallable for TypeMethod {
|
|
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 {
|
|
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 {
|
|
operation: Operation,
|
|
session: Shared,
|
|
}
|
|
|
|
impl NativeCallable for NativeFactory {
|
|
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"))?;
|
|
let result = Function {
|
|
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 {
|
|
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::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 show(cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared) -> Result<(), VmError> {
|
|
let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?;
|
|
cx.dict_set(
|
|
state.tabs,
|
|
Value::Text(page.to_string()),
|
|
Value::Object(page),
|
|
)?;
|
|
cx.set_attr(state.desktop, "pending", Value::Object(page))?;
|
|
state.pending = Some(Some(page));
|
|
Ok(())
|
|
}
|
|
|
|
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 invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
|
use Operation::*;
|
|
let (minimum, maximum) = match self.operation {
|
|
IsInstance | IsSubtype => (2, 2),
|
|
Stats | Mounts => (0, 0),
|
|
State | StateSet => (1, 1),
|
|
Increment => (0, 0),
|
|
TextOf => (2, 4),
|
|
Text => (1, 3),
|
|
Rect => (2, 4),
|
|
OpenValue | Spacer | SetText | SetColor | Append | Remove | Render | Navigate
|
|
| OnClick | Present => (1, 1),
|
|
Row | Column => (2, MAX_UI_NODES + 2),
|
|
Button => (1, 1),
|
|
Replace | Move => (2, 2),
|
|
Page | Unmount | Open | Close | Activate | Back | Forward | Children => (0, 0),
|
|
Click => (0, 1),
|
|
On => (2, 3),
|
|
Dispatch => (1, 2),
|
|
OnEvent => (1, 1),
|
|
};
|
|
if !(minimum..=maximum).contains(&args.len()) {
|
|
return Err(invalid("incorrect UI method argument count"));
|
|
}
|
|
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, 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 (id, weak) in &session.mounts.active {
|
|
if let Some(object) = cx.upgrade(*weak)?
|
|
&& cx
|
|
.store()
|
|
.payload::<crate::mount::Mount>(object, "Mount")?
|
|
.block
|
|
== owner
|
|
{
|
|
positions.push((*id, object));
|
|
}
|
|
}
|
|
positions.sort_unstable_by_key(|(id, _)| *id);
|
|
let set = crate::mount::collection(
|
|
cx,
|
|
positions.into_iter().map(|(_, object)| object).collect(),
|
|
)?;
|
|
return Ok(NativeResult::Return(Value::Object(set)));
|
|
}
|
|
if matches!(self.operation, Stats) {
|
|
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),
|
|
("arranged", session.layout.arranged as i64),
|
|
] {
|
|
cx.set_attr(stats, name, Value::Number(value.into()))?;
|
|
}
|
|
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 {
|
|
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) {
|
|
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"))?;
|
|
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;
|
|
}
|
|
_ => 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")?
|
|
}
|
|
_ => return Err(invalid("this Block has no color")),
|
|
},
|
|
Append | Remove | Replace | Move => {
|
|
if !data.is_container() {
|
|
return Err(invalid("expected a container Block"));
|
|
}
|
|
if matches!(self.operation, Append) {
|
|
let child = object(args.first())?;
|
|
block::validate(cx.store(), child, Some(data_ref))?;
|
|
let slot = data.next_slot;
|
|
data.next_slot = slot.checked_add(1).ok_or(VmError::IdentityExhausted)?;
|
|
data.children.push(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 = integer(args.get(1))? as usize;
|
|
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)?;
|
|
}
|
|
Open | Activate => {
|
|
show(cx, owner, &self.session)?;
|
|
return Ok(NativeResult::Return(Value::Object(owner)));
|
|
}
|
|
Unmount => {
|
|
data.children.clear();
|
|
}
|
|
Close => {
|
|
let mut session = self
|
|
.session
|
|
.lock()
|
|
.map_err(|_| invalid("UI session poisoned"))?;
|
|
cx.dict_delete(session.tabs, &Value::Text(owner.to_string()))?;
|
|
if session.active == Some(owner) || session.pending == Some(Some(owner)) {
|
|
let next =
|
|
cx.store()
|
|
.dict_items(session.tabs)?
|
|
.first()
|
|
.and_then(|(_, value)| {
|
|
if let Value::Object(page) = value {
|
|
Some(*page)
|
|
} else {
|
|
None
|
|
}
|
|
});
|
|
cx.set_attr(
|
|
session.desktop,
|
|
"pending",
|
|
next.map(Value::Object).unwrap_or(Value::Nil),
|
|
)?;
|
|
session.pending = Some(next);
|
|
}
|
|
return Ok(NativeResult::Return(Value::Nil));
|
|
}
|
|
OnClick => {
|
|
let handler = object(args.first())?;
|
|
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))?;
|
|
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 => 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))
|
|
}
|
|
}
|
|
|
|
pub struct UiBridge {
|
|
pub ui: ObjectRef,
|
|
session: Shared,
|
|
}
|
|
|
|
impl UiBridge {
|
|
pub fn install_globals(&self, vm: &mut Vm, globals: ObjectRef) -> Result<()> {
|
|
vm.dict_set(globals, Value::Text("ui".into()), Value::Object(self.ui))?;
|
|
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"))?
|
|
.pending
|
|
.flatten()
|
|
.is_some())
|
|
}
|
|
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(())
|
|
}
|
|
pub fn dispatch(&self, vm: &mut Vm, input: &crate::events::Input) -> Result<()> {
|
|
let (weak, desktop) = {
|
|
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,
|
|
)
|
|
};
|
|
let mount = 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((input.mount 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;
|
|
state.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"))?;
|
|
state.pending = None;
|
|
vm.set_attr(state.desktop, "pending", Value::Nil)?;
|
|
if state.active.is_some_and(|v| vm.object(v).is_err()) {
|
|
state.active = None;
|
|
}
|
|
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);
|
|
state.render_cache.invalidate(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 changed = state.pending.is_some() || !same || focus_changed;
|
|
if changed {
|
|
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();
|
|
state.mounts.synchronize(vm, desktop, page, nodes, frame)?;
|
|
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))?;
|
|
state.last_snapshot = snapshot;
|
|
state.published_version = frame.layout_version;
|
|
Ok(())
|
|
}
|
|
pub fn layout_frame(&self, vm: &Vm, root: &UiNode) -> Result<crate::document::Frame> {
|
|
let mut state = self
|
|
.session
|
|
.lock()
|
|
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
|
let mut frame = state.layout.frame(root)?;
|
|
frame.focus = match vm.get_attr(state.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));
|
|
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);
|
|
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 {
|
|
desktop,
|
|
tabs,
|
|
active: None,
|
|
pending: None,
|
|
nodes: vec![],
|
|
last_snapshot: None,
|
|
render_cache: crate::snapshot::RenderCache::default(),
|
|
layout: crate::layout::LayoutRuntime::default(),
|
|
mounts: crate::mount::MountTable::default(),
|
|
published_version: 0,
|
|
snapshot_visited: 0,
|
|
snapshot_reused: 0,
|
|
}));
|
|
for (name, operation) in [
|
|
("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),
|
|
] {
|
|
let function = vm.allocate(ContextNativeFunction::new(
|
|
name,
|
|
Function {
|
|
operation,
|
|
owner: None,
|
|
session: session.clone(),
|
|
requested_type: None,
|
|
},
|
|
vec![desktop],
|
|
))?;
|
|
roots.push(function);
|
|
vm.set_attr(ui, name, Value::Object(function))?;
|
|
}
|
|
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 [
|
|
("Function", "Object", None),
|
|
("Block", "Object", None),
|
|
("ContainerBlock", "Block", None),
|
|
("TextBlock", "Block", Some("text")),
|
|
("RectBlock", "Block", Some("rect")),
|
|
("Spacer", "Block", Some("spacer")),
|
|
("Row", "ContainerBlock", Some("row")),
|
|
("Column", "ContainerBlock", Some("column")),
|
|
("Button", "ContainerBlock", Some("button")),
|
|
("Page", "ContainerBlock", Some("page")),
|
|
("Mount", "Object", None),
|
|
("Event", "Object", Some("event")),
|
|
("Subscription", "Object", None),
|
|
] {
|
|
let base = object(Some(&vm.get_attr(ui, base_name)?))?;
|
|
let constructor = match constructor_name {
|
|
Some("event") => {
|
|
let factory = vm.allocate(ContextNativeFunction::new(
|
|
"event",
|
|
crate::event_dispatch::Constructor,
|
|
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,
|
|
"row" => Operation::Row,
|
|
"column" => Operation::Column,
|
|
"button" => Operation::Button,
|
|
"page" => Operation::Page,
|
|
_ => return Err(invalid("unknown native type allocator").into()),
|
|
};
|
|
let factory = vm.allocate(ContextNativeFunction::new(
|
|
constructor_name,
|
|
NativeFactory {
|
|
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,
|
|
};
|
|
let definition = vm.new_type(name, base, constructor)?;
|
|
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")?))?;
|
|
for name in [
|
|
"open",
|
|
"text",
|
|
"rect",
|
|
"spacer",
|
|
"row",
|
|
"column",
|
|
"button",
|
|
"page",
|
|
"present",
|
|
"state",
|
|
"textOf",
|
|
"stats",
|
|
"isInstance",
|
|
"isSubtype",
|
|
] {
|
|
let function = object(Some(&vm.get_attr(ui, name)?))?;
|
|
vm.attach_type(function, function_type)?;
|
|
}
|
|
for (type_name, name, operation) in [
|
|
("Block", "mounts", Operation::Mounts),
|
|
("Block", "onClick", Operation::OnClick),
|
|
("Block", "on", Operation::On),
|
|
("Block", "dispatch", Operation::Dispatch),
|
|
("Block", "onEvent", Operation::OnEvent),
|
|
("Block", "click", Operation::Click),
|
|
("TextBlock", "setText", Operation::SetText),
|
|
("TextBlock", "setColor", Operation::SetColor),
|
|
("RectBlock", "setColor", Operation::SetColor),
|
|
("ContainerBlock", "append", Operation::Append),
|
|
("ContainerBlock", "remove", Operation::Remove),
|
|
("ContainerBlock", "replace", Operation::Replace),
|
|
("ContainerBlock", "move", Operation::Move),
|
|
("ContainerBlock", "children", Operation::Children),
|
|
("Page", "render", Operation::Render),
|
|
("Page", "unmount", Operation::Unmount),
|
|
("Page", "open", Operation::Open),
|
|
("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 {
|
|
operation,
|
|
expected,
|
|
session: session.clone(),
|
|
},
|
|
vec![expected, desktop],
|
|
))?;
|
|
roots.push(method);
|
|
vm.attach_type(method, function_type)?;
|
|
vm.define_method(expected, name, method)?;
|
|
}
|
|
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)?;
|
|
Ok(UiBridge { ui, session })
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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)
|
|
}
|