Drafter.App behaviour (drafter v0.5.0)

Copy Markdown View Source

Declarative API for building a Drafter terminal application.

An application is a module that does use Drafter.App and implements mount/1, render/1, and one or both handle_event callbacks. Drafter owns the terminal, the event loop, layout, theming, and rendering; the module supplies state and a component tree.

Complete application

This module compiles and runs as written:

defmodule Counter do
  use Drafter.App

  def mount(_props), do: %{count: 0}

  def render(state) do
    vertical([
      header("Counter"),
      label("Count: #{state.count}", style: %{bold: true}),
      horizontal(
        [
          button("Decrement", on_click: :decrement),
          button("Increment", on_click: :increment, variant: :primary)
        ],
        gap: 2
      ),
      footer()
    ])
  end

  keybinding :q, "quit" do
    {:stop, :normal}
  end

  def handle_event(:increment, _data, state), do: {:ok, %{state | count: state.count + 1}}
  def handle_event(:decrement, _data, state), do: {:ok, %{state | count: state.count - 1}}
  def handle_event(_name, _data, state), do: {:noreply, state}
end

Drafter.run(Counter)

use Drafter.App options

  • :css_path - String.t(), path to a CSS file used for widget styling. Default nil.
  • :styles - map() of inline style overrides. Default %{}.
  • :mouse_hover - boolean(). true (default) puts the terminal into hover tracking mode for this app. false cuts mouse event volume for apps with no hover effects.
  • :key_release - boolean(), default false. true turns on the kitty keyboard protocol for this app's sessions where the terminal supports it: every key press is followed by {:key_down, key, modifiers}, every release arrives as {:key_up, key, modifiers}, and {:key_release_support, true} is delivered once the terminal confirms it will report releases — false from a terminal that speaks the protocol without them. The {:key, ...} events are unchanged. See Drafter.Terminal.KittyKeyboard for the key names.
  • :cell_size - boolean(), default false. true asks each session's terminal how many pixels a cell is; the answer arrives as {:cell_size, {width, height}} and Drafter.Session.Context.cell_size/0 keeps it. Widgets placing images at pixel offsets need it.
  • :pixel_mouse - boolean(), default false. true asks each session's terminal for its cell size and then for mouse reports in pixels (SGR-Pixels, DECSET 1016). Mouse payloads keep x and y in cells and, once the terminal confirms with {:pixel_mouse, true}, add px: {x, y}, the zero-based pixel under the pointer. A terminal that ignores the mode answers {:pixel_mouse, false} and its payloads carry no :px. Drafter.Session.Context.pixel_mouse?/0 reports which. Local and ssh sessions.
  • :controller - boolean(), default false. true reads the game controllers attached to the machine the app runs on through the gamepad package, which the app adds to its own deps, and delivers their events as {:controller, event} — buttons, axes, connections — in the shape Gamepad.Wire describes. Local terminal sessions only; a client over ssh or telnet has its controller on its own machine. Without the package a note is printed and no events arrive.
  • :frame_pacing - :animations (default) or :always. :always draws at most once per refresh_rate/0 interval whatever caused the change, so a burst of events or timer ticks becomes one frame; :animations paces only animation frames and draws every other change as it happens.
  • :runtime - the runtime backend: a module, or the shorthand :callback, :reducer, or :shared. Default Drafter.Runtime.Callback, the mount/render/handle_event style described here. See Drafter.Runtime.

No other key is read; an unknown one is silently ignored. Each of these is exposed on the module as __css_path__/0, __inline_styles__/0, __mouse_hover__/0, __key_release__/0, __cell_size__/0, __pixel_mouse__/0, __controller__/0, __frame_pacing__/0, and __runtime__/0, which the loop calls and which are not overridable.

use Drafter.App imports this module, so every element constructor (vertical/2, label/2, button/2, …) and the keybinding/3 macro are available unqualified. A separate import Drafter.App is redundant.

It also defines overridable defaults for mount/1 (returns %{}), render/1 (returns []), on_ready/1, unmount/1, and keybindings/0, and appends catch-all clauses for handle_event/2 (returns {:noreply, state}) and on_timer/2 (returns the state) after every clause the module defined, each only when the module has no catch-all clause of its own for that function. A module therefore never has to write those two catch-alls, and may write its own. handle_event/3 gets no catch-all: a module that defines any handle_event/3 clause must define its own final clause or an unmatched named callback raises FunctionClauseError.

Callbacks

  • mount/1 — mount(props :: map()) :: state. Builds the initial state from the map passed under :props to Drafter.run/2 (%{} when absent). state is any term; a map is what the element constructors and Drafter.Test expect.
  • render/1 — render(state) :: element | [element]. Returns the component tree: the tuples produced by the constructors in this module, the {WidgetModule, props_map} tuples produced by Drafter.label/2 and friends, or a list of either. Called after every state change; it must be pure.

  • handle_event/2 — handle_event(event, state) :: result. Raw input events: {:key, key}, {:key, key, modifiers}, {:char, codepoint}, {:mouse, map}, {:resize, {width, height}}, and the other members of Drafter.Event.t/0. Printable ASCII arrives as a named key — q is {:key, :q}, / is {:key, :/} — and only codepoints above ASCII 126 arrive as {:char, codepoint}. See Drafter.Terminal.ANSI for the full set of shapes, and "Where a key is handled" below for which keys reach this callback at all.
  • handle_event/3 — handle_event(name, data, state) :: result. Named callbacks: name is the atom given to a widget as on_click:, on_change:, and so on; data is that widget's payload (nil for widgets with no payload). Also how Drafter.send_app_event/2 and a popped screen's result arrive.

Optional lifecycle callbacks, each returning the new state — omit them and the state passes through unchanged:

Where a key is handled

handle_event/2 — and therefore every keybinding/3 clause, which is only a handle_event/2 clause — is not the first thing to see an input event. Three things run ahead of it.

Ctrl+Q stops the loop before any callback runs, always. A keybinding {:q, [:ctrl]} clause and a hand-written {:key, :q, [:ctrl]} clause are both unreachable. Ctrl+C is not reserved and does reach the module.

A non-empty screen stack takes every event. After a {:push, ...}, {:replace, ...} or {:show_modal, ...} the root application module's handle_event/2 is not called at all; input goes to the screen stack. Its keybinding/3 clauses are dormant until the last screen pops.

The widget hierarchy runs before the app whenever a widget holds focus. A tree containing no focusable widget has no focus and never enters this path; a tree containing focusable widgets focuses the first of them on the initial render. So:

  • Nothing focused — handle_event/2 runs first, and {:noreply, state} then offers the event to the hierarchy.
  • Something focused — the hierarchy runs first. Only events it leaves unconsumed reach handle_event/2, and the hierarchy has already had its turn by then, so {:noreply, state} does not offer the event a second time.

The hierarchy counts an event as consumed when a widget produced an action or when focus moved. That makes the following keys reserved while a widget is focused:

  • Tab and Shift+Tab cycle focus and are consumed. The exception is a tree holding exactly one focusable widget: focus cannot move, so the key falls through to handle_event/2. A focused widget carrying both focused and trap_focus in its state — Drafter.Widget.TextArea with trap_focus: true — receives the key itself instead of moving focus.
  • ↑, ↓, ←, → are offered to directional focus navigation first. They are consumed when a neighbouring widget takes focus, and otherwise fall through to the focused widget and then to handle_event/2.
  • Every other key is dispatched to the focused widget, so what is reserved depends on which widget that is. A focused Drafter.Widget.Button consumes Enter and Space and fires its :on_click. A focused Drafter.Widget.TextInput consumes every printable key, Space and the editing keys, but leaves Enter alone.

Esc, PageUp/PageDown, Home/End, Ctrl+C and the function keys are not reserved by the framework and reach handle_event/2 unless the focused widget claims them.

A key the hierarchy consumed still reaches the module through handle_event/3 when the widget carried a callback: Enter on button("Save", on_click: :save) calls handle_event(:save, nil, state).

Event results

