274 lines
8.8 KiB
Python
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
|