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
Current section
Files
lib/raxol/components/selection/list.ex
defmodule Raxol.Components.Selection.List do
@moduledoc """
A component for displaying a selectable list of items.
"""
# Use standard component behaviour
use Raxol.UI.Components.Base.Component
require Logger
# Require view macros
require Raxol.View.Elements
@default_height 10
# Define state struct
defstruct id: nil,
items: [],
selected_index: 0,
scroll_offset: 0,
# Example default
width: 30,
height: @default_height,
style: %{},
focused: false,
on_select: nil,
# Remove default function capture
item_renderer: nil
# --- Component Behaviour Callbacks ---
@impl Raxol.UI.Components.Base.Component
def init(props) do
# Initialize state
%__MODULE__{
id: props[:id],
items: props[:items] || [],
selected_index: props[:initial_index] || 0,
width: props[:width] || 30,
height: props[:height] || @default_height,
style: props[:style] || %{},
focused: props[:focused] || false,
on_select: props[:on_select],
# Set default renderer here if not provided
item_renderer: props[:item_renderer] || (&default_item_renderer/1)
}
end
@impl Raxol.UI.Components.Base.Component
def update(msg, state) do
# Handle internal messages (selection, scrolling)
Logger.debug("List #{state.id} received message: #{inspect(msg)}")
case msg do
:select_next -> select_item(state.selected_index + 1, state)
:select_prev -> select_item(state.selected_index - 1, state)
:focus -> {%{state | focused: true}, []}
:blur -> {%{state | focused: false}, []}
{:select_index, index} -> select_item(index, state)
_ -> {state, []}
end
end
@impl Raxol.UI.Components.Base.Component
# Correct arity
def handle_event(event, %{} = _props, state) do
# Handle keyboard (up/down/enter), mouse clicks
Logger.debug("List #{state.id} received event: #{inspect(event)}")
case event do
%{type: :key, data: %{key: "Up"}} ->
update(:select_prev, state)
%{type: :key, data: %{key: "Down"}} ->
update(:select_next, state)
%{type: :key, data: %{key: "Enter"}} ->
confirm_selection(state)
%{type: :mouse, data: %{button: :left, action: :press, y: y_pos}} ->
handle_click(y_pos, state)
_ ->
{state, []}
end
end
# --- Render Logic ---
@impl Raxol.UI.Components.Base.Component
# Correct arity
def render(state, %{} = _props) do
# Determine visible items based on scroll offset and height
visible_items = Enum.slice(state.items, state.scroll_offset, state.height)
# Render each visible item
item_elements =
Enum.with_index(visible_items, state.scroll_offset)
|> Enum.map(fn {item, index} ->
is_selected = index == state.selected_index
render_item(item, is_selected, state)
end)
dsl_result =
Raxol.View.Elements.box id: state.id,
style: %{width: state.width, height: state.height} do
Raxol.View.Elements.column do
item_elements
end
end
# Return the element structure directly
dsl_result
end
# --- Internal Render Helpers ---
defp render_item(item_data, is_selected, state) do
# Ensure item fills width
base_style = %{width: :fill}
selected_style = %{bg: :blue, fg: :white}
style =
if is_selected and state.focused,
do: Map.merge(base_style, selected_style),
else: base_style
# Use the custom renderer or default
content = state.item_renderer.(item_data)
# Ensure display_content is always a valid element or list of elements
display_content =
cond do
is_binary(content) -> Raxol.View.Elements.label(content: content)
# Assume valid element
is_map(content) and Map.has_key?(content, :type) -> content
# Default fallback
true -> Raxol.View.Elements.label(content: to_string(content))
end
# Pass as list
Raxol.View.Elements.box style: style do
[display_content]
end
end
# --- Internal Logic Helpers ---
defp select_item(index, state) do
new_index = clamp(index, 0, length(state.items) - 1)
new_offset = adjust_scroll(new_index, state.scroll_offset, state.height)
{%{state | selected_index: new_index, scroll_offset: new_offset}, []}
end
defp confirm_selection(state) do
selected_item = Enum.at(state.items, state.selected_index)
commands =
if state.on_select && selected_item,
do: [{state.on_select, selected_item}],
else: []
{state, commands}
end
defp handle_click(y_pos, state) do
# Calculate index based on click position relative to component top
clicked_index = state.scroll_offset + y_pos
# Select the clicked item and confirm it
{new_state, _} = select_item(clicked_index, state)
confirm_selection(new_state)
end
defp clamp(value, min_val, max_val) do
value |> max(min_val) |> min(max_val)
end
defp adjust_scroll(index, offset, height) do
cond do
index < offset -> index
index >= offset + height -> index - height + 1
true -> offset
end
end
# Default item renderer just converts to string
defp default_item_renderer(item), do: to_string(item)
end