Files
endorphin-tessera-gui/src/render/native_visual.rs
T

547 lines
24 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 让原生组件也通过 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(())
}