//! 通过真正的源/原生 hitTest 协议确定指针目标。 //! 按逆绘制顺序筛选候选,并在同一只读 VM 调用与预算中比较按下、释放目标;透明或未命中的子项可以继续回退。 //! //! 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)] /// 验证 hitTest 返回的是 Bool,避免把任意真值隐式当成命中。 struct Boolean; impl NativeCallable for Boolean { /// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(1)) } /// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。 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 { /// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(2)) } /// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。 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) } /// 按逆绘制顺序构造候选 hitTest 调用,先进行几何与局部坐标筛选。 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 point = crate::geometry::Point::new(point[0], point[1]); let Some(point) = geometry.logical_point(point) else { continue; }; let x = point.x - geometry.x; let y = point.y - 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 { /// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(0)) } /// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。 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") } } }