//! 通过真正的源/原生 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,避免把任意真值隐式当成命中。 // selector 把源 hitTest 返回值交给此适配器,之后字节码才根据 Bool 决定返回或继续查找。 struct Boolean; impl NativeCallable for Boolean { /// 接收 hitTest 返回的唯一值,交给 invoke 严格检查 Bool。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(1)) } /// 只接受真正的 Bool 并原样返回;数字或对象不能以宿主真值规则替代源协议契约。 fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result { // 协议要求明确 Bool;拒绝数字、对象等“真值”,使用户实现错误立即暴露。 let [Value::Bool(value)] = args else { return Err(VmError::InvalidArgument("hitTest must return Bool")); }; Ok(NativeResult::Return(Value::Bool(*value))) } } #[derive(Debug)] /// 比较按下与释放的选择结果,只有同一目标才能继续点击分发。 // Prepare 生成的驱动先执行两次 selector,最后调用此比较器;对象相等比较的是实际 Mount 身份。 struct SameTarget; impl NativeCallable for SameTarget { /// 接收按下与释放两次命中选择的结果。 fn arity(&self) -> Option { Some(endor_core::CallArity::exact(2)) } /// 仅当两结果为同一 Mount 对象才返回目标,否则返回 Nil;共享 Block 的不同出现位置也不能合成点击。 fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result { // 比较的是实际出现 Mount 的身份;即使共享同一个 Block,不同出现之间拖动也不构成点击。 let value = match args { [Value::Object(first), Value::Object(second)] if first == second => { Value::Object(*first) } _ => Value::Nil, }; Ok(NativeResult::Return(value)) } } /// 构造只读命中选择驱动所需的托管代码与函数对象。 // selector 和 Prepare 共用代码工厂;传入常量形成托管引用边,保持协议函数、实参和中间驱动存活。 fn code( cx: &mut NativeContext<'_>, instructions: Vec, constants: Vec, layout: CodeLayout, ) -> Result { // 校验并编码指令,同时设置诊断位置;独立空 globals 避免驱动依赖业务全局命名空间。 let code = EndorCode::from_instructions(&instructions, constants, layout)?.with_location( "", "", 1, ); // 先把校验后的字节码分配到当前 NativeContext,输出函数经托管引用拥有它及其常量。 let code = cx.allocate(EndorCodeObject::new(code))?; // 驱动只读取显式常量和局部槽,空 globals 使它不依赖或修改业务脚本全局变量。 let globals = cx.new_dict()?; cx.new_function("pointer hitTest", code, globals) } /// 按逆绘制顺序构造候选 hitTest 调用,先进行几何与局部坐标筛选。 // Prepare 分别为 press/release 构造选择器;准备阶段做原生粗筛,执行阶段才调用真实 hitTest 方法。 fn selector( cx: &mut NativeContext<'_>, candidates: &[WeakObjectRef], point: [f32; 2], desktop: ObjectRef, ) -> Result { // 所有候选共享一个 Bool 验证器,放常量槽 0,避免每个候选重复创建适配器。 let validate = cx.allocate(ContextNativeFunction::new( "validate hitTest", Boolean, vec![], ))?; // 可变指令和常量池按候选依次追加;前面候选返回 true 即提前 ReturnValue,剩余候选不会执行。 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"))?; // 克隆 Mount 描述以便后面分配源 Point;不持有跨 NativeContext 分配的 store 借用。 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; } // 几何是本次出现的布局与投影快照,同一 Block 的其他 Mount 不能共享这里的局部坐标。 let geometry = &target.geometry; let point = crate::geometry::Point::new(point[0], point[1]); // 把投影坐标反算到节点逻辑空间;不可逆/无效投影没有可用于 hitTest 的局部坐标。 let Some(point) = geometry.logical_point(point) else { continue; }; // 源 Point 以节点左上为原点;这里再次验证范围,避免变换后的点落到节点盒子之外。 let x = point.x - geometry.x; let y = point.y - geometry.y; // 经过逻辑逆变换后再按本地盒检查,源 hitTest 不接收明显超出该出现范围的点。 if x < 0.0 || y < 0.0 || x > geometry.width || y > geometry.height { continue; } // 依据真实 Block 的名义类型解析 hitTest,不用原生矩形命中替代源协议。 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"))?; // 为本次候选分配本桌面的 Point,先写 x/y 再加入常量池,辅助代码用它作为第三实参。 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()))?; // 每个候选占四个常量:协议函数、Block、Mount、局部 Point;索引供下面字节码复用。 // 记录当前候选四个常量的基址;start+2 是最终返回的 Mount,不能误返回共享 Block。 let start = constants.len() as u32; constants.extend([ Value::Object(function), Value::Object(target.block), Value::Object(mount), Value::Object(local), ]); // false 分支只跳过“加载本 Mount 并返回”两条指令。按编码宽度计算, // 常量索引大到需要扩展操作数时仍能跳到下一候选。 // 跳转偏移以两字节指令单元计,因此编码字节长度除以 2;扩展操作数的长度也计入。 let skip = (Instruction::encode_all(&[ Instruction::new(Opcode::LoadConst, start + 2), Instruction::simple(Opcode::ReturnValue), ])? .len() / 2) as u32; // 先调用 hitTest(block, mount, local),再把结果传给 validate。 // true 立即返回本 Mount;false 继续下一个候选,实现透明区域向下穿透。 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), ]); } // 所有候选均未命中时显式返回 Nil;不会把最后一个 false 当作选择结果。 // 末尾追加独立 Nil 常量,零候选或全部返回 false 都走同一个无命中返回路径。 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)) } /// 为按下/释放各建立只读选择器,把两次结果放入同一 VM 调用序列,再交 SameTarget 比较实际出现身份。 fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result { // 按下和释放分别构造选择器,但都在本次只读执行内调用,使用同一预算域。 let down = selector(cx, &self.candidates, self.press, self.desktop)?; // 释放点重用同一候选序列;按下 selector 的最终结果只在执行期出现,准备时不提前猜测目标。 let up = selector(cx, &self.candidates, self.release, self.desktop)?; // 分配比较器无须捕获业务根;真正待比较的 Mount 由驱动局部槽及实参保持生命周期。 let compare = cx.allocate(ContextNativeFunction::new( "same pointer target", SameTarget, vec![], ))?; // 两次选择结果先存入局部槽,再作为两个实参交给 SameTarget;局部槽也保留中间结果生命周期。 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), ]; // 为两个选择结果显式声明局部槽,常量顺序与上面的 LoadConst 0/1/2 对应。 let driver = code( cx, instructions, vec![ Value::Object(down), Value::Object(up), Value::Object(compare), ], CodeLayout { locals: vec!["press".into(), "release".into()], ..CodeLayout::default() }, )?; // 把执行权交回 VM 运行驱动,不在宿主里递归开启另一次 VM 调用。 Ok(NativeResult::Invoke { function: driver, arguments: vec![], }) } } /// Returned Mount owns one root. No-hit and mismatched targets return None. /// 在一次有界只读调用中分别选择按下与释放目标;仅二者为同一 Mount 时返回其拥有根。 // ui_bridge 的点击分发路径调用,成功的 Some 根由调用方使用后释放;本函数不直接派发业务 click。 pub fn resolve( vm: &mut Vm, desktop: ObjectRef, candidates: Vec, press: [f32; 2], release: [f32; 2], ) -> anyhow::Result> { // 显式 captures 保活 desktop;候选列表仍是弱引用,不因等待选择而保活全部旧 Mount。 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 返回错误时也能完成入口清理。 vm.release(prepare)?; // 只取选择值,丢弃只读依赖集合;一次性输入命中不建立绘制或布局的持久失效依赖。 let (value, _) = result.map_err(|error| crate::endor_host::runtime_error(vm, error))?; // 接受 Mount 或 Nil 两类驱动结果,其他值先释放其可能拥有根,再报告内部协议错误。 match value { // 成功返回的 Mount 仍拥有一次根引用,交由调用方使用并释放;None 不转移任何对象根。 Value::Object(mount) => Ok(Some(mount)), Value::Nil => Ok(None), value => { vm.release_value(value)?; anyhow::bail!("invalid hitTest target") } } }