Files
endorphin-tessera-gui/src/native_visual.rs
T
2026-10-22 22:01:28 +08:00

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(())
}