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. Defaultnil.:styles-map()of inline style overrides. Default%{}.:mouse_hover-boolean().true(default) puts the terminal into hover tracking mode for this app.falsecuts mouse event volume for apps with no hover effects.:runtime- the runtime backend: a module, or the shorthand:callback,:reducer, or:shared. DefaultDrafter.Runtime.Callback, themount/render/handle_eventstyle described here. SeeDrafter.Runtime.
No other key is read; an unknown one is silently ignored. Each of these four is
exposed on the module as __css_path__/0, __inline_styles__/0,
__mouse_hover__/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.
A module therefore never has to write those two catch-alls. 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:propstoDrafter.run/2(%{}when absent).stateis any term; a map is what the element constructors andDrafter.Testexpect.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 byDrafter.label/2and 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 ofDrafter.Event.t/0. Printable ASCII arrives as a named key —qis{:key, :q},/is{:key, :/}— and only codepoints above ASCII 126 arrive as{:char, codepoint}. SeeDrafter.Terminal.ANSIfor 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:nameis the atom given to a widget ason_click:,on_change:, and so on;datais that widget's payload (nilfor widgets with no payload). Also howDrafter.send_app_event/2and a popped screen's result arrive.
Optional lifecycle callbacks, each returning the new state — omit them and the state passes through unchanged:
on_ready/1— after the first render, before the first event. The place to start timers withDrafter.set_interval/2.on_timer/2—on_timer(timer_id, state), once per firing of a timer registered withDrafter.set_interval/2orDrafter.set_timeout/2.on_message/2—on_message(msg, state)for any process message delivered to the loop that Drafter does not itself handle (PubSub broadcasts,send/2).on_scroll_active/1— on the first scroll event of a scroll gesture.on_scroll_idle/1— 150 ms after the last scroll event of a gesture.update/2—update(props, state), when a parent re-renders this module with new props.unmount/1— returns:ok; called once as the app stops.refresh_rate/0— the frame interval:"30fps","7.5fps", a millisecond integer, or:unlimited. Default"30fps".
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/2runs 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:
TabandShift+Tabcycle focus and are consumed. The exception is a tree holding exactly one focusable widget: focus cannot move, so the key falls through tohandle_event/2. A focused widget carrying bothfocusedandtrap_focusin its state —Drafter.Widget.TextAreawithtrap_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 tohandle_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.ButtonconsumesEnterandSpaceand fires its:on_click. A focusedDrafter.Widget.TextInputconsumes every printable key,Spaceand the editing keys, but leavesEnteralone.
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_statereplaces 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_statereplaces 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.:normalmakesDrafter.run/2return: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}— seeshow_modal/3.{:show_toast, message, opts}— seeshow_toast/2.{:pop, result}— seepop_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 byDrafter.get_widget_value/1,Drafter.focus/1, andDrafter.Test. Defaultnil, 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. Defaultnil. A:keyproduces aString.t()widget id of the form"Counter_label#abc", not an atom, so pass that string toDrafter.focus/1andDrafter.Test.query_one/2. Ignored when:idis also given.:visible-boolean(), defaulttrue.falseomits the element and its children from the hierarchy entirely and its siblings close the gap.:visibility-:hiddenallots the element its space and draws nothing in it. Defaultnil, 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. Defaultnil, i.e. no margin. Any other shape is treated as no margin.:dock-:top,:bottom,:left, or:right. Defaultnil. A docked element leaves the normal flow and takes the full span of that edge; the remaining space is shared by its undocked siblings. Afooter/1element is docked to:bottomwhether or not the option is given. Only avertical/2container honours this — inside ahorizontal/2a 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
@type direction() :: :vertical | :horizontal
The direction a layout container arranges its children in.
@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.
@type opts() :: keyword()
The trailing keyword list every element constructor accepts.
@type props() :: map()
@type rect() :: Drafter.Widget.rect()
@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.
@type state() :: term()
Callbacks
@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.
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.
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.
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.
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.
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 first argument is the timer_id given to Drafter.set_interval/2 or
Drafter.set_timeout/2. Returns the new state.
@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.
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.
@callback unmount(state()) :: :ok
Releases resources as the application stops. Returns :ok.
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
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}.
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. Defaultnil.:border-:none,:single,:double,:rounded,:heavy,:dashed, or:ascii. Default: the current character set's border style, falling back to:rounded.:nonestill costs no rows.:padding-non_neg_integer()cells inside the border. Default: the character set's padding, falling back to1— not0.: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]}
A path trail. Returns {:breadcrumb, items, opts}.
items is a list of crumbs, each either a String.t() used as both label and id
or a {label, id} tuple.
Options beyond the shared element keys, all forwarded to
Drafter.Widget.Breadcrumb:
:separator-String.t()between crumbs. Default" › ".:on_click- atom event name or one-arity function receiving the crumb's id. Defaultnil.:active-boolean(), whether crumbs respond to clicks. Defaulttrue.:style-map()of style attributes. Default%{}.:class- an atom or list of atoms. Default[].
A focused breadcrumb consumes ←, →, Enter and Space.
iex> breadcrumb(["home", {"Reports", :reports}], on_click: :navigate)
{:breadcrumb, ["home", {"Reports", :reports}], [on_click: :navigate]}
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 tohandle_event/3when the button is activated, or a zero-arity function. Defaultnil, and no callback fires. Forced tonilwhen:disabledistrue.:variant-:default(the default),:primary, and the other theme variants.:typeis read as a fallback when:variantis absent.:disabled-boolean(), defaultfalse. A disabled button takes no focus and fires no callback.:compact-boolean(), defaultfalse. 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]}
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]}
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. Defaultnil.: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. Defaultnil.:background- panel background colour. Defaultnil.:color- content foreground colour. Defaultnil.: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"]}
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]}
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. Defaultfalse.:bind- app-state key atom for two-way binding of the checked state. Defaultnil. Only with a:binddoes a later:checkedchange reach the mounted widget.:on_change- atom event name or one-arity function called with the newboolean(). Defaultnil.: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]}
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]}
@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. Defaultfalse.:on_toggle- atom event name or one-arity function receiving the newboolean(). Defaultnil.:content_height-pos_integer()rows reserved for the expanded content. Defaultnil, 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]}
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 raisesFunctionClauseErrorwhen 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", []}], []}
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"}]]}
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. Defaultnil, 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. Defaultsnil,0, andnil.
Examples
iex> digits("12:04", align: :center)
{:digits, "12:04", [align: :center]}
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"]}
@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}
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]}
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 isnil. Default"".:show_clock-boolean(), defaultfalse. 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 byDrafter.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]}
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. Default0.:padding- space inside the container, in the same shapes as:margin. Defaultnil, 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]}
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}}
endA 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+Qstops 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.
EnterandSpaceare consumed by a focusedDrafter.Widget.Button; the printable keys,Spaceand the editing keys are consumed by a focusedDrafter.Widget.TextInput;TabandShift+Tabare 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.
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]}
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. Defaultnil, and activating it does nothing.:text- fallback caption, used whentextisnil. Defaultnil.:tooltip-String.t()shown on hover. Defaultnil.: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"]}
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(). Defaulttrue.:gradient_colors- list of{r, g, b}cycled through the caption. Defaultnil, i.e. a plain caption.:gradient_speed- milliseconds per gradient step. Default50.: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]}
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]}
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 whencontentisnil. Default"".:style-map()of style attributes. Default%{}.:padding-non_neg_integer()cells of inset. Default1.
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]}
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: "****-****"]}
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. Default0.0.:label-String.t()caption. Defaultnil.:orientation-:horizontal(default) or:vertical.:thresholds- the widget's colour bands. Default: its own threshold list.:show_value-boolean(). Defaulttrue.:show_label-boolean(). Defaulttrue.: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"]}
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]}
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]}
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:textoption, itself defaulting to"Placeholder".:text- fallback caption when:labelis absent. Default"Placeholder".:padding-non_neg_integer()cells of inset. Default2.:align-:center(default),:left, or:right.:border-boolean(). Defaultfalse.: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]}
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}}
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. Defaultfalse.:syntax_highlighting-boolean(). Defaulttrue.: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]}
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]}
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}, []}
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]}
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}}
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]}
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. Defaultnil.: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"]}
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", []}], []}
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(). Defaulttrue.falsekeeps 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. Defaultfalse.: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 ofchildren.
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]}
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]}
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]}
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]}
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]}
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]}
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. Default20.
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]}
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. Default0.0and1.0.:step-number()the value moves in. Defaultnil, a hundredth of the range, or whole numbers when:minand:maxare both integers.:bind- app-state key atom for two-way binding. Defaultnil. Only with a:binddoes a later:valuechange reach the mounted widget.:label-String.t()drawn ahead of the track. Defaultnil.:show_value- draw the readout after the track. Defaulttrue.:format-(number() -> String.t())for the readout. Defaultnil.:precision- decimals in the readout. Default: as many as:stepneeds.:orientation-:horizontal(default) or:vertical.:disabled-boolean(). Defaultfalse.: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 throughfrench_curve.
Examples
iex> slider(value: 0.5, label: "Gain", on_change: :set_gain)
{:slider, [value: 0.5, label: "Gain", on_change: :set_gain]}
Returns {:slider, [{:value, value} | opts]}.
iex> slider(0.25, label: "Mix")
{:slider, [value: 0.25, label: "Mix"]}
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. Defaultsnil, i.e. taken from the data.:color-{r, g, b}for the whole line. Defaultnil.:min_color/:max_color- endpoints of a value-based gradient. Defaultsnil.:summary-boolean(), appends min/max/last. Defaultfalse.: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]}
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;:verticalstacks them.:ratio-float()in0.0..1.0, the divider's starting position. Default0.5. It is a mount-time value only: a later:ratiochange does not reach the already-created divider.:show_handle-boolean(), draw the grip glyph. Defaulttrue. Mount-time only.:resize_mode-:quick(default) or the other modesDrafter.Widget.SplitPaneDivideraccepts. 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]}
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}]}
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. Defaultfalse. This is the key the widget reads, not:value.:bind- app-state key atom for two-way binding. Defaultnil. Only with a:binddoes a later:enabledchange reach the mounted widget.:label-String.t()shown to the right of the track. Defaultnil.:on_change- atom event name or one-arity function receiving the newboolean(). Defaultnil.: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]}
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"]}
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]}
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]}
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]}
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"]}
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, []}
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: []]}
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. Default0.:padding- space inside the container, in the same shapes as:margin. Defaultnil, i.e. none.:width- the column width given to every child. Defaultnil, 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, []}], []}