Pure layout calculation for the component tree.
All functions are stateless. They take component descriptors and rects, return geometry (rects or size lists), and have no side effects.
Summary
Types
A component descriptor tuple, as an app's render/1 returns.
A size as written in a component's options.
A widget hierarchy map, consulted for widgets that report their own size.
A rectangle in terminal cells.
Edge insets as {top, right, bottom, left}.
Functions
Shrink a rect by a component's margin, leaving the space outside its box.
Shrink a rect by a component's padding, leaving the space inside its border.
Place children on a two-dimensional grid.
Lay children out across rect, returning a %{x: x, width: width} per child.
Stack children down rect, returning a %{y: y, height: height} per child.
Clamp a size to the :min_height/:max_height or :min_width/:max_width in opts.
Whether a component paints.
The options keyword list of a component, whatever its arity.
How many widget slots a component descriptor occupies, counting nested children.
Which edge a component is pinned to, if any.
A child's vertical sizing inputs, as {preferred, weight, flexes?, max_height}.
Margin around a component, as {top, right, bottom, left}.
Padding inside a component, as {top, right, bottom, left}.
How tall a component would like to be, in cells.
Split components into those docked to an edge and those in normal flow.
Build a rect map from its four components.
Resolve a dimension against the space available to it.
Types
@type component() :: tuple()
A component descriptor tuple, as an app's render/1 returns.
@type dimension() :: non_neg_integer() | {:percent, number()} | {:fr, number()} | :auto | nil
A size as written in a component's options.
A non-negative integer is a cell count, {:percent, n} a share of the container,
{:fr, n} a share of what is left after fixed siblings, and :auto defers to the
component's own preferred size.
@type hierarchy() :: map()
A widget hierarchy map, consulted for widgets that report their own size.
@type rect() :: %{ x: integer(), y: integer(), width: non_neg_integer(), height: non_neg_integer() }
A rectangle in terminal cells.
x/y are the top-left corner and may be negative for a component scrolled out of
view. width/height may be 0 — apply_margin/2 produces zero-sized rects when
the margin exceeds the space.
Edge insets as {top, right, bottom, left}.
Functions
Shrink a rect by a component's margin, leaving the space outside its box.
Width and height floor at 0, so a margin larger than the rect collapses it.
Examples
iex> Drafter.Layout.apply_margin(%{x: 0, y: 0, width: 10, height: 6}, {1, 2, 1, 2})
%{x: 2, y: 1, width: 6, height: 4}
iex> Drafter.Layout.apply_margin(%{x: 0, y: 0, width: 2, height: 2}, {5, 5, 5, 5})
%{x: 5, y: 5, width: 0, height: 0}
Shrink a rect by a component's padding, leaving the space inside its border.
Unlike apply_margin/2, width and height floor at 1, so a padded component always
keeps a cell to draw in.
Examples
iex> Drafter.Layout.apply_padding(%{x: 0, y: 0, width: 10, height: 6}, {1, 2, 1, 2})
%{x: 2, y: 1, width: 6, height: 4}
iex> Drafter.Layout.apply_padding(%{x: 0, y: 0, width: 2, height: 2}, {5, 5, 5, 5})
%{x: 5, y: 5, width: 1, height: 1}
Place children on a two-dimensional grid.
:columns fixes the column count; without it, columns are inferred from
:rows, or fall back to a single row of all children. :gap separates cells
in both directions, or {row_gap, col_gap} separates them independently.
Children may span with :col_span and :row_span.
The grid always fills its rect: columns left over from uneven division are handed out one cell at a time to the leftmost columns.
@spec calculate_horizontal_layout([component()], rect(), keyword()) :: [ %{x: integer(), width: pos_integer()} ]
Lay children out across rect, returning a %{x: x, width: width} per child.
Options
:children_opts- list of per-child option keywords, positionally matched tochildren. Default:[]. When no entry carries:widthor:flex, the rect is divided evenly and:gapapplies; otherwise those per-child options drive the widths and:gapis not used.:gap- blank columns between children. Default:0. Honoured only on the even-division path described above.
@spec calculate_vertical_layout( [component()], rect(), keyword(), hierarchy() | nil ) :: [%{y: integer(), height: pos_integer()}]
Stack children down rect, returning a %{y: y, height: height} per child.
Fixed-height children keep their preferred height; the rest share what is left in
proportion to their flex weight, with a floor of one cell each. Every result is
clipped to the bottom of rect, so a child that does not fit gets height 0.
Options
:gap- blank rows between children. Default:0. Gaps are subtracted from the space the flexing children share.
Each child's own :height, :min_height, :max_height, :flex and :fr options
are read from the child descriptor, not from opts.
@spec clamp_size(non_neg_integer(), keyword(), :height | :width) :: non_neg_integer()
Clamp a size to the :min_height/:max_height or :min_width/:max_width in opts.
A bound that is absent or not an integer is ignored. When both are present and contradict each other, the maximum wins.
Examples
iex> Drafter.Layout.clamp_size(3, [min_height: 5], :height)
5
iex> Drafter.Layout.clamp_size(30, [max_width: 20], :width)
20
iex> Drafter.Layout.clamp_size(3, [], :height)
3
The options keyword list of a component, whatever its arity.
@spec count_component_slots(component()) :: pos_integer()
How many widget slots a component descriptor occupies, counting nested children.
A layout contributes nothing of its own; a box, card, scrollable or collapsible
contributes one plus its children. Anything else is 1.
Examples
iex> Drafter.Layout.count_component_slots({:label, [text: "hi"]})
1
iex> Drafter.Layout.count_component_slots({:layout, :vertical, [{:label, []}, {:label, []}], []})
2
iex> Drafter.Layout.count_component_slots({:box, [{:label, []}], []})
2
@spec dock_edge(component()) :: :top | :bottom | :left | :right | nil
Which edge a component is pinned to, if any.
A docked component is taken out of the normal flow and given the full span of
its edge; the remaining space is what the undocked siblings share. Reads the
:dock option of any component, and treats :footer as docked to :bottom.
Returns nil when the component is not docked.
@spec get_child_vertical_spec(component(), hierarchy() | nil) :: {pos_integer() | :auto, non_neg_integer(), boolean(), pos_integer() | nil}
A child's vertical sizing inputs, as {preferred, weight, flexes?, max_height}.
weight is the child's :fr count when it has one, otherwise its :flex value,
never less than 1. flexes? says whether the child should absorb leftover space.
max_height is its :max_height option, or nil.
Margin around a component, as {top, right, bottom, left}.
Shares the shorthand forms of get_padding/1: a single value applies to all
sides, a pair is vertical then horizontal.
Returns {0, 0, 0, 0} when :margin is absent or unrecognised.
Examples
iex> Drafter.Layout.get_margin(margin: 2)
{2, 2, 2, 2}
iex> Drafter.Layout.get_margin(margin: {1, 4})
{1, 4, 1, 4}
iex> Drafter.Layout.get_margin([])
{0, 0, 0, 0}
Padding inside a component, as {top, right, bottom, left}.
A single integer applies to all four sides, a {vertical, horizontal} pair to two
each, and a full four-tuple is taken as written. Returns {0, 0, 0, 0} when
:padding is absent or unrecognised.
Examples
iex> Drafter.Layout.get_padding(padding: 1)
{1, 1, 1, 1}
iex> Drafter.Layout.get_padding(padding: {0, 2, 3, 4})
{0, 2, 3, 4}
iex> Drafter.Layout.get_padding([])
{0, 0, 0, 0}
@spec get_preferred_height(component(), hierarchy() | nil) :: pos_integer() | :auto
How tall a component would like to be, in cells.
Container components sum their children plus their own chrome; a registered widget
is asked through the widget registry. hierarchy supplies live widget state
where a widget's height depends on it, and defaults to nil. Anything unrecognised
is 1.
Split components into those docked to an edge and those in normal flow.
@spec rect(integer(), integer(), non_neg_integer(), non_neg_integer()) :: rect()
Build a rect map from its four components.
No clamping or validation is applied.
Examples
iex> Drafter.Layout.rect(1, 2, 30, 4)
%{x: 1, y: 2, width: 30, height: 4}
@spec resolve_dimension(term(), non_neg_integer(), non_neg_integer() | :auto) :: non_neg_integer() | :auto
Resolve a dimension against the space available to it.
Accepts a plain cell count, {:percent, n} for a share of the container,
{:fr, n} for a share of what remains after fixed siblings, or :auto to
defer to the component's own preferred size.
Returns a cell count for integers and percentages. nil, :auto, {:fr, n}
and any unrecognised value return fallback.
Examples
iex> Drafter.Layout.resolve_dimension(10, 100, :auto)
10
iex> Drafter.Layout.resolve_dimension({:percent, 25}, 80, :auto)
20
iex> Drafter.Layout.resolve_dimension(nil, 80, 7)
7
iex> Drafter.Layout.resolve_dimension({:fr, 2}, 80, 7)
7