Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Placement
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
6 / 6
31
100.00% covered (success)
100.00%
1 / 1
 flow
100.00% covered (success)
100.00%
18 / 18
100.00% covered (success)
100.00%
1 / 1
7
 place
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
4
 overlap
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 rows
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
6
 overlaps
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 free
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Cosray\Block;
6
7/**
8 * Positions on a blocks grid. A block on the field's grid sits at the
9 * column and row it starts at; the reading order follows from the
10 * positions, row by row and left to right, and rows are kept in it. A block without a position
11 * (an import, a programmatic write) is placed below the placed ones,
12 * flowing in order the way the browser's sparse row flow places spans.
13 * The same flow gave the blocks of a grid saved before positions
14 * existed their place, indent included.
15 */
16final class Placement
17{
18    /**
19     * Where the sparse row flow puts each block: at the first spot from
20     * the cursor where its indent and span fit, the box past the indent.
21     * Spans wider than the grid are narrowed to it.
22     *
23     * @param list<array{colspan: int, rowspan: int, indent?: int}> $spans
24     * @return list<array{col: int, row: int}>
25     */
26    public static function flow(array $spans, int $columns): array
27    {
28        $columns = max(1, $columns);
29        $taken = [];
30        [$top, $left] = [0, 0];
31        $result = [];
32
33        foreach ($spans as $span) {
34            $colspan = max(1, min($columns, $span['colspan']));
35            $rowspan = max(1, $span['rowspan']);
36            $width = min($columns, $colspan + max(0, $span['indent'] ?? 0));
37
38            for ($row = $top, $col = $left;; $row++, $col = 0) {
39                for (; ($col + $width) <= $columns; $col++) {
40                    if (self::free($taken, $row, $col, $width, $rowspan)) {
41                        break 2;
42                    }
43                }
44            }
45
46            for ($r = $row; $r < ($row + $rowspan); $r++) {
47                for ($c = $col; $c < ($col + $width); $c++) {
48                    $taken["{$r}:{$c}"] = true;
49                }
50            }
51
52            [$top, $left] = [$row, $col + $width];
53            $result[] = ['col' => $col + $width - $colspan + 1, 'row' => $row + 1];
54        }
55
56        return $result;
57    }
58
59    /**
60     * Every layout with a position: placed ones keep theirs, the others
61     * flow below them in order.
62     *
63     * @param list<Layout> $layouts
64     * @return list<Layout>
65     */
66    public static function place(array $layouts, int $columns): array
67    {
68        $bottom = 0;
69        $loose = [];
70
71        foreach ($layouts as $index => $layout) {
72            if ($layout->placed()) {
73                $bottom = max($bottom, $layout->row + $layout->rowspan - 1);
74            } else {
75                $loose[$index] = ['colspan' => $layout->colspan, 'rowspan' => $layout->rowspan];
76            }
77        }
78
79        $spots = self::flow(array_values($loose), $columns);
80
81        foreach (array_keys($loose) as $position => $index) {
82            $layouts[$index] = $layouts[$index]->at($spots[$position]['col'], $bottom + $spots[$position]['row']);
83        }
84
85        return array_values($layouts);
86    }
87
88    /**
89     * The index of the first layout overlapping one before it, null when
90     * none do. Layouts without a position take no cells.
91     *
92     * @param list<Layout> $layouts
93     */
94    public static function overlap(array $layouts): ?int
95    {
96        foreach ($layouts as $index => $layout) {
97            for ($other = 0; $other < $index; $other++) {
98                if (self::overlaps($layouts[$other], $layout)) {
99                    return $index;
100                }
101            }
102        }
103
104        return null;
105    }
106
107    /**
108     * Stored rows with every top-level layout clamped to the grid and
109     * placed, in reading order; rows that are no arrays go last.
110     *
111     * @param array<array-key, mixed> $rows
112     * @return list<mixed>
113     */
114    public static function rows(array $rows, int $columns, int $min): array
115    {
116        $rows = array_values($rows);
117        $layouts = [];
118
119        foreach ($rows as $index => $row) {
120            if (is_array($row)) {
121                $layouts[$index] = Layout::normalize($row['layout'] ?? null, $columns, $min);
122            }
123        }
124
125        $placed = array_combine(array_keys($layouts), self::place(array_values($layouts), $columns));
126        $order = array_keys($rows);
127
128        usort($order, static fn(int $a, int $b): int => isset($placed[$a], $placed[$b])
129            ? [$placed[$a]->row, $placed[$a]->col, $a] <=> [$placed[$b]->row, $placed[$b]->col, $b]
130            : [isset($placed[$b]), $a] <=> [isset($placed[$a]), $b]);
131
132        return array_map(static function (int $index) use ($rows, $placed): mixed {
133            $row = $rows[$index];
134
135            if (isset($placed[$index]) && is_array($row)) {
136                $row['layout'] = $placed[$index]->array();
137            }
138
139            return $row;
140        }, $order);
141    }
142
143    private static function overlaps(Layout $a, Layout $b): bool
144    {
145        return (
146            $a->placed()
147                && $b->placed()
148                && $a->col < ($b->col + $b->colspan)
149                && $b->col < ($a->col + $a->colspan)
150                && $a->row < ($b->row + $b->rowspan)
151                && $b->row < ($a->row + $a->rowspan)
152        );
153    }
154
155    /** @param array<string, true> $taken */
156    private static function free(array $taken, int $top, int $left, int $width, int $height): bool
157    {
158        for ($row = $top; $row < ($top + $height); $row++) {
159            for ($col = $left; $col < ($left + $width); $col++) {
160                if (isset($taken["{$row}:{$col}"])) {
161                    return false;
162                }
163            }
164        }
165
166        return true;
167    }
168}