1use crate::ast::*;
4use crate::error::NewtError;
5use crate::value::{eval_expr, EvalContext, Value};
6use serde::Serialize;
7
8#[derive(Debug, Clone, Copy, Serialize)]
9pub struct Rect {
10 pub x: f32,
11 pub y: f32,
12 pub w: f32,
13 pub h: f32,
14}
15
16impl Rect {
17 pub fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
18 Self { x, y, w, h }
19 }
20}
21
22#[derive(Debug, Clone, Serialize)]
23pub struct LayoutNode {
24 pub kind: LayoutKind,
25 pub rect: Rect,
26 pub fill: Option<(u8, u8, u8, u8)>,
27 pub stroke: Option<(u8, u8, u8, u8)>,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub stroke_width: Option<f32>,
30 pub radius: f32,
31 pub text: Option<String>,
32 #[serde(skip_serializing_if = "Option::is_none")]
33 pub content_template: Option<String>,
34 pub font_size: f32,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub font_weight: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub shadow: Option<f32>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub transition_ms: Option<f32>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub role: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub aria_label: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub focus_order: Option<i32>,
47 #[serde(skip_serializing_if = "Option::is_none", rename = "onClick")]
48 pub on_click: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub href: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub name: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub aspect_ratio: Option<f32>,
55 pub children: Vec<LayoutNode>,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
59pub enum LayoutKind {
60 Box,
61 Text,
62 Row,
63 Column,
64 Grid,
65 Stack,
66 Center,
67 Spacer,
68 Image,
69 Button,
70 Input,
71 Modal,
72}
73
74impl LayoutNode {
75 fn empty(kind: LayoutKind, rect: Rect) -> Self {
76 Self {
77 kind,
78 rect,
79 fill: None,
80 stroke: None,
81 stroke_width: None,
82 radius: 0.0,
83 text: None,
84 content_template: None,
85 font_size: 16.0,
86 font_weight: None,
87 shadow: None,
88 transition_ms: None,
89 role: None,
90 aria_label: None,
91 focus_order: None,
92 on_click: None,
93 href: None,
94 name: None,
95 aspect_ratio: None,
96 children: Vec::new(),
97 }
98 }
99}
100
101fn get_prop_number(ctx: &EvalContext, props: &[Prop], name: &str, default: f32) -> f32 {
102 for p in props {
103 let match_name = match &p.name {
104 PropName::Ident(s) => s == name,
105 PropName::Width => name == "width",
106 PropName::Height => name == "height",
107 PropName::Padding => name == "padding",
108 PropName::Gap => name == "gap",
109 PropName::Radius => name == "radius",
110 PropName::FontSize => name == "fontSize",
111 PropName::MinWidth => name == "minWidth",
112 PropName::MaxWidth => name == "maxWidth",
113 PropName::MinHeight => name == "minHeight",
114 PropName::MaxHeight => name == "maxHeight",
115 PropName::Shadow => name == "shadow",
116 PropName::Transition => name == "transition",
117 _ => false,
118 };
119 if !match_name {
120 continue;
121 }
122 match &p.value {
123 PropValue::Number(n) => return *n as f32,
124 PropValue::Expr(e) => {
125 if let Ok(Value::Number(n)) = eval_expr(ctx, e) {
126 return n as f32;
127 }
128 }
129 _ => {}
130 }
131 }
132 default
133}
134
135fn get_prop_color(ctx: &EvalContext, props: &[Prop], name: &str) -> Option<(u8, u8, u8, u8)> {
136 for p in props {
137 let match_name = match &p.name {
138 PropName::Ident(s) => s == name,
139 PropName::Fill => name == "fill",
140 PropName::Stroke => name == "stroke",
141 _ => false,
142 };
143 if !match_name {
144 continue;
145 }
146 match &p.value {
147 PropValue::Color { r, g, b, a } => return Some((*r, *g, *b, *a)),
148 PropValue::Expr(e) => {
149 if let Ok(Value::Color { r, g, b, a }) = eval_expr(ctx, e) {
150 return Some((r, g, b, a));
151 }
152 }
153 _ => {}
154 }
155 }
156 None
157}
158
159fn parse_grid_tracks(s: &str) -> Vec<(f32, bool)> {
161 let mut out = Vec::new();
162 for part in s.split_whitespace() {
163 let part = part.trim();
164 if part.is_empty() {
165 continue;
166 }
167 if part.ends_with("fr") {
168 let v: f32 = part[..part.len() - 2].trim().parse().unwrap_or(1.0);
169 out.push((v.max(0.0), true));
170 } else if let Ok(v) = part.parse::<f32>() {
171 out.push((v.max(0.0), false));
172 }
173 }
174 if out.is_empty() {
175 out.push((1.0, true));
176 }
177 out
178}
179
180fn get_child_width(ctx: &EvalContext, expr: &Expr) -> f32 {
181 match expr {
182 Expr::Element { props, .. } => get_prop_number(ctx, props, "width", 0.0),
183 _ => 0.0,
184 }
185}
186
187fn get_child_height(ctx: &EvalContext, expr: &Expr) -> f32 {
188 match expr {
189 Expr::Element { props, .. } => get_prop_number(ctx, props, "height", 0.0),
190 _ => 0.0,
191 }
192}
193
194fn constrain_aspect_ratio(rect: Rect, ratio: f32) -> Rect {
196 if ratio <= 0.0 {
197 return rect;
198 }
199 let (w, h) = (rect.w, rect.h);
200 let target_h = w / ratio;
201 if target_h <= h && target_h > 0.0 {
202 Rect::new(rect.x, rect.y, w, target_h)
203 } else {
204 let target_w = h * ratio;
205 Rect::new(rect.x, rect.y, target_w.min(w), h)
206 }
207}
208
209fn get_prop_string(ctx: &EvalContext, props: &[Prop], name: &str) -> Option<String> {
210 for p in props {
211 let match_name = match &p.name {
212 PropName::Ident(s) => s == name,
213 PropName::Content => name == "content",
214 PropName::Role => name == "role",
215 PropName::AriaLabel => name == "ariaLabel",
216 _ => name == "onClick" || name == "href" || name == "name",
217 };
218 if !match_name {
219 continue;
220 }
221 match &p.value {
222 PropValue::String(s) => return Some(s.clone()),
223 PropValue::Expr(e) => {
224 if let Ok(Value::String(s)) = eval_expr(ctx, e) {
225 return Some(s);
226 }
227 }
228 _ => {}
229 }
230 }
231 None
232}
233
234fn expr_to_handler_string(expr: &Expr) -> String {
235 match expr {
236 Expr::Assignment { name, value, .. } => {
237 format!("{} = {}", name, expr_to_handler_string(value))
238 }
239 Expr::Binary { left, op, right, .. } => {
240 let op_str = match op {
241 BinaryOp::Add => "+",
242 BinaryOp::Sub => "-",
243 BinaryOp::Mul => "*",
244 BinaryOp::Div => "/",
245 BinaryOp::Mod => "%",
246 BinaryOp::Eq => "==",
247 BinaryOp::Ne => "!=",
248 BinaryOp::Lt => "<",
249 BinaryOp::Le => "<=",
250 BinaryOp::Gt => ">",
251 BinaryOp::Ge => ">=",
252 BinaryOp::And => "&&",
253 BinaryOp::Or => "||",
254 };
255 format!(
256 "{} {} {}",
257 expr_to_handler_string(left),
258 op_str,
259 expr_to_handler_string(right)
260 )
261 }
262 Expr::Ident(name, _) => name.clone(),
263 Expr::Literal(Literal::Number(n)) => {
264 if *n == (*n as i64) as f64 {
265 format!("{}", *n as i64)
266 } else {
267 format!("{}", n)
268 }
269 }
270 Expr::Literal(Literal::String(s)) => format!("\"{}\"", s),
271 Expr::Literal(Literal::Bool(b)) => format!("{}", b),
272 Expr::Block { stmts, .. } => {
273 let parts: Vec<String> = stmts
274 .iter()
275 .filter_map(|s| match s {
276 Stmt::Expr(e) => Some(expr_to_handler_string(e)),
277 _ => None,
278 })
279 .collect();
280 parts.join("; ")
281 }
282 Expr::Unary { op, inner, .. } => {
283 let op_str = match op {
284 UnaryOp::Not => "!",
285 UnaryOp::Neg => "-",
286 };
287 format!("{}{}", op_str, expr_to_handler_string(inner))
288 }
289 _ => String::new(),
290 }
291}
292
293fn get_prop_handler(props: &[Prop]) -> Option<String> {
294 for p in props {
295 let is_onclick = match &p.name {
296 PropName::Ident(s) => s == "onClick",
297 _ => false,
298 };
299 if !is_onclick {
300 continue;
301 }
302 match &p.value {
303 PropValue::String(s) => return Some(s.clone()),
304 PropValue::Expr(e) => return Some(expr_to_handler_string(e)),
305 _ => {}
306 }
307 }
308 None
309}
310
311fn get_prop_content_template(props: &[Prop]) -> Option<String> {
312 for p in props {
313 let is_content = match &p.name {
314 PropName::Content => true,
315 PropName::Ident(s) => s == "content",
316 _ => false,
317 };
318 if !is_content {
319 continue;
320 }
321 if let PropValue::Expr(Expr::InterpolatedString { parts, .. }) = &p.value {
322 let mut tpl = String::new();
323 for seg in parts {
324 match seg {
325 InterpSegment::Literal(s) => tpl.push_str(s),
326 InterpSegment::Expr(e) => {
327 tpl.push('{');
328 tpl.push_str(&expr_to_handler_string(e));
329 tpl.push('}');
330 }
331 }
332 }
333 return Some(tpl);
334 }
335 }
336 None
337}
338
339fn layout_kind_from_element(k: ElementKind) -> LayoutKind {
340 use ElementKind::*;
341 match k {
342 Header | Footer | Container | Sidebar | Section | Widget => LayoutKind::Column,
343 Accordion | Bento | Breadcrumb | Hamburger | Kebab | Meatballs | Doner
344 | Tabs | Pagination | LinkList | Nav | Form | Feed | Carousel => LayoutKind::Column,
345 Modal | Drawer | Popover => LayoutKind::Modal,
346 Card | Box | ConfirmDialog | Toast | Notification | Alert | MessageBox
347 | Tooltip | Loader | ProgressBar | Badge | Icon | Tag | Comment | Chart => LayoutKind::Box,
348 Text => LayoutKind::Text,
349 Row => LayoutKind::Row,
350 Column => LayoutKind::Column,
351 Grid => LayoutKind::Grid,
352 Stack => LayoutKind::Stack,
353 Center => LayoutKind::Center,
354 Spacer | Separator => LayoutKind::Spacer,
355 Image | Avatar | Skeleton | Rating => LayoutKind::Image,
356 Button => LayoutKind::Button,
357 Input | Password | Search | Select | Textarea | FileUpload | ColorPicker => LayoutKind::Input,
358 Checkbox | Radio | Dropdown | Combobox | Multiselect | DatePicker | Picker
359 | Slider | Stepper | Toggle => LayoutKind::Box,
360 Table | Timeline | TreeView | CommandPalette => LayoutKind::Column,
361 Splitter => LayoutKind::Row,
362 }
363}
364
365pub fn layout_tree(ctx: &EvalContext, expr: &Expr, rect: Rect) -> Result<LayoutNode, NewtError> {
366 layout_tree_with_viewport(ctx, expr, rect, rect)
367}
368
369fn layout_tree_with_viewport(
370 ctx: &EvalContext,
371 expr: &Expr,
372 rect: Rect,
373 viewport: Rect,
374) -> Result<LayoutNode, NewtError> {
375 match expr {
376 Expr::Element { kind, props, children, .. } => {
377 let layout_kind = layout_kind_from_element(*kind);
378 let aspect_ratio = get_prop_number(ctx, props, "aspectRatio", 0.0);
379 let rect = if aspect_ratio > 0.0 {
380 constrain_aspect_ratio(rect, aspect_ratio)
381 } else {
382 rect
383 };
384 let padding = get_prop_number(ctx, props, "padding", 0.0);
385 let gap = get_prop_number(ctx, props, "gap", 0.0);
386 let fill = get_prop_color(ctx, props, "fill");
387 let stroke = get_prop_color(ctx, props, "stroke");
388 let stroke_width = get_prop_number(ctx, props, "strokeWidth", 1.0);
389 let stroke_width = if stroke_width > 0.0 { Some(stroke_width) } else { None };
390 let radius = get_prop_number(ctx, props, "radius", 0.0);
391 let font_size = get_prop_number(ctx, props, "fontSize", 16.0);
392 let text = get_prop_string(ctx, props, "content");
393 let font_weight = get_prop_string(ctx, props, "fontWeight");
394 let shadow = get_prop_number(ctx, props, "shadow", 0.0);
395 let shadow = if shadow > 0.0 { Some(shadow) } else { None };
396 let transition_ms = get_prop_number(ctx, props, "transition", 0.0);
397 let transition_ms = if transition_ms > 0.0 { Some(transition_ms as i32 as f32) } else { None };
398 let role = get_prop_string(ctx, props, "role");
399 let aria_label = get_prop_string(ctx, props, "ariaLabel");
400 let focus_order = get_prop_number(ctx, props, "focusOrder", f32::NAN);
401 let focus_order = if !focus_order.is_nan() { Some(focus_order as i32) } else { None };
402 let on_click = get_prop_handler(props);
403 let content_template = get_prop_content_template(props);
404 let href = get_prop_string(ctx, props, "href");
405 let name = get_prop_string(ctx, props, "name");
406
407 let min_w = get_prop_number(ctx, props, "minWidth", 0.0);
408 let max_w = get_prop_number(ctx, props, "maxWidth", f32::MAX);
409 let min_h = get_prop_number(ctx, props, "minHeight", 0.0);
410 let max_h = get_prop_number(ctx, props, "maxHeight", f32::MAX);
411 let visible = (min_w <= 0.0 || viewport.w >= min_w)
412 && (max_w >= f32::MAX || viewport.w <= max_w)
413 && (min_h <= 0.0 || viewport.h >= min_h)
414 && (max_h >= f32::MAX || viewport.h <= max_h);
415 if !visible {
416 return Ok(LayoutNode::empty(layout_kind_from_element(*kind), rect));
417 }
418
419 let inner = Rect::new(
420 rect.x + padding,
421 rect.y + padding,
422 (rect.w - 2.0 * padding).max(0.0),
423 (rect.h - 2.0 * padding).max(0.0),
424 );
425
426 let child_nodes = match layout_kind {
427 LayoutKind::Row => {
428 let mut nodes = Vec::new();
429 let total_children = children.len();
430 if total_children == 0 {
431 } else {
432 let mut fixed_w: f32 = 0.0;
433 let mut flexible_count = 0usize;
434 for child in children.iter() {
435 let cw = get_child_width(ctx, child);
436 if cw > 0.0 {
437 fixed_w += cw;
438 } else {
439 flexible_count += 1;
440 }
441 }
442 let total_gap = gap * (total_children as f32 - 1.0);
443 let remaining_w = (inner.w - total_gap - fixed_w).max(0.0);
444 let flexible_w = if flexible_count > 0 {
445 (remaining_w - gap * (flexible_count as f32 - 1.0)).max(0.0) / flexible_count as f32
446 } else {
447 0.0
448 };
449 let mut x = inner.x;
450 for child in children {
451 let cw = get_child_width(ctx, child);
452 let ch = get_child_height(ctx, child);
453 let child_w = if cw > 0.0 {
454 cw.min((inner.w - (x - inner.x)).max(0.0))
455 } else {
456 flexible_w
457 };
458 let child_h = if ch > 0.0 { ch.min(inner.h) } else { inner.h };
459 let child_rect = Rect::new(x, inner.y, child_w, child_h);
460 nodes.push(layout_tree_with_viewport(ctx, child, child_rect, viewport)?);
461 x += child_w + gap;
462 }
463 }
464 nodes
465 }
466 LayoutKind::Column => {
467 let mut nodes = Vec::new();
468 let total_children = children.len();
469 if total_children == 0 {
470 } else {
471 let mut fixed_h: f32 = 0.0;
472 let mut flexible_count = 0usize;
473 for child in children.iter() {
474 let ch = get_child_height(ctx, child);
475 if ch > 0.0 {
476 fixed_h += ch;
477 } else {
478 flexible_count += 1;
479 }
480 }
481 let total_gap = gap * (total_children as f32 - 1.0);
482 let remaining_h = (inner.h - total_gap - fixed_h).max(0.0);
483 let flexible_h = if flexible_count > 0 {
484 (remaining_h - gap * (flexible_count as f32 - 1.0)).max(0.0) / flexible_count as f32
485 } else {
486 0.0
487 };
488 let mut y = inner.y;
489 for child in children {
490 let cw = get_child_width(ctx, child);
491 let ch = get_child_height(ctx, child);
492 let child_w = if cw > 0.0 { cw.min(inner.w) } else { inner.w };
493 let child_h = if ch > 0.0 {
494 ch.min((inner.h - (y - inner.y)).max(0.0))
495 } else {
496 flexible_h
497 };
498 let child_rect = Rect::new(inner.x, y, child_w, child_h);
499 nodes.push(layout_tree_with_viewport(ctx, child, child_rect, viewport)?);
500 y += child_h + gap;
501 }
502 }
503 nodes
504 }
505 LayoutKind::Grid => {
506 let mut nodes = Vec::new();
507 let columns_str = get_prop_string(ctx, props, "columns").unwrap_or_else(|| "1fr".to_string());
508 let col_specs = parse_grid_tracks(&columns_str);
509 if col_specs.is_empty() || children.is_empty() {
510 } else {
511 let num_cols = col_specs.len();
512 let fixed_w: f32 = col_specs.iter().filter(|(_, fr)| !fr).map(|(v, _)| *v).sum();
513 let fr_total: f32 = col_specs.iter().filter(|(_, fr)| *fr).map(|(v, _)| *v).sum();
514 let remaining_w = (inner.w - fixed_w).max(0.0);
515 let col_widths: Vec<f32> = col_specs
516 .iter()
517 .map(|(v, fr)| if *fr { remaining_w * v / fr_total.max(1.0) } else { *v })
518 .collect();
519 let num_rows = (children.len() + num_cols - 1) / num_cols;
520 let row_h = if num_rows > 0 {
521 (inner.h / num_rows as f32).max(0.0)
522 } else {
523 0.0
524 };
525 let col_offsets: Vec<f32> = (0..num_cols)
526 .map(|i| inner.x + col_widths[..i].iter().sum::<f32>())
527 .collect();
528 for (i, child) in children.iter().enumerate() {
529 let row = i / num_cols;
530 let col = i % num_cols;
531 let x = col_offsets[col];
532 let child_rect = Rect::new(x, inner.y + row as f32 * row_h, col_widths[col], row_h);
533 nodes.push(layout_tree_with_viewport(ctx, child, child_rect, viewport)?);
534 }
535 }
536 nodes
537 }
538 LayoutKind::Modal => {
539 let mut nodes = Vec::new();
540 if children.is_empty() {
541 } else if children.len() == 1 {
542 nodes.push(layout_tree_with_viewport(ctx, &children[0], inner, viewport)?);
543 } else {
544 nodes.push(layout_tree_with_viewport(ctx, &children[0], inner, viewport)?);
545 let content_w = (inner.w * 0.8).min(400.0);
546 let content_h = (inner.h * 0.6).min(300.0);
547 let cx = inner.x + (inner.w - content_w) / 2.0;
548 let cy = inner.y + (inner.h - content_h) / 2.0;
549 let content_rect = Rect::new(cx, cy, content_w, content_h);
550 nodes.push(layout_tree_with_viewport(ctx, &children[1], content_rect, viewport)?);
551 }
552 nodes
553 }
554 LayoutKind::Stack | LayoutKind::Center | LayoutKind::Box | LayoutKind::Button | LayoutKind::Input => {
555 let mut nodes = Vec::new();
556 for child in children {
557 let r = if layout_kind == LayoutKind::Center && children.len() == 1 {
558 inner
559 } else {
560 inner
561 };
562 nodes.push(layout_tree_with_viewport(ctx, child, r, viewport)?);
563 }
564 nodes
565 }
566 LayoutKind::Text | LayoutKind::Spacer | LayoutKind::Image => Vec::new(),
567 };
568
569 Ok(LayoutNode {
570 kind: layout_kind,
571 rect,
572 fill,
573 stroke,
574 stroke_width,
575 radius,
576 text,
577 content_template,
578 font_size,
579 font_weight,
580 shadow,
581 transition_ms,
582 role,
583 aria_label,
584 focus_order,
585 on_click,
586 href,
587 name,
588 aspect_ratio: if aspect_ratio > 0.0 { Some(aspect_ratio) } else { None },
589 children: child_nodes,
590 })
591 }
592 Expr::Call {
593 callee,
594 args,
595 slot_args,
596 span,
597 ..
598 } => {
599 let comp = ctx
600 .components
601 .get(callee)
602 .ok_or_else(|| NewtError::semantic(*span, format!("unknown component '{}'", callee)))?;
603 let body = if let Some(ref slots) = slot_args {
604 crate::ast::substitute_slots(&comp.body, slots)
605 } else {
606 comp.body.clone()
607 };
608 let mut new_ctx = EvalContext {
609 variables: ctx.variables.clone(),
610 components: ctx.components.clone(),
611 };
612 for (i, param) in comp.params.iter().enumerate() {
613 if let Some(arg) = args.get(i) {
614 if let Ok(v) = eval_expr(ctx, arg) {
615 new_ctx.variables.insert(param.clone(), v);
616 }
617 }
618 }
619 layout_tree_with_viewport(&new_ctx, &body, rect, viewport)
620 }
621 Expr::If {
622 cond,
623 then_branch,
624 else_branch,
625 ..
626 } => {
627 let cond_val = eval_expr(ctx, cond)?;
628 let branch = if cond_val.as_bool().unwrap_or(false) {
629 then_branch.as_ref()
630 } else if let Some(eb) = else_branch {
631 eb.as_ref()
632 } else {
633 return Ok(LayoutNode::empty(LayoutKind::Box, rect));
634 };
635 layout_tree_with_viewport(ctx, branch, rect, viewport)
636 }
637 Expr::For { var, iter, body, span, .. } => {
638 let iter_val = eval_expr(ctx, iter)?;
639 let arr = iter_val.as_array().map_err(|_| {
640 NewtError::semantic(*span, "for loop expects an array or range(n)")
641 })?;
642 let mut child_nodes = Vec::new();
643 let n = arr.len();
644 if n == 0 {
645 return Ok(LayoutNode::empty(LayoutKind::Column, rect));
646 }
647 let inner_h = rect.h.max(0.0) / n as f32;
648 let mut y = rect.y;
649 for val in arr.iter() {
650 let mut new_ctx = EvalContext {
651 variables: ctx.variables.clone(),
652 components: ctx.components.clone(),
653 };
654 new_ctx.variables.insert(var.clone(), val.clone());
655 let child_rect = Rect::new(rect.x, y, rect.w, inner_h);
656 child_nodes.push(layout_tree_with_viewport(&new_ctx, body.as_ref(), child_rect, viewport)?);
657 y += inner_h;
658 }
659 let mut node = LayoutNode::empty(LayoutKind::Column, rect);
660 node.children = child_nodes;
661 Ok(node)
662 }
663 Expr::Block { stmts, .. } => {
664 let mut block_ctx = EvalContext {
666 variables: ctx.variables.clone(),
667 components: ctx.components.clone(),
668 };
669 for s in stmts {
670 match s {
671 Stmt::Let { name, value, .. } => {
672 if let Ok(val) = eval_expr(&block_ctx, value) {
673 block_ctx.variables.insert(name.clone(), val);
674 }
675 }
676 Stmt::StateDecl(sd) => {
677 if let Ok(val) = eval_expr(&block_ctx, &sd.initial_value) {
678 block_ctx.variables.insert(sd.name.clone(), val);
679 }
680 }
681 _ => {}
682 }
683 }
684 let mut child_nodes = Vec::new();
686 let total = stmts.iter().filter(|s| matches!(s, Stmt::Expr(_))).count();
687 if total == 0 {
688 return Ok(LayoutNode::empty(LayoutKind::Box, rect));
689 }
690 let inner_h = rect.h.max(0.0) / total as f32;
691 let mut y = rect.y;
692 for s in stmts {
693 if let Stmt::Expr(e) = s {
694 let child_rect = Rect::new(rect.x, y, rect.w, inner_h);
695 child_nodes.push(layout_tree_with_viewport(&block_ctx, e, child_rect, viewport)?);
696 y += inner_h;
697 }
698 }
699 let mut node = LayoutNode::empty(LayoutKind::Column, rect);
700 node.children = child_nodes;
701 Ok(node)
702 }
703 _ => Ok(LayoutNode::empty(LayoutKind::Box, rect)),
704 }
705}