Harlock.Elements (harlock v0.4.1)

Copy Markdown View Source

View-tree constructors. Auto-imported into apps via use Harlock.App, so most apps use text(...), vbox(...), box(...) etc. directly without qualification.

An element is a plain struct (Harlock.Element); building a view is just calling these functions to assemble a tree. The renderer walks the tree once per dirty frame and produces a Frame ready for the diff renderer.

Primitives

  • text/2 — single-line text content
  • text_input/1 — single-line editable input (paired with Harlock.TextBuffer)
  • vbox/1 / hbox/1 — vertical / horizontal stacks with layout constraints (:length, :percentage, :fill)
  • box/1 — single-child container with border + title + padding
  • spacer/0 — empty element that occupies a layout slot
  • overlay/1 — render a foreground element on top of a background with optional focus_trap
  • table/1 / list/2 — row-based primitives with selection and focus highlighting
  • column/1 — column spec for table/1

All elements that accept focus take a :focusable opt — the runtime walks the tree to collect focusable ids for Tab traversal.

Summary

Functions

A single-child container with a border and optional inner padding.

Build a column spec for use inside table/1.

Horizontal stack. Children share the box's height; width is split.

Single-line bar showing key bindings as [k] label [k] label.

Single-column table with chrome hidden. :row_id defaults to & &1 because lists are usually homogeneous; pass an explicit :row_id if yours aren't.

Render over on top of child in a sub-rectangle anchored within the parent region.

Single-line horizontal progress bar.

Empty cell that occupies a layout slot. Useful with constraints.

Single-cell animated spinner.

Single-line bar with left- and right-aligned text. Useful as the pinned-bottom row of a screen.

Table primitive.

Single-line horizontal tab bar.

A text element. content is rendered as a single line; callers split multi-line content themselves.

Single-line text input.

Vertical stack. Children share the box's width; height is split according to :constraints.

Scrollable container.

Functions

box(opts)

@spec box(keyword()) :: Harlock.Element.t()

A single-child container with a border and optional inner padding.

Required options:

  • :child — the element drawn inside the box

Optional:

  • :title — string overlaid on the top border (truncated to fit)
  • :title_align:left (default) | :center | :right

  • :border:single (default) | :double | :rounded | :thick | :none

  • :border_style%Style{} or keyword applied to the border + title
  • :padding — non-negative integer (uniform), {v, h}, or {top, right, bottom, left}
  • :focusable, :focus_style — when focused, the focus style replaces the border style (the child is left alone)

For multiple children, wrap them in vbox/1 or hbox/1 and pass the result as :child. The box reserves one cell on each side for the border (unless :border is :none); when the region is smaller than that the border is skipped and the child takes the full region.

column(opts \\ [])

@spec column(keyword()) :: Harlock.Element.Column.t()

Build a column spec for use inside table/1.

Options:

  • :title — header label
  • :width — layout constraint (default {:fill, 1})
  • :align:left | :right | :center

  • :renderfn row -> string | iodata

hbox(opts \\ [])

@spec hbox(keyword()) :: Harlock.Element.t()

Horizontal stack. Children share the box's height; width is split.

Options as vbox/1.

keybar(opts)

@spec keybar(keyword()) :: Harlock.Element.t()

Single-line bar showing key bindings as [k] label [k] label.

Required:

  • :bindings — list of {key, label} tuples. key may be a char like ?q or any atom (:tab, :enter); it's rendered via to_string/1.

Optional:

  • :style%Style{} (default %Style{reverse: true})
  • :separator — string between bindings (default " ")
  • :right — extra right-aligned text (e.g. clock, status)

list(items, opts \\ [])

@spec list(
  Enumerable.t(),
  keyword()
) :: Harlock.Element.t()

Single-column table with chrome hidden. :row_id defaults to & &1 because lists are usually homogeneous; pass an explicit :row_id if yours aren't.

Options:

overlay(opts)

@spec overlay(keyword()) :: Harlock.Element.t()

Render over on top of child in a sub-rectangle anchored within the parent region.

Required options:

  • :child — the background element (rendered first)
  • :over — the foreground element (rendered on top)

Anchor + sizing:

  • :anchor:center (default), :top_left, :top_right, :bottom_left, :bottom_right, or {row, col} for absolute placement
  • :width — width of the over region in cells (default: full parent)
  • :height — height of the over region in cells (default: full parent)

Focus:

  • :focus_trap — when true, focus traversal wraps within the over subtree until the overlay disappears. Prior focus is stashed and restored automatically when the overlay closes.

Overlays nest cleanly: just put another overlay as :over.

progress(opts)

@spec progress(keyword()) :: Harlock.Element.t()

Single-line horizontal progress bar.

Required options:

  • :value — current value (non-negative)
  • :max — denominator (positive)

Optional:

  • :width — explicit bar width in cells (default: full region width)
  • :style%Style{} for the unfilled portion
  • :fill_style%Style{} for the filled portion

