defmodule Raxol.Terminal.Rendering.OptimizedStyleRenderer do @moduledoc """ Phase 3 optimized terminal renderer with efficient style handling. Key optimizations: 1. Pre-compiled style patterns at compile time 2. Direct pattern matching instead of cache lookups 3. Minimal memory allocations 4. No process dictionary usage 5. Efficient string building with iodata Target: <500μs render time (from current 1200-2600μs) """ alias Raxol.Terminal.ScreenBuffer # alias Raxol.Terminal.Cell # Not used currently alias Raxol.Terminal.ANSI.TextFormatting # Pre-compiled at compile time for zero runtime cost # @default_style %TextFormatting{} # Not used currently # Common style patterns pre-compiled as binaries @style_patterns %{ # Empty/default style {nil, nil, false, false, false} => "", # Basic colors (ANSI 16) {:black, nil, false, false, false} => "color:#000000", {:red, nil, false, false, false} => "color:#cc0000", {:green, nil, false, false, false} => "color:#4e9a06", {:yellow, nil, false, false, false} => "color:#c4a000", {:blue, nil, false, false, false} => "color:#3465a4", {:magenta, nil, false, false, false} => "color:#75507b", {:cyan, nil, false, false, false} => "color:#06989a", {:white, nil, false, false, false} => "color:#d3d7cf", # Bright colors {:bright_black, nil, false, false, false} => "color:#555753", {:bright_red, nil, false, false, false} => "color:#ef2929", {:bright_green, nil, false, false, false} => "color:#8ae234", {:bright_yellow, nil, false, false, false} => "color:#fce94f", {:bright_blue, nil, false, false, false} => "color:#729fcf", {:bright_magenta, nil, false, false, false} => "color:#ad7fa8", {:bright_cyan, nil, false, false, false} => "color:#34e2e2", {:bright_white, nil, false, false, false} => "color:#eeeeec", # Bold variants {nil, nil, true, false, false} => "font-weight:bold", {:red, nil, true, false, false} => "color:#cc0000;font-weight:bold", {:green, nil, true, false, false} => "color:#4e9a06;font-weight:bold", {:blue, nil, true, false, false} => "color:#3465a4;font-weight:bold", # Underline variants {nil, nil, false, false, true} => "text-decoration:underline", {:red, nil, false, false, true} => "color:#cc0000;text-decoration:underline", # Italic variants {nil, nil, false, true, false} => "font-style:italic" } @doc """ Renders the screen buffer to HTML with optimized style handling. """ def render(%ScreenBuffer{} = buffer, options \\ %{}) do cursor_pos = Map.get(options, :cursor, nil) buffer.cells |> render_cells_to_iodata() |> wrap_with_container() |> maybe_add_cursor(cursor_pos, buffer.width) |> IO.iodata_to_binary() end # Use iodata for efficient string building defp render_cells_to_iodata(cells) do cells |> Enum.with_index() |> Enum.map(fn {row, _y} -> render_row_optimized(row) end) |> Enum.intersperse("\n") end defp render_row_optimized(row) do # Group consecutive cells with same style row |> group_by_style() |> Enum.map(&render_styled_span/1) end defp group_by_style(row) do row |> Enum.chunk_by(& &1.style) |> Enum.map(fn cells -> style = hd(cells).style text = cells |> Enum.map(& &1.char) |> IO.iodata_to_binary() {style, text} end) end defp render_styled_span({style, text}) do case get_style_string_fast(style) do # No style needed "" -> text style_str -> ["", text, ""] end end # Direct pattern matching for maximum performance defp get_style_string_fast( %TextFormatting{ foreground: fg, background: bg, bold: bold, italic: italic, underline: underline } = style ) do # Try pre-compiled patterns first (most common cases) key = {fg, bg, bold, italic, underline} case Map.get(@style_patterns, key) do nil -> # Fall back to building style string for uncommon combinations build_style_string_minimal(style) style_str -> style_str end end # Minimal style string builder for uncommon cases defp build_style_string_minimal(%TextFormatting{} = style) do parts = [] parts = case style.foreground do nil -> parts color -> [color_to_css(color, "color") | parts] end parts = case style.background do nil -> parts color -> [color_to_css(color, "background-color") | parts] end parts = if style.bold, do: ["font-weight:bold" | parts], else: parts parts = if style.italic, do: ["font-style:italic" | parts], else: parts parts = if style.underline, do: ["text-decoration:underline" | parts], else: parts parts = if style.strikethrough, do: ["text-decoration:line-through" | parts], else: parts Enum.join(parts, ";") end # Efficient color conversion with pattern matching defp color_to_css(color, property) when is_atom(color) do hex = case color do :black -> "#000000" :red -> "#cc0000" :green -> "#4e9a06" :yellow -> "#c4a000" :blue -> "#3465a4" :magenta -> "#75507b" :cyan -> "#06989a" :white -> "#d3d7cf" :bright_black -> "#555753" :bright_red -> "#ef2929" :bright_green -> "#8ae234" :bright_yellow -> "#fce94f" :bright_blue -> "#729fcf" :bright_magenta -> "#ad7fa8" :bright_cyan -> "#34e2e2" :bright_white -> "#eeeeec" _ -> "#ffffff" end "#{property}:#{hex}" end defp color_to_css({:rgb, r, g, b}, property) do "#{property}:rgb(#{r},#{g},#{b})" end defp color_to_css({:indexed, idx}, property) when idx in 0..255 do # Convert 256-color index to RGB hex = indexed_to_hex(idx) "#{property}:#{hex}" end defp color_to_css(_, property) do "#{property}:#ffffff" end # Pre-computed 256-color palette (first 16 entries shown, rest computed) defp indexed_to_hex(idx) when idx < 16 do # Standard ANSI colors case idx do 0 -> "#000000" 1 -> "#cc0000" 2 -> "#4e9a06" 3 -> "#c4a000" 4 -> "#3465a4" 5 -> "#75507b" 6 -> "#06989a" 7 -> "#d3d7cf" 8 -> "#555753" 9 -> "#ef2929" 10 -> "#8ae234" 11 -> "#fce94f" 12 -> "#729fcf" 13 -> "#ad7fa8" 14 -> "#34e2e2" 15 -> "#eeeeec" end end defp indexed_to_hex(idx) when idx < 232 do # 216-color cube (6x6x6) idx_offset = idx - 16 r = div(idx_offset, 36) * 51 g = rem(div(idx_offset, 6), 6) * 51 b = rem(idx_offset, 6) * 51 "##{to_hex(r)}#{to_hex(g)}#{to_hex(b)}" end defp indexed_to_hex(idx) do # Grayscale ramp gray = 8 + (idx - 232) * 10 "##{to_hex(gray)}#{to_hex(gray)}#{to_hex(gray)}" end defp to_hex(n) do Integer.to_string(n, 16) |> String.pad_leading(2, "0") end defp wrap_with_container(content) do ["
", content, "
"] end defp maybe_add_cursor(content, nil, _width), do: content defp maybe_add_cursor(content, {y, x}, _width) do # Add cursor overlay element cursor_style = "position:absolute;left:#{x}ch;top:#{y}lh;width:1ch;height:1lh;background:rgba(255,255,255,0.5)" [content, "
"] end end