Files
endorphin-tessera-gui/src/render/source_hit.rs

266 lines
11 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 {
/// 接收 hitTest 返回的唯一值,交给 invoke 严格检查 Bool。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(1))
}
/// 只接受真正的 Bool 并原样返回;数字或对象不能以宿主真值规则替代源协议契约。
fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
// 协议要求明确 Bool;拒绝数字、对象等“真值”,使用户实现错误立即暴露。
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<endor_core::CallArity> {
Some(endor_core::CallArity::exact(2))
}
/// 仅当两结果为同一 Mount 对象才返回目标,否则返回 Nil;共享 Block 的不同出现位置也不能合成点击。
fn invoke(&self, _: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
// 比较的是实际出现 Mount 的身份;即使共享同一个 Block,不同出现之间拖动也不构成点击。
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> {
// 校验并编码指令,同时设置诊断位置;独立空 globals 避免驱动依赖业务全局命名空间。
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]);
// 把投影坐标反算到节点逻辑空间;不可逆/无效投影没有可用于 hitTest 的局部坐标。
let Some(point) = geometry.logical_point(point) else {
continue;
};
// 源 Point 以节点左上为原点;这里再次验证范围,避免变换后的点落到节点盒子之外。
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;
}
// 依据真实 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"))?;
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;索引供下面字节码复用。
let start = constants.len() as u32;
constants.extend([
Value::Object(function),
Value::Object(target.block),
Value::Object(mount),
Value::Object(local),
]);
// false 分支只跳过“加载本 Mount 并返回”两条指令。按编码宽度计算,
// 常量索引大到需要扩展操作数时仍能跳到下一候选。
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 当作选择结果。
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 {
/// 候选、按下点和释放点已保存于适配器,无需外部实参。
fn arity(&self) -> Option<endor_core::CallArity> {
Some(endor_core::CallArity::exact(0))
}
/// 为按下/释放各建立只读选择器,把两次结果放入同一 VM 调用序列,再交 SameTarget 比较实际出现身份。
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![],
))?;
// 两次选择结果先存入局部槽,再作为两个实参交给 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),
];
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 时返回其拥有根。
pub fn resolve(
vm: &mut Vm,
desktop: ObjectRef,
candidates: Vec<WeakObjectRef>,
press: [f32; 2],
release: [f32; 2],
) -> anyhow::Result<Option<ObjectRef>> {
// 显式 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))?;
match value {
// 成功返回的 Mount 仍拥有一次根引用,交由调用方使用并释放;None 不转移任何对象根。
Value::Object(mount) => Ok(Some(mount)),
Value::Nil => Ok(None),
value => {
vm.release_value(value)?;
anyhow::bail!("invalid hitTest target")
}
}
}