value is clamped to [0, max]. The bar fills round(value / max * width) cells with in fill_style and the rest with space in style.

spacer()

@spec spacer() :: Harlock.Element.t()

Empty cell that occupies a layout slot. Useful with constraints.

spinner(opts)

@spec spinner(keyword()) :: Harlock.Element.t()

Single-cell animated spinner.

Required options:

  • :tick — integer; the current animation frame counter (caller-owned in the app's model). Pair with a subscription that increments this on a timer.

Optional:

  • :frames — list of grapheme strings to cycle through (default: braille spinner ["⠋", "⠙", …])
  • :style%Style{} applied to the rendered frame

Renders Enum.at(frames, rem(tick, length(frames))). The widget doesn't subscribe to anything itself — wire Harlock.Sub.interval/2 in your app's subs/1 and increment tick in update/2.

statusbar(opts \\ [])

@spec statusbar(keyword()) :: Harlock.Element.t()

Single-line bar with left- and right-aligned text. Useful as the pinned-bottom row of a screen.

Options:

  • :left — string (default "")
  • :right — string (default "")
  • :style%Style{} (default %Style{reverse: true})

If left and right together exceed the region width, right is truncated first.

table(opts)

@spec table(keyword()) :: Harlock.Element.t()

Table primitive.

Required options:

  • :columns — list of column/1 specs
  • :rows — enumerable of row data
  • :row_id — fn(row) -> id. Row identity is by id, not index, so focus and selection survive sort/filter.

Optional:

  • :focused_row — currently-focused row id
  • :selection:none | {:single, id} | {:multi, MapSet}

  • :show_header — default true
  • :focusable, :focus_trap — same as other elements

tabs(opts)

@spec tabs(keyword()) :: Harlock.Element.t()

Single-line horizontal tab bar.

Required:

  • :items — list of {id, label} tuples
  • :active — id of the currently active tab

Optional:

  • :focusable — focus id; when focused, Left/Right cycle tabs (use Harlock.Tabs.apply_key/3 in update/2)
  • :style%Style{} for inactive tabs (default Theme.get(:header))
  • :active_style%Style{} for the active tab (default Theme.get(:focus))
  • :separator — string between tabs (default " │ ")

Renders only the tab bar — the body for the active tab is rendered separately by the app. Typical pattern:

vbox(
  constraints: [length: 1, fill: 1],
  children: [
    tabs(items: [{:a, "Alpha"}, {:b, "Beta"}], active: m.tab, focusable: :tabs),
    case m.tab do
      :a -> alpha_body(m)
      :b -> beta_body(m)
    end
  ]
)

text(content, opts \\ [])

@spec text(
  String.t(),
  keyword()
) :: Harlock.Element.t()

A text element. content is rendered as a single line; callers split multi-line content themselves.

Options:

  • :style%Harlock.Render.Style{} or keyword list of style attrs.

text_input(opts)

@spec text_input(keyword()) :: Harlock.Element.t()

Single-line text input.

Required options:

  • :value — the current string contents (caller-owned)
  • :cursor — grapheme index of the cursor (0..length)
  • :focusable — id for focus traversal

Optional:

  • :placeholder — shown when value is empty and the input isn't focused
  • :max_length — soft hint; the element doesn't enforce it, but Harlock.TextBuffer.apply_key/3 respects it if you wire it in your app
  • :style%Style{} for the value text
  • :placeholder_style%Style{} for the placeholder
  • :password — when true, render each grapheme as

The element is a dumb renderer. The app's update/2 owns the value and cursor; call Harlock.TextBuffer.apply_key/3 to react to key events when this input is focused. When focused, the renderer positions the terminal cursor at the visual column matching :cursor.

vbox(opts \\ [])

@spec vbox(keyword()) :: Harlock.Element.t()

Vertical stack. Children share the box's width; height is split according to :constraints.

Options:

  • :constraints — list of layout constraints, one per child. Defaults to [fill: 1] for each child if not provided.
  • :children — list of child elements.

viewport(opts)

@spec viewport(keyword()) :: Harlock.Element.t()

Scrollable container.

Required options:

  • :child — the element to scroll
  • :offset — top-row offset into the child (0-indexed, app-owned)
  • :content_height — total rows the child occupies

Optional:

  • :scrollbar — render a single-column cosmetic scrollbar on the right edge (default false). The scrollbar consumes one column from the child's available width.
  • :scrollbar_style%Style{} for the scrollbar track + thumb

The viewport renders the child into a temporary frame of width × content_height, then blits rows offset..offset+visible_height into the real region. The app owns the scroll offset; pair with Harlock.Viewport.apply_key/4 in update/2 to translate scroll-key events into new offsets.

Vertical-only for now. The child is given full width (minus scrollbar column if enabled) so horizontal layout proceeds normally.