Both handle_event callbacks return one of:

  • {:ok, new_state} — the app handled the event. new_state replaces the state and the app re-renders. The event is not offered to the widget hierarchy again.
  • {:noreply, new_state} — the app did not handle the event. new_state replaces the state, and the event is routed to the widget hierarchy — the focused widget first, then bubbling to its ancestors — only if the hierarchy has not already been offered it, which per "Where a key is handled" it has whenever a widget holds focus. This is what a catch-all clause returns; returning {:ok, state} from a catch-all swallows every event the hierarchy did not already claim.
  • {:stop, reason} — the loop terminates. :normal makes Drafter.run/2 return :ok; any other reason makes it return {:error, reason}. Inside a nested session, control returns to the parent app instead of exiting.
  • {:error, reason} — the event is discarded and the state is left unchanged.
  • {:show_modal, screen_module, props, opts} — see show_modal/3.
  • {:show_toast, message, opts} — see show_toast/2.
  • {:pop, result} — see pop_screen/1.
  • {:push, screen_module, props, opts} and {:replace, screen_module, props, opts} — push a screen onto, or replace the top of, the screen stack.

Any other term returned from handle_event/3 is offered to the handlers registered with Drafter.ActionRegistry; see Drafter.ActionHandler. An unrecognised term leaves the state unchanged. Any other term returned from handle_event/2 raises FunctionClauseError in the loop.

Element options

Every constructor takes a trailing keyword list. These keys are read for all of them:

  • :id - atom(), the widget id used by Drafter.get_widget_value/1, Drafter.focus/1, and Drafter.Test. Default nil, which generates :"<LastModulePart>_<type>_<n>", e.g. :Counter_label_1.
  • :key - a stable identity for a widget whose position in the tree moves, used in place of the positional part of the generated id. Default nil. A :key produces a String.t() widget id of the form "Counter_label#abc", not an atom, so pass that string to Drafter.focus/1 and Drafter.Test.query_one/2. Ignored when :id is also given.
  • :visible - boolean(), default true. false omits the element and its children from the hierarchy entirely and its siblings close the gap.
  • :visibility - :hidden allots the element its space and draws nothing in it. Default nil, which draws normally.
  • :margin - space outside the element: an integer for all four sides, a {vertical, horizontal} tuple, or a {top, right, bottom, left} tuple. Default nil, i.e. no margin. Any other shape is treated as no margin.
  • :dock - :top, :bottom, :left, or :right. Default nil. A docked element leaves the normal flow and takes the full span of that edge; the remaining space is shared by its undocked siblings. A footer/1 element is docked to :bottom whether or not the option is given. Only a vertical/2 container honours this — inside a horizontal/2 a docked child is laid out like any other.

A layout container reads :gap (default 0) and :padding from its own options, and :width, :height, :flex, :min_width/:max_width, and :min_height/:max_height from each child's options to divide its space. :width and :height take a cell count, {:percent, n}, {:fr, n} for a share of what is left after fixed siblings, or :auto for the child's own preferred size. :padding takes the same shapes as :margin.

Remaining keys are the individual widget's props; each is documented on the widget module under Drafter.Widget. Note that the element constructors here do not forward every prop a widget's mount/1 accepts — each constructor below names the keys its own widget reads only when mounted directly.

Summary

Types

The direction a layout container arranges its children in.

A node of a component tree.

The trailing keyword list every element constructor accepts.

A screen-stack instruction both handle_event callbacks dispatch.

Callbacks

Handles a raw input event.

Handles a named callback.

Builds the application's initial state from its mount props.

Handles a process message delivered to the loop that Drafter does not itself consume — PubSub broadcasts, send/2 from a task, GenServer replies.

Runs once after the first render, before the first event.

Runs on the first scroll event of a scroll gesture. Returns the new state.

Runs once a scroll gesture settles, 150 ms after its last scroll event. Returns the new state.

Runs once per firing of a registered timer.

The application's frame interval.

Returns the component tree for the current state.

Releases resources as the application stops. Returns :ok.

Rebuilds the state when the module is re-rendered with new props.

Functions

Appends the catch-all handle_event/2 and on_timer/2 clauses after every clause the module defined, and defines keybindings/0 when keybinding/3 was used.

A bordered container. Returns {:box, children, opts}.

A path trail. Returns {:breadcrumb, items, opts}.

A pressable button. Returns {:button, text, opts}.

A month calendar. Returns {:calendar, opts}.

A titled panel. Returns {:card, children, opts}.

A line, bar, or area chart. Returns {:chart, data, opts}.

A checkbox with a caption. Returns {:checkbox, label, opts}.

A syntax-highlighted source view. Returns {:code_view, opts}.

A titled section that folds away. Returns {:collapsible, title, content, opts}.

A layout container whose direction comes from its options.

A scrollable table with selectable rows. Returns {:data_table, opts}.

Large seven-segment style digits. Returns {:digits, value, opts}.

A browsable filesystem tree. Returns {:directory_tree, opts}.

Closes the topmost modal. Returns {:pop, :dismissed}, so the revealed screen's handle_event/3 receives :dismissed as its data.

The application's key-hint bar. Returns {:footer, opts}.

A dial-style gauge. Returns {:gauge, opts}.

The application's title bar. Returns {:header, title, opts}.

Lays children out left to right. Returns {:layout, :horizontal, children, opts} with layout: :horizontal added to opts.

Defines a handle_event/2 clause for a key, and registers it for display in the footer.

A line of text. Returns {:label, text, opts}.

A clickable hyperlink. Returns {:link, text, opts}.

An animated spinner. Returns {:loading_indicator, opts}.

A scrolling line log. Returns {:log, opts}.

Rendered Markdown. Returns {:markdown, content, opts}.

A text field that renders its content as mask characters.

A labelled value bar. Returns {:meter, opts}.

A single-selection list. Returns {:option_list, items, opts}.

A pie chart. Returns {:pie_chart, data, opts}.

A labelled block that fills its space, for laying out a screen before its content exists. Returns {:placeholder, opts}.

Pops the top screen off the screen stack.

An inspected Elixir term, formatted and syntax-coloured.

A horizontal progress bar. Returns {:progress_bar, opts}.

Returns {:push_screen, screen_module, props, opts}.

A group of mutually exclusive radio buttons. Returns {:radio_set, options, opts}.

Returns {:replace, screen_module, props}.

A scrolling log that keeps per-line styling. Returns {:rich_log, opts}.

A divider line. Returns {:rule, opts}.

Assembles a header/content/footer screen into one vertical layout.

A viewport that scrolls its children. Returns {:scrollable, children, opts}.

A list whose entries can be checked. Returns {:selection_list, options, opts}.

Opens screen_module as a modal over the current screen.

Returns {:push_screen, screen_module, props, [{:type, :panel} | opts]}.

Returns {:push_screen, screen_module, props, [{:type, :popover} | opts]}.

Shows a transient message over the current screen.

A fixed-width column beside a column that takes the rest of the width.

A draggable value slider. Returns {:slider, opts}.

Returns {:slider, [{:value, value} | opts]}.

A single-row trend line. Returns {:sparkline, data, opts}.

Two panes separated by a draggable divider.

Pre-rendered content that is drawn as given. Returns {:static, content, opts}.

An on/off switch. Returns {:switch, opts}.

Returns {:switch, [{:value, value} | opts]}.

A two-column block of switches that all report to one callback.

A tab bar over switchable panes. Returns {:tabbed_content, tabs, opts}.

A multi-line text editor. Returns {:text_area, opts}.

A single-line text field. Returns {:text_input, opts}.

A list of the installed themes that switches theme on selection.

An expandable tree of nodes. Returns {:tree, opts}.

Stacks children top to bottom. Returns {:layout, :vertical, children, opts}.

Types

direction()

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

The direction a layout container arranges its children in.

element()

@type element() ::
  {atom(), opts()}
  | {atom(), term(), opts()}
  | {atom(), term(), term(), opts()}
  | {module(), map()}

A node of a component tree.

Either a tag tuple built by one of the constructors in this module — the tag atom, zero to two positional arguments, and the options keyword list last — or a {WidgetModule, props_map} tuple as Drafter.label/2 and friends build.

opts()

@type opts() :: keyword()

The trailing keyword list every element constructor accepts.

props()

@type props() :: map()

rect()

@type rect() :: Drafter.Widget.rect()

screen_action()

@type screen_action() ::
  {:show_modal, module(), props(), opts()}
  | {:show_toast, String.t(), opts()}
  | {:push, module(), props(), opts()}
  | {:replace, module(), props(), opts()}
  | {:pop, term()}

A screen-stack instruction both handle_event callbacks dispatch.

Note the four-element {:push, ...} and {:replace, ...}, which are what the loop matches — not the tags push_screen/3 and replace_screen/3 build.

state()

@type state() :: term()

Callbacks

handle_event(t, state)

@callback handle_event(Drafter.Event.t(), state()) ::
  {:ok, state()}
  | {:error, term()}
  | {:noreply, state()}
  | {:stop, term()}
  | screen_action()

