246 lines
9.2 KiB
Rust
246 lines
9.2 KiB
Rust
//! 通过真正的源/原生 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<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(1))
|
|
}
|
|
|
|
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
|
fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
|
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<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(2))
|
|
}
|
|
|
|
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
|
fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
|
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<Instruction>,
|
|
constants: Vec<Value>,
|
|
layout: CodeLayout,
|
|
) -> Result<ObjectRef, VmError> {
|
|
let code = EndorCode::from_instructions(&instructions, constants, layout)?.with_location(
|
|
"<pointer hitTest>",
|
|
"<tessera>",
|
|
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<ObjectRef, VmError> {
|
|
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, "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<WeakObjectRef>,
|
|
press: [f32; 2],
|
|
release: [f32; 2],
|
|
desktop: ObjectRef,
|
|
}
|
|
impl NativeCallable for Prepare {
|
|
/// 声明宿主可调用对象接受的参数个数,供 VM 在执行前检查。
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(0))
|
|
}
|
|
|
|
/// VM 调入本宿主能力的入口;按所属操作解码参数并返回托管值或后续调用。
|
|
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
|
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<WeakObjectRef>,
|
|
press: [f32; 2],
|
|
release: [f32; 2],
|
|
) -> anyhow::Result<Option<ObjectRef>> {
|
|
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")
|
|
}
|
|
}
|
|
}
|