PhoenixKitDashboards.Grid (PhoenixKitDashboards v0.1.0)

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 breakpoint's column grid. 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.

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 cols-wide grid: each gets the first free rectangle in reading order, spans clamped to the column count. Returns the placements with "x"/"y" set.

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 only if the grid is packed solid through max_rows/0 (practically unreachable).

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.

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)

@spec compact([map()], pos_integer()) :: [map()]

Pack placements (their w/h spans, in list order) into explicit cells on a cols-wide grid: each gets the first free rectangle in reading order, spans clamped to the column count. Returns the placements with "x"/"y" set.

This is the "reflow + compact" primitive: it materializes legacy order-only layouts and derives un-customized breakpoints from a designed tier (sorted to reading order first by the caller).

first_free(others, w, h, cols)

@spec first_free([map()], 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 only if the grid is packed solid through max_rows/0 (practically unreachable).

fit_size(x, y, req_w, req_h, orig_h, others, cols, arg)

@spec fit_size(
  integer(),
  integer(),
  integer(),
  integer(),
  pos_integer(),
  [map()],
  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.

max_rows()

@spec max_rows() :: pos_integer()

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