161 lines
7.2 KiB
Rust
161 lines
7.2 KiB
Rust
//! 有限布局盒子的尺寸策略。区分 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],
|
||
)?,
|
||
})
|
||
}
|
||
}
|