The behaviour every screen implements — the LiveView half of Atui.
A view is a pure function of its own state: mount/1 builds that state,
render/2 turns it into cells, and the callbacks return a new state plus an
optional instruction to the runtime. Views never touch the terminal, which is
what makes them testable — render/2 returns an Atui.Screen you can assert
on without a tty in sight.
Callback replies
{:ok, state}— carry on with new state{:pass, state}— this key was not mine; let the focused view have it{:push, module, opts, state}— stack another view on top (a popup){:pop, state}— close this view and return to the one below{:halt, state}— quit the application
Stacked, or tiled
The runtime stacks views: a {:push, module, opts, state} reply draws the
new view over what is below it, which is what a popup wants. For several views
visible at once — tiled, each with its own ticker — a view holds an
Atui.Panes in its state instead and becomes their container.
Keys, and who gets them
The root view — the one the application started with — sees every key first,
so global keys like quit live in one place. It answers {:pass, state} for
the keys it does not claim, and those go on to the view with focus, which is
the topmost one. That is the default reply, so a view that says nothing about
a key never swallows it.
Ticks
A view that wants a clock says how often in tick_interval/1; the runtime
gives it a timer of its own and calls handle_tick/2 with that view's own
count. Views without an interval never tick, and a slow view does not drag on
a fast one.
Example
defmodule MyView do
use Atui.View
def mount(_opts), do: {:ok, %{count: 0}}
def tick_interval(_state), do: 1_000
def handle_key({:char, "q"}, state), do: {:halt, state}
def handle_key(_key, state), do: {:pass, state}
def render(state, rect) do
Atui.Screen.new(rect.width, rect.height)
|> Atui.Screen.box(rect, title: " demo ")
|> Atui.Screen.put_lines_centered(rect, ["count: #{state.count}"])
end
end
Summary
Callbacks
Handles anything that is neither a key nor a tick.
Handles a key press. Only the topmost view receives keys.
Handles a timer tick. tick counts this view's own ticks, from zero.
Builds the view's initial state.
Chooses where the view sits inside the viewport.
Draws the view.
How often this view wants a tick, in milliseconds.
Called before the view is discarded (pop, halt or shutdown).
Functions
Puts several values at once: assign(state, now: t, count: n).
Puts a value in a map-shaped state.
Types
Callbacks
Handles anything that is neither a key nor a tick.
Events come from Atui.Runtime.send_event/2 and from plain messages sent to
the runtime process — a Task reply, a PubSub broadcast, a poller telling the
UI that something changed. This is the handle_info/2 of a LiveView.
@callback handle_key(key :: Atui.Key.t(), state()) :: reply()
Handles a key press. Only the topmost view receives keys.
@callback handle_tick(tick :: non_neg_integer(), state()) :: reply()
Handles a timer tick. tick counts this view's own ticks, from zero.
Builds the view's initial state.
@callback place(state(), viewport :: Atui.Rect.t()) :: Atui.Rect.t()
Chooses where the view sits inside the viewport.
Defaults to the whole viewport; popups override it to centre themselves.
@callback render(state(), rect :: Atui.Rect.t()) :: Atui.Screen.t()
Draws the view.
rect is the view's own area with its origin at {0, 0} — a view always
draws in local coordinates, and the runtime places the result.
@callback tick_interval(state()) :: pos_integer() | nil
How often this view wants a tick, in milliseconds.
nil — the default — means no ticker at all. Read fresh before each timer is
set, so a view can change its own rate as its state changes.
@callback unmount(state()) :: :ok
Called before the view is discarded (pop, halt or shutdown).
Functions
Puts several values at once: assign(state, now: t, count: n).
Puts a value in a map-shaped state.
Sugar for the LiveView habit of naming what changed rather than rebuilding the
map. import Atui.View to use it unqualified.
def handle_tick(_tick, state), do: {:ok, assign(state, :now, DateTime.utc_now())}