Pure cell-placement math for grid dashboards.
A grid widget occupies an explicit rectangle of cells — x (column, 0-based),
y (row, 0-based), spanning w×h — on a layout's lattice. Widgets
can sit anywhere (gaps are allowed, that's the point of the grid type) but
never overlap; these helpers are the single source of truth for what fits
where, shared by every placement mutation and by the render-time resolution
of layouts that predate explicit coordinates:
collides?/5— would a rectangle overlap any placed widget?first_free/4— the first free rectangle in reading order (row-major).compact/2— pack a list of spans into explicit cells in list order (legacy/derived layouts: "reflow + compact").fit_size/8— clamp a requested resize so it grows until blocked by a neighbour or the grid edge, never onto another widget.
Everything is integer cell math on plain string-keyed placement maps
(%{"x" => _, "y" => _, "w" => _, "h" => _}) — no DOM, no DB. Stored w/h
are coerced through Lattice.to_int/2 before arithmetic, so a tampered/legacy
JSONB span carrying a string or float can't raise ArithmeticError.
Summary
Functions
The first row below every placed widget — the non-overlapping fallback spot
when a packing scan comes up empty (a grid solid through max_rows/0).
Whether a w×h rectangle at (x, y) overlaps any of the others (their
placement maps; entries without both x and y are ignored — they have no
cells yet).
Pack placements (their w/h spans, in list order) into explicit cells on
a fresh cols-wide grid: each gets the first free rectangle in reading order
(via slot/5), w clamped to the column count. Returns the placements with
"x"/"y"/"w" set. rows defaults to max_rows/0; pass the layout's real
row count so a full screenful falls back to below_all/1 instead of packing
past the visible bottom edge.
The first {x, y} (reading order: row by row, left to right) where a w×h
rectangle fits without overlapping others. Spans wider than the grid are
handled by the caller (clamp first); nil if the grid is packed solid
through rows (the caller's below_all/1 fallback then stacks below the
screenful — expected on a small/full layout, not "practically unreachable").
rows defaults to max_rows/0 for callers with no row bound of their own.
Clamp a requested req_w×req_h resize of the widget anchored at (x, y) so
it stays within the grid and grows until blocked — the nearest neighbour
(any of others) or the grid edge stops it, instead of overlapping or
rejecting the whole resize. Width is fitted first (at the current height
orig_h), then height at the fitted width; the result never collides because
the original placement doesn't.
The maximum row index + span extent a placement may reach.
Pack placements (their w/h spans, in list order) into explicit cells,
each dropped at its slot/5 onto occupied — the already-placed
rectangles the packing must flow around (pass [] for a fresh grid). Spans
are coerced through Lattice.to_int/2 and clamped into the cols×rows
screenful; the returned placements carry integer "x"/"y"/"w"/"h".
The cell {x, y} to drop a w×h rectangle onto occupied: the first free
slot in reading order, or — when the grid is packed solid through rows —
stacked directly below every placed widget (below_all/1). The single
placement primitive shared by compact/3, pack/4, new-widget seeding, and
render-time resolution (each previously re-spelled the same
first_free/5 || {0, below_all/1}).
Functions
@spec below_all([map()]) :: non_neg_integer()
The first row below every placed widget — the non-overlapping fallback spot
when a packing scan comes up empty (a grid solid through max_rows/0).
@spec collides?(integer(), integer(), pos_integer(), pos_integer(), [map()]) :: boolean()
Whether a w×h rectangle at (x, y) overlaps any of the others (their
placement maps; entries without both x and y are ignored — they have no
cells yet).
@spec compact([map()], pos_integer(), pos_integer()) :: [map()]
Pack placements (their w/h spans, in list order) into explicit cells on
a fresh cols-wide grid: each gets the first free rectangle in reading order
(via slot/5), w clamped to the column count. Returns the placements with
"x"/"y"/"w" set. rows defaults to max_rows/0; pass the layout's real
row count so a full screenful falls back to below_all/1 instead of packing
past the visible bottom edge.
The "reflow + compact" primitive behind widget reorder and duplicate-layout
(sorted to reading order first by the caller). Unlike pack/4 it does not
rewrite or row-clamp a caller's stored h — it repacks already-resolved
placements (1 ≤ h ≤ rows), so preserving h verbatim keeps a legacy/oversized
span exactly where the old code stacked it rather than silently reshaping it.
@spec first_free([map()], pos_integer(), pos_integer(), pos_integer(), pos_integer()) :: {non_neg_integer(), non_neg_integer()} | nil
The first {x, y} (reading order: row by row, left to right) where a w×h
rectangle fits without overlapping others. Spans wider than the grid are
handled by the caller (clamp first); nil if the grid is packed solid
through rows (the caller's below_all/1 fallback then stacks below the
screenful — expected on a small/full layout, not "practically unreachable").
rows defaults to max_rows/0 for callers with no row bound of their own.
@spec fit_size( integer(), integer(), integer(), integer(), pos_integer(), [map()], {pos_integer(), pos_integer()}, {%{w: pos_integer(), h: pos_integer()}, %{w: pos_integer(), h: pos_integer()}} ) :: {pos_integer(), pos_integer()}
Clamp a requested req_w×req_h resize of the widget anchored at (x, y) so
it stays within the grid and grows until blocked — the nearest neighbour
(any of others) or the grid edge stops it, instead of overlapping or
rejecting the whole resize. Width is fitted first (at the current height
orig_h), then height at the fitted width; the result never collides because
the original placement doesn't.
bounds are the widget type's {min, max} size maps; rows is the
layout's own row count — a widget can never resize past the screenful's
bottom edge.
@spec max_rows() :: pos_integer()
The maximum row index + span extent a placement may reach.
@spec pack([map()], [map()], pos_integer(), pos_integer()) :: [map()]
Pack placements (their w/h spans, in list order) into explicit cells,
each dropped at its slot/5 onto occupied — the already-placed
rectangles the packing must flow around (pass [] for a fresh grid). Spans
are coerced through Lattice.to_int/2 and clamped into the cols×rows
screenful; the returned placements carry integer "x"/"y"/"w"/"h".
This is the seeded primitive behind render-time resolution
(Dashboards.resolve_designed), which packs a layout's order-only widgets
around the ones that already hold explicit cells. compact/3 is the sibling
fresh-grid packer — deliberately NOT this function, because it must not
rewrite/clamp a caller's stored h (see its note).
@spec slot([map()], pos_integer(), pos_integer(), pos_integer(), pos_integer()) :: {non_neg_integer(), non_neg_integer()}
The cell {x, y} to drop a w×h rectangle onto occupied: the first free
slot in reading order, or — when the grid is packed solid through rows —
stacked directly below every placed widget (below_all/1). The single
placement primitive shared by compact/3, pack/4, new-widget seeding, and
render-time resolution (each previously re-spelled the same
first_free/5 || {0, below_all/1}).