Drafter.Layout (drafter v0.3.2)

Copy Markdown View Source

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

component()

@type component() :: tuple()

A component descriptor tuple, as an app's render/1 returns.

dimension()

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

hierarchy()

@type hierarchy() :: map()

A widget hierarchy map, consulted for widgets that report their own size.

rect()

@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 0apply_margin/2 produces zero-sized rects when the margin exceeds the space.

sides()

@type sides() :: {integer(), integer(), integer(), integer()}

Edge insets as {top, right, bottom, left}.

Functions

apply_margin(rect, arg)

@spec apply_margin(rect(), sides()) :: rect()

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}

apply_padding(rect, arg)

@spec apply_padding(rect(), sides()) :: rect()

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}

calculate_grid_layout(children, rect, opts)

@spec calculate_grid_layout([component()], rect(), keyword()) :: [rect()]

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.

calculate_horizontal_layout(children, rect, opts)

@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 to children. Default: []. When no entry carries :width or :flex, the rect is divided evenly and :gap applies; otherwise those per-child options drive the widths and :gap is not used.
  • :gap - blank columns between children. Default: 0. Honoured only on the even-division path described above.

calculate_vertical_layout(children, rect, opts, hierarchy)

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

clamp_size(size, opts, atom)

@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

component_hidden?(component)

@spec component_hidden?(component()) :: boolean()

Whether a component paints.

Distinct from component_visible?/1: a hidden component keeps the space it was allotted and simply does not draw, whereas an invisible one is removed from the layout entirely and its siblings close the gap.

component_opts(arg1)

@spec component_opts(component()) :: keyword()

The options keyword list of a component, whatever its arity.

component_visible?(component)

@spec component_visible?(component()) :: boolean()

count_component_slots(arg1)

@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

dock_edge(component)

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

get_child_vertical_spec(child, hierarchy)

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

get_margin(opts)

@spec get_margin(keyword()) :: sides()

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}

get_padding(opts)

@spec get_padding(keyword()) :: sides()

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}

get_preferred_height(component, hierarchy \\ nil)

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

partition_docked(children)

@spec partition_docked([component()]) :: {[{component(), atom()}], [component()]}

Split components into those docked to an edge and those in normal flow.

rect(x, y, width, height)

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

resolve_dimension(n, available, fallback)

@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