//! 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::{ ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectRef, 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) -> Result { let frame = layout::frame(node).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, } impl NativeCallable for Method { 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())) }) .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()) .map_err(|_| invalid("native measurement exceeds layout limits"))? } else { size(&node)? }; 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)) .collect::, _>>()? }; let points = layout::arrange_native(&node, x, y, &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, } => 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)), ], 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, } impl NativeCallable for PointMethod { 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> { for (name, query) in [ ("measure", Query::Measure), ("arrange", Query::Arrange), ("paint", Query::Paint), ("hitTest", Query::HitTest), ] { let function = vm.allocate(ContextNativeFunction::new( name, Method { query, desktop }, 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, }, vec![point_type], ))?; roots.push(function); vm.attach_type(function, function_type)?; vm.define_method(point_type, name, function)?; } Ok(()) }