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

136 lines
4.8 KiB
Rust

//! 有限布局盒子的尺寸策略。区分 auto、固定值和 fill,并处理 min/max 对约束与最终尺寸的限制。
//! margin、border、padding 的空间分配由布局层结合本模块完成;场景世界范围不被当成无限布局盒子。
//!
//! 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,
Fixed(f32),
Fill,
}
impl Dimension {
/// 把内部尺寸策略投影为相应的源属性表示。
pub fn value(self) -> Value {
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> {
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| {
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 {
pub width: Dimension,
pub height: Dimension,
pub min_width: f32,
pub min_height: f32,
pub max_width: f32,
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.
/// 根据固定/auto/fill 与 min/max 规则细化父级提供的可用约束。
pub fn available(self, mut constraints: [f32; 4]) -> [f32; 4] {
constraints[2] = constraints[2].min(self.max_width);
constraints[3] = constraints[3].min(self.max_height);
if let Dimension::Fixed(width) = self.width {
constraints[2] = constraints[2].min(width.max(self.min_width));
}
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> {
let value = match policy {
Dimension::Auto => intrinsic,
Dimension::Fixed(value) => value,
Dimension::Fill => {
if available >= MAX_DIMENSION as f32 {
return Err(VmError::InvalidArgument(
"fill requires a finite parent allocation on this axis",
)
.into());
}
available
}
};
Ok(value.clamp(min, max).min(available))
}
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],
)?,
})
}
}