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

161 lines
7.2 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.
//! 有限布局盒子的尺寸策略。区分 auto、固定值和 fill,并处理 min/max 对约束与最终尺寸的限制。
//! margin、border、padding 的空间分配由布局层结合本模块完成;场景世界范围不被当成无限布局盒子。
//!
//! 调用路径:Block 属性解码使用 Dimension.parse,native_properties 用 value 公开当前策略。
//! layout 的测量/分配入口调用 available 收窄内容约束,再用 resolve 结合自然尺寸求边框盒大小。
//! 本模块只处理数值策略,不读 VM 或安排 Mount 生命周期;min/max 一致性由模型提取预先检查。
//!
//! Native border-box sizing. An unbounded axis cannot resolve `fill`.
use crate::{layout::Size, ui_bridge::MAX_DIMENSION};
use anyhow::Result;
use endor_core::{Value, VmError};
#[derive(Debug, Clone, Copy, Default, PartialEq)]
/// 一个布局轴的尺寸策略:随内容、固定值或占用有限可用空间。
pub enum Dimension {
#[default]
// 从自然内容尺寸求候选值;不是占满父轴的同义词。
Auto,
// 显式边框盒长度,最终仍会受自身 min/max 与父可用上限限制。
Fixed(f32),
// 使用父级有限分配上限,父轴无界时必须报错,不能生成无限几何。
Fill,
}
impl Dimension {
/// 把内部尺寸策略投影为相应的源属性表示。
pub fn value(self) -> Value {
// 把内部枚举编码为源字段值,保留 auto/fill 字符串与固定数值的可区分性。
match self {
Self::Auto => Value::Text("auto".into()),
Self::Fill => Value::Text("fill".into()),
Self::Fixed(value) => Value::Number((value as f64).into()),
}
}
/// 把外部源码或属性表示转换为内部枚举/结构,并反馈不支持的输入。
pub fn parse(value: &Value) -> Result<Self, VmError> {
// 仅接受约定文本或真实 Number,错误类型/未知字符串不做隐式转换。
match value {
Value::Text(text) if text == "auto" => Ok(Self::Auto),
Value::Text(text) if text == "fill" => Ok(Self::Fill),
Value::Number(_) => Ok(Self::Fixed(crate::scroll::coordinate(value).and_then(
|n| {
// coordinate 已检查有限性与绝对值预算,盒尺寸再额外排除负数。
if n >= 0.0 {
Ok(n)
} else {
Err(VmError::InvalidArgument(
"box dimensions must be nonnegative",
))
}
},
)?)),
_ => Err(VmError::InvalidArgument(
"box dimension expects auto, fill or a nonnegative number",
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
/// border-box 尺寸策略与双轴上下限,实际求解仍要结合父级约束。
pub struct Sizing {
// 横轴 auto/fixed/fill 策略,按边框盒而非内容盒解释。
pub width: Dimension,
// 纵轴独立尺寸策略,不能沿用横轴是否有限的判断。
pub height: Dimension,
// 横轴自身下限,父可用宽更小时仍以父上限优先。
pub min_width: f32,
// 纵轴自身下限,单位为逻辑长度。
pub min_height: f32,
// 横轴自身上限,available 在内容测量前应用以产生正确文字换行。
pub max_width: f32,
// 纵轴自身上限,默认 MAX_DIMENSION 同时作为无界分配哨兵。
pub max_height: f32,
}
impl Default for Sizing {
/// 建立类型约定的初始值,供尚未发布或尚未交互的状态使用。
fn default() -> Self {
// 默认跟随内容,零下限与全局最大值不提前冻结自然尺寸;构造器可随后覆盖。
Self {
width: Dimension::Auto,
height: Dimension::Auto,
min_width: 0.0,
min_height: 0.0,
max_width: MAX_DIMENSION as f32,
max_height: MAX_DIMENSION as f32,
}
}
}
impl Sizing {
/// Constrain descendants using the actual finite box before wrapping text.
/// 将 [最小宽, 最小高, 最大宽, 最大高] 收窄,供子内容测量和文本换行使用。
/// 固定尺寸限制最大可用空间;最后缩小最小约束以保持 min <= max。这里不直接计算最终盒子。
pub fn available(self, mut constraints: [f32; 4]) -> [f32; 4] {
// 先套自身 max,再应用 Fixed;Auto/Fill 在测量阶段保留父级允许的空间。
constraints[2] = constraints[2].min(self.max_width);
constraints[3] = constraints[3].min(self.max_height);
// 固定宽先抬到自身 min_width,再收紧父最大宽,保证文本按实际可用盒宽测量。
if let Dimension::Fixed(width) = self.width {
constraints[2] = constraints[2].min(width.max(self.min_width));
}
// 纵轴独立应用同一规则;不改变另一轴策略,也不在此填充 Fill。
if let Dimension::Fixed(height) = self.height {
constraints[3] = constraints[3].min(height.max(self.min_height));
}
constraints[0] = constraints[0].min(constraints[2]);
constraints[1] = constraints[1].min(constraints[3]);
constraints
}
/// 结合内容期望尺寸、轴策略和父级约束求最终盒子尺寸;无界轴不能解析 fill。
pub fn resolve(self, intrinsic: Size, constraints: [f32; 4]) -> Result<Size> {
/// 针对单个尺寸轴应用约束与边界规则,供双轴尺寸求解复用。
fn axis(
policy: Dimension,
intrinsic: f32,
min: f32,
max: f32,
available: f32,
) -> Result<f32> {
// 先按策略选候选值:Auto 用内容测量值,Fixed 用指定值,Fill 用父级分配上限。
let value = match policy {
Dimension::Auto => intrinsic,
Dimension::Fixed(value) => value,
Dimension::Fill => {
// MAX_DIMENSION 充当无界哨兵;无界空间没有可填满的有限尺寸,必须拒绝。
if available >= MAX_DIMENSION as f32 {
return Err(VmError::InvalidArgument(
"fill requires a finite parent allocation on this axis",
)
.into());
}
available
}
};
// 先满足自身 min/max,再服从父级上限;父级空间比 min 小时,父级上限优先。
Ok(value.clamp(min, max).min(available))
}
// 横纵轴分别求解,只取父最大分配 constraints[2/3];输入的最小约束不在此额外强加。
Ok(Size {
width: axis(
self.width,
intrinsic.width,
self.min_width,
self.max_width,
constraints[2],
)?,
height: axis(
self.height,
intrinsic.height,
self.min_height,
self.max_height,
constraints[3],
)?,
})
}
}