Harlock.Layout (harlock v0.4.1)

Copy Markdown View Source

Ratatui-style constraint layout solver.

Splits a region along a direction (:vertical splits height into rows, :horizontal splits width into cols) according to a list of constraints:

  • {:length, n} — exactly n cells
  • {:percentage, p}p% of the available space (rounded down)
  • {:min, n} — at least n cells; grows like a {:fill, 1} if there's room. Pair with :fill or other :min / :max slots to set a floor under what would otherwise be flexible.
  • {:max, n} — at most n cells; behaves like a {:fill, 1} capped at n. Combine with other fills to share remaining space without overflowing.
  • {:fill, weight} — distributes remaining space proportional to weight

Apps typically don't call this directly — vbox/1 and hbox/1 from Harlock.Elements take a :constraints opt and the renderer invokes the solver internally. This module exists in the public surface so the constraint shapes are documented and stable.

Solver

  1. Compute each slot's lower bound (:length and :percentage get their full size; :min(n) gets n; :fill and :max get 0). If the lower bounds already exceed the available space, truncate from the tail and log a warning — the over-constrained behavior is identical to v0.2.

  2. Distribute the remainder across flexible slots. :fill(weight), :min, and :max all participate; :fill carries its declared weight, :min and :max carry weight 1. (Override by writing {:fill, w} if you want explicit weighting.)

  3. Check :max caps. Any slot exceeding its cap is clamped to the cap and frozen; the excess goes back into the remainder. Iterate until no new freezes happen or until we hit length(constraints) passes (which is the absolute upper bound — each pass either freezes ≥1 slot or terminates).

  4. If :max caps leave space unallocated (e.g. [{:max, 10}, {:max, 10}] in a 30-cell region), the trailing region is simply not used — children don't overflow their caps to fill the space.

Round-off from percentages and fill divisions is absorbed by the last flexible slot, so for over-fill-saturating layouts the returned sizes sum exactly to the requested total.

Summary

Types

constraint()

@type constraint() ::
  {:length, non_neg_integer()}
  | {:percentage, non_neg_integer()}
  | {:min, non_neg_integer()}
  | {:max, non_neg_integer()}
  | {:fill, pos_integer()}

direction()

@type direction() :: :vertical | :horizontal

Functions

split(region, direction, constraints)