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