547 lines
24 KiB
Rust
547 lines
24 KiB
Rust
//! 让原生组件也通过 Endor 的 measure/arrange/paint 协议被调用。
|
||
//! 将 Constraints、Size、Bounds 与 PaintCommand 在托管值和 Rust 值之间转换;原生方法与源覆盖方法共用调用契约。
|
||
//!
|
||
//! Native visual protocol entry points. Values are ordinary managed Endor
|
||
//! objects; geometry is scoped to a live occurrence, never stored on a Block.
|
||
use crate::{
|
||
block,
|
||
document::RenderCommand,
|
||
layout::{self, Size},
|
||
mount::Mount,
|
||
ui_bridge::{MAX_DIMENSION, attach_desktop_type},
|
||
};
|
||
use endor_core::{
|
||
AnnotationType, CallEffect, CallSignature, ContextNativeFunction, NativeCallable,
|
||
NativeContext, NativeResult, ObjectRef, ObjectStore, Value, Vm, VmError,
|
||
};
|
||
|
||
/// 统一构造本模块的参数错误,让调用方停止处理无效输入。
|
||
fn invalid(message: &'static str) -> VmError {
|
||
VmError::InvalidArgument(message)
|
||
}
|
||
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
||
fn object(value: &Value) -> Result<ObjectRef, VmError> {
|
||
if let Value::Object(object) = value {
|
||
Ok(*object)
|
||
} else {
|
||
Err(invalid("visual protocol requires objects"))
|
||
}
|
||
}
|
||
/// 在源 Value 与所需数值表示之间转换,并沿用本调用的有限值/范围检查。
|
||
pub(crate) fn number(
|
||
cx: &NativeContext<'_>,
|
||
object: ObjectRef,
|
||
name: &str,
|
||
signed: bool,
|
||
) -> Result<f32, VmError> {
|
||
let Value::Number(value) = cx.store().get_attr(object, name)? else {
|
||
return Err(invalid("visual field must be a Number"));
|
||
};
|
||
let value = value
|
||
.to_string()
|
||
.parse::<f32>()
|
||
.map_err(|_| invalid("invalid visual number"))?;
|
||
// 所有协议数值须有限且位于世界尺寸预算内;signed 区分坐标(可负)与长度/约束(非负)。
|
||
if !value.is_finite() || value.abs() > MAX_DIMENSION as f32 || (!signed && value < 0.0) {
|
||
return Err(invalid("visual number exceeds bounds"));
|
||
}
|
||
Ok(value)
|
||
}
|
||
/// 将原生布局计算结果封装为协议需要的托管 Value。
|
||
pub(crate) fn value(value: f32) -> Value {
|
||
// 原生 f32 提升到 f64 包装为 Endor Number;本函数不单独校验,调用路径必须已保证输入合法。
|
||
Value::Number((value as f64).into())
|
||
}
|
||
/// 新建指定名义类型的托管记录,并逐项设置字段,作为视觉协议的结果返回。
|
||
pub(crate) fn record(
|
||
cx: &mut NativeContext<'_>,
|
||
desktop: ObjectRef,
|
||
kind: &str,
|
||
fields: Vec<(&str, Value)>,
|
||
) -> Result<ObjectRef, VmError> {
|
||
// 先赋精确名义类型,再初始化字段;只读协议可创建结果对象,但不能借机写入旧业务图。
|
||
let object = cx.new_object()?;
|
||
attach_desktop_type(cx, object, kind, desktop)?;
|
||
for (key, value) in fields {
|
||
cx.set_attr(object, key, value)?;
|
||
}
|
||
Ok(object)
|
||
}
|
||
/// 以最大宽高作视口执行原生布局并提取根尺寸;最小/最大最终钳制由 Measure 分支负责。
|
||
fn size(node: &crate::ui_bridge::UiNode, constraints: [f32; 4]) -> Result<Size, VmError> {
|
||
// 无 staged 子测量结果时,使用有限最大约束运行原生布局取得根尺寸;最终 min/max 钳制在调用方 Measure 分支。
|
||
let frame = layout::frame_with_viewport(
|
||
node,
|
||
Size {
|
||
width: constraints[2],
|
||
height: constraints[3],
|
||
},
|
||
)
|
||
.map_err(|_| invalid("native measurement exceeds layout limits"))?;
|
||
let root = frame
|
||
.mounts
|
||
.first()
|
||
.ok_or(invalid("empty native measurement"))?;
|
||
Ok(Size {
|
||
width: root.width,
|
||
height: root.height,
|
||
})
|
||
}
|
||
#[derive(Debug, Clone, Copy)]
|
||
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
|
||
enum Query {
|
||
Measure,
|
||
Arrange,
|
||
Paint,
|
||
HitTest,
|
||
}
|
||
#[derive(Debug)]
|
||
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
|
||
struct Method {
|
||
query: Query,
|
||
desktop: ObjectRef,
|
||
signature: CallSignature,
|
||
}
|
||
/// Only this actual implementation can use the matching intrinsic fast path.
|
||
/// 判断方法是否为匹配协议的宿主原生实现,决定能否安全使用快速路径。
|
||
pub(crate) fn is_intrinsic(
|
||
store: &ObjectStore,
|
||
function: ObjectRef,
|
||
name: &str,
|
||
desktop: ObjectRef,
|
||
) -> Result<bool, VmError> {
|
||
// 快速路径仅认可实际 ContextNativeFunction 的 Method 实现,函数名相同或签名相似的源函数不能冒充。
|
||
let Some(function) = store
|
||
.object(function)?
|
||
.payload()
|
||
.as_any()
|
||
.downcast_ref::<ContextNativeFunction>()
|
||
else {
|
||
return Ok(false);
|
||
};
|
||
let Some(method) = function.implementation::<Method>() else {
|
||
return Ok(false);
|
||
};
|
||
// Per-window desktops share the same nominal registry and intrinsic bodies.
|
||
// 同一 VM 不同窗口 desktop 可共享 types 字典,所以以注册表身份允许复用原生方法体。
|
||
let same_registry = method.desktop == desktop
|
||
|| matches!(
|
||
(store.get_attr(method.desktop, "types")?, store.get_attr(desktop, "types")?),
|
||
(Value::Object(left), Value::Object(right)) if left == right
|
||
);
|
||
Ok(same_registry
|
||
&& matches!(
|
||
(name, method.query),
|
||
("measure", Query::Measure)
|
||
| ("arrange", Query::Arrange)
|
||
| ("paint", Query::Paint)
|
||
| ("hitTest", Query::HitTest)
|
||
))
|
||
}
|
||
|
||
impl NativeCallable for Method {
|
||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||
fn signature(&self) -> Option<CallSignature> {
|
||
Some(self.signature.clone())
|
||
}
|
||
/// 接收三项底层参数:Block 接收者、具体 Mount、Constraints/Bounds/Point 协议输入。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(3))
|
||
}
|
||
|
||
/// 检查 Block/Mount 归属与有效性,再按 measure/arrange/paint/hitTest 执行原生视觉协议并返回受检查的托管值。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||
// 三个实际参数含接收者 Block、具体 Mount 和协议输入;几何属于 Mount,不能仅凭 Block 身份查询。
|
||
let [owner, context, input] = args else {
|
||
return Err(invalid("visual method expects a Mount and an input value"));
|
||
};
|
||
let owner = object(owner)?;
|
||
let mount = cx.store().payload::<Mount>(object(context)?, "Mount")?;
|
||
// 默认协议也校验上下文归属,防止调用者把另一个出现的几何套到当前组件。
|
||
if !mount.mounted || mount.block != owner {
|
||
return Err(invalid(
|
||
"visual context is detached or belongs to another Block",
|
||
));
|
||
}
|
||
let geometry = mount.geometry.clone();
|
||
// 布局阶段从临时 Context 取当前节点、孩子实测尺寸与首选尺寸;克隆派生数据后释放锁再进行计算。
|
||
let staged = mount
|
||
.layout
|
||
.as_ref()
|
||
.map(|layout| {
|
||
let layout = layout
|
||
.lock()
|
||
.map_err(|_| invalid("layout context poisoned"))?;
|
||
Ok::<_, VmError>((
|
||
layout.node.clone(),
|
||
layout.children.clone(),
|
||
layout.preferred.clone(),
|
||
))
|
||
})
|
||
.transpose()?;
|
||
let input = object(input)?;
|
||
// 默认 hitTest 检查局部点在已提交盒内,采用左上包含、右下排除;布局阶段几何不稳定,禁止调用。
|
||
if matches!(self.query, Query::HitTest) {
|
||
if staged.is_some() {
|
||
return Err(invalid("hitTest is unavailable during layout"));
|
||
}
|
||
let x = number(cx, input, "x", true)?;
|
||
let y = number(cx, input, "y", true)?;
|
||
return Ok(NativeResult::Return(Value::Bool(
|
||
x >= 0.0 && y >= 0.0 && x < geometry.width && y < geometry.height,
|
||
)));
|
||
}
|
||
// 存在布局 Context 但尚无当前节点,说明准备顺序不完整,不能回退读取旧快照伪装成当前数据。
|
||
if matches!(&staged, Some((None, _, _))) {
|
||
return Err(invalid(
|
||
"layout context is not prepared for this occurrence",
|
||
));
|
||
}
|
||
// 准备好则复用本轮不可变节点,非 staged 调用才从托管 Block 提取视觉快照。
|
||
let node = if let Some((Some(node), _, _)) = &staged {
|
||
node.clone()
|
||
} else {
|
||
block::snapshot(cx.store(), owner)?
|
||
};
|
||
let result = match self.query {
|
||
Query::Measure => {
|
||
let min_w = number(cx, input, "minWidth", false)?;
|
||
let min_h = number(cx, input, "minHeight", false)?;
|
||
let max_w = number(cx, input, "maxWidth", false)?;
|
||
let max_h = number(cx, input, "maxHeight", false)?;
|
||
// 最小值不可超过最大值,否则后面的 clamp 会无意义甚至失败;各数值已通过非负有限检查。
|
||
if min_w > max_w || min_h > max_h {
|
||
return Err(invalid("minimum exceeds maximum constraint"));
|
||
}
|
||
// 混合源/原生布局使用已经测好的子尺寸,避免重复执行子树;独立调用则走完整原生测量。
|
||
let size = if let Some((_, children, _)) = &staged {
|
||
layout::measure_native(&node, children.clone(), [min_w, min_h, max_w, max_h])
|
||
.map_err(|_| invalid("native measurement exceeds layout limits"))?
|
||
} else {
|
||
size(&node, [min_w, min_h, max_w, max_h])?
|
||
};
|
||
Value::Object(record(
|
||
cx,
|
||
self.desktop,
|
||
"MeasureResult",
|
||
vec![
|
||
// MeasureResult 最终明确落在传入 min/max 内,原生内容自然大小不能突破源协议约束。
|
||
("width", value(size.width.clamp(min_w, max_w))),
|
||
("height", value(size.height.clamp(min_h, max_h))),
|
||
],
|
||
)?)
|
||
}
|
||
Query::Arrange => {
|
||
let x = number(cx, input, "x", true)?;
|
||
let y = number(cx, input, "y", true)?;
|
||
let width = number(cx, input, "width", false)?;
|
||
let height = number(cx, input, "height", false)?;
|
||
if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 {
|
||
return Err(invalid("visual bounds exceed limits"));
|
||
}
|
||
// Arrange 优先使用本轮子测量;没有 staged 时按孩子的实际分配约束逐个测量后再安排。
|
||
let children = if let Some((_, children, _)) = &staged {
|
||
children.clone()
|
||
} else {
|
||
node.children()
|
||
.iter()
|
||
.enumerate()
|
||
.map(|(child_index, child)| {
|
||
size(
|
||
child,
|
||
layout::occurrence_constraints(
|
||
&node,
|
||
child,
|
||
layout::arranged_child_constraints(
|
||
&node,
|
||
[0.0, 0.0, width, height],
|
||
Size { width, height },
|
||
),
|
||
Size { width, height },
|
||
child_index,
|
||
),
|
||
)
|
||
})
|
||
.collect::<Result<Vec<_>, _>>()?
|
||
};
|
||
// preferred 保留弹性分配所需首选大小,与已受约束的实测 children 区分;无 staged 时两者相同。
|
||
let preferred = staged
|
||
.as_ref()
|
||
.map_or(children.as_slice(), |(_, _, preferred)| {
|
||
preferred.as_slice()
|
||
});
|
||
// 先依据父盒分配 flex 尺寸,再由 arrange_native 求位置,避免先定位再改变孩子大小导致重叠。
|
||
let children =
|
||
layout::flex_sizes(&node, Size { width, height }, &children, preferred);
|
||
let points = layout::arrange_native(&node, x, y, Size { width, height }, &children)
|
||
.map_err(|_| invalid("native arrangement exceeds limits"))?;
|
||
let mut result = Vec::new();
|
||
// 每个返回 Bounds 附稳定 slot,与 children 当前顺序分开;调用方可按 slot 校验组成身份。
|
||
for ((slot, size), (x, y)) in node.slots.iter().zip(children).zip(points) {
|
||
let bounds = record(
|
||
cx,
|
||
self.desktop,
|
||
"Bounds",
|
||
vec![
|
||
("x", value(x)),
|
||
("y", value(y)),
|
||
("width", value(size.width)),
|
||
("height", value(size.height)),
|
||
],
|
||
)?;
|
||
cx.set_attr(bounds, "slot", Value::Number((*slot as i64).into()))?;
|
||
result.push(bounds);
|
||
}
|
||
// Arrangement 返回受控 ObjectList,不返回宿主 Vec 或任意形状的源对象集合。
|
||
let result = crate::object_list::collection(cx, result, self.desktop)?;
|
||
Value::Object(record(
|
||
cx,
|
||
self.desktop,
|
||
"Arrangement",
|
||
vec![("children", Value::Object(result))],
|
||
)?)
|
||
}
|
||
Query::Paint => {
|
||
let x = number(cx, input, "x", true)?;
|
||
let y = number(cx, input, "y", true)?;
|
||
let width = number(cx, input, "width", false)?;
|
||
let height = number(cx, input, "height", false)?;
|
||
if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 {
|
||
return Err(invalid("visual bounds exceed limits"));
|
||
}
|
||
let mut commands = Vec::new();
|
||
// 将 Rust 默认绘制命令反向编码成源 PaintCommand,允许源覆盖方法调用基类 paint 后继续组合。
|
||
for command in layout::paint_commands(&node, x, y, Size { width, height }) {
|
||
let fields = match command {
|
||
RenderCommand::Scene3D {
|
||
x,
|
||
y,
|
||
width,
|
||
height,
|
||
view,
|
||
} => {
|
||
// 3D 通过不可变 PaintView payload 暴露场景快照,不把运行中 VM 的场景对象交给后续绘制线程。
|
||
let snapshot = cx.allocate(crate::scene3d::PaintView(view))?;
|
||
vec![
|
||
("kind", Value::Text("scene3d".into())),
|
||
("x", value(x)),
|
||
("y", value(y)),
|
||
("width", value(width)),
|
||
("height", value(height)),
|
||
("scene", Value::Object(snapshot)),
|
||
]
|
||
}
|
||
RenderCommand::Text {
|
||
x,
|
||
y,
|
||
value: text,
|
||
size,
|
||
color,
|
||
wrap_bounds,
|
||
} => {
|
||
let mut fields = vec![
|
||
("kind", Value::Text("text".into())),
|
||
("x", value(x)),
|
||
("y", value(y)),
|
||
("text", Value::Text(text)),
|
||
("size", Value::Number((size as i64).into())),
|
||
("color", Value::Text(color)),
|
||
];
|
||
// 可选文本换行边界成对写入;缺省时两个字段都不创建,与源 paint 解码契约保持一致。
|
||
if let Some([width, height]) = wrap_bounds {
|
||
fields.push(("wrapWidth", value(width)));
|
||
fields.push(("wrapHeight", value(height)));
|
||
}
|
||
fields
|
||
}
|
||
RenderCommand::RoundedRect {
|
||
x,
|
||
y,
|
||
width,
|
||
height,
|
||
radius,
|
||
color,
|
||
} => vec![
|
||
("kind", Value::Text("roundedRect".into())),
|
||
("x", value(x)),
|
||
("y", value(y)),
|
||
("width", value(width)),
|
||
("height", value(height)),
|
||
("radius", value(radius)),
|
||
("color", Value::Text(color)),
|
||
],
|
||
// 当前反向编码支持明确列出的命令;新增 RenderCommand 变体时须同步此转换,不能静默丢弃。
|
||
_ => return Err(invalid("unsupported native paint command")),
|
||
};
|
||
let command = record(cx, self.desktop, "PaintCommand", fields)?;
|
||
commands.push(command);
|
||
}
|
||
let commands = crate::object_list::collection(cx, commands, self.desktop)?;
|
||
Value::Object(record(
|
||
cx,
|
||
self.desktop,
|
||
"DisplayList",
|
||
vec![("commands", Value::Object(commands))],
|
||
)?)
|
||
}
|
||
Query::HitTest => unreachable!("handled above"),
|
||
};
|
||
Ok(NativeResult::Return(result))
|
||
}
|
||
}
|
||
|
||
#[derive(Debug)]
|
||
/// Point 的矩形/圆形命中辅助方法,保存结果类型和具体几何判定种类。
|
||
struct PointMethod {
|
||
expected: ObjectRef,
|
||
circle: bool,
|
||
signature: CallSignature,
|
||
}
|
||
impl NativeCallable for PointMethod {
|
||
/// 给出参数与返回值的托管类型契约;实际操作在 invoke 中执行。
|
||
fn signature(&self) -> Option<CallSignature> {
|
||
Some(self.signature.clone())
|
||
}
|
||
/// 圆形测试接收 Point 加圆心/半径共四项;矩形测试接收 Point 和 Bounds 共两项。
|
||
fn arity(&self) -> Option<endor_core::CallArity> {
|
||
Some(endor_core::CallArity::exact(if self.circle {
|
||
4
|
||
} else {
|
||
2
|
||
}))
|
||
}
|
||
|
||
/// 先验证 Point 名义类型与有限坐标,再分别计算圆内距离或 Bounds 包含关系,返回 Bool。
|
||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||
let Some(Value::Object(point)) = args.first() else {
|
||
return Err(invalid("Point method requires a Point"));
|
||
};
|
||
// Point 辅助方法验证名义接收者,不能只要有 x/y 字段就接受普通对象。
|
||
if !cx.store().is_instance(*point, self.expected)? {
|
||
return Err(invalid("Point method requires a Point"));
|
||
}
|
||
let x = number(cx, *point, "x", true)?;
|
||
let y = number(cx, *point, "y", true)?;
|
||
let inside = if self.circle {
|
||
let [
|
||
_,
|
||
Value::Number(cx_value),
|
||
Value::Number(cy_value),
|
||
Value::Number(radius),
|
||
] = args
|
||
else {
|
||
return Err(invalid("inCircle requires center x/y and radius"));
|
||
};
|
||
// 圆心和半径按 f64 解析及求平方距离,在有界数值内避免不必要的 f32 精度损失。
|
||
let parse = |value: &endor_core::EndorNumber| -> Result<f64, VmError> {
|
||
let value = value
|
||
.to_string()
|
||
.parse::<f64>()
|
||
.map_err(|_| invalid("invalid circle number"))?;
|
||
if !value.is_finite() || value.abs() > MAX_DIMENSION {
|
||
return Err(invalid("circle number exceeds bounds"));
|
||
}
|
||
Ok(value)
|
||
};
|
||
let dx = x as f64 - parse(cx_value)?;
|
||
let dy = y as f64 - parse(cy_value)?;
|
||
let radius = parse(radius)?;
|
||
// 半径不能负;零半径只包含圆心,边界使用 <= 因此圆周也命中。
|
||
if radius < 0.0 {
|
||
return Err(invalid("circle radius must be nonnegative"));
|
||
}
|
||
dx * dx + dy * dy <= radius * radius
|
||
} else {
|
||
let [_, Value::Object(bounds)] = args else {
|
||
return Err(invalid("inside requires Bounds"));
|
||
};
|
||
let left = number(cx, *bounds, "x", true)?;
|
||
let top = number(cx, *bounds, "y", true)?;
|
||
let width = number(cx, *bounds, "width", false)?;
|
||
let height = number(cx, *bounds, "height", false)?;
|
||
// 矩形辅助同样使用半开右/下边界;这与 geometry::Rect 的闭区间 contains 不同,调用方应留意语义。
|
||
x >= left && y >= top && x < left + width && y < top + height
|
||
};
|
||
Ok(NativeResult::Return(Value::Bool(inside)))
|
||
}
|
||
}
|
||
|
||
/// 把本模块的类型、方法或能力注册到所属运行时上下文。
|
||
pub fn install(
|
||
vm: &mut Vm,
|
||
roots: &mut Vec<ObjectRef>,
|
||
desktop: ObjectRef,
|
||
block_type: ObjectRef,
|
||
function_type: ObjectRef,
|
||
) -> Result<(), VmError> {
|
||
let nominal = |name| {
|
||
crate::ui_bridge::desktop_type(vm.object_store(), name, desktop)
|
||
.map(AnnotationType::Nominal)
|
||
};
|
||
let mount = nominal("Mount")?;
|
||
let constraints = nominal("Constraints")?;
|
||
let bounds = nominal("Bounds")?;
|
||
let point = nominal("Point")?;
|
||
let measure = nominal("MeasureResult")?;
|
||
let arrangement = nominal("Arrangement")?;
|
||
let display = nominal("DisplayList")?;
|
||
// 把四个默认方法安装到 Block,源子类可覆盖;所有视觉协议声明 ReadOnly,允许派生结果分配而不修改模型。
|
||
for (name, query, input, returns) in [
|
||
("measure", Query::Measure, constraints, measure),
|
||
("arrange", Query::Arrange, bounds, arrangement),
|
||
("paint", Query::Paint, bounds, display),
|
||
("hitTest", Query::HitTest, point, AnnotationType::Bool),
|
||
] {
|
||
let function = vm.allocate(ContextNativeFunction::new(
|
||
name,
|
||
Method {
|
||
query,
|
||
desktop,
|
||
signature: CallSignature::exact(
|
||
vec![AnnotationType::Nominal(block_type), mount, input],
|
||
returns,
|
||
CallEffect::ReadOnly,
|
||
),
|
||
},
|
||
vec![desktop],
|
||
))?;
|
||
roots.push(function);
|
||
vm.attach_type(function, function_type)?;
|
||
vm.define_method(block_type, name, function)?;
|
||
}
|
||
let registry = object(&vm.get_attr(desktop, "types")?)?;
|
||
let point_type = object(
|
||
&vm.dict_get(registry, &Value::Text("Point".into()))?
|
||
.ok_or(invalid("missing Point type"))?,
|
||
)?;
|
||
// Point.inside 接收 Bounds,inCircle 接收圆心 x/y 和半径;签名都包含隐式 Point receiver。
|
||
for (name, circle) in [("inside", false), ("inCircle", true)] {
|
||
let function = vm.allocate(ContextNativeFunction::new(
|
||
name,
|
||
PointMethod {
|
||
expected: point_type,
|
||
circle,
|
||
signature: CallSignature::exact(
|
||
if circle {
|
||
vec![
|
||
point,
|
||
AnnotationType::Number,
|
||
AnnotationType::Number,
|
||
AnnotationType::Number,
|
||
]
|
||
} else {
|
||
vec![point, bounds]
|
||
},
|
||
AnnotationType::Bool,
|
||
CallEffect::ReadOnly,
|
||
),
|
||
},
|
||
vec![point_type],
|
||
))?;
|
||
roots.push(function);
|
||
vm.attach_type(function, function_type)?;
|
||
vm.define_method(point_type, name, function)?;
|
||
}
|
||
Ok(())
|
||
}
|