docs: explain native visual protocols and legacy page coordinate rules
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
//! 让原生组件也通过 Endor 的 measure/arrange/paint 协议被调用。
|
||||
//! 将 Constraints、Size、Bounds 与 PaintCommand 在托管值和 Rust 值之间转换;原生方法与源覆盖方法共用调用契约。
|
||||
//!
|
||||
//! 调用路径:bootstrap 调用 install 安装 Block 默认协议与 Point 辅助方法。
|
||||
//! 源子类可显式调用基类方法;source_layout 在 staged Context 中复用已测孩子,独立调用则运行完整原生布局。
|
||||
//! is_intrinsic 供协议驱动判断能否跳过 VM 调用;仅认可本实现及同一名义类型注册表,不能凭函数名判定。
|
||||
//!
|
||||
//! 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::{
|
||||
@@ -21,6 +25,7 @@ fn invalid(message: &'static str) -> VmError {
|
||||
}
|
||||
/// 从源值中提取并检查托管对象身份,错误类型沿 VM 错误返回。
|
||||
fn object(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
// 协议 receiver/输入必须为真实托管对象,不能接受仅可字符串化的标量。
|
||||
if let Value::Object(object) = value {
|
||||
Ok(*object)
|
||||
} else {
|
||||
@@ -34,9 +39,11 @@ pub(crate) fn number(
|
||||
name: &str,
|
||||
signed: bool,
|
||||
) -> Result<f32, VmError> {
|
||||
// 所有几何字段从当前属性存储读取,类型必须为 Number,不在这里尝试源函数或转换运算。
|
||||
let Value::Number(value) = cx.store().get_attr(object, name)? else {
|
||||
return Err(invalid("visual field must be a Number"));
|
||||
};
|
||||
// 转到布局实际使用的 f32 后再检查有限/范围,防止大数溢出为无穷。
|
||||
let value = value
|
||||
.to_string()
|
||||
.parse::<f32>()
|
||||
@@ -62,6 +69,7 @@ pub(crate) fn record(
|
||||
// 先赋精确名义类型,再初始化字段;只读协议可创建结果对象,但不能借机写入旧业务图。
|
||||
let object = cx.new_object()?;
|
||||
attach_desktop_type(cx, object, kind, desktop)?;
|
||||
// 新结果记录在本次调用内逐字段初始化,VM 可区分派生对象写入与旧业务图修改。
|
||||
for (key, value) in fields {
|
||||
cx.set_attr(object, key, value)?;
|
||||
}
|
||||
@@ -78,6 +86,7 @@ fn size(node: &crate::ui_bridge::UiNode, constraints: [f32; 4]) -> Result<Size,
|
||||
},
|
||||
)
|
||||
.map_err(|_| invalid("native measurement exceeds layout limits"))?;
|
||||
// 只取已求出的根出现尺寸,空布局不是合法测量返回,明确报错。
|
||||
let root = frame
|
||||
.mounts
|
||||
.first()
|
||||
@@ -90,16 +99,23 @@ fn size(node: &crate::ui_bridge::UiNode, constraints: [f32; 4]) -> Result<Size,
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
/// 该原生能力的查询/操作分类,方法包装用它选择具体实现。
|
||||
enum Query {
|
||||
// 按 Constraints 返回本出现的自然/受限 MeasureResult。
|
||||
Measure,
|
||||
// 按 Bounds 和孩子测量返回带 slot 的 Arrangement。
|
||||
Arrange,
|
||||
// 把默认原生绘制编码为源可组合的 DisplayList。
|
||||
Paint,
|
||||
// 按本出现已提交的宽高检查局部 Point,返回 Bool。
|
||||
HitTest,
|
||||
}
|
||||
#[derive(Debug)]
|
||||
/// 保存 owner、签名或操作枚举的原生方法适配器,统一由 VM 调用。
|
||||
struct Method {
|
||||
// 操作分类决定输入/返回契约与分派实现。
|
||||
query: Query,
|
||||
// 名义类型注册环境,native captures 保活它;同 VM 窗口可共享 types。
|
||||
desktop: ObjectRef,
|
||||
// 含 Block receiver、Mount 和协议输入的三参只读契约。
|
||||
signature: CallSignature,
|
||||
}
|
||||
/// Only this actual implementation can use the matching intrinsic fast path.
|
||||
@@ -119,6 +135,7 @@ pub(crate) fn is_intrinsic(
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
// 即使是 ContextNativeFunction,也必须确为本 Method 实现,其他 native payload 不享受该快速路径。
|
||||
let Some(method) = function.implementation::<Method>() else {
|
||||
return Ok(false);
|
||||
};
|
||||
@@ -155,7 +172,9 @@ impl NativeCallable for Method {
|
||||
let [owner, context, input] = args else {
|
||||
return Err(invalid("visual method expects a Mount and an input value"));
|
||||
};
|
||||
// receiver 必须为对象,后面的 Mount.block 归属检查使用同一原始业务身份。
|
||||
let owner = object(owner)?;
|
||||
// 从显式上下文对象取得真实 Mount,不凭 Bounds 形状推断出现关系。
|
||||
let mount = cx.store().payload::<Mount>(object(context)?, "Mount")?;
|
||||
// 默认协议也校验上下文归属,防止调用者把另一个出现的几何套到当前组件。
|
||||
if !mount.mounted || mount.block != owner {
|
||||
@@ -163,12 +182,14 @@ impl NativeCallable for Method {
|
||||
"visual context is detached or belongs to another Block",
|
||||
));
|
||||
}
|
||||
// 复制最后提交几何,只有非 staged 的命中/独立调用使用这份出现值。
|
||||
let geometry = mount.geometry.clone();
|
||||
// 布局阶段从临时 Context 取当前节点、孩子实测尺寸与首选尺寸;克隆派生数据后释放锁再进行计算。
|
||||
let staged = mount
|
||||
.layout
|
||||
.as_ref()
|
||||
.map(|layout| {
|
||||
// 锁内只克隆当前节点和孩子尺寸,源/原生计算期间不持临时布局 Context 锁。
|
||||
let layout = layout
|
||||
.lock()
|
||||
.map_err(|_| invalid("layout context poisoned"))?;
|
||||
@@ -179,13 +200,18 @@ impl NativeCallable for Method {
|
||||
))
|
||||
})
|
||||
.transpose()?;
|
||||
// 第三参数须为对象,具体名义输入由安装的签名约束,字段数值在各分支继续检查。
|
||||
let input = object(input)?;
|
||||
// 默认 hitTest 检查局部点在已提交盒内,采用左上包含、右下排除;布局阶段几何不稳定,禁止调用。
|
||||
// 先处理只依赖已提交局部盒的默认命中,成功后无需构建视觉树或结果记录。
|
||||
if matches!(self.query, Query::HitTest) {
|
||||
// 布局期间可能有临时尺寸,默认 hitTest 不能把它当作最终可交互几何。
|
||||
if staged.is_some() {
|
||||
return Err(invalid("hitTest is unavailable during layout"));
|
||||
}
|
||||
// 默认 hitTest 的 Point 为本出现局部坐标,允许带符号以自然排除盒外点。
|
||||
let x = number(cx, input, "x", true)?;
|
||||
// 纵坐标独立解析,与 x 一起采用右/下排除的半开盒范围。
|
||||
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,
|
||||
@@ -203,11 +229,17 @@ impl NativeCallable for Method {
|
||||
} else {
|
||||
block::snapshot(cx.store(), owner)?
|
||||
};
|
||||
// measure/arrange/paint 产生本域名义结果记录,几何从不写回共享 Block。
|
||||
let result = match self.query {
|
||||
// 读取并验证四项约束,staged 复用孩子测量,非 staged 走完整原生测量。
|
||||
Query::Measure => {
|
||||
// 横轴最小约束非负有限,与 maxWidth 配对检查顺序。
|
||||
let min_w = number(cx, input, "minWidth", false)?;
|
||||
// 纵轴最小约束非负有限,不能从横向约束推断。
|
||||
let min_h = number(cx, input, "minHeight", false)?;
|
||||
// 横轴最大可用宽影响文字换行与子项分配。
|
||||
let max_w = number(cx, input, "maxWidth", false)?;
|
||||
// 纵轴最大可用高与 minHeight 组合,非法区间拒绝而非交换两端。
|
||||
let max_h = number(cx, input, "maxHeight", false)?;
|
||||
// 最小值不可超过最大值,否则后面的 clamp 会无意义甚至失败;各数值已通过非负有限检查。
|
||||
if min_w > max_w || min_h > max_h {
|
||||
@@ -231,11 +263,17 @@ impl NativeCallable for Method {
|
||||
],
|
||||
)?)
|
||||
}
|
||||
// 排列输入 Bounds 使用绝对布局位置和实际分配大小,结果按稳定 slot 标识孩子。
|
||||
Query::Arrange => {
|
||||
// 父布局框横原点可负,需受有限世界范围约束。
|
||||
let x = number(cx, input, "x", true)?;
|
||||
// 父布局框纵原点可负,和高度共同验证下端点。
|
||||
let y = number(cx, input, "y", true)?;
|
||||
// 分配宽必须非负,不能把负宽当作镜像变换。
|
||||
let width = number(cx, input, "width", false)?;
|
||||
// 分配高必须非负,两轴不依赖旧 Mount 自然尺寸。
|
||||
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"));
|
||||
}
|
||||
@@ -273,11 +311,14 @@ impl NativeCallable for Method {
|
||||
// 先依据父盒分配 flex 尺寸,再由 arrange_native 求位置,避免先定位再改变孩子大小导致重叠。
|
||||
let children =
|
||||
layout::flex_sizes(&node, Size { width, height }, &children, preferred);
|
||||
// 尺寸求解完成后再排列位置,返回顺序对应 children/slots 三者的同一索引。
|
||||
let points = layout::arrange_native(&node, x, y, Size { width, height }, &children)
|
||||
.map_err(|_| invalid("native arrangement exceeds limits"))?;
|
||||
// 收集每个孩子的新 Bounds 记录,最后包装为受控 ObjectList。
|
||||
let mut result = Vec::new();
|
||||
// 每个返回 Bounds 附稳定 slot,与 children 当前顺序分开;调用方可按 slot 校验组成身份。
|
||||
for ((slot, size), (x, y)) in node.slots.iter().zip(children).zip(points) {
|
||||
// 每条返回记录只描述当前孩子的分配,另附 slot 供源布局驱动验证归属。
|
||||
let bounds = record(
|
||||
cx,
|
||||
self.desktop,
|
||||
@@ -301,17 +342,25 @@ impl NativeCallable for Method {
|
||||
vec![("children", Value::Object(result))],
|
||||
)?)
|
||||
}
|
||||
// 把默认绘制原生命令编码回源 PaintCommand,供源覆盖调用基类后追加或改新结果。
|
||||
Query::Paint => {
|
||||
// 本次 paint 使用给定布局 x,不把共享业务 x 当成最终出现位置。
|
||||
let x = number(cx, input, "x", true)?;
|
||||
// 本次 paint 使用给定布局 y,投影到面板/设备空间在后续显示阶段处理。
|
||||
let y = number(cx, input, "y", true)?;
|
||||
// 绘制宽是当前出现分配的非负宽度,与其他共享出现可以不同。
|
||||
let width = number(cx, input, "width", false)?;
|
||||
// 绘制高独立解析,结合 y 检查远端上界。
|
||||
let height = number(cx, input, "height", false)?;
|
||||
// 绘制输入也执行与 arrange 相同的正向端点预算检查。
|
||||
if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 {
|
||||
return Err(invalid("visual bounds exceed limits"));
|
||||
}
|
||||
// 本次 paint 的派生结果集合,不能包含运行时可变 VM 场景对象。
|
||||
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,
|
||||
@@ -321,6 +370,7 @@ impl NativeCallable for Method {
|
||||
view,
|
||||
} => {
|
||||
// 3D 通过不可变 PaintView payload 暴露场景快照,不把运行中 VM 的场景对象交给后续绘制线程。
|
||||
// PaintView payload 包装已拥有的原生 View,后续源解码无需从渲染线程读托管场景。
|
||||
let snapshot = cx.allocate(crate::scene3d::PaintView(view))?;
|
||||
vec![
|
||||
("kind", Value::Text("scene3d".into())),
|
||||
@@ -339,6 +389,7 @@ impl NativeCallable for Method {
|
||||
color,
|
||||
wrap_bounds,
|
||||
} => {
|
||||
// 先写文字必需字段,只有存在 wrap_bounds 才成对添加 wrapWidth/wrapHeight。
|
||||
let mut fields = vec![
|
||||
("kind", Value::Text("text".into())),
|
||||
("x", value(x)),
|
||||
@@ -373,9 +424,11 @@ impl NativeCallable for Method {
|
||||
// 当前反向编码支持明确列出的命令;新增 RenderCommand 变体时须同步此转换,不能静默丢弃。
|
||||
_ => return Err(invalid("unsupported native paint command")),
|
||||
};
|
||||
// 给每条命令附本域 PaintCommand 类型,字段数据仍只属于本次结果。
|
||||
let command = record(cx, self.desktop, "PaintCommand", fields)?;
|
||||
commands.push(command);
|
||||
}
|
||||
// 先生成命令对象列表,再放进名义 DisplayList 记录的 commands 字段。
|
||||
let commands = crate::object_list::collection(cx, commands, self.desktop)?;
|
||||
Value::Object(record(
|
||||
cx,
|
||||
@@ -393,8 +446,11 @@ impl NativeCallable for Method {
|
||||
#[derive(Debug)]
|
||||
/// Point 的矩形/圆形命中辅助方法,保存结果类型和具体几何判定种类。
|
||||
struct PointMethod {
|
||||
// Point 的当前桌面名义类型,用于检查 receiver,而非仅比较 x/y 字段。
|
||||
expected: ObjectRef,
|
||||
// true 选择圆内距离,否则选择 Bounds 半开矩形包含判断。
|
||||
circle: bool,
|
||||
// 两个辅助方法均为只读 Bool 返回契约,参数形状由 circle 决定。
|
||||
signature: CallSignature,
|
||||
}
|
||||
impl NativeCallable for PointMethod {
|
||||
@@ -413,6 +469,7 @@ impl NativeCallable for PointMethod {
|
||||
|
||||
/// 先验证 Point 名义类型与有限坐标,再分别计算圆内距离或 Bounds 包含关系,返回 Bool。
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
// 取隐式 Point receiver,错误实参形状在字段读取之前拒绝。
|
||||
let Some(Value::Object(point)) = args.first() else {
|
||||
return Err(invalid("Point method requires a Point"));
|
||||
};
|
||||
@@ -420,9 +477,13 @@ impl NativeCallable for PointMethod {
|
||||
if !cx.store().is_instance(*point, self.expected)? {
|
||||
return Err(invalid("Point method requires a Point"));
|
||||
}
|
||||
// 先读并验证 receiver 的有限 x,绝对值不超过视觉世界范围。
|
||||
let x = number(cx, *point, "x", true)?;
|
||||
// y 使用同样规则,后续圆距离再提升到 f64 运算。
|
||||
let y = number(cx, *point, "y", true)?;
|
||||
// 按适配器分类检查圆心/半径或 Bounds,最终统一返回 Bool。
|
||||
let inside = if self.circle {
|
||||
// 圆分支要求四项实际参数,receiver 已在前面检查,另外三项必须为 Number。
|
||||
let [
|
||||
_,
|
||||
Value::Number(cx_value),
|
||||
@@ -434,17 +495,22 @@ impl NativeCallable for PointMethod {
|
||||
};
|
||||
// 圆心和半径按 f64 解析及求平方距离,在有界数值内避免不必要的 f32 精度损失。
|
||||
let parse = |value: &endor_core::EndorNumber| -> Result<f64, VmError> {
|
||||
// 圆心/半径转 f64,避免距离平方过早损失到 f32 精度。
|
||||
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)
|
||||
};
|
||||
// Point.x 到圆心横差,以 f64 求平方距离。
|
||||
let dx = x as f64 - parse(cx_value)?;
|
||||
// Point.y 到圆心纵差,不能在圆形测试中误用矩形裁剪规则。
|
||||
let dy = y as f64 - parse(cy_value)?;
|
||||
// 半径先按通用圆数值规则解析,再排除负数;零只包含圆心。
|
||||
let radius = parse(radius)?;
|
||||
// 半径不能负;零半径只包含圆心,边界使用 <= 因此圆周也命中。
|
||||
if radius < 0.0 {
|
||||
@@ -452,12 +518,17 @@ impl NativeCallable for PointMethod {
|
||||
}
|
||||
dx * dx + dy * dy <= radius * radius
|
||||
} else {
|
||||
// 矩形分支必须正好两参,Bounds 名义类型由安装签名检查,字段数值仍逐项验证。
|
||||
let [_, Value::Object(bounds)] = args else {
|
||||
return Err(invalid("inside requires Bounds"));
|
||||
};
|
||||
// 边框左原点可负,Point 坐标必须与 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
|
||||
@@ -474,16 +545,24 @@ pub fn install(
|
||||
block_type: ObjectRef,
|
||||
function_type: ObjectRef,
|
||||
) -> Result<(), VmError> {
|
||||
// bootstrap 已创建视觉值类型,以下只取得当前桌面的名义身份来构造契约。
|
||||
let nominal = |name| {
|
||||
crate::ui_bridge::desktop_type(vm.object_store(), name, desktop)
|
||||
.map(AnnotationType::Nominal)
|
||||
};
|
||||
// 第二参数固定为 Mount,协议不能只拿 Block 或任意 Bounds 冒充上下文。
|
||||
let mount = nominal("Mount")?;
|
||||
// measure 输入约束记录的名义类型。
|
||||
let constraints = nominal("Constraints")?;
|
||||
// arrange/paint 输入边框盒的名义类型,Point.inside 也使用它。
|
||||
let bounds = nominal("Bounds")?;
|
||||
// hitTest 输入局部 Point,后续 Point 辅助方法 receiver 也用该注解。
|
||||
let point = nominal("Point")?;
|
||||
// measure 返回 MeasureResult,源覆写可返回其更具体子类。
|
||||
let measure = nominal("MeasureResult")?;
|
||||
// arrange 返回 Arrangement,孩子 Bounds 经受控列表持有。
|
||||
let arrangement = nominal("Arrangement")?;
|
||||
// paint 返回源 DisplayList,区别于窗口线程消费的原生显示列表容器。
|
||||
let display = nominal("DisplayList")?;
|
||||
// 把四个默认方法安装到 Block,源子类可覆盖;所有视觉协议声明 ReadOnly,允许派生结果分配而不修改模型。
|
||||
for (name, query, input, returns) in [
|
||||
@@ -492,6 +571,7 @@ pub fn install(
|
||||
("paint", Query::Paint, bounds, display),
|
||||
("hitTest", Query::HitTest, point, AnnotationType::Bool),
|
||||
] {
|
||||
// 每个默认协议函数 captures 保活桌面环境,方法表再持有该函数。
|
||||
let function = vm.allocate(ContextNativeFunction::new(
|
||||
name,
|
||||
Method {
|
||||
@@ -509,19 +589,23 @@ pub fn install(
|
||||
vm.attach_type(function, function_type)?;
|
||||
vm.define_method(block_type, name, function)?;
|
||||
}
|
||||
// Point 辅助方法安装在同一桌面的真实类型注册表上。
|
||||
let registry = object(&vm.get_attr(desktop, "types")?)?;
|
||||
// 取得具体 Point Type 对象,用作 expected receiver 及 define_method 目标。
|
||||
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)] {
|
||||
// 分别建立 inside/inCircle 的参数契约与适配器,captures 保活 Point 类型。
|
||||
let function = vm.allocate(ContextNativeFunction::new(
|
||||
name,
|
||||
PointMethod {
|
||||
expected: point_type,
|
||||
circle,
|
||||
signature: CallSignature::exact(
|
||||
// 圆测试为 receiver+三数,矩形测试为 receiver+Bounds,参数元数据与 PointMethod.arity 一致。
|
||||
if circle {
|
||||
vec![
|
||||
point,
|
||||
|
||||
Reference in New Issue
Block a user