///|
/// 布局求解:两遍法(先解尺寸、后解位置),见 docs/03。
///
/// 失败 ≠ 崩溃:求解器永远产出有限的矩形;解不开的约束记入 failures,
/// 由谓词层(predicates.mbt)定级为“约束系统无解”。这个接口稳定,
/// 未来可把实现换成 Cassowary / SMT 求解器而不影响验证协议。
pub(all) enum SolveFailure {
MissingRoot
MissingNode(String)
NegativeResidual(String, Double) // 容器 id,剩余主轴空间(负值)
UnsupportedSpec(String) // v1 求解器不支持的规格组合
}
///|
pub fn SolveFailure::describe(self : SolveFailure) -> String {
match self {
MissingRoot => "根节点缺失,无法求解"
MissingNode(id) => "布局引用了不存在的节点:\{id}"
NegativeResidual(id, r) =>
"容器 \{id} 可用主轴空间不足,缺口 \{r}"
UnsupportedSpec(id) =>
"节点 \{id} 的尺寸规格在 v1 求解器下无定义(Hug 仅 Text 支持,根节点须 Fixed)"
}
}
///|
pub(all) struct SolvedLayout {
rects : Map[String, Rect]
failures : Array[SolveFailure]
}
///|
pub fn SolvedLayout::rect_of(self : SolvedLayout, id : String) -> Rect? {
self.rects.get(id)
}
///|
/// 求解入口:输入文档,输出每个节点的画布绝对矩形 + 求解失败列表
pub fn solve(doc : Document) -> SolvedLayout {
let rects : Map[String, Rect] = Map::new()
let failures : Array[SolveFailure] = Array::new()
match doc.nodes.get(doc.root_id) {
None => failures.push(MissingRoot)
Some(root) => {
let w = match root.width {
Fixed(v) => v
_ => 0.0
}
let h = match root.height {
Fixed(v) => v
_ => 0.0
}
let root_ok = match (root.width, root.height) {
(Fixed(_), Fixed(_)) => true
_ => false
}
if !root_ok {
failures.push(UnsupportedSpec(root.id))
}
solve_node(doc, root, Rect::new(0.0, 0.0, w, h), rects, failures)
}
}
{ rects, failures, }
}
///|
fn solve_node(
doc : Document,
node : Node,
frame : Rect,
rects : Map[String, Rect],
failures : Array[SolveFailure],
) -> Unit {
rects.set(node.id, frame)
// 布局仅对 Frame 生效;其余种类一律按绝对定位处理其子节点
let layout_opt : StackLayout? = match node.kind {
Frame => node.layout
_ => None
}
match layout_opt {
Some(l) => solve_stack(doc, node, l, frame, rects, failures)
None => solve_absolute(doc, node, frame, rects, failures)
}
}
///|
/// 内在尺寸:Hug 只支持 Text(v1 限制),其余记为求解失败并给 0
fn intrinsic_size(node : Node, failures : Array[SolveFailure]) -> Size {
match node.kind {
Text =>
text_intrinsic(
node.text,
node.style.font_size,
line_height=node.style.line_height,
)
_ => {
failures.push(UnsupportedSpec(node.id))
{ width: 0.0, height: 0.0, }
}
}
}
///|
/// 位置规格解析:在给定盒(栈布局的内容盒,或绝对定位的父矩形)内求坐标。
/// 栈内主轴位置由游标决定,不经过本函数。
fn resolve_pos(spec : PosSpec, area : Rect, size : Double) -> Double {
match spec {
Fixed(v) => area.x + v
AlignStart(off) => area.x + off
AlignEnd(off) => area.right() - off - size
CenterInParent => area.x + (area.width - size) / 2.0
}
}
///|
/// 位置规格解析(Y 轴):在给定盒内求纵坐标
fn resolve_pos_y(spec : PosSpec, area : Rect, size : Double) -> Double {
match spec {
Fixed(v) => area.y + v
AlignStart(off) => area.y + off
AlignEnd(off) => area.bottom() - off - size
CenterInParent => area.y + (area.height - size) / 2.0
}
}
///|
/// 栈布局求解:先汇总主轴尺寸并分配 Fill,再按游标定位并递归
fn solve_stack(
doc : Document,
node : Node,
l : StackLayout,
frame : Rect,
rects : Map[String, Rect],
failures : Array[SolveFailure],
) -> Unit {
let content = frame.inset(
l.padding.top,
l.padding.right,
l.padding.bottom,
l.padding.left,
)
let n = node.children.length()
let main_sizes : Array[Double] = Array::new()
let cross_sizes : Array[Double] = Array::new()
let fill_idx : Array[Int] = Array::new()
let mut fixed_total = 0.0
for cid in node.children.iter() {
match doc.nodes.get(cid) {
None => {
failures.push(MissingNode(cid))
main_sizes.push(0.0)
cross_sizes.push(0.0)
}
Some(child) => {
let cross : Double = match l.dir {
Vertical =>
match child.width {
Fixed(v) => v
Fill => content.width
Hug => intrinsic_size(child, failures).width
}
Horizontal =>
match child.height {
Fixed(v) => v
Fill => content.height
Hug => intrinsic_size(child, failures).height
}
}
let main : Double = match l.dir {
Vertical =>
match child.height {
Fixed(v) => {
fixed_total = fixed_total + v
v
}
Fill => {
fill_idx.push(main_sizes.length())
0.0
}
Hug => {
let s = intrinsic_size(child, failures)
fixed_total = fixed_total + s.height
s.height
}
}
Horizontal =>
match child.width {
Fixed(v) => {
fixed_total = fixed_total + v
v
}
Fill => {
fill_idx.push(main_sizes.length())
0.0
}
Hug => {
let s = intrinsic_size(child, failures)
fixed_total = fixed_total + s.width
s.width
}
}
}
cross_sizes.push(cross)
main_sizes.push(main)
}
}
}
let gap_total = if n > 0 { l.gap * (n - 1).to_double() } else { 0.0 }
let content_main : Double = match l.dir {
Vertical => content.height
Horizontal => content.width
}
let residual = content_main - fixed_total - gap_total
let fill_count = fill_idx.length()
// Fill 子项分不到非负空间 = 约束系统无解(硬谓词 ConstraintSolvable 的来源)
if fill_count > 0 && residual < 0.0 {
failures.push(NegativeResidual(node.id, residual))
}
let fill_share : Double = if fill_count > 0 && residual > 0.0 {
residual / fill_count.to_double()
} else {
0.0
}
for i in fill_idx.iter() {
main_sizes[i] = fill_share
}
let leftover : Double = if fill_count == 0 && residual > 0.0 {
residual
} else {
0.0
}
let main_offset : Double = match l.justify {
Start => 0.0
Center => leftover / 2.0
End => leftover
}
let mut cursor : Double = match l.dir {
Vertical => content.y + main_offset
Horizontal => content.x + main_offset
}
for i in 0.. {
let child_rect : Rect = match l.dir {
Vertical => {
let x = resolve_pos(child.x, content, cw)
Rect::new(x, cursor, cw, cm)
}
Horizontal => {
let y = resolve_pos_y(child.y, content, cw)
Rect::new(cursor, y, cm, cw)
}
}
solve_node(doc, child, child_rect, rects, failures)
}
None => ()
}
cursor = cursor + cm + l.gap
}
}
///|
/// 绝对定位求解(Group / 无布局的 Frame):
/// Fill = 从已解析偏移起填满父矩形剩余空间,因此“正确使用时构造性不溢出”
fn solve_absolute(
doc : Document,
node : Node,
frame : Rect,
rects : Map[String, Rect],
failures : Array[SolveFailure],
) -> Unit {
for cid in node.children.iter() {
match doc.nodes.get(cid) {
None => failures.push(MissingNode(cid))
Some(child) => {
let x_base : Double = match child.x {
Fixed(v) => v
AlignStart(off) => off
_ => 0.0
}
let y_base : Double = match child.y {
Fixed(v) => v
AlignStart(off) => off
_ => 0.0
}
let w : Double = match child.width {
Fixed(v) => v
Fill => frame.width - x_base
Hug => intrinsic_size(child, failures).width
}
let h : Double = match child.height {
Fixed(v) => v
Fill => frame.height - y_base
Hug => intrinsic_size(child, failures).height
}
let x = resolve_pos(child.x, frame, w)
let y = resolve_pos_y(child.y, frame, h)
solve_node(doc, child, Rect::new(x, y, w, h), rects, failures)
}
}
}
}