Packages

Multi-surface application runtime for Elixir. One TEA module renders to terminal, browser (LiveView), SSH, and MCP (agents). 30+ widgets, flexbox + CSS grid, AI agent runtime, distributed swarm with CRDTs, time-travel debugging, session recording, sandboxed REPL, and agentic commerce.

Current section

Files

Jump to
raxol lib raxol ui components input single_line_input.ex
Raw

lib/raxol/ui/components/input/single_line_input.ex

defmodule Raxol.UI.Components.Input.SingleLineInput do
@moduledoc """
A simple single-line text input component.
"""
@typedoc """
State for the SingleLineInput component.
- :id - unique identifier
- :value - current text value
- :placeholder - placeholder text
- :style - style map
- :focused - whether the field is focused
- :cursor_pos - cursor position
- :on_change - callback for value change
- :on_submit - callback for submit action
"""
@type t :: %__MODULE__{
id: any(),
value: String.t(),
placeholder: String.t(),
style: map(),
focused: boolean(),
cursor_pos: non_neg_integer(),
on_change: (String.t() -> any()) | nil,
on_submit: (-> any()) | nil
}
# Use standard component behaviour
use Raxol.UI.Components.Base.Component
# Required for using View.Elements macros
require Raxol.View.Elements
require Raxol.Core.Renderer.View
require Raxol.Core.Runtime.Log
# Require view macros
# require Raxol.View.Elements # Removed
# Define state struct
defstruct id: nil,
value: "",
placeholder: "",
style: %{},
focused: false,
cursor_pos: 0,
on_change: nil,
on_submit: nil
# --- Component Behaviour Callbacks ---
@doc """
Initializes the SingleLineInput component state from the given props.
"""
@spec init(map()) :: __MODULE__.t()
@impl Raxol.UI.Components.Base.Component
def init(props) do
# Initialize state from props
%__MODULE__{
id: props[:id],
value: props[:initial_value] || "",
placeholder: props[:placeholder] || "",
style: props[:style] || %{},
on_change: props[:on_change],
on_submit: props[:on_submit],
cursor_pos: String.length(props[:initial_value] || "")
}
end
@doc """
Updates the SingleLineInput component state in response to messages or prop changes.
"""
@spec update(term(), __MODULE__.t()) :: {__MODULE__.t(), list()}
@impl Raxol.UI.Components.Base.Component
def update(msg, state) do
# Handle internal messages (e.g., from key events)
Raxol.Core.Runtime.Log.debug(
"SingleLineInput #{state.id} received message: #{inspect(msg)}"
)
case msg do
{:insert_char, char} -> insert_char(char, state)
:move_cursor_left -> move_cursor(-1, state)
:move_cursor_right -> move_cursor(1, state)
:backspace -> backspace(state)
:delete -> delete(state)
:move_cursor_start -> move_cursor_to(0, state)
:move_cursor_end -> move_cursor_to(String.length(state.value), state)
:submit -> submit(state)
:focus -> {%{state | focused: true}, []}
:blur -> {%{state | focused: false}, []}
_ -> {state, []}
end
end
@doc """
Handles events for the SingleLineInput component, such as keypresses and mouse clicks.
"""
@spec handle_event(term(), map(), __MODULE__.t()) :: {__MODULE__.t(), list()}
@impl Raxol.UI.Components.Base.Component
def handle_event(event, %{} = _props, state) do
# Handle keyboard events, mouse clicks (focus), etc.
Raxol.Core.Runtime.Log.debug(
"SingleLineInput #{state.id} received event: #{inspect(event)}"
)
case event do
%{type: :key, data: key_data} ->
handle_key_event(key_data, state)
# Focus on click
%{type: :mouse, data: %{button: :left, action: :press}} ->
{state, [{:focus, state.id}]}
_ ->
{state, []}
end
end
# --- Render Logic ---
@doc """
Renders the SingleLineInput component using the current state and props.
"""
@spec render(__MODULE__.t(), map()) :: any()
@impl Raxol.UI.Components.Base.Component
# Correct arity
def render(state, %{} = _props) do
display_text =
case {state.value == "", state.focused} do
{true, false} -> state.placeholder
_ -> state.value
end
# Placeholder color handling
text_color =
case {state.value == "", state.focused} do
{true, false} -> :gray
_ -> :white
end
# Render with cursor if focused
rendered_content =
case state.focused do
true ->
before = String.slice(display_text, 0, state.cursor_pos)
after_cursor =
String.slice(
display_text,
state.cursor_pos,
String.length(display_text)
)
# Use simple characters, assume fixed width font
[
Raxol.View.Elements.label(content: before),
Raxol.View.Elements.label(content: "|"),
Raxol.View.Elements.label(content: after_cursor)
]
false ->
Raxol.View.Elements.label(content: display_text)
end
dsl_result =
Raxol.View.Elements.box id: state.id,
style: Map.put(state.style, :color, text_color) do
rendered_content
end
# Return the element structure directly
dsl_result
# Or wrap if needed by container: View.to_element(dsl_result)
end
# --- Internal Helpers ---
defp handle_key_event(key_data, state) do
msg = map_key_to_message(key_data)
case msg do
nil -> {state, []}
message -> update(message, state)
end
end
defp map_key_to_message(%{key: k, modifiers: []})
when is_binary(k) and byte_size(k) == 1 do
{:insert_char, k}
end
defp map_key_to_message(%{key: "Enter", modifiers: []}), do: :submit
defp map_key_to_message(%{key: "Backspace", modifiers: []}), do: :backspace
defp map_key_to_message(%{key: "Delete", modifiers: []}), do: :delete
defp map_key_to_message(%{key: "Left", modifiers: []}), do: :move_cursor_left
defp map_key_to_message(%{key: "Right", modifiers: []}),
do: :move_cursor_right
defp map_key_to_message(%{key: "Home", modifiers: []}), do: :move_cursor_start
defp map_key_to_message(%{key: "End", modifiers: []}), do: :move_cursor_end
defp map_key_to_message(_), do: nil
defp insert_char(char, state) do
new_value =
String.slice(state.value, 0, state.cursor_pos) <>
char <>
String.slice(state.value, max(0, state.cursor_pos)..-1//1)
new_cursor_pos = state.cursor_pos + 1
new_state = %{state | value: new_value, cursor_pos: new_cursor_pos}
commands =
case state.on_change do
nil -> []
callback -> [{callback, new_value}]
end
{new_state, commands}
end
defp move_cursor(offset, state) do
new_cursor_pos =
clamp(state.cursor_pos + offset, 0, String.length(state.value))
{%{state | cursor_pos: new_cursor_pos}, []}
end
defp move_cursor_to(pos, state) do
new_cursor_pos = clamp(pos, 0, String.length(state.value))
{%{state | cursor_pos: new_cursor_pos}, []}
end
defp backspace(state) do
case state.cursor_pos > 0 do
true ->
new_value =
String.slice(state.value, 0, max(0, state.cursor_pos - 1)) <>
String.slice(state.value, max(0, state.cursor_pos)..-1//1)
new_cursor_pos = state.cursor_pos - 1
new_state = %{state | value: new_value, cursor_pos: new_cursor_pos}
commands =
case state.on_change do
nil -> []
callback -> [{callback, new_value}]
end
{new_state, commands}
false ->
{state, []}
end
end
defp delete(state) do
case state.cursor_pos < String.length(state.value) do
true ->
new_value =
String.slice(state.value, 0, state.cursor_pos) <>
String.slice(state.value, max(0, state.cursor_pos + 1)..-1//1)
new_state = %{state | value: new_value}
commands =
case state.on_change do
nil -> []
callback -> [{callback, new_value}]
end
{new_state, commands}
false ->
{state, []}
end
end
defp submit(state) do
commands =
case state.on_submit do
nil -> []
callback -> [{callback, state.value}]
end
{state, commands}
end
defp clamp(value, min_val, max_val) do
value |> max(min_val) |> min(max_val)
end
@doc """
Mount hook - called when component is mounted.
No special setup needed for SingleLineInput.
"""
@impl true
@spec mount(map()) :: {map(), list()}
def mount(state), do: {state, []}
@doc """
Unmount hook - called when component is unmounted.
No cleanup needed for SingleLineInput.
"""
@impl true
@spec unmount(map()) :: map()
def unmount(state), do: state
end