///|
/// SimpleLayoutEngine computes layout for UI node trees.
///|
pub struct SimpleLayoutEngine {
tree : UITree
}
///|
pub fn SimpleLayoutEngine::new(tree : UITree) -> SimpleLayoutEngine {
{ tree, }
}
///|
fn resolve_size(sizing : UISizing, available : Double) -> Double {
match sizing {
UISizing::Fixed(v) => v
UISizing::Percent(p) => available * p / 100.0
UISizing::Auto => available
}
}
///|
/// A measured child subtree: temp layout results at (0,0) plus its size.
priv struct ChildMeasure {
temp_results : Array[LayoutResult]
width : Double
height : Double
}
///|
/// A flex line: a group of children that fit on one row/column.
priv struct FlexLine {
items : Array[(Int, Double, Double)]
main_size : Double
cross_size : Double
}
///|
/// Organize measured children into flex lines.
fn build_flex_lines(
measures : Array[ChildMeasure],
is_row : Bool,
container_main : Double,
gap : Double,
flex_wrap : UIFlexWrap,
) -> Array[FlexLine] {
let lines : Array[FlexLine] = []
if measures.length() == 0 {
return lines
}
let mut current_items : Array[(Int, Double, Double)] = []
let mut current_main = 0.0
let mut current_cross = 0.0
for i, m in measures {
let item_main = if is_row { m.width } else { m.height }
let item_cross = if is_row { m.height } else { m.width }
let would_be = if current_items.length() > 0 {
current_main + gap + item_main
} else {
item_main
}
// Wrap to new line if needed
if current_items.length() > 0 &&
flex_wrap is UIFlexWrap::Wrap &&
would_be > container_main &&
container_main > 0.0 {
lines.push({
items: current_items,
main_size: current_main,
cross_size: current_cross,
})
current_items = []
current_main = 0.0
current_cross = 0.0
}
current_items.push((i, item_main, item_cross))
current_main = if current_items.length() == 1 {
item_main
} else {
current_main + gap + item_main
}
if item_cross > current_cross {
current_cross = item_cross
}
}
if current_items.length() > 0 {
lines.push({
items: current_items,
main_size: current_main,
cross_size: current_cross,
})
}
lines
}
///|
fn layout_node(
tree : UITree,
node_id : UINodeId,
constraint : LayoutConstraint,
results : Array[LayoutResult],
offset_x : Double,
offset_y : Double,
) -> (Double, Double) {
let node = match tree.get_node(node_id) {
Some(n) => n
None => return (0.0, 0.0)
}
let style = node.style
let avail_w = constraint.max_width
let avail_h = constraint.max_height
let content_w = resolve_size(style.width, avail_w)
let content_h = resolve_size(style.height, avail_h)
let inner_w = content_w - style.padding_left - style.padding_right
let inner_h = content_h - style.padding_top - style.padding_bottom
let clamped_w = ui_clamp(
inner_w,
0.0,
avail_w - style.padding_left - style.padding_right,
)
let clamped_h = ui_clamp(
inner_h,
0.0,
avail_h - style.padding_top - style.padding_bottom,
)
let is_row = match style.direction {
UIDirection::Row => true
UIDirection::Column => false
}
// Leaf node: no children
if node.children.length() == 0 {
let final_w = match style.width {
UISizing::Auto => 0.0
_ => content_w
}
let final_h = match style.height {
UISizing::Auto => 0.0
_ => content_h
}
results.push({
node_id,
rect: { x: offset_x, y: offset_y, width: final_w, height: final_h, },
})
return (final_w, final_h)
}
// Phase 1: Measure each child at (0, 0)
let measures : Array[ChildMeasure] = []
for child_id in node.children {
let child_constraint : LayoutConstraint = {
min_width: 0.0,
min_height: 0.0,
max_width: if clamped_w > 0.0 {
clamped_w
} else {
0.0
},
max_height: if clamped_h > 0.0 {
clamped_h
} else {
0.0
},
}
let temp : Array[LayoutResult] = []
let (cw, ch) = layout_node(tree, child_id, child_constraint, temp, 0.0, 0.0)
measures.push({ temp_results: temp, width: cw, height: ch, })
}
// Phase 2: Organize into flex lines
let container_main = if is_row { clamped_w } else { clamped_h }
let lines = build_flex_lines(
measures,
is_row,
container_main,
style.gap,
style.flex_wrap,
)
// Phase 3: Compute total content size for auto sizing
let mut total_cross = 0.0
let mut max_line_main = 0.0
for i, line in lines {
if line.main_size > max_line_main {
max_line_main = line.main_size
}
total_cross = total_cross + line.cross_size
if i > 0 {
total_cross = total_cross + style.gap
}
}
let final_w = match style.width {
UISizing::Auto =>
if is_row {
style.padding_left + max_line_main + style.padding_right
} else {
style.padding_left + total_cross + style.padding_right
}
_ => content_w
}
let final_h = match style.height {
UISizing::Auto =>
if is_row {
style.padding_top + total_cross + style.padding_bottom
} else {
style.padding_top + max_line_main + style.padding_bottom
}
_ => content_h
}
// The actual main-axis space for justify (only when fixed/percent sized)
let actual_main = if is_row {
final_w - style.padding_left - style.padding_right
} else {
final_h - style.padding_top - style.padding_bottom
}
// Phase 4: Position children with justify-content and align-items
let base_x = offset_x + style.padding_left
let base_y = offset_y + style.padding_top
let mut cross_offset = 0.0
for i, line in lines {
if i > 0 {
cross_offset = cross_offset + style.gap
}
let free_main = actual_main - line.main_size
let (main_start, main_extra_gap) = ui_compute_justify(
style.justify_content,
if free_main > 0.0 {
free_main
} else {
0.0
},
line.items.length(),
)
let mut main_pos = main_start
for _, item in line.items {
let (child_idx, item_main, item_cross) = item
let free_cross = line.cross_size - item_cross
let cross_align = ui_compute_align_offset(
style.align,
if free_cross > 0.0 {
free_cross
} else {
0.0
},
)
// Compute final position delta
let dx = if is_row { main_pos } else { cross_offset + cross_align }
let dy = if is_row { cross_offset + cross_align } else { main_pos }
// Emit shifted child results
let m = measures[child_idx]
for r in m.temp_results {
results.push({
node_id: r.node_id,
rect: {
x: r.rect.x + base_x + dx,
y: r.rect.y + base_y + dy,
width: r.rect.width,
height: r.rect.height,
},
})
}
main_pos = main_pos + item_main + style.gap + main_extra_gap
}
cross_offset = cross_offset + line.cross_size
}
// Add container result
results.push({
node_id,
rect: { x: offset_x, y: offset_y, width: final_w, height: final_h, },
})
(final_w, final_h)
}
///|
pub impl LayoutEngine for SimpleLayoutEngine with fn compute_layout(
self,
root,
constraint,
) {
let results : Array[LayoutResult] = []
let _ = layout_node(self.tree, root, constraint, results, 0.0, 0.0)
results
}
///|
pub extend SimpleLayoutEngine with LayoutEngine::{compute_layout}