"""Bottom-up measure pass — compute min/preferred width and height for each node.""" from __future__ import annotations from uframe.ir import ( IRNode, Page, Box, Row, Col, Spacer, Pad, Heading, Text, Label, Divider, Link, ListNode, ListItem, Gauge, Sparkline, Status, Table, Form, Field, Password, Radio, Checkbox, FormButton, Let, Source, IfBlock, ForLoop, CacheControl, OnSubmit, StateDecl, BigTitle, ImageNode, ) from uframe.fonts import FONT_HEIGHTS, get_text_width from uframe.imaging import get_image_height def _text_height(text: str, width: int) -> int: """Compute how many lines a text string needs at a given width.""" if not text or width <= 0: return 1 words = text.split() lines = 1 col = 0 for word in words: wlen = len(word) if col > 0 and col + 1 + wlen > width: lines += 1 col = wlen elif col == 0: col = wlen else: col += 1 + wlen return max(lines, 1) def measure(node: IRNode, available_width: int) -> None: """Recursively compute min/preferred sizes for an IR subtree. This is a bottom-up pass: children are measured before their parent. """ # Dispatch to node type if isinstance(node, Page): w = node.width node.pref_width = w node.min_width = w # Measure children with full page width total_h = 0 for child in node.children: measure(child, w) total_h += child.pref_height node.pref_height = total_h node.min_height = total_h elif isinstance(node, Box): # Box adds 2 chars for borders on each axis (left+right, top+bottom) inner_w = available_width - 2 total_h = 0 for child in node.children: measure(child, inner_w) total_h += child.pref_height node.pref_width = available_width node.min_width = 4 # minimum: border + 2 chars + border node.pref_height = total_h + 2 # +2 for top/bottom border node.min_height = 3 # top border + 1 line + bottom border elif isinstance(node, Row): # Children laid out horizontally, split available width n = len(node.children) if n == 0: node.pref_width = available_width node.pref_height = 0 node.min_width = 0 node.min_height = 0 return gap_total = node.gap * (n - 1) usable = available_width - gap_total # First pass: measure children to get their preferred sizes # Distribute width proportionally or equally fixed_width_children = [] flex_children = [] fixed_total = 0 for child in node.children: if isinstance(child, Col) and child.col_width is not None: fixed_width_children.append(child) fixed_total += child.col_width else: flex_children.append(child) flex_each = 0 if flex_children: flex_each = max(1, (usable - fixed_total) // len(flex_children)) max_h = 0 for child in node.children: if isinstance(child, Col) and child.col_width is not None: child_w = child.col_width else: child_w = flex_each measure(child, child_w) max_h = max(max_h, child.pref_height) node.pref_width = available_width node.min_width = n # at minimum 1 char per child node.pref_height = max_h node.min_height = max_h elif isinstance(node, Col): w = node.col_width if node.col_width is not None else available_width total_h = 0 for child in node.children: measure(child, w) total_h += child.pref_height node.pref_width = w node.min_width = min(w, 1) node.pref_height = total_h node.min_height = total_h elif isinstance(node, Spacer): node.pref_width = available_width node.min_width = 0 node.pref_height = node.lines node.min_height = node.lines elif isinstance(node, Pad): inner_w = available_width - node.left - node.right total_h = 0 for child in node.children: measure(child, inner_w) total_h += child.pref_height node.pref_width = available_width node.min_width = node.left + node.right + 1 node.pref_height = total_h + node.top + node.bottom node.min_height = node.top + node.bottom elif isinstance(node, Heading): node.pref_width = available_width node.min_width = len(node.text) + 1 node.pref_height = 1 node.min_height = 1 elif isinstance(node, Text): node.pref_width = available_width node.min_width = 1 node.pref_height = _text_height(node.content, available_width) node.min_height = 1 elif isinstance(node, Label): node.pref_width = available_width node.min_width = len(node.key) + 2 + len(node.value) node.pref_height = 1 node.min_height = 1 elif isinstance(node, Divider): node.pref_width = available_width node.min_width = 1 node.pref_height = 1 node.min_height = 1 elif isinstance(node, Link): node.pref_width = available_width node.min_width = len(node.display) + 2 node.pref_height = 1 node.min_height = 1 elif isinstance(node, ListNode): total_h = 0 for child in node.children: measure(child, available_width - 2) # indent for bullet total_h += child.pref_height node.pref_width = available_width node.min_width = 4 node.pref_height = total_h node.min_height = total_h elif isinstance(node, ListItem): node.pref_width = available_width node.min_width = len(node.content) + 1 node.pref_height = _text_height(node.content, available_width) node.min_height = 1 elif isinstance(node, Gauge): node.pref_width = available_width node.min_width = node.bar_width + len(node.label) + 6 node.pref_height = 1 node.min_height = 1 elif isinstance(node, Sparkline): node.pref_width = available_width node.min_width = node.spark_width + len(node.label) + 4 node.pref_height = 1 node.min_height = 1 elif isinstance(node, Status): node.pref_width = available_width node.min_width = len(node.label) + 4 node.pref_height = 1 node.min_height = 1 elif isinstance(node, Table): num_rows = len(node.rows) node.pref_width = available_width node.min_width = len(node.columns) * 3 + 1 node.pref_height = num_rows + 4 node.min_height = 4 elif isinstance(node, Form): total_h = 0 for child in node.children: measure(child, available_width) total_h += child.pref_height node.pref_width = available_width node.min_width = 10 node.pref_height = total_h node.min_height = total_h elif isinstance(node, Field): node.pref_width = available_width node.min_width = len(node.field_name) + node.field_width + 6 node.pref_height = 1 node.min_height = 1 elif isinstance(node, Password): node.pref_width = available_width node.min_width = len(node.field_name) + node.field_width + 6 node.pref_height = 1 node.min_height = 1 elif isinstance(node, Radio): node.pref_width = available_width node.min_width = len(node.group) + sum(len(o) + 6 for o in node.options) node.pref_height = 1 node.min_height = 1 elif isinstance(node, Checkbox): node.pref_width = available_width node.min_width = len(node.checkbox_label) + 6 node.pref_height = 1 node.min_height = 1 elif isinstance(node, FormButton): node.pref_width = available_width node.min_width = len(node.button_label) + 6 node.pref_height = 1 node.min_height = 1 elif isinstance(node, ImageNode): h = get_image_height(node.path, node.mode, node.img_width) if node.caption: h += 1 # extra line for caption node.pref_width = available_width node.min_width = node.img_width node.pref_height = h node.min_height = 1 elif isinstance(node, BigTitle): # Try the requested font, fall back to smaller if too wide tw = get_text_width(node.text, node.font) if tw <= available_width: h = FONT_HEIGHTS.get(node.font, 6) elif get_text_width(node.text, "pixel") <= available_width: h = FONT_HEIGHTS.get("pixel", 3) elif get_text_width(node.text, "thin") <= available_width: h = FONT_HEIGHTS.get("thin", 3) else: h = 1 # fallback to single styled line node.pref_width = available_width node.min_width = 1 node.pref_height = h node.min_height = 1 elif isinstance(node, (Let, Source, CacheControl, StateDecl)): # Zero-height metadata nodes — no visual output node.pref_width = 0 node.min_width = 0 node.pref_height = 0 node.min_height = 0 elif isinstance(node, (IfBlock, ForLoop, OnSubmit)): # Container nodes — height = sum of children total_h = 0 for child in node.children: measure(child, available_width) total_h += child.pref_height node.pref_width = available_width node.min_width = 1 node.pref_height = total_h node.min_height = 0 else: # Generic: just measure children total_h = 0 for child in node.children: measure(child, available_width) total_h += child.pref_height node.pref_width = available_width node.min_width = 1 node.pref_height = max(total_h, 1) node.min_height = 1