A single-line text field a view holds in its own state.
Like Atui.Panes, this is a struct with callbacks rather than a view: it owns
no process and no part of the screen. The host keeps one in its state, offers
keys to it, and draws it into a row of its own screen — so a field can sit in
a border, in a table header, in a popup, wherever the host has a row to spare.
Holding one
defmodule Search do
use Atui.View
alias Atui.TextInput
def mount(_opts), do: {:ok, %{input: TextInput.new(prompt: "search: ")}}
def handle_key(:enter, state), do: {:ok, submit(state)}
def handle_key(key, state) do
case state.input |> TextInput.handle_key(key) |> TextInput.into(state) do
{:pass, state} -> shortcut(state, key)
reply -> reply
end
end
def render(state, rect) do
Screen.new(rect.width, rect.height)
|> Screen.box(rect)
|> TextInput.draw(state.input, Rect.new(1, 1, rect.width - 2, 1))
end
endinto/3 rewrites a reply about the field into a reply about the state that
holds it, which keeps the delegation one line long. Bind the keys the host
wants for itself before delegating: a field claims every printable
character, so a list underneath one navigates with the arrows and Enter, not
with letters.
Which keys it claims
Printable characters, and the editing keys a readline user expects:
← → | one character |
Ctrl/Alt with ← → | one word |
| Home, End, Ctrl-A, Ctrl-E | either end |
| Backspace, Delete | one character either side of the cursor |
| Ctrl-W | the word before the cursor |
| Ctrl-U, Ctrl-K | to the start, to the end |
Everything else — Enter, Tab, ESC, the vertical arrows — comes back as
{:pass, input} for the host to deal with. A field never swallows a key it
has no use for.
The cursor
Atui paints cells, and the terminal's own cursor stays hidden in the
alternate screen, so the cursor here is a cell: draw/4 reverses the one
the cursor sits on. That is why a field needs no cooperation from the runtime
to be drawn anywhere — and why an unfocused field, drawn with focus: false,
simply has no cursor in it.
Long values scroll horizontally: what is drawn is the window of the value that keeps the cursor in view, which is a pure function of the value, the cursor and the width, so nothing about scrolling is kept in the struct.
Summary
Functions
Empties the field.
Draws the field into one row of screen.
True while there is nothing in the field.
Offers a key to the field.
Rewrites a reply about the field into a reply about the state holding it.
Builds a field.
Replaces the text, putting the cursor at the end of it.
The text in the field.
The window of the value that draw/4 would show in width columns.
Types
@type t() :: %Atui.TextInput{ cursor: non_neg_integer(), max_length: pos_integer() | nil, placeholder: String.t(), prompt: String.t(), value: String.t() }
Functions
Empties the field.
@spec draw(Atui.Screen.t(), t(), Atui.Rect.t(), keyword()) :: Atui.Screen.t()
Draws the field into one row of screen.
rect is where it goes; only its first row is used. Options:
:style— the value's style:prompt_style— the prompt's (defaults to:style):placeholder_style— the placeholder's (defaults to:style):cursor_style— the cell the cursor is on (reverse video by default):focus— draw the cursor at all (defaulttrue)
True while there is nothing in the field.
@spec handle_key(t(), Atui.Key.t()) :: reply()
Offers a key to the field.
Answers {:ok, input} for a key it used and {:pass, input} for one it did
not, the same contract Atui.View.handle_key/2 has — see the module docs
for which keys are which.
Rewrites a reply about the field into a reply about the state holding it.
state.input |> TextInput.handle_key(key) |> TextInput.into(state)key is where the field sits in the state, :input by default.
Builds a field.
Options:
:value— the initial text (the cursor starts after it):placeholder— what to draw while the value is empty:prompt— a label drawn before the value, e.g."search: ":max_length— the most characters the value may hold
Replaces the text, putting the cursor at the end of it.
The text in the field.
@spec window(t(), pos_integer()) :: {String.t(), non_neg_integer()}
The window of the value that draw/4 would show in width columns.
Returns {text, cursor_column}, the column being where in text the cursor
falls. Exposed because a host that draws the value itself still wants the
scrolling rule, and because it is the honest thing to assert on in a test.