Files
micronomicon/backend/uframe/measure.py
2026-04-01 08:28:44 +02:00

274 lines
8.8 KiB
Python

"""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,
)
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, (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