//! 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 { 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 { 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::() .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 { 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 { 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 { let Some(function) = store .object(function)? .payload() .as_any() .downcast_ref::() else { return Ok(false); }; let Some(method) = function.implementation::() 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 { Some(self.signature.clone()) } fn arity(&self) -> Option { Some(endor_core::CallArity::exact(3)) } fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { 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::(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(), layout.preferred.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", 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 children = if let Some((_, children, _)) = &staged { children.clone() } else { node.children() .iter() .map(|child| { size( child, layout::occurrence_constraints( &node, child, layout::arranged_child_constraints( &node, [0.0, 0.0, width, height], Size { width, height }, ), Size { width, height }, ), ) }) .collect::, _>>()? }; let preferred = staged .as_ref() .map_or(children.as_slice(), |(_, _, preferred)| { preferred.as_slice() }); let children = layout::flex_sizes(&node, Size { width, height }, &children, preferred); 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(); for command in layout::paint_commands(&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 { Some(self.signature.clone()) } fn arity(&self) -> Option { Some(endor_core::CallArity::exact(if self.circle { 4 } else { 2 })) } fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result { 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 { let value = value .to_string() .parse::() .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, 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(()) }