Handles a raw input event.

event is a Drafter.Event.t/0 tuple — {:key, key}, {:key, key, modifiers}, {:char, codepoint}, {:mouse, map}, {:resize, {width, height}}, and the rest. Printable ASCII 32..126 arrives as a named key ({:key, :q}, {:key, :" "}); only codepoints above 126 arrive as {:char, codepoint}.

This callback does not see every event. Ctrl+Q stops the loop first; a non-empty screen stack takes the event instead; and whenever a widget holds focus the widget hierarchy is offered the event before this callback and anything it consumes never arrives. See "Where a key is handled" in Drafter.App.

Return {:noreply, state} for events the app does not claim: the event is then routed to the focused widget and its ancestors, unless the hierarchy was already offered it. {:ok, new_state} consumes the event and re-renders, {:stop, reason} ends the app, {:error, reason} discards the event, and a screen_action/0 manipulates the screen stack. Any other term raises FunctionClauseError in the loop.

A catch-all clause returning {:noreply, state} is appended automatically, so a module need only define the clauses it cares about.

handle_event(event_name, data, state)

(optional)
@callback handle_event(event_name :: term(), data :: term(), state()) :: term()

Handles a named callback.

event_name is whatever term a widget was given as on_click:, on_change:, and so on, or the name passed to Drafter.send_app_event/2. It is normally an atom, but any non-function term is passed through unchanged — switch_group/2 delivers the tuple {:switch_group_changed, group_name, value} this way. data is the widget's payload, nil for widgets that carry none.

Returns {:ok, new_state} to accept the change and re-render, {:noreply, new_state} to accept it without claiming the event, {:stop, reason} to end the app, or {:error, reason} to discard it, or a screen_action/0. Any other term is offered to the handlers registered with Drafter.ActionRegistry, and an unrecognised one leaves the state unchanged — which is why the return type here is term() rather than a closed union.

No catch-all clause is generated for this callback: a module that defines any clause must also define a final one, or an unmatched name raises FunctionClauseError.

mount(props)

@callback mount(props()) :: state()

Builds the application's initial state from its mount props.

props is the map passed under :props to Drafter.run/2, Drafter.run_session/3, or Drafter.Test.start_headless/3, and is %{} when none were given. The returned term is the state handed to every other callback.

on_message(msg, state)

(optional)
@callback on_message(msg :: term(), state()) :: state()

Handles a process message delivered to the loop that Drafter does not itself consume — PubSub broadcasts, send/2 from a task, GenServer replies.

Returns the new state.

on_ready(state)

(optional)
@callback on_ready(state()) :: state()

Runs once after the first render, before the first event.

Returns the state to continue with. Timers started here with Drafter.set_interval/2 are registered before the loop begins receiving.

on_scroll_active(state)

(optional)
@callback on_scroll_active(state()) :: state()

Runs on the first scroll event of a scroll gesture. Returns the new state.

on_scroll_idle(state)

(optional)
@callback on_scroll_idle(state()) :: state()

Runs once a scroll gesture settles, 150 ms after its last scroll event. Returns the new state.

on_timer(atom, state)

(optional)
@callback on_timer(atom(), state()) :: state()

Runs once per firing of a registered timer.

The first argument is the timer_id given to Drafter.set_interval/2 or Drafter.set_timeout/2. Returns the new state.

refresh_rate()

(optional)
@callback refresh_rate() :: pos_integer() | String.t() | :unlimited

The application's frame interval.

Returns "30fps"-style strings, "7.5fps", a positive millisecond integer, or :unlimited to render without pacing. "30fps" is used when the callback is not defined. Anything else raises ArgumentError when the loop starts.

render(state)

@callback render(state()) :: term()

Returns the component tree for the current state.

An element tuple from a constructor in this module, a {WidgetModule, props_map} tuple, or a list of either. Called after every state change, so it must be free of side effects.

unmount(state)

(optional)
@callback unmount(state()) :: :ok

Releases resources as the application stops. Returns :ok.

update(props, state)

(optional)
@callback update(props(), state()) :: state()

Rebuilds the state when the module is re-rendered with new props.

props is the new prop map, state the state as it stands. Returns the state to continue with.

Functions

__before_compile__(env)

(macro)

Appends the catch-all handle_event/2 and on_timer/2 clauses after every clause the module defined, and defines keybindings/0 when keybinding/3 was used.

box(children, opts \\ [])

@spec box([element()], opts()) :: {:box, [element()], opts()}

A bordered container. Returns {:box, children, opts}.

children is a list of child elements, laid out inside the rect left after the border and padding are subtracted.

Options beyond the shared element keys — these four are the only ones this element forwards to Drafter.Widget.Box:

  • :title - String.t() embedded in the top border. Default nil.
  • :border - :none, :single, :double, :rounded, :heavy, :dashed, or :ascii. Default: the current character set's border style, falling back to :rounded. :none still costs no rows.
  • :padding - non_neg_integer() cells inside the border. Default: the character set's padding, falling back to 1 — not 0.
  • :style - map() of style attributes for the box as a whole. Default %{}.

:border_style, :title_style, :content_style and :classes are documented on Drafter.Widget.Box because its mount/1 reads them, but this element does not forward them and passing them here does nothing.

iex> box([label("Ready")], title: "Status", border: :double)
{:box, [{:label, "Ready", []}], [title: "Status", border: :double]}

button(text, opts \\ [])

@spec button(String.t(), opts()) :: {:button, String.t(), opts()}

A pressable button. Returns {:button, text, opts}.

text is the String.t() caption.

A focused button consumes Enter and Space, so neither key reaches the app's handle_event/2 or a keybinding/3 clause while it holds focus. Activation arrives as handle_event(on_click_atom, nil, state) instead.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Button:

  • :on_click - the atom delivered to handle_event/3 when the button is activated, or a zero-arity function. Default nil, and no callback fires. Forced to nil when :disabled is true.
  • :variant - :default (the default), :primary, and the other theme variants. :type is read as a fallback when :variant is absent.
  • :disabled - boolean(), default false. A disabled button takes no focus and fires no callback.
  • :compact - boolean(), default false. Drops the button's border rows.
  • :style - map() of style attributes. Default %{}.
  • :class - an atom or list of atoms matched by CSS selectors. Default [].

Examples

iex> button("Save", on_click: :save, variant: :primary)
{:button, "Save", [on_click: :save, variant: :primary]}

calendar(opts \\ [])

@spec calendar(opts()) :: {:calendar, opts()}

A month calendar. Returns {:calendar, opts}.

See Drafter.Widget.Calendar for its options; the positional argument the widget would take is not used by this element, so everything comes from opts. A focused calendar consumes the four arrow keys, Enter and Space.

iex> calendar(id: :when)
{:calendar, [id: :when]}

card(children, opts \\ [])

@spec card([String.t()], opts()) :: {:card, [String.t()], opts()}

A titled panel. Returns {:card, children, opts}.

children must be a list of strings, one per content row. Each entry is run through to_string/1, so passing element tuples — the natural reading of "children" and what box/2 and vertical/2 take — raises Protocol.UndefinedError for String.Chars when the tree renders. A card is a text panel, not a layout container.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Card:

  • :title - String.t() in the top border. Default nil.
  • :border - border style atom. Default: the current character set's border style, falling back to :rounded.
  • :style - map() of style attributes. Default %{}.
  • :border_color - border colour. Default nil.
  • :background - panel background colour. Default nil.
  • :color - content foreground colour. Default nil.
  • :class - an atom or list of atoms. Default []; reaches the widget as :classes.

Examples

iex> card(["3 files changed"], title: "Summary")
{:card, ["3 files changed"], [title: "Summary"]}

chart(data, opts \\ [])

@spec chart(term(), opts()) :: {:chart, term(), opts()}

A line, bar, or area chart. Returns {:chart, data, opts}.

data is the series to plot; any non-list is treated as absent and the :data option is used instead. See Drafter.Widget.Chart for the accepted series shapes and the full option list. The chart kind is :chart_type (default :line), not :type. :width and :height default to the rect the parent allocated.

iex> chart([1, 2, 3], chart_type: :bar)
{:chart, [1, 2, 3], [chart_type: :bar]}

checkbox(label, opts \\ [])

@spec checkbox(String.t(), opts()) :: {:checkbox, String.t(), opts()}

A checkbox with a caption. Returns {:checkbox, label, opts}.

