aboutsummaryrefslogtreecommitdiffstats
path: root/research/experiments/044-specs/layout.py
blob: 1cea31af207bdbfb1f9c126e63d533511db506f9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
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