Atui.View behaviour (Atui v0.1.0)

Copy Markdown View Source

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

reply()

@type reply() ::
  {:ok, state()}
  | {:pass, state()}
  | {:push, module(), keyword(), state()}
  | {:pop, state()}
  | {:halt, state()}

state()

@type state() :: term()

Callbacks

handle_event(event, state)

@callback handle_event(event :: term(), state()) :: reply()

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.

handle_key(key, state)

@callback handle_key(key :: Atui.Key.t(), state()) :: reply()

Handles a key press. Only the topmost view receives keys.

handle_tick(tick, state)

@callback handle_tick(tick :: non_neg_integer(), state()) :: reply()

Handles a timer tick. tick counts this view's own ticks, from zero.

mount(opts)

@callback mount(opts :: keyword()) :: {:ok, state()}

Builds the view's initial state.

place(state, viewport)

@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.

render(state, rect)

@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.

tick_interval(state)

@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.

unmount(state)

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

Called before the view is discarded (pop, halt or shutdown).

Functions

assign(state, keyword_or_map)

Puts several values at once: assign(state, now: t, count: n).

assign(state, key, value)

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())}