label is the String.t() shown beside the box. Drafter.get_widget_value/1 returns the checked boolean(). A focused checkbox consumes Space.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Checkbox:

  • :checked - boolean() initial state. Default false.
  • :bind - app-state key atom for two-way binding of the checked state. Default nil. Only with a :bind does a later :checked change reach the mounted widget.
  • :on_change - atom event name or one-arity function called with the new boolean(). Default nil.
  • :style - map() of style attributes. Default %{}.
  • :class - an atom or list of atoms. Default [].

Examples

iex> checkbox("Enable logging", checked: true, on_change: :toggle_logging)
{:checkbox, "Enable logging", [checked: true, on_change: :toggle_logging]}

code_view(opts \\ [])

@spec code_view(opts()) :: {:code_view, opts()}

A syntax-highlighted source view. Returns {:code_view, opts}.

Called with a single keyword list, the source comes from the options. Called as code_view(content, opts) with a String.t() first argument, the content is prepended as source: content, so an explicit :source in opts is shadowed by the positional argument. A non-list opts or a non-binary first argument raises FunctionClauseError.

Highlighting requires Drafter.run(app, syntax_highlighting: true); without it the source renders unstyled. See Drafter.Widget.CodeView for the options.

A focused code view consumes the arrow keys, Enter, Space, Escape, Home, End, PageUp, PageDown, Backspace and the printable keys, so almost nothing reaches a keybinding/3 clause while it has focus.

iex> code_view(source: "IO.puts(:hi)", language: :elixir)
{:code_view, [source: "IO.puts(:hi)", language: :elixir]}

iex> code_view("IO.puts(:hi)", language: :elixir)
{:code_view, [source: "IO.puts(:hi)", language: :elixir]}

code_view(content, opts)

@spec code_view(String.t(), opts()) :: {:code_view, opts()}

collapsible(title, content, opts \\ [])

@spec collapsible(String.t(), [element()], opts()) ::
  {:collapsible, String.t(), [element()], opts()}

A titled section that folds away. Returns {:collapsible, title, content, opts}.

title is the String.t() on the header row; content is the list of elements shown while expanded. Drafter.get_widget_value/1 returns the expanded boolean(), and a focused collapsible consumes Enter and Space.

Options beyond the shared element keys — these three are the only ones this element forwards to Drafter.Widget.Collapsible:

  • :expanded - boolean() initial state. Default false.
  • :on_toggle - atom event name or one-arity function receiving the new boolean(). Default nil.
  • :content_height - pos_integer() rows reserved for the expanded content. Default nil, i.e. the content's own preferred height.

Its widget id is not positional: absent an :id or :key, the generated id is derived from a hash of title, so two collapsibles with the same title in one tree collide.

iex> collapsible("Advanced", [label("Verbose")], expanded: true)
{:collapsible, "Advanced", [{:label, "Verbose", []}], [expanded: true]}

container(children, opts \\ [])

@spec container([element()], opts()) :: {:layout, direction(), [element()], opts()}

A layout container whose direction comes from its options.

Returns {:layout, direction, children, opts} where direction is the :layout option, default :vertical. Otherwise identical to vertical/2 and horizontal/2, which are the direct forms.

  • :layout - :vertical (default) or :horizontal. Any other value is copied into the tuple unchanged and raises FunctionClauseError when the tree is rendered — the renderer has no clause for a third direction.

Examples

iex> container([label("a")], layout: :horizontal)
{:layout, :horizontal, [{:label, "a", []}], [layout: :horizontal]}

iex> container([label("a")])
{:layout, :vertical, [{:label, "a", []}], []}

data_table(opts \\ [])

@spec data_table(opts()) :: {:data_table, opts()}

A scrollable table with selectable rows. Returns {:data_table, opts}.

Options are the widget's props; Drafter.Widget.DataTable documents all of them. There is no positional argument: columns go in :columns and rows in :data, not :rows. :width and :height default to the rect the parent allocated. Drafter.get_widget_value/1 returns the list of selected row indices.

A focused data table consumes ↑, ↓, PageUp, PageDown, Home, End, Enter and Space, so none of those reach a keybinding/3 clause.

iex> data_table(id: :rows, columns: [{:name, "Name"}], data: [%{name: "a"}])
{:data_table, [id: :rows, columns: [{:name, "Name"}], data: [%{name: "a"}]]}

digits(value, opts \\ [])

@spec digits(term(), opts()) :: {:digits, term(), opts()}

Large seven-segment style digits. Returns {:digits, value, opts}.

value is the text to draw; it is run through to_string/1, so an integer or float works as well as a String.t().

Options beyond the shared element keys, all forwarded to Drafter.Widget.Digits:

  • :style - map() of style attributes. Default %{}.
  • :align - :left (default), :center, or :right.
  • :size - :large (default) and the other sizes the widget lists.
  • :font - glyph set name. Default nil, i.e. the built-in font.
  • :renderer - :text (default) or the image renderers.
  • :color - {r, g, b} for the lit segments. Default {0, 150, 255}.
  • :bg_data, :bg_min, :bg_max - background sparkline data and its range. Defaults nil, 0, and nil.

Examples

iex> digits("12:04", align: :center)
{:digits, "12:04", [align: :center]}

directory_tree(opts \\ [])

@spec directory_tree(opts()) :: {:directory_tree, opts()}

A browsable filesystem tree. Returns {:directory_tree, opts}.

See Drafter.Widget.DirectoryTree for its :path and other options. A focused directory tree consumes the arrow keys, Enter, Space, Escape, Home, End, PageUp, PageDown, Backspace and the printable keys it uses to type-to-find, so almost nothing reaches a keybinding/3 clause while it has focus.

iex> directory_tree(id: :files, path: "/tmp")
{:directory_tree, [id: :files, path: "/tmp"]}

dismiss_modal()

@spec dismiss_modal() :: {:pop, :dismissed}

Closes the topmost modal. Returns {:pop, :dismissed}, so the revealed screen's handle_event/3 receives :dismissed as its data.

Identical to pop_screen(:dismissed); it pops whatever is on top of the screen stack, modal or not.

iex> dismiss_modal()
{:pop, :dismissed}

footer(opts \\ [])

@spec footer(opts()) :: {:footer, opts()}

The application's key-hint bar. Returns {:footer, opts}.

Takes options only — unlike Drafter.footer/2, there is no text argument. use Drafter.App imports this module and not Drafter, so an unqualified footer(...) in an application module is always this function; reaching Drafter.footer/2 needs the Drafter. prefix. Adding import Drafter alongside use Drafter.App does not give you a choice — it makes footer/1, vertical/1 and every other name the two modules share ambiguous, and the module fails to compile.

A footer element is docked to the bottom of its enclosing vertical/2 container whether or not dock: :bottom is given.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Footer:

  • :bindings - a list of {key_label, hint} tuples to show. Default nil, which reads the active screen's keybindings/0 — the app module's when no screen is on the screen stack — i.e. the hints registered by keybinding/3, and [] when the module defines none.
  • :separator - String.t() placed between entries. Default " ".
  • :style - map() of style attributes for the hint text. Default nil.
  • :key_style - map() of style attributes for the key labels. Default nil.

The hints are what keybinding/3 registered, which is not the same as what the keys do: a hint whose key the focused widget consumes is displayed anyway. See "Where a key is handled" in Drafter.App.

iex> footer()
{:footer, []}

iex> footer(bindings: [{"Q", "quit"}], separator: " | ")
{:footer, [bindings: [{"Q", "quit"}], separator: " | "]}

gauge(opts \\ [])

@spec gauge(opts()) :: {:gauge, opts()}

A dial-style gauge. Returns {:gauge, opts}.

Options are the widget's props; see Drafter.Widget.Gauge.

iex> gauge(value: 72, max: 100)
{:gauge, [value: 72, max: 100]}

header(title, opts \\ [])

@spec header(String.t() | nil, opts()) :: {:header, String.t() | nil, opts()}

The application's title bar. Returns {:header, title, opts}.

title is the String.t() shown on the left, or nil to take the :title option instead.

Options beyond the shared element keys:

  • :title - fallback title, used when the first argument is nil. Default "".
  • :show_clock - boolean(), default false. Draws a self-updating clock on the right, and starts the 1-second timer that keeps it current.
  • :clock_format - :time (default) or the other formats accepted by Drafter.Widget.Header.

Only the title reaches an already-mounted header, so changing :show_clock or :clock_format on a re-render has no effect. Not focusable, so it reserves no keys.

