//! Resolve pointer targets in paint order through source/native hitTest methods. //! Both press and release selectors share one read-only VM call and budget. use crate::{mount::Mount, ui_bridge::attach_desktop_type}; use endor_core::{ CodeLayout, ContextNativeFunction, EndorCode, EndorCodeObject, Instruction, NativeCallable, NativeContext, NativeResult, ObjectRef, Opcode, Value, Vm, VmError, WeakObjectRef, }; #[derive(Debug)] struct Boolean; impl NativeCallable for Boolean { fn arity(&self) -> Option { Some(endor_core::CallArity::exact(1)) } fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result { let [Value::Bool(value)] = args else { return Err(VmError::InvalidArgument("hitTest must return Bool")); }; Ok(NativeResult::Return(Value::Bool(*value))) } } #[derive(Debug)] struct SameTarget; impl NativeCallable for SameTarget { fn arity(&self) -> Option { Some(endor_core::CallArity::exact(2)) } fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result { let value = match args { [Value::Object(first), Value::Object(second)] if first == second => { Value::Object(*first) } _ => Value::Nil, }; Ok(NativeResult::Return(value)) } } fn code( cx: &mut NativeContext<'_>, instructions: Vec, constants: Vec, layout: CodeLayout, ) -> Result { let code = EndorCode::from_instructions(&instructions, constants, layout)?.with_location( "", "", 1, ); let code = cx.allocate(EndorCodeObject::new(code))?; let globals = cx.new_dict()?; cx.new_function("pointer hitTest", code, globals) } fn selector( cx: &mut NativeContext<'_>, candidates: &[WeakObjectRef], point: [f32; 2], desktop: ObjectRef, ) -> Result { let validate = cx.allocate(ContextNativeFunction::new( "validate hitTest", Boolean, vec![], ))?; let mut instructions = Vec::new(); let mut constants = vec![Value::Object(validate)]; for weak in candidates.iter().rev() { let mount = cx .upgrade(*weak)? .ok_or(VmError::InvalidArgument("pointer Mount expired"))?; let target = cx.store().payload::(mount, "Mount")?.clone(); if !target.mounted || !target.geometry.interactive || !target .geometry .contains(crate::geometry::Point::new(point[0], point[1])) { continue; } let geometry = &target.geometry; let x = point[0] - geometry.x; let y = point[1] - geometry.y; if x < 0.0 || y < 0.0 || x > geometry.width || y > geometry.height { continue; } let definition = cx .store() .type_of(target.block)? .ok_or(VmError::InvalidArgument("untyped hit target"))?; let function = cx .store() .lookup_method(definition, "hitTest")? .ok_or(VmError::InvalidArgument("missing hitTest method"))?; let local = cx.new_object()?; attach_desktop_type(cx, local, "Point", desktop)?; cx.set_attr(local, "x", Value::Number((x as f64).into()))?; cx.set_attr(local, "y", Value::Number((y as f64).into()))?; let start = constants.len() as u32; constants.extend([ Value::Object(function), Value::Object(target.block), Value::Object(mount), Value::Object(local), ]); let skip = (Instruction::encode_all(&[ Instruction::new(Opcode::LoadConst, start + 2), Instruction::simple(Opcode::ReturnValue), ])? .len() / 2) as u32; instructions.extend([ Instruction::new(Opcode::LoadConst, 0), Instruction::simple(Opcode::PushNull), Instruction::new(Opcode::LoadConst, start), Instruction::simple(Opcode::PushNull), Instruction::new(Opcode::LoadConst, start + 1), Instruction::new(Opcode::LoadConst, start + 2), Instruction::new(Opcode::LoadConst, start + 3), Instruction::new(Opcode::Call, 3), Instruction::new(Opcode::Call, 1), Instruction::new(Opcode::PopJumpIfFalse, skip), Instruction::new(Opcode::LoadConst, start + 2), Instruction::simple(Opcode::ReturnValue), ]); } let end = constants.len() as u32; constants.push(Value::Nil); instructions.extend([ Instruction::new(Opcode::LoadConst, end), Instruction::simple(Opcode::ReturnValue), ]); code(cx, instructions, constants, CodeLayout::default()) } #[derive(Debug)] struct Prepare { candidates: Vec, press: [f32; 2], release: [f32; 2], desktop: ObjectRef, } impl NativeCallable for Prepare { fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { let down = selector(cx, &self.candidates, self.press, self.desktop)?; let up = selector(cx, &self.candidates, self.release, self.desktop)?; let compare = cx.allocate(ContextNativeFunction::new( "same pointer target", SameTarget, vec![], ))?; let instructions = vec![ Instruction::new(Opcode::LoadConst, 0), Instruction::simple(Opcode::PushNull), Instruction::new(Opcode::Call, 0), Instruction::new(Opcode::StoreFast, 0), Instruction::new(Opcode::LoadConst, 1), Instruction::simple(Opcode::PushNull), Instruction::new(Opcode::Call, 0), Instruction::new(Opcode::StoreFast, 1), Instruction::new(Opcode::LoadConst, 2), Instruction::simple(Opcode::PushNull), Instruction::new(Opcode::LoadFast, 0), Instruction::new(Opcode::LoadFast, 1), Instruction::new(Opcode::Call, 2), Instruction::simple(Opcode::ReturnValue), ]; let driver = code( cx, instructions, vec![ Value::Object(down), Value::Object(up), Value::Object(compare), ], CodeLayout { locals: vec!["press".into(), "release".into()], ..CodeLayout::default() }, )?; Ok(NativeResult::Invoke { function: driver, arguments: vec![], }) } } /// Returned Mount owns one root. No-hit and mismatched targets return None. pub fn resolve( vm: &mut Vm, desktop: ObjectRef, candidates: Vec, press: [f32; 2], release: [f32; 2], ) -> anyhow::Result> { let prepare = vm.allocate(ContextNativeFunction::new( "prepare pointer hitTest", Prepare { candidates, press, release, desktop, }, vec![desktop], ))?; let result = vm.call_read_only_with_limits( prepare, &[], endor_core::ExecutionLimits { max_frames: 128, max_instructions: 1_000_000, }, ); vm.release(prepare)?; let (value, _) = result.map_err(|error| crate::endor_host::runtime_error(vm, error))?; match value { Value::Object(mount) => Ok(Some(mount)), Value::Nil => Ok(None), value => { vm.release_value(value)?; anyhow::bail!("invalid hitTest target") } } }