PhoenixKitDashboards.Grid (PhoenixKitDashboards v0.2.1)

Copy Markdown View Source

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

below_all(others)

@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).

collides?(x, y, w, h, others)

@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).

compact(placements, cols, rows \\ 160)

@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.

first_free(others, w, h, cols, rows \\ 160)

@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.

fit_size(x, y, req_w, req_h, orig_h, others, arg1, arg2)

@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.

max_rows()

@spec max_rows() :: pos_integer()

The maximum row index + span extent a placement may reach.

pack(placements, occupied, cols, rows)

@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).

slot(occupied, w, h, cols, rows)

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}).