//! Layout consumes only owned native UI data, after the candidate VM is gone. use anyhow::{Result, bail}; use crate::{ document::{Frame, RenderCommand}, text::{self, TextStyle}, ui_bridge::{MAX_DIMENSION, UiKind, UiNode}, }; const MAX_LAYOUT_VISITS: usize = 10_000; #[derive(Default, Clone, Copy)] struct Size { width: f32, height: f32, } pub fn frame(root: &UiNode) -> Result { let mut frame = Frame::default(); let mut visits = 0; layout(root, 0.0, 0.0, &mut frame.commands, &mut visits)?; Ok(frame) } fn bounded(value: f32) -> Result { if !value.is_finite() || !(0.0..=MAX_DIMENSION as f32).contains(&value) { bail!("UI layout exceeds the 1000000 coordinate/size limit"); } Ok(value) } fn layout( node: &UiNode, x: f32, y: f32, commands: &mut Vec, visits: &mut usize, ) -> Result { *visits += 1; if *visits > MAX_LAYOUT_VISITS { bail!("expanded UI layout exceeds 10000 nodes"); } bounded(x)?; bounded(y)?; let size = match &node.kind { UiKind::Text { value, size, color } => { commands.push(RenderCommand::Text { x, y, value: value.clone(), size: *size, color: color.clone(), }); let measured = text::measure(value, TextStyle::ui(*size)); Size { width: measured.width, height: measured.height, } } UiKind::Rect { width, height, radius, color, } => { commands.push(RenderCommand::RoundedRect { x, y, width: *width, height: *height, radius: *radius, color: color.clone(), }); Size { width: *width, height: *height, } } UiKind::Spacer(size) => Size { width: *size, height: *size, }, UiKind::Column { padding, gap, children, } | UiKind::Row { padding, gap, children, } => { let row = matches!(node.kind, UiKind::Row { .. }); let mut cursor = *padding; let mut cross: f32 = 0.0; for (index, child) in children.iter().enumerate() { if index > 0 { cursor = bounded(cursor + gap)?; } let (child_x, child_y) = if row { (x + cursor, y + padding) } else { (x + padding, y + cursor) }; let size = layout(child, child_x, child_y, commands, visits)?; cursor = bounded(cursor + if row { size.width } else { size.height })?; cross = cross.max(if row { size.height } else { size.width }); } let along = bounded(cursor + padding)?; let across = bounded(cross + padding * 2.0)?; if row { Size { width: along, height: across, } } else { Size { width: across, height: along, } } } }; bounded(x + size.width)?; bounded(y + size.height)?; Ok(size) }