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/ui/components/input/multi_line_input/render_helper.ex
defmodule Raxol.UI.Components.Input.MultiLineInput.RenderHelper do
@moduledoc """
Rendering helper functions for MultiLineInput component.
"""
alias Raxol.UI.Components.Input.MultiLineInput
@doc """
Renders a single line with proper styling for cursor and selection.
"""
@spec render_line(integer(), String.t(), MultiLineInput.t(), map()) :: list()
def render_line(line_index, line_content, state, theme) do
# Get theme styles
text_style =
get_theme_style(theme, [:components, :multi_line_input, :text_style], %{})
cursor_style =
get_theme_style(theme, [:components, :multi_line_input, :cursor_style], %{
background: :red
})
selection_style =
get_theme_style(
theme,
[:components, :multi_line_input, :selection_style],
%{background: :blue}
)
# Check if this line has cursor
{cursor_row, cursor_col} = state.cursor_pos
has_cursor = line_index == cursor_row and state.focused
# Check if this line has selection
{selection_start, selection_end} = normalize_selection(state)
line_has_selection =
line_in_selection?(line_index, selection_start, selection_end)
# Build line content with styles
if line_has_selection do
render_line_with_selection(
line_content,
line_index,
state,
text_style,
selection_style,
cursor_style,
has_cursor,
cursor_col
)
else
render_line_simple(
line_content,
text_style,
cursor_style,
has_cursor,
cursor_col
)
end
end
defp render_line_simple(
line_content,
text_style,
cursor_style,
has_cursor,
cursor_col
) do
line_length = String.length(line_content)
if has_cursor and cursor_col <= line_length do
# Split line at cursor position
{before_cursor, at_and_after} = String.split_at(line_content, cursor_col)
{cursor_char, after_cursor} =
case at_and_after do
# Show cursor at end of line
"" -> {" ", ""}
rest -> String.split_at(rest, 1)
end
[
if(before_cursor != "",
do: {:text, before_cursor, text_style},
else: nil
),
{:text, cursor_char, cursor_style},
if(after_cursor != "", do: {:text, after_cursor, text_style}, else: nil)
]
|> Enum.filter(&(&1 != nil))
else
# No cursor on this line
if line_content != "" do
[{:text, line_content, text_style}]
else
# Empty line placeholder
[{:text, " ", text_style}]
end
end
end
defp render_line_with_selection(
line_content,
line_index,
state,
text_style,
selection_style,
cursor_style,
has_cursor,
cursor_col
) do
{selection_start, selection_end} = normalize_selection(state)
{start_row, start_col} = selection_start
{end_row, end_col} = selection_end
line_length = String.length(line_content)
# Determine selection bounds for this line
sel_start = if line_index == start_row, do: start_col, else: 0
sel_end = if line_index == end_row, do: end_col, else: line_length
# Ensure bounds are valid
sel_start = max(0, min(sel_start, line_length))
sel_end = max(sel_start, min(sel_end, line_length))
parts = []
# Before selection
parts =
if sel_start > 0 do
before_sel = String.slice(line_content, 0, sel_start)
parts ++ [{:text, before_sel, text_style}]
else
parts
end
# Selection
parts =
if sel_end > sel_start do
selected_text =
String.slice(line_content, sel_start, sel_end - sel_start)
parts ++ [{:text, selected_text, selection_style}]
else
parts
end
# After selection
parts =
if sel_end < line_length do
after_sel = String.slice(line_content, sel_end, line_length - sel_end)
parts ++ [{:text, after_sel, text_style}]
else
parts
end
# Add cursor if on this line
parts =
if has_cursor do
add_cursor_to_parts(parts, cursor_col, cursor_style)
else
parts
end
if parts == [] do
# Empty line placeholder
[{:text, " ", text_style}]
else
parts
end
end
defp add_cursor_to_parts(parts, _cursor_col, cursor_style) do
# This is a simplified implementation
# In a full implementation, you'd need to carefully insert the cursor
# at the correct position within the styled parts
parts ++ [{:text, " ", cursor_style}]
end
defp normalize_selection(state) do
case {state.selection_start, state.selection_end} do
{nil, _} ->
{nil, nil}
{_, nil} ->
{nil, nil}
{start_pos, end_pos} ->
if pos_to_index(start_pos, state) <= pos_to_index(end_pos, state) do
{start_pos, end_pos}
else
{end_pos, start_pos}
end
end
end
defp line_in_selection?(_line_index, nil, _), do: false
defp line_in_selection?(_line_index, _, nil), do: false
defp line_in_selection?(line_index, {start_row, _}, {end_row, _}) do
line_index >= start_row and line_index <= end_row
end
defp pos_to_index({row, col}, state) do
# Calculate linear position in text
lines_before = Enum.take(state.lines, row)
# +1 for newline
chars_before = Enum.sum(Enum.map(lines_before, &(String.length(&1) + 1)))
chars_before + col
end
defp get_theme_style(theme, path, default) do
case get_in(theme, path) do
nil -> default
style -> style
end
end
end