463 lines
17 KiB
Rust
463 lines
17 KiB
Rust
//! Native visual protocol entry points. Values are ordinary managed Endor
|
|
//! objects; geometry is scoped to a live occurrence, never stored on a Block.
|
|
use crate::{
|
|
block,
|
|
document::RenderCommand,
|
|
layout::{self, Size},
|
|
mount::Mount,
|
|
ui_bridge::{MAX_DIMENSION, attach_desktop_type},
|
|
};
|
|
use endor_core::{
|
|
AnnotationType, CallEffect, CallSignature, ContextNativeFunction, NativeCallable,
|
|
NativeContext, NativeResult, ObjectRef, ObjectStore, Value, Vm, VmError,
|
|
};
|
|
|
|
fn invalid(message: &'static str) -> VmError {
|
|
VmError::InvalidArgument(message)
|
|
}
|
|
fn object(value: &Value) -> Result<ObjectRef, VmError> {
|
|
if let Value::Object(object) = value {
|
|
Ok(*object)
|
|
} else {
|
|
Err(invalid("visual protocol requires objects"))
|
|
}
|
|
}
|
|
pub(crate) fn number(
|
|
cx: &NativeContext<'_>,
|
|
object: ObjectRef,
|
|
name: &str,
|
|
signed: bool,
|
|
) -> Result<f32, VmError> {
|
|
let Value::Number(value) = cx.store().get_attr(object, name)? else {
|
|
return Err(invalid("visual field must be a Number"));
|
|
};
|
|
let value = value
|
|
.to_string()
|
|
.parse::<f32>()
|
|
.map_err(|_| invalid("invalid visual number"))?;
|
|
if !value.is_finite() || value.abs() > MAX_DIMENSION as f32 || (!signed && value < 0.0) {
|
|
return Err(invalid("visual number exceeds bounds"));
|
|
}
|
|
Ok(value)
|
|
}
|
|
pub(crate) fn value(value: f32) -> Value {
|
|
Value::Number((value as f64).into())
|
|
}
|
|
pub(crate) fn record(
|
|
cx: &mut NativeContext<'_>,
|
|
desktop: ObjectRef,
|
|
kind: &str,
|
|
fields: Vec<(&str, Value)>,
|
|
) -> Result<ObjectRef, VmError> {
|
|
let object = cx.new_object()?;
|
|
attach_desktop_type(cx, object, kind, desktop)?;
|
|
for (key, value) in fields {
|
|
cx.set_attr(object, key, value)?;
|
|
}
|
|
Ok(object)
|
|
}
|
|
fn size(node: &crate::ui_bridge::UiNode, constraints: [f32; 4]) -> Result<Size, VmError> {
|
|
let frame = layout::frame_with_viewport(
|
|
node,
|
|
Size {
|
|
width: constraints[2],
|
|
height: constraints[3],
|
|
},
|
|
)
|
|
.map_err(|_| invalid("native measurement exceeds layout limits"))?;
|
|
let root = frame
|
|
.mounts
|
|
.first()
|
|
.ok_or(invalid("empty native measurement"))?;
|
|
Ok(Size {
|
|
width: root.width,
|
|
height: root.height,
|
|
})
|
|
}
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum Query {
|
|
Measure,
|
|
Arrange,
|
|
Paint,
|
|
HitTest,
|
|
}
|
|
#[derive(Debug)]
|
|
struct Method {
|
|
query: Query,
|
|
desktop: ObjectRef,
|
|
signature: CallSignature,
|
|
}
|
|
/// Only this actual implementation can use the matching intrinsic fast path.
|
|
pub(crate) fn is_intrinsic(
|
|
store: &ObjectStore,
|
|
function: ObjectRef,
|
|
name: &str,
|
|
desktop: ObjectRef,
|
|
) -> Result<bool, VmError> {
|
|
let Some(function) = store
|
|
.object(function)?
|
|
.payload()
|
|
.as_any()
|
|
.downcast_ref::<ContextNativeFunction>()
|
|
else {
|
|
return Ok(false);
|
|
};
|
|
let Some(method) = function.implementation::<Method>() else {
|
|
return Ok(false);
|
|
};
|
|
// Per-window desktops share the same nominal registry and intrinsic bodies.
|
|
let same_registry = method.desktop == desktop
|
|
|| matches!(
|
|
(store.get_attr(method.desktop, "types")?, store.get_attr(desktop, "types")?),
|
|
(Value::Object(left), Value::Object(right)) if left == right
|
|
);
|
|
Ok(same_registry
|
|
&& matches!(
|
|
(name, method.query),
|
|
("measure", Query::Measure)
|
|
| ("arrange", Query::Arrange)
|
|
| ("paint", Query::Paint)
|
|
| ("hitTest", Query::HitTest)
|
|
))
|
|
}
|
|
|
|
impl NativeCallable for Method {
|
|
fn signature(&self) -> Option<CallSignature> {
|
|
Some(self.signature.clone())
|
|
}
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(3))
|
|
}
|
|
|
|
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
|
let [owner, context, input] = args else {
|
|
return Err(invalid("visual method expects a Mount and an input value"));
|
|
};
|
|
let owner = object(owner)?;
|
|
let mount = cx.store().payload::<Mount>(object(context)?, "Mount")?;
|
|
if !mount.mounted || mount.block != owner {
|
|
return Err(invalid(
|
|
"visual context is detached or belongs to another Block",
|
|
));
|
|
}
|
|
let geometry = mount.geometry.clone();
|
|
let staged = mount
|
|
.layout
|
|
.as_ref()
|
|
.map(|layout| {
|
|
let layout = layout
|
|
.lock()
|
|
.map_err(|_| invalid("layout context poisoned"))?;
|
|
Ok::<_, VmError>((layout.node.clone(), layout.children.clone()))
|
|
})
|
|
.transpose()?;
|
|
let input = object(input)?;
|
|
if matches!(self.query, Query::HitTest) {
|
|
if staged.is_some() {
|
|
return Err(invalid("hitTest is unavailable during layout"));
|
|
}
|
|
let x = number(cx, input, "x", true)?;
|
|
let y = number(cx, input, "y", true)?;
|
|
return Ok(NativeResult::Return(Value::Bool(
|
|
x >= 0.0 && y >= 0.0 && x < geometry.width && y < geometry.height,
|
|
)));
|
|
}
|
|
if matches!(&staged, Some((None, _))) {
|
|
return Err(invalid(
|
|
"layout context is not prepared for this occurrence",
|
|
));
|
|
}
|
|
let node = if let Some((Some(node), _)) = &staged {
|
|
node.clone()
|
|
} else {
|
|
block::snapshot(cx.store(), owner)?
|
|
};
|
|
let result = match self.query {
|
|
Query::Measure => {
|
|
let min_w = number(cx, input, "minWidth", false)?;
|
|
let min_h = number(cx, input, "minHeight", false)?;
|
|
let max_w = number(cx, input, "maxWidth", false)?;
|
|
let max_h = number(cx, input, "maxHeight", false)?;
|
|
if min_w > max_w || min_h > max_h {
|
|
return Err(invalid("minimum exceeds maximum constraint"));
|
|
}
|
|
let size = if let Some((_, children)) = &staged {
|
|
layout::measure_native(&node, children.clone(), [min_w, min_h, max_w, max_h])
|
|
.map_err(|_| invalid("native measurement exceeds layout limits"))?
|
|
} else {
|
|
size(&node, [min_w, min_h, max_w, max_h])?
|
|
};
|
|
Value::Object(record(
|
|
cx,
|
|
self.desktop,
|
|
"MeasureResult",
|
|
vec![
|
|
("width", value(size.width.clamp(min_w, max_w))),
|
|
("height", value(size.height.clamp(min_h, max_h))),
|
|
],
|
|
)?)
|
|
}
|
|
Query::Arrange => {
|
|
let x = number(cx, input, "x", false)?;
|
|
let y = number(cx, input, "y", false)?;
|
|
let width = number(cx, input, "width", false)?;
|
|
let height = number(cx, input, "height", false)?;
|
|
if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 {
|
|
return Err(invalid("visual bounds exceed limits"));
|
|
}
|
|
let children = if let Some((_, children)) = &staged {
|
|
children.clone()
|
|
} else {
|
|
node.children()
|
|
.iter()
|
|
.map(|child| {
|
|
size(
|
|
child,
|
|
layout::arranged_child_constraints(
|
|
&node,
|
|
[0.0, 0.0, width, height],
|
|
Size { width, height },
|
|
),
|
|
)
|
|
})
|
|
.collect::<Result<Vec<_>, _>>()?
|
|
};
|
|
let children = layout::flex_sizes(&node, Size { width, height }, &children);
|
|
let points = layout::arrange_native(&node, x, y, Size { width, height }, &children)
|
|
.map_err(|_| invalid("native arrangement exceeds limits"))?;
|
|
let mut result = Vec::new();
|
|
for ((slot, size), (x, y)) in node.slots.iter().zip(children).zip(points) {
|
|
let bounds = record(
|
|
cx,
|
|
self.desktop,
|
|
"Bounds",
|
|
vec![
|
|
("x", value(x)),
|
|
("y", value(y)),
|
|
("width", value(size.width)),
|
|
("height", value(size.height)),
|
|
],
|
|
)?;
|
|
cx.set_attr(bounds, "slot", Value::Number((*slot as i64).into()))?;
|
|
result.push(bounds);
|
|
}
|
|
let result = crate::object_list::collection(cx, result, self.desktop)?;
|
|
Value::Object(record(
|
|
cx,
|
|
self.desktop,
|
|
"Arrangement",
|
|
vec![("children", Value::Object(result))],
|
|
)?)
|
|
}
|
|
Query::Paint => {
|
|
let x = number(cx, input, "x", true)?;
|
|
let y = number(cx, input, "y", true)?;
|
|
let width = number(cx, input, "width", false)?;
|
|
let height = number(cx, input, "height", false)?;
|
|
if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 {
|
|
return Err(invalid("visual bounds exceed limits"));
|
|
}
|
|
let mut commands = Vec::new();
|
|
if let Some(command) = layout::paint_native(&node, x, y, Size { width, height }) {
|
|
let fields = match command {
|
|
RenderCommand::Text {
|
|
x,
|
|
y,
|
|
value: text,
|
|
size,
|
|
color,
|
|
wrap_bounds,
|
|
} => {
|
|
let mut fields = vec![
|
|
("kind", Value::Text("text".into())),
|
|
("x", value(x)),
|
|
("y", value(y)),
|
|
("text", Value::Text(text)),
|
|
("size", Value::Number((size as i64).into())),
|
|
("color", Value::Text(color)),
|
|
];
|
|
if let Some([width, height]) = wrap_bounds {
|
|
fields.push(("wrapWidth", value(width)));
|
|
fields.push(("wrapHeight", value(height)));
|
|
}
|
|
fields
|
|
}
|
|
RenderCommand::RoundedRect {
|
|
x,
|
|
y,
|
|
width,
|
|
height,
|
|
radius,
|
|
color,
|
|
} => vec![
|
|
("kind", Value::Text("roundedRect".into())),
|
|
("x", value(x)),
|
|
("y", value(y)),
|
|
("width", value(width)),
|
|
("height", value(height)),
|
|
("radius", value(radius)),
|
|
("color", Value::Text(color)),
|
|
],
|
|
_ => return Err(invalid("unsupported native paint command")),
|
|
};
|
|
let command = record(cx, self.desktop, "PaintCommand", fields)?;
|
|
commands.push(command);
|
|
}
|
|
let commands = crate::object_list::collection(cx, commands, self.desktop)?;
|
|
Value::Object(record(
|
|
cx,
|
|
self.desktop,
|
|
"DisplayList",
|
|
vec![("commands", Value::Object(commands))],
|
|
)?)
|
|
}
|
|
Query::HitTest => unreachable!("handled above"),
|
|
};
|
|
Ok(NativeResult::Return(result))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct PointMethod {
|
|
expected: ObjectRef,
|
|
circle: bool,
|
|
signature: CallSignature,
|
|
}
|
|
impl NativeCallable for PointMethod {
|
|
fn signature(&self) -> Option<CallSignature> {
|
|
Some(self.signature.clone())
|
|
}
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(if self.circle {
|
|
4
|
|
} else {
|
|
2
|
|
}))
|
|
}
|
|
|
|
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
|
let Some(Value::Object(point)) = args.first() else {
|
|
return Err(invalid("Point method requires a Point"));
|
|
};
|
|
if !cx.store().is_instance(*point, self.expected)? {
|
|
return Err(invalid("Point method requires a Point"));
|
|
}
|
|
let x = number(cx, *point, "x", true)?;
|
|
let y = number(cx, *point, "y", true)?;
|
|
let inside = if self.circle {
|
|
let [
|
|
_,
|
|
Value::Number(cx_value),
|
|
Value::Number(cy_value),
|
|
Value::Number(radius),
|
|
] = args
|
|
else {
|
|
return Err(invalid("inCircle requires center x/y and radius"));
|
|
};
|
|
let parse = |value: &endor_core::EndorNumber| -> Result<f64, VmError> {
|
|
let value = value
|
|
.to_string()
|
|
.parse::<f64>()
|
|
.map_err(|_| invalid("invalid circle number"))?;
|
|
if !value.is_finite() || value.abs() > MAX_DIMENSION {
|
|
return Err(invalid("circle number exceeds bounds"));
|
|
}
|
|
Ok(value)
|
|
};
|
|
let dx = x as f64 - parse(cx_value)?;
|
|
let dy = y as f64 - parse(cy_value)?;
|
|
let radius = parse(radius)?;
|
|
if radius < 0.0 {
|
|
return Err(invalid("circle radius must be nonnegative"));
|
|
}
|
|
dx * dx + dy * dy <= radius * radius
|
|
} else {
|
|
let [_, Value::Object(bounds)] = args else {
|
|
return Err(invalid("inside requires Bounds"));
|
|
};
|
|
let left = number(cx, *bounds, "x", true)?;
|
|
let top = number(cx, *bounds, "y", true)?;
|
|
let width = number(cx, *bounds, "width", false)?;
|
|
let height = number(cx, *bounds, "height", false)?;
|
|
x >= left && y >= top && x < left + width && y < top + height
|
|
};
|
|
Ok(NativeResult::Return(Value::Bool(inside)))
|
|
}
|
|
}
|
|
|
|
pub fn install(
|
|
vm: &mut Vm,
|
|
roots: &mut Vec<ObjectRef>,
|
|
desktop: ObjectRef,
|
|
block_type: ObjectRef,
|
|
function_type: ObjectRef,
|
|
) -> Result<(), VmError> {
|
|
let nominal = |name| {
|
|
crate::ui_bridge::desktop_type(vm.object_store(), name, desktop)
|
|
.map(AnnotationType::Nominal)
|
|
};
|
|
let mount = nominal("Mount")?;
|
|
let constraints = nominal("Constraints")?;
|
|
let bounds = nominal("Bounds")?;
|
|
let point = nominal("Point")?;
|
|
let measure = nominal("MeasureResult")?;
|
|
let arrangement = nominal("Arrangement")?;
|
|
let display = nominal("DisplayList")?;
|
|
for (name, query, input, returns) in [
|
|
("measure", Query::Measure, constraints, measure),
|
|
("arrange", Query::Arrange, bounds, arrangement),
|
|
("paint", Query::Paint, bounds, display),
|
|
("hitTest", Query::HitTest, point, AnnotationType::Bool),
|
|
] {
|
|
let function = vm.allocate(ContextNativeFunction::new(
|
|
name,
|
|
Method {
|
|
query,
|
|
desktop,
|
|
signature: CallSignature::exact(
|
|
vec![AnnotationType::Nominal(block_type), mount, input],
|
|
returns,
|
|
CallEffect::ReadOnly,
|
|
),
|
|
},
|
|
vec![desktop],
|
|
))?;
|
|
roots.push(function);
|
|
vm.attach_type(function, function_type)?;
|
|
vm.define_method(block_type, name, function)?;
|
|
}
|
|
let registry = object(&vm.get_attr(desktop, "types")?)?;
|
|
let point_type = object(
|
|
&vm.dict_get(registry, &Value::Text("Point".into()))?
|
|
.ok_or(invalid("missing Point type"))?,
|
|
)?;
|
|
for (name, circle) in [("inside", false), ("inCircle", true)] {
|
|
let function = vm.allocate(ContextNativeFunction::new(
|
|
name,
|
|
PointMethod {
|
|
expected: point_type,
|
|
circle,
|
|
signature: CallSignature::exact(
|
|
if circle {
|
|
vec![
|
|
point,
|
|
AnnotationType::Number,
|
|
AnnotationType::Number,
|
|
AnnotationType::Number,
|
|
]
|
|
} else {
|
|
vec![point, bounds]
|
|
},
|
|
AnnotationType::Bool,
|
|
CallEffect::ReadOnly,
|
|
),
|
|
},
|
|
vec![point_type],
|
|
))?;
|
|
roots.push(function);
|
|
vm.attach_type(function, function_type)?;
|
|
vm.define_method(point_type, name, function)?;
|
|
}
|
|
Ok(())
|
|
}
|