diff --git a/src/render/mod.rs b/src/render/mod.rs index a7999af..0d4d7f8 100644 --- a/src/render/mod.rs +++ b/src/render/mod.rs @@ -1,4 +1,7 @@ //! 渲染模块入口。排版、源视觉协议、相机投影和最终光栅化各自独立,通过不可变帧衔接。 +//! 调用链:model/snapshot 提供不可变 UiNode,layout/source_layout 求几何,source_paint 产出发布命令。 +//! document 转成 display_list 后由 software_renderer 光栅化;scene 组合窗口壳层,viewport/scroll 与输入复用坐标规则。 +//! main.rs 的 crate 根重导出保留现有内部路径;image/text_flow 另以 render 子模块路径供桥接调用。 //! // 目录确定模块归属;main 中的 crate 根别名保持内部调用路径稳定。 /// 计算边框、内外边距和内容区域,并应用容器主轴/交叉轴分配规则。 @@ -32,5 +35,7 @@ pub(crate) mod text; /// 处理二维场景相机、局部视口投影和空间查询,供绘制与命中使用相同变换。 pub(crate) mod viewport; +/// 解码与缓存不可变 RGBA 资源、预渲染文字,并提供带 alpha 的纹理采样与轮廓。 pub mod image; +/// 以预备字词图片和避让轮廓安排自由文字排版,几何与输出绘制资源分开保留。 pub mod text_flow; diff --git a/src/render/native_visual.rs b/src/render/native_visual.rs index 9ef8ccc..69d1bcf 100644 --- a/src/render/native_visual.rs +++ b/src/render/native_visual.rs @@ -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 { + // 协议 receiver/输入必须为真实托管对象,不能接受仅可字符串化的标量。 if let Value::Object(object) = value { Ok(*object) } else { @@ -34,9 +39,11 @@ pub(crate) fn number( name: &str, signed: bool, ) -> Result { + // 所有几何字段从当前属性存储读取,类型必须为 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::() @@ -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 Result() 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::(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 { + // 取隐式 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,避免距离平方过早损失到 f32 精度。 let value = value .to_string() .parse::() .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, diff --git a/src/render/page.rs b/src/render/page.rs index cc6b1d1..57bb09a 100644 --- a/src/render/page.rs +++ b/src/render/page.rs @@ -1,6 +1,10 @@ //! 旧 Page.mode 的兼容空间策略,以及页面级的正逆相机变换。 //! 新组合应优先使用普通布局、ScrollView 和独立场景视口;保留这里是为了兼容已有 window/vertical/horizontal/canvas 页面。 //! +//! 调用路径:Block.mode 属性解码用 Mode.parse;根布局按 Mode.constraints 选择有限/可延伸轴。 +//! scene/输入导航使用 Surface 的正逆变换和可见性判断,内容变化后用 clamp 收敛兼容页面偏移。 +//! 本模块不创建窗口、不修改 VM;Page 表面导航与独立 CanvasViewport/ScrollBlock 是不同状态。 +//! //! Page space is independent of the native window. The camera is applied once //! for painting and inverted once for input; layout and Mounts stay in world units. use crate::{geometry::Point, layout::Size}; @@ -25,6 +29,7 @@ pub enum Mode { impl Mode { /// 返回内部枚举或对象对应的稳定源名称。 pub fn name(self) -> &'static str { + // 源字段编码与序列化采用同一组小写策略名,布局方向不由显示标题推断。 match self { Self::Window => "window", Self::Vertical => "vertical", @@ -35,6 +40,7 @@ impl Mode { /// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。 pub fn parse(name: &str) -> Result { + // 仅接受四个兼容模式,未知值明确拒绝,不能默认为 window。 match name { "window" => Ok(Self::Window), "vertical" => Ok(Self::Vertical), @@ -52,6 +58,7 @@ impl Mode { let Some(size) = viewport else { return crate::layout::root_constraints(None); }; + // MAX_DIMENSION 在根约束中充当可延伸轴上限,不把它当作真实窗口尺寸。 let limit = crate::ui_bridge::MAX_DIMENSION as f32; // 数组采用 [最小宽, 最小高, 最大宽, 最大高];竖/横页面仍至少占满当前视口。 match self { @@ -66,12 +73,14 @@ impl Mode { #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] /// 页面级兼容相机状态和有限内容范围,支持绘制与命中正逆变换。 pub struct Surface { + // 兼容 Page 空间策略,决定根约束及偏移夹取规则。 pub mode: Mode, // 发布给原生侧的页面身份文本,不是托管强引用。 pub page: String, // 视图左上角对应的世界坐标;绘制时先减 pan 再乘 zoom。 pub pan: Point, #[serde(default = "unit")] + // 世界到视图的均匀比例,反变换要求非零有限;模型/文档入口负责合法范围。 pub zoom: f32, // 布局产出的内容宽高,用于有限模式钳制;不决定 Canvas 的导航边界。 pub extent: Size, @@ -85,6 +94,7 @@ fn unit() -> f32 { impl Default for Surface { /// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。 fn default() -> Self { + // 初始 Window 表面无页面身份、零偏移且单位缩放,可作为合法空发布值。 Self { mode: Mode::Window, page: String::new(), @@ -127,6 +137,7 @@ impl Surface { pub fn contains(&self, point: [f32; 2], viewport: Option) -> bool { // 传入的数组是世界坐标。转换后使用左/上包含、右/下排除的视口边界; // 未知视口时仍排除负坐标,但不限制右/下界。 + // 传入世界坐标先投影到视图空间,再按视口半开边界检查,不能与 Rect.contains 的闭边界混淆。 let point = self.to_view(Point::new(point[0], point[1])); point.x >= 0.0 && point.y >= 0.0 @@ -138,10 +149,12 @@ impl Surface { // Window 重置所有导航;单轴模式清除另一轴并恢复单位缩放。 // 内容小于视口时 max(0) 令可滚动范围归零,避免 clamp 出现反向上下限。 match self.mode { + // 普通窗口页面关闭导航能力,每次夹取恢复零 pan 与 zoom=1。 Mode::Window => { self.pan = Point::default(); self.zoom = 1.0; } + // 纵向模式仅允许 y 在内容高−视口高内移动,清横向偏移并禁用缩放。 Mode::Vertical => { self.zoom = 1.0; self.pan.x = 0.0; @@ -150,6 +163,7 @@ impl Surface { .y .clamp(0.0, (self.extent.height - viewport.height).max(0.0)); } + // 横向模式同理仅允许 x 移动,内容缩短后最大偏移随之减少。 Mode::Horizontal => { self.zoom = 1.0; self.pan.y = 0.0; @@ -158,6 +172,7 @@ impl Surface { .x .clamp(0.0, (self.extent.width - viewport.width).max(0.0)); } + // 自由画布不按内容边缘夹取,保留可查看负世界坐标与空白区域的导航。 Mode::Canvas => {} } } @@ -174,9 +189,12 @@ mod tests { let mut view = Surface::new(Mode::Canvas); view.pan = Point::new(-240.0, 120.0); view.zoom = 0.75; + // 固定视图指针位置作为缩放锚点,计算缩放前它对应的世界点。 let pointer = Point::new(300.0, 180.0); + // 保存锚点世界位置,改变 zoom 后反求 pan,保证 to_view(world) 仍等于 pointer。 let world = view.to_world(pointer); assert_eq!(view.to_view(world), pointer); + // 模拟指针锚定缩放而不是绕世界原点缩放,随后检查可见与不可见点。 view.zoom = 2.0; view.pan = Point::new( world.x - pointer.x / view.zoom, @@ -214,6 +232,7 @@ mod tests { height: 600.0, }); assert_eq!(view.pan, Point::new(0.0, 200.0)); + // 内容缩到视口之内后重新夹取,旧 y=200 必须回零,不能继续显示内容外空白。 view.extent.height = 100.0; view.clamp(Size { width: 400.0,