From 6215b1177e377b512aaa1296faa6311f809135f2 Mon Sep 17 00:00:00 2001 From: eunhhu Date: Mon, 19 Oct 2026 06:18:49 +0100 Subject: 140's 8, box layout, written in aup with no new opcode (aup's examples/layout.aup: a subset of CSS Flexbox 1 written down first, one line, row or column, padding, gap, six justify-content, four align-items, grow and shrink with 9.7's loop, cross sizes stretched, nothing sized by content): 2,000 random trees (29,388 nodes; 7,659 items frozen at their padding by the loop) are Taffy 0.15's within 0.01 (layout_taffy, built by cargo; the worst 8.3e-5) and layout.py's bit for bit on the three paths, the paths the same; the single-pass subset first tried refused 66% of trees, so the loop came in; Taffy given min sizes equal to the padding (given 0 it floors at 0 while resolving lengths, where 9.7 floors the content box), the reference adding from the left (sum compensates since Python 3.12); module_specs in make check: 100 trees (tests/specs/layout); bars: 116 lines against Python's 54, 4,754 ns a node, Taffy's 644 (7.4x), pure Python 5,396 (Pure 1.14x faster), 1,494 instructions a node (9.7's loop 44%); four frictions; aup's rows read by position no longer copy (99,067 ns a node before); the suite table's row 8; CHECKPOINT's direction row --- research/experiments/044-specs/layout.py | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 research/experiments/044-specs/layout.py (limited to 'research/experiments/044-specs/layout.py') diff --git a/research/experiments/044-specs/layout.py b/research/experiments/044-specs/layout.py new file mode 100644 index 0000000..1cea31a --- /dev/null +++ b/research/experiments/044-specs/layout.py @@ -0,0 +1,67 @@ +# Box layout, a subset of CSS Flexbox 1, in plain Python: the reference 140 counts aup's lines against. Containers lay +# their items out in one line (no wrap) along a row or a column: padding, a gap between items, justify-content (start, +# end, center, space-between, space-around, space-evenly) and align-items (start, end, center, stretch). An item's main +# size is its flex basis grown or shrunk (9.7), its cross size its own or, auto, stretched to the line. Every size is +# known from above: nothing is sized by its content, and min sizes are 0 (an item's padding at least). Flex factors +# are whole numbers: 9.7's sum below one (4b) does not arise. +from functools import reduce +from operator import add + +START, END, CENTER, BETWEEN, AROUND, EVENLY = range(6) # justify-content; align-items: START, END, CENTER, STRETCH +STRETCH = 3 + + +class Refused(Exception): + pass + + +def total(xs): # added from the left, as the app adds (sum compensates since Python 3.12) + return reduce(add, xs, 0) + + +def layout(nodes): + """nodes in preorder, each (parent, column, w, h, grow, shrink, basis, left, right, top, bottom, gap, justify, + align), w and h -1 when auto; each node's box [x, y, w, h], relative to its parent's.""" + boxes = [[0.0, 0.0, 0.0, 0.0] for _ in nodes] + boxes[0][2:] = nodes[0][2], nodes[0][3] + for i, (_, column, _, _, _, _, _, left, right, top, bottom, gap, justify, align) in enumerate(nodes): + kids = [j for j in range(i + 1, len(nodes)) if nodes[j][0] == i] + if not kids: + continue + m, c = (3, 2) if column else (2, 3) # the main and the cross sizes' places + inner = boxes[i][m] - (top + bottom if column else left + right) + line = max(boxes[i][c] - (left + right if column else top + bottom), 0) + pads = [(nodes[k][9] + nodes[k][10], nodes[k][7] + nodes[k][8]) if column else + (nodes[k][7] + nodes[k][8], nodes[k][9] + nodes[k][10]) for k in kids] + bases = [max(nodes[k][6], p) for k, (p, _) in zip(kids, pads)] + gaps = gap * (len(kids) - 1) + free = inner - (total(bases) + gaps) + n, growing = len(kids), free > 0 + sizes, frozen = bases[:], [free == 0 or nodes[k][4] == nodes[k][5] == 0 for k in kids] + while not all(frozen): # 9.7: each pass freezes one item at least + free = inner - (total(sizes[x] if frozen[x] else bases[x] for x in range(n)) + gaps) + loose = [x for x in range(n) if not frozen[x]] + factors = [nodes[kids[x]][4] if growing else (bases[x] - pads[x][0]) * nodes[kids[x]][5] for x in loose] + if free != 0 and total(factors) > 0: # grown by the grow factors, or shrunk by the shrink factors scaled + for x, f in zip(loose, factors): + sizes[x] = bases[x] + free * (f / total(factors)) + violations = [max(sizes[x], pads[x][0]) - sizes[x] for x in loose] # min violations: below the padding + for x, v in zip(loose, violations): + sizes[x], frozen[x] = max(sizes[x], pads[x][0]), v > 0 or total(violations) == 0 + free = inner - (total(sizes) + gaps) # 9.5: what is left, by justify-content + if (len(kids) == 1 or free <= 0) and justify in (BETWEEN, AROUND, EVENLY): + justify = START if free <= 0 or justify == BETWEEN else CENTER + first = {START: 0, END: free, CENTER: free / 2, BETWEEN: 0, AROUND: free / n / 2, EVENLY: free / (n + 1)}[justify] + between = {BETWEEN: free / max(n - 1, 1), AROUND: free / n, EVENLY: free / (n + 1)}.get(justify, 0) + gap + at = top if column else left + for x, (k, size, (_, p)) in enumerate(zip(kids, sizes, pads)): + at = at + (first if x == 0 else between) + own = nodes[k][c] + if own < 0 and align != STRETCH: + raise Refused('an auto cross size not stretched: sized by its content') + across = max(line if own < 0 else own, p) + offset = {START: 0, END: line - across, CENTER: (line - across) / 2}.get(align, 0) + boxes[k][m - 2], boxes[k][c - 2] = at, (left if column else top) + offset + boxes[k][m], boxes[k][c] = size, across + at = at + size + return boxes -- cgit v1.2.3