iex> header("Counter")
{:header, "Counter", []}

iex> header(nil, title: "Counter", show_clock: false)
{:header, nil, [title: "Counter", show_clock: false]}

horizontal(children, opts \\ [])

@spec horizontal([element()], opts()) :: {:layout, :horizontal, [element()], opts()}

Lays children out left to right. Returns {:layout, :horizontal, children, opts} with layout: :horizontal added to opts.

children is a list of elements.

Options beyond the shared element keys:

  • :gap - non_neg_integer() columns between children. Default 0.
  • :padding - space inside the container, in the same shapes as :margin. Default nil, i.e. none.
  • :layout - overwritten with :horizontal; passing it has no effect.

Each child's own :width and :flex options decide how the row is divided. When no child carries either, the width is split evenly.

iex> horizontal([button("No", on_click: :no)], gap: 2)
{:layout, :horizontal, [{:button, "No", [on_click: :no]}],
 [layout: :horizontal, gap: 2]}

keybinding(key_spec, hint, list)

(macro)

Defines a handle_event/2 clause for a key, and registers it for display in the footer.

key_spec is either a bare key atom (:q, :enter, :f1) or a {key, modifiers} tuple whose modifiers are drawn from :ctrl, :shift, and :alt ({:s, [:ctrl]}). hint is the String.t() shown beside the key label in footer/1 and returned from the module's generated keybindings/0 as {"Ctrl+S", "save"}.

The block is the body of the clause. The current state is bound to state, and the block must return one of the handle_event/2 results.

keybinding :q, "quit" do
  {:stop, :normal}
end

keybinding {:s, [:ctrl]}, "save" do
  {:ok, %{state | saved: true}}
end

A bare key atom generates a clause matching {:key, key} exactly, so it does not fire when a modifier is held; a {key, modifiers} spec generates a clause matching {:key, key, modifiers} with that modifier list exactly. A codepoint above ASCII 126 arrives as {:char, codepoint} and needs a hand-written handle_event/2 clause.

Clause order

The generated clause is placed where the macro call appears, in source order among the module's own handle_event/2 clauses and ahead of the catch-all __before_compile__/1 appends. A keybinding/3 written after a hand-written catch-all handle_event(_event, state) is unreachable and the compiler warns.

Which keys can be bound

A keybinding/3 clause is a handle_event/2 clause, so it is subject to everything under "Where a key is handled" in Drafter.App. It is not a global hotkey and it does not pre-empt the widget hierarchy. In particular:

  • {:q, [:ctrl]} never fires. Ctrl+Q stops the loop before any callback runs.
  • No keybinding on the root application module fires while the screen stack is non-empty. Bind the key on the screen module that is on top instead.
  • With a widget focused — which is the normal case, since the first focusable widget in the tree is focused on the initial render — the hierarchy sees the key first and a key it consumes never reaches the clause. Enter and Space are consumed by a focused Drafter.Widget.Button; the printable keys, Space and the editing keys are consumed by a focused Drafter.Widget.TextInput; Tab and Shift+Tab are consumed whenever the tree has two or more focusable widgets; the arrow keys are consumed whenever directional navigation finds a neighbour to move focus to.
  • With no focusable widget in the tree, nothing is reserved except Ctrl+Q, and every binding — Tab, Enter, :" " included — fires.

The bindings that behave the way their footer hint implies regardless of what is focused are the letters, punctuation and function keys no focused widget claims — :q, :k, :f1 and the like. A modifier combination is not automatically safe: a focused Drafter.Widget.TextInput claims Ctrl+A, Ctrl+← and Ctrl+→ among others, and each widget's own moduledoc lists what it takes.

label(text, opts \\ [])

@spec label(String.t() | [{String.t(), map()}], opts()) ::
  {:label, String.t() | [{String.t(), map()}], opts()}

A line of text. Returns {:label, text, opts}.

text is a String.t(); embedded newlines wrap onto further rows.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Label:

  • :style - map() of style attributes, e.g. %{bold: true, fg: :cyan}. Default %{}.
  • :align - :left (default), :center, or :right.
  • :variant - theme variant atom. Default :default.
  • :class - an atom or list of atoms matched by CSS selectors. Default []; reaches the widget as :classes.

Examples

iex> label("Ready")
{:label, "Ready", []}

iex> label("Count: 3", style: %{bold: true}, align: :center)
{:label, "Count: 3", [style: %{bold: true}, align: :center]}

text may instead be a list of {text, style} runs, drawn side by side on one line as one widget, each run in its own style over the label's:

iex> label([{"ok ", %{fg: :green}}, {"3 failed", %{fg: :red}}])
{:label, [{"ok ", %{fg: :green}}, {"3 failed", %{fg: :red}}], []}

link(text, opts \\ [])

@spec link(String.t() | nil, String.t() | opts()) :: {:link, String.t() | nil, opts()}

A clickable hyperlink. Returns {:link, text, opts}.

text is the caption, or nil to take the :text option instead. The second argument is either the URL as a String.t(), which is wrapped into [url: url] and discards every other option, or a keyword list. Anything else — an atom, a map, an integer — raises FunctionClauseError.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Link:

  • :url - the address opened when the link is activated. Default nil, and activating it does nothing.
  • :text - fallback caption, used when text is nil. Default nil.
  • :tooltip - String.t() shown on hover. Default nil.
  • :style - map() of style attributes. Default %{}.
  • :class - an atom or list of atoms. Default [].

A focused link consumes Enter, which shells out to open, xdg-open, or start for the platform.

iex> link("Docs", "https://hexdocs.pm/drafter")
{:link, "Docs", [url: "https://hexdocs.pm/drafter"]}

iex> link("Docs", url: "https://hexdocs.pm/drafter", tooltip: "Hex")
{:link, "Docs", [url: "https://hexdocs.pm/drafter", tooltip: "Hex"]}

loading_indicator(opts \\ [])

@spec loading_indicator(opts()) :: {:loading_indicator, opts()}

An animated spinner. Returns {:loading_indicator, opts}.

Options beyond the shared element keys, all forwarded to Drafter.Widget.LoadingIndicator:

  • :text - caption beside the spinner. Default "Loading...".
  • :spinner_type - :default (default), :dots, :line, or :points.
  • :running - boolean(). Default true.
  • :gradient_colors - list of {r, g, b} cycled through the caption. Default nil, i.e. a plain caption.
  • :gradient_speed - milliseconds per gradient step. Default 50.
  • :style - map() of style attributes. Default %{}.
  • :class - an atom or list of atoms. Default [].

A re-render only refreshes the animation timestamp, so changing any of these options after the first mount has no effect.

iex> loading_indicator(text: "Fetching…", spinner_type: :line)
{:loading_indicator, [text: "Fetching…", spinner_type: :line]}

log(opts \\ [])

@spec log(opts()) :: {:log, opts()}

A scrolling line log. Returns {:log, opts}.

See Drafter.Widget.Log for its options. Lines are appended with Drafter.push_data/2 when the element carries both :buffer and :refresh; those two keys are lifted out of opts and used to open the widget's data channel, and :image_throttle is lifted the same way.

Not focusable, so it reserves no keys.

iex> log(id: :output, max_lines: 500)
{:log, [id: :output, max_lines: 500]}

markdown(content, opts \\ [])

@spec markdown(String.t() | nil, opts()) :: {:markdown, String.t() | nil, opts()}

Rendered Markdown. Returns {:markdown, content, opts}.

content is the Markdown source as a String.t(), or nil to take the :content option instead.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Markdown:

  • :content - fallback source, used when content is nil. Default "".
  • :style - map() of style attributes. Default %{}.
  • :padding - non_neg_integer() cells of inset. Default 1.

Only :content reaches an already-mounted widget, so changing :style or :padding on a re-render has no effect. Not focusable, so it reserves no keys.

iex> markdown("# Title", padding: 0)
{:markdown, "# Title", [padding: 0]}

masked_input(opts)

@spec masked_input(opts()) :: {:masked_input, opts()}

A text field that renders its content as mask characters.

Returns {:masked_input, opts}. opts is required and must be a keyword list — there is no zero-arity form, so masked_input() is an UndefinedFunctionError and any non-list argument a FunctionClauseError. See Drafter.Widget.MaskedInput for its options.

A focused masked input consumes ←, → and Enter.

iex> masked_input(id: :pin, mask: "****-****")
{:masked_input, [id: :pin, mask: "****-****"]}

meter(opts \\ [])

@spec meter(opts()) :: {:meter, opts()}

A labelled value bar. Returns {:meter, opts}.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Meter:

  • :value - the reading, 0.0..1.0. Default 0.0.
  • :label - String.t() caption. Default nil.
  • :orientation - :horizontal (default) or :vertical.
  • :thresholds - the widget's colour bands. Default: its own threshold list.
  • :show_value - boolean(). Default true.
  • :show_label - boolean(). Default true.
  • :style - map() of style attributes. Default %{}.
  • :class - an atom or list of atoms. Default [].

:style, :class and the app module are read at mount time only; the rest are live-updatable. :height is ignored for a horizontal meter.

iex> meter(value: 0.82, label: "CPU")
{:meter, [value: 0.82, label: "CPU"]}

option_list(items, opts \\ [])

@spec option_list([term()], opts()) :: {:option_list, [term()], opts()}

A single-selection list. Returns {:option_list, items, opts}.

items is the list of options; see Drafter.Widget.OptionList for their shape and for the widget's options. Drafter.get_widget_value/1 returns the selected option's id.

A focused option list consumes ↓, Home, End, PageUp, PageDown, Enter and Space, and ↑ except on the first entry, where it bubbles so focus can leave upwards.

iex> option_list([%{id: :a, label: "Alpha"}], on_select: :chose)
{:option_list, [%{id: :a, label: "Alpha"}], [on_select: :chose]}

pie_chart(data, opts \\ [])

@spec pie_chart(term(), opts()) :: {:pie_chart, term(), opts()}

A pie chart. Returns {:pie_chart, data, opts}.

data is the list of slices, each {label, value} or {label, value, {r, g, b}}. An empty list or a keyword list is treated as absent and the :data option is used instead. See Drafter.Widget.PieChart for the rest of its options; the ones with non-obvious defaults are :show_legend (true), :show_percentages (true), and :renderer (:text).

iex> pie_chart([{"Elixir", 3}, {"Erlang", 1}], show_legend: false)
{:pie_chart, [{"Elixir", 3}, {"Erlang", 1}], [show_legend: false]}

placeholder(opts \\ [])

@spec placeholder(opts()) :: {:placeholder, opts()}

A labelled block that fills its space, for laying out a screen before its content exists. Returns {:placeholder, opts}.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Placeholder:

  • :label - the caption. Default: the :text option, itself defaulting to "Placeholder".
  • :text - fallback caption when :label is absent. Default "Placeholder".
  • :padding - non_neg_integer() cells of inset. Default 2.
  • :align - :center (default), :left, or :right.
  • :border - boolean(). Default false.
  • :style - map() of style attributes. Default %{}.

Only the caption reaches an already-mounted widget; :padding, :align, :border and :style are read at mount time.

iex> placeholder(label: "Chart goes here", border: true)
{:placeholder, [label: "Chart goes here", border: true]}

pop_screen(result \\ nil)

@spec pop_screen(term()) :: {:pop, term()}

Pops the top screen off the screen stack.

Returns {:pop, result}, which both handle_event/2 and handle_event/3 dispatch. result (default nil) is delivered to the revealed screen's handle_event/3.

iex> pop_screen()
{:pop, nil}

iex> pop_screen({:selected, 3})
{:pop, {:selected, 3}}

pretty(data, opts \\ [])

@spec pretty(term(), opts()) :: {:pretty, term(), opts()}

An inspected Elixir term, formatted and syntax-coloured.

Returns {:pretty, data, opts}. data is any term.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Pretty:

  • :expand - boolean(), print one field per line. Default false.
  • :syntax_highlighting - boolean(). Default true.
  • :style - map() of style attributes. Default %{}.
  • :class - an atom or list of atoms. Default [].

Only :data reaches an already-mounted widget, so changing any of the others on a re-render has no effect. Not focusable, so it reserves no keys.

iex> pretty(%{status: :ok}, expand: true)
{:pretty, %{status: :ok}, [expand: true]}

progress_bar(opts \\ [])

@spec progress_bar(opts()) :: {:progress_bar, opts()}

A horizontal progress bar. Returns {:progress_bar, opts}.

Options are the widget's props; see Drafter.Widget.ProgressBar. Not focusable, so it reserves no keys. :width and :height fall back to the rect the parent allocated.

iex> progress_bar(value: 0.4)
{:progress_bar, [value: 0.4]}

push_screen(screen_module, props \\ %{}, opts \\ [])

@spec push_screen(module(), props(), opts()) ::
  {:push_screen, module(), props(), opts()}

Returns {:push_screen, screen_module, props, opts}.

screen_module is another Drafter.App module, props the map handed to its mount/1 (default %{}), opts the screen options (default []).

The application loop and Drafter.ActionHandler dispatch on {:push, module, props, opts}, not on this tag: returning this tuple from handle_event/2 raises FunctionClauseError in the loop, and returning it from handle_event/3 leaves the state unchanged with no screen pushed. Return {:push, screen_module, props, opts} to push a screen.

iex> push_screen(MyApp.Detail, %{id: 7})
{:push_screen, MyApp.Detail, %{id: 7}, []}

radio_set(options, opts \\ [])

@spec radio_set([term()], opts()) :: {:radio_set, [term()], opts()}

A group of mutually exclusive radio buttons. Returns {:radio_set, options, opts}.

options is the list of choices; see Drafter.Widget.RadioSet for their shape and for the widget's options. Drafter.get_widget_value/1 returns the selected option's id. A :width given here is ignored: the set is drawn to the rect the parent allocated.

A focused radio set consumes ↑, ↓, Enter and Space — except ↑ on the first entry, which bubbles so focus can leave upwards.

iex> radio_set([%{id: :a, label: "A"}], on_change: :picked)
{:radio_set, [%{id: :a, label: "A"}], [on_change: :picked]}

replace_screen(screen_module, props \\ %{}, opts \\ [])

@spec replace_screen(module(), props(), opts()) :: {:replace, module(), props()}

Returns {:replace, screen_module, props}.

screen_module is another Drafter.App module and props the map handed to its mount/1 (default %{}). The third argument is accepted and discarded.

The application loop and Drafter.ActionHandler dispatch on the four-element {:replace, module, props, opts}, not on this three-element tuple: returning this from handle_event/2 raises FunctionClauseError in the loop, and returning it from handle_event/3 leaves the state unchanged with no screen replaced. Return {:replace, screen_module, props, opts} to replace the top screen.

iex> replace_screen(MyApp.Detail, %{id: 7}, title: "ignored")
{:replace, MyApp.Detail, %{id: 7}}

rich_log(opts \\ [])

@spec rich_log(opts()) :: {:rich_log, opts()}

A scrolling log that keeps per-line styling. Returns {:rich_log, opts}.

See Drafter.Widget.RichLog for its options. As with log/1, :buffer, :refresh and :image_throttle open a Drafter.push_data/2 channel. Not focusable, so it reserves no keys.

iex> rich_log(id: :events)
{:rich_log, [id: :events]}

rule(opts \\ [])

@spec rule(opts()) :: {:rule, opts()}

A divider line. Returns {:rule, opts}.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Rule:

  • :orientation - :horizontal (default) or :vertical.
  • :title - String.t() set into the line. Default nil.
  • :title_align - :center (default), :left, or :right.
  • :line_style - :solid (default) and the other styles the widget lists.
  • :style - map() of style attributes. Default %{}.

Every option is live-updatable through the component tree.

iex> rule(title: "Details")
{:rule, [title: "Details"]}

screen_layout(parts)

@spec screen_layout(keyword()) :: {:layout, :vertical, [element()], []}

Assembles a header/content/footer screen into one vertical layout.

parts is a keyword list read for :header, :content, and :footer; each defaults to nil and an omitted part leaves no gap. Every other key is ignored. :content may be a single element or a list of them. When a :footer is present the content is wrapped in a vertical(content, flex: 1) so it absorbs the leftover rows; without a footer a list of content elements is spliced in directly.

Returns a vertical/2 element whose own options are always [], so :gap, :padding and the shared element keys cannot be set through this function — build the vertical/2 by hand when you need them.

iex> screen_layout(header: header("Files"), content: [tree(id: :files)], footer: footer())
{:layout, :vertical,
 [
   {:header, "Files", []},
   {:layout, :vertical, [{:tree, [id: :files]}], [flex: 1]},
   {:footer, []}
 ], []}

iex> screen_layout(content: label("body"))
{:layout, :vertical, [{:label, "body", []}], []}

scrollable(children, opts \\ [])

@spec scrollable([element()], opts()) :: {:scrollable, [element()], opts()}

A viewport that scrolls its children. Returns {:scrollable, children, opts}.

children is a list of elements, whose summed preferred heights become the scrollable content height. One column on the right is reserved for the vertical scrollbar.

Options beyond the shared element keys — these five are the only ones this element reads, and the rest of Drafter.Widget.ScrollableContainer's options are not forwarded:

  • :focusable - boolean(). Default true. false keeps the container out of the Tab order, and the keys below then reach whatever else has focus.
  • :click_to_scroll - boolean(), jump the viewport to a clicked position. Default false.
  • :show_vertical_scrollbar - :auto (default), :always, or :never.
  • :show_horizontal_scrollbar - :never (default), :auto, or :always.
  • :height - pos_integer() used as the element's preferred height in a parent's layout. Default: the summed heights of children.

A focused scrollable consumes ↑, ↓, Home, End, PageUp and PageDown.

iex> scrollable([label("row 1")], show_vertical_scrollbar: :always)
{:scrollable, [{:label, "row 1", []}], [show_vertical_scrollbar: :always]}

selection_list(options, opts \\ [])

@spec selection_list([term()], opts()) :: {:selection_list, [term()], opts()}

A list whose entries can be checked. Returns {:selection_list, options, opts}.

options is the list of choices; see Drafter.Widget.SelectionList for their shape and the widget's options. Drafter.get_widget_value/1 returns the list of selected option ids. A focused selection list consumes ↑, ↓, Home, End, Enter and Space.

iex> selection_list([%{id: :a, label: "A"}], on_change: :selected)
{:selection_list, [%{id: :a, label: "A"}], [on_change: :selected]}

show_modal(screen_module, props \\ %{}, opts \\ [])

@spec show_modal(module(), props(), opts()) ::
  {:show_modal, module(), props(), opts()}

Opens screen_module as a modal over the current screen.

Returns {:show_modal, screen_module, props, opts}, which both handle_event/2 and handle_event/3 dispatch. props (default %{}) is the map handed to the modal's mount/1; opts (default []) is passed to Drafter.App.show_modal/3 and carries the modal's :title, :width, :height, and :border.

iex> show_modal(ConfirmModal, %{}, title: "Confirm", width: 45, height: 10)
{:show_modal, ConfirmModal, %{}, [title: "Confirm", width: 45, height: 10]}

show_panel(screen_module, props \\ %{}, opts \\ [])

@spec show_panel(module(), props(), opts()) ::
  {:push_screen, module(), props(), opts()}

Returns {:push_screen, screen_module, props, [{:type, :panel} | opts]}.

A :type already in opts is overwritten with :panel. Carries the same unmatched tag as push_screen/3 and has the same failure modes.

iex> show_panel(MyApp.Sidebar, %{}, width: 30)
{:push_screen, MyApp.Sidebar, %{}, [type: :panel, width: 30]}

show_popover(screen_module, props \\ %{}, opts \\ [])

@spec show_popover(module(), props(), opts()) ::
  {:push_screen, module(), props(), opts()}

Returns {:push_screen, screen_module, props, [{:type, :popover} | opts]}.

A :type already in opts is overwritten with :popover. Carries the same unmatched tag as push_screen/3 and has the same failure modes: the loop raises FunctionClauseError on it from handle_event/2, and ignores it from handle_event/3.

iex> show_popover(MyApp.Menu)
{:push_screen, MyApp.Menu, %{}, [type: :popover]}

show_toast(message, opts \\ [])

@spec show_toast(String.t(), opts()) :: {:show_toast, String.t(), opts()}

Shows a transient message over the current screen.

Returns {:show_toast, message, opts}, which both handle_event/2 and handle_event/3 dispatch. message is a String.t(); opts (default []) is passed to Drafter.App.show_toast/2, which documents the keys it reads.

iex> show_toast("Saved", duration: 2000)
{:show_toast, "Saved", [duration: 2000]}

sidebar(left_children, right_children, opts \\ [])

@spec sidebar([element()], [element()], opts()) ::
  {:layout, :horizontal, [element()], opts()}

A fixed-width column beside a column that takes the rest of the width.

left_children and right_children are lists of elements. opts is passed on to the enclosing horizontal/2 unchanged — including :sidebar_width itself, which the layout ignores — and is additionally read for:

  • :sidebar_width - pos_integer() columns for the left column. Default 20.

The left column becomes vertical(left_children, width: sidebar_width) and the right vertical(right_children, flex: 1). Neither column receives any of opts.

iex> sidebar([label("nav")], [label("body")], sidebar_width: 12)
{:layout, :horizontal,
 [
   {:layout, :vertical, [{:label, "nav", []}], [width: 12]},
   {:layout, :vertical, [{:label, "body", []}], [flex: 1]}
 ], [layout: :horizontal, sidebar_width: 12]}

slider(opts \\ [])

@spec slider(opts()) :: {:slider, opts()}

A draggable value slider. Returns {:slider, opts}.

Drafter.get_widget_value/1 returns the number() it sits at. A focused slider moves on ←/↓ and →/↑, ten steps on PageUp/PageDown, and to the ends of the range on Home/End; a press or drag anywhere on the track moves the thumb there.

Options beyond the shared element keys, forwarded to Drafter.Widget.Slider:

  • :value - number() to start at, clamped and snapped. Default: :min.
  • :min / :max - ends of the range. Default 0.0 and 1.0.
  • :step - number() the value moves in. Default nil, a hundredth of the range, or whole numbers when :min and :max are both integers.
  • :bind - app-state key atom for two-way binding. Default nil. Only with a :bind does a later :value change reach the mounted widget.
  • :label - String.t() drawn ahead of the track. Default nil.
  • :show_value - draw the readout after the track. Default true.
  • :format - (number() -> String.t()) for the readout. Default nil.
  • :precision - decimals in the readout. Default: as many as :step needs.
  • :orientation - :horizontal (default) or :vertical.
  • :disabled - boolean(). Default false.
  • :on_change - atom event name or one-arity function receiving the new number.
  • :track_color / :fill_color / :thumb_color - {r, g, b} overrides.
  • :renderer - :text (default), :braille, or a graphics protocol atom, which draws the track through french_curve.

Examples

iex> slider(value: 0.5, label: "Gain", on_change: :set_gain)
{:slider, [value: 0.5, label: "Gain", on_change: :set_gain]}

slider(value, opts)

@spec slider(number(), opts()) :: {:slider, opts()}

Returns {:slider, [{:value, value} | opts]}.

iex> slider(0.25, label: "Mix")
{:slider, [value: 0.25, label: "Mix"]}

sparkline(data, opts \\ [])

@spec sparkline([number()], opts()) :: {:sparkline, [number()], opts()}

A single-row trend line. Returns {:sparkline, data, opts}.

data is a list of numbers. A keyword list, or an empty list, is treated as absent and the :data option is used instead.

Options beyond the shared element keys, all forwarded to Drafter.Widget.Sparkline:

  • :data - fallback series. Default [].
  • :min_value / :max_value - fix the vertical range. Defaults nil, i.e. taken from the data.
  • :color - {r, g, b} for the whole line. Default nil.
  • :min_color / :max_color - endpoints of a value-based gradient. Defaults nil.
  • :summary - boolean(), appends min/max/last. Default false.
  • :orientation - :vertical (default) or :horizontal.
  • :style - map() of style attributes. Default %{}.
  • :class - an atom or list of atoms. Default [].

Examples

iex> sparkline([1, 4, 2, 8], summary: true)
{:sparkline, [1, 4, 2, 8], [summary: true]}

split_pane(children, opts \\ [])

@spec split_pane([element()], opts()) :: {:split_pane, [element()], opts()}

Two panes separated by a draggable divider.

Returns {:split_pane, children, opts}. children is the list of pane elements; only the first two are laid out — a third and beyond are dropped.

Options beyond the shared element keys:

  • :orientation - :horizontal (default) puts the panes side by side with a vertical divider; :vertical stacks them.
  • :ratio - float() in 0.0..1.0, the divider's starting position. Default 0.5. It is a mount-time value only: a later :ratio change does not reach the already-created divider.
  • :show_handle - boolean(), draw the grip glyph. Default true. Mount-time only.
  • :resize_mode - :quick (default) or the other modes Drafter.Widget.SplitPaneDivider accepts. Mount-time only.
  • :id - names the divider :"<id>_divider" rather than the generated :"<Module>_split_divider_<n>".

See Drafter.Widget.SplitPaneDivider for the divider itself.

iex> split_pane([label("left"), label("right")], ratio: 0.3)
{:split_pane, [{:label, "left", []}, {:label, "right", []}], [ratio: 0.3]}

static(content, opts \\ [])

@spec static(term(), opts()) :: {:static, term(), opts()}

Pre-rendered content that is drawn as given. Returns {:static, content, opts}.

content is the text to draw. It becomes a Drafter.Widget.Label whose style is the theme's foreground and background merged with :style.

  • :style - map() of style attributes merged over the theme's. Default %{}.

Every other option is ignored, including :id and :key: a static element's widget id is always :static_<n> from its position in the tree, so it cannot be reached by Drafter.get_widget_value/1 or Drafter.focus/1 under a name you chose. Use label/2 when you need an id.

iex> static("v1.4.0", style: %{dim: true})
{:static, "v1.4.0", [style: %{dim: true}]}

switch(opts \\ [])

@spec switch(opts()) :: {:switch, opts()}

An on/off switch. Returns {:switch, opts}.

Drafter.get_widget_value/1 returns a boolean(). A focused switch acts on Enter and Space to toggle and on ←/→ to force off/on; a key that leaves the state as it was falls through to handle_event/2.

Options beyond the shared element keys, forwarded to Drafter.Widget.Switch:

  • :enabled - boolean() initial state. Default false. This is the key the widget reads, not :value.
  • :bind - app-state key atom for two-way binding. Default nil. Only with a :bind does a later :enabled change reach the mounted widget.
  • :label - String.t() shown to the right of the track. Default nil.
  • :on_change - atom event name or one-arity function receiving the new boolean(). Default nil.
  • :size - :normal (default), :small, or :compact.
  • :width / :height - column and row counts. Default: the width and height of the rect the parent allocated.

:on_color and :off_color are read by the widget's mount/1 only and are not forwarded by this element.

iex> switch(enabled: true, label: "Dark mode", on_change: :toggle_theme)
{:switch, [enabled: true, label: "Dark mode", on_change: :toggle_theme]}

switch(value, opts)

@spec switch(term(), opts()) :: {:switch, opts()}

Returns {:switch, [{:value, value} | opts]}.

value is stored under :value, which Drafter.Widget.Switch does not read — the widget's on/off state comes from :enabled. Use this form only for switch_group/2, which reads :value itself to build each switch's callback payload; use switch/1 with enabled: to set the state of a standalone switch.

iex> switch("compact", label: "Compact")
{:switch, [value: "compact", label: "Compact"]}

switch_group(group_name, switches)

@spec switch_group(term(), [opts()]) :: {:layout, :horizontal, [element()], opts()}

A two-column block of switches that all report to one callback.

group_name is any term; switches is a list of keyword lists, each the options for one switch/1. Each switch's :on_change is overwritten with {:switch_group_changed, group_name, value}, where value is that switch's :value option or, absent that, its :label. Its :label is re-set from its own :label, and every other option is passed through.

That tuple reaches the module as handle_event({:switch_group_changed, group_name, value}, enabled?, state) — handle_event/3 with a tuple rather than an atom as its first argument, and the switch's new boolean() as data.

Returns the horizontal/2 element holding two vertical/2 columns of width: 30 with gap: 2 between them. The first div(length(switches) + 1, 2) entries fill the left column, so an odd count puts the extra switch on the left.

iex> switch_group(:sizes, [[label: "Small", value: :s], [label: "Large"]])
{:layout, :horizontal,
 [
   {:layout, :vertical,
    [{:switch, [value: :s, label: "Small",
       on_change: {:switch_group_changed, :sizes, :s}]}], [width: 30]},
   {:layout, :vertical,
    [{:switch, [label: "Large",
       on_change: {:switch_group_changed, :sizes, "Large"}]}], [width: 30]}
 ], [layout: :horizontal, gap: 2]}

tabbed_content(tabs, opts \\ [])

@spec tabbed_content([term()], opts()) :: {:tabbed_content, [term()], opts()}

A tab bar over switchable panes. Returns {:tabbed_content, tabs, opts}.

tabs is the list of tabs — a string label, a {label, content} tuple, or a map with :id, :label and :content. Drafter.get_widget_value/1 returns the active tab index. See Drafter.Widget.TabbedContent for the widget's options; the non-obvious defaults are :active_tab (0), :title_align (:left), and :width (the available rect width).

:on_item_select is read by the widget's mount/1 only and is not forwarded by this element.

A focused tabbed content consumes →, ↑, ↓ and Enter, and ← except on the first tab, where it falls through. It explicitly declines Tab so focus can still move on.

iex> tabbed_content([%{id: :a, label: "A", content: ["x"]}], on_tab_change: :tab)
{:tabbed_content, [%{id: :a, label: "A", content: ["x"]}], [on_tab_change: :tab]}

text_area(opts \\ [])

@spec text_area(opts()) :: {:text_area, opts()}

A multi-line text editor. Returns {:text_area, opts}.

Drafter.get_widget_value/1 returns the current text as a String.t(). Drafter.Widget.TextArea documents every option; the frequently used ones are :value (default ""), :show_line_numbers (default false), :read_only (default false), :language (default nil), :trap_focus (default false), :tab_behavior (:focus by default), and :tab_size (default 2).

:placeholder_style, :focused_style, :selection_style, :line_number_style and :max_checkpoints are read by the widget's mount/1 only — this element does not forward them.

:width and :height are taken from the rect the parent allocated rather than from the widget's own defaults. With trap_focus: true the editor receives Tab itself instead of Tab moving focus.

iex> text_area(id: :body, show_line_numbers: true, language: :elixir)
{:text_area, [id: :body, show_line_numbers: true, language: :elixir]}

text_input(opts \\ [])

@spec text_input(opts()) :: {:text_input, opts()}

A single-line text field. Returns {:text_input, opts}.

Drafter.get_widget_value/1 returns the current text as a String.t(). Drafter.Widget.TextInput documents every option and every key it binds; the frequently used ones are :value (default ""), :placeholder (default ""), :on_change, :on_submit, :disabled (default false), :readonly (default false), :password (default false), and :type (:text by default).

:max_length and :width are read by the widget's mount/1 only — this element does not forward them, so setting either here has no effect.

While this widget has focus it consumes every printable key, Space, ←, →, Home, End, Backspace and Ctrl+A, so none of those reach handle_event/2 or a keybinding/3 clause. ↑, ↓, Escape, PageUp and PageDown pass through, and so does Enter unless :on_submit is set.

iex> text_input(id: :name, placeholder: "Your name")
{:text_input, [id: :name, placeholder: "Your name"]}

theme_selector(opts \\ [])

@spec theme_selector(opts()) :: {:theme_selector, opts()}

A list of the installed themes that switches theme on selection.

Returns {:theme_selector, opts}. Every option is accepted and ignored, including :id and :key: the widget is a Drafter.Widget.OptionList built from Drafter.ThemeManager's registered themes, its id is always :theme_selector_<n> from its position in the tree, and it takes focus the first time it renders. Highlighting or selecting an entry applies that theme immediately.

iex> theme_selector()
{:theme_selector, []}

tree(opts \\ [])

@spec tree(opts()) :: {:tree, opts()}

An expandable tree of nodes. Returns {:tree, opts}.

Options are the widget's props; see Drafter.Widget.Tree, whose "Key bindings" section lists what a focused tree acts on — the arrow keys, Enter, Space, +, -, * and / among them. A key it acts on does not reach handle_event/2 or a keybinding/3 clause; one that leaves the tree unchanged, such as ← on an already-collapsed node, falls through. Drafter.get_widget_value/1 returns the list of selected node ids.

iex> tree(id: :files, nodes: [])
{:tree, [id: :files, nodes: []]}

vertical(children, opts \\ [])

@spec vertical([element()], opts()) :: {:layout, :vertical, [element()], opts()}

Stacks children top to bottom. Returns {:layout, :vertical, children, opts}.

children is a list of elements. opts is stored verbatim; unlike horizontal/2 no :layout key is added.

Options beyond the shared element keys:

  • :gap - non_neg_integer() rows between children. Default 0.
  • :padding - space inside the container, in the same shapes as :margin. Default nil, i.e. none.
  • :width - the column width given to every child. Default nil, which uses the container's own width.

Each child's :height and :flex divide the column.

iex> vertical([header("Title"), footer()])
{:layout, :vertical, [{:header, "Title", []}, {:footer, []}], []}