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 text_wrapping_cached.ex
Raw

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

defmodule Raxol.UI.Components.Input.TextWrappingCached do
@moduledoc """
Cached version of text wrapping utilities with performance optimizations.
This module extends the basic TextWrapping functionality with caching
for improved performance on repeated operations.
"""
# Cache for visual widths to avoid recalculating
@cache_name :text_wrapping_cache
@doc """
Initializes the cache if not already present.
"""
def ensure_cache do
unless :ets.whereis(@cache_name) != :undefined do
:ets.new(@cache_name, [:set, :public, :named_table])
end
end
@doc """
Wraps text by visual width with caching.
"""
def wrap_line_by_visual_width(text, width)
when is_binary(text) and is_integer(width) and width > 0 do
ensure_cache()
cache_key = {:visual_width_wrap, text, width}
case :ets.lookup(@cache_name, cache_key) do
[{^cache_key, result}] ->
result
[] ->
# Use basic word wrapping as approximation for visual width
result = wrap_by_visual_width_impl(text, width)
:ets.insert(@cache_name, {cache_key, result})
result
end
end
def wrap_line_by_visual_width("", _width), do: []
@doc """
Wraps text by word boundaries with caching.
"""
def wrap_line_by_word(text, width)
when is_binary(text) and is_integer(width) and width > 0 do
ensure_cache()
cache_key = {:word_wrap, text, width}
case :ets.lookup(@cache_name, cache_key) do
[{^cache_key, result}] ->
result
[] ->
result = wrap_by_word_impl(text, width)
:ets.insert(@cache_name, {cache_key, result})
result
end
end
def wrap_line_by_word("", _width), do: []
@doc """
Gets the visual width of text with caching.
"""
def get_visual_width(text) when is_binary(text) do
ensure_cache()
cache_key = {:visual_width, text}
case :ets.lookup(@cache_name, cache_key) do
[{^cache_key, width}] ->
width
[] ->
width = calculate_visual_width(text)
:ets.insert(@cache_name, {cache_key, width})
width
end
end
@doc """
Warms up the cache with common strings.
"""
def warmup_cache(strings) when is_list(strings) do
ensure_cache()
Enum.each(strings, fn string ->
# Pre-calculate and cache visual width
get_visual_width(string)
# Pre-calculate common wrap widths
for width <- [20, 40, 60, 80] do
wrap_line_by_word(string, width)
wrap_line_by_visual_width(string, width)
end
end)
:ok
end
@doc """
Wraps text based on pixel width and font metrics.
"""
def wrap_to_pixel_width(text, pixel_width, font_manager) do
ensure_cache()
cache_key = {:pixel_wrap, text, pixel_width, font_manager}
case :ets.lookup(@cache_name, cache_key) do
[{^cache_key, result}] ->
result
[] ->
# Estimate character width from pixel width and font
char_width = estimate_char_width_from_pixels(pixel_width, font_manager)
result = wrap_line_by_word(text, char_width)
:ets.insert(@cache_name, {cache_key, result})
result
end
end
# Private implementation functions
defp wrap_by_visual_width_impl("", _width), do: []
defp wrap_by_visual_width_impl(text, width) do
# Split text into words and wrap based on visual width
words = String.split(text, " ")
wrap_words_by_visual_width(words, width, [], [])
end
defp wrap_words_by_visual_width([], _width, [], lines) do
Enum.reverse(lines)
end
defp wrap_words_by_visual_width([], _width, current_line, lines) do
completed_line = Enum.join(Enum.reverse(current_line), " ")
Enum.reverse([completed_line | lines])
end
defp wrap_words_by_visual_width([word | rest], width, current_line, lines) do
current_text =
if current_line == [],
do: "",
else: Enum.join(Enum.reverse(current_line), " ")
new_text =
if current_line == [], do: word, else: current_text <> " " <> word
if calculate_visual_width(new_text) <= width do
wrap_words_by_visual_width(rest, width, [word | current_line], lines)
else
# Check if we can fit just the word on current line
if current_line == [] do
# Word is too long, but we need to place it somewhere
if calculate_visual_width(word) > width do
# Break the word itself
word_parts = break_word_by_visual_width(word, width)
first_part = List.first(word_parts)
remaining_parts = Enum.drop(word_parts, 1)
# Add remaining parts back to processing queue
updated_rest = Enum.map(remaining_parts, & &1) ++ rest
wrap_words_by_visual_width(updated_rest, width, [], [
first_part | lines
])
else
wrap_words_by_visual_width(rest, width, [word], lines)
end
else
# Complete current line and start new line with current word
completed_line = Enum.join(Enum.reverse(current_line), " ")
wrap_words_by_visual_width([word | rest], width, [], [
completed_line | lines
])
end
end
end
defp break_word_by_visual_width(word, width) do
graphemes = String.graphemes(word)
break_graphemes_by_visual_width(graphemes, width, [], [], 0)
end
defp break_graphemes_by_visual_width(
[],
_width,
current_part,
parts,
_current_width
) do
final_parts =
if current_part != [],
do: [Enum.join(Enum.reverse(current_part), "") | parts],
else: parts
Enum.reverse(final_parts)
end
defp break_graphemes_by_visual_width(
[char | rest],
width,
current_part,
parts,
current_width
) do
char_width = calculate_visual_width(char)
new_width = current_width + char_width
if new_width <= width do
break_graphemes_by_visual_width(
rest,
width,
[char | current_part],
parts,
new_width
)
else
# Complete current part
completed_part =
if current_part != [],
do: Enum.join(Enum.reverse(current_part), ""),
else: ""
new_parts =
if completed_part != "", do: [completed_part | parts], else: parts
break_graphemes_by_visual_width([char | rest], width, [], new_parts, 0)
end
end
defp wrap_by_word_impl(text, width) do
# Simple word wrapping implementation
words = String.split(text, " ")
wrap_words(words, width, [], [])
end
defp wrap_words([], _width, current_line, lines) do
final_lines =
if current_line != [],
do: [Enum.join(Enum.reverse(current_line), " ") | lines],
else: lines
Enum.reverse(final_lines)
end
defp wrap_words([word | rest], width, current_line, lines) do
current_text = Enum.join(Enum.reverse(current_line), " ")
new_text =
if current_line == [], do: word, else: current_text <> " " <> word
if String.length(new_text) <= width do
wrap_words(rest, width, [word | current_line], lines)
else
# Start new line with the word
completed_line =
if current_line != [],
do: Enum.join(Enum.reverse(current_line), " "),
else: ""
new_lines =
if completed_line != "", do: [completed_line | lines], else: lines
# Handle very long words that exceed width
if String.length(word) > width do
# Break long word into chunks
word_chunks = break_long_word(word, width)
wrap_words(
rest,
width,
[List.last(word_chunks)],
new_lines ++ Enum.reverse(Enum.drop(word_chunks, -1))
)
else
wrap_words(rest, width, [word], new_lines)
end
end
end
defp break_long_word(word, width) do
graphemes = String.graphemes(word)
chunk_graphemes(graphemes, width, [], [])
end
defp chunk_graphemes([], _width, current_chunk, chunks) do
final_chunks =
if current_chunk != [],
do: [Enum.join(Enum.reverse(current_chunk), "") | chunks],
else: chunks
Enum.reverse(final_chunks)
end
defp chunk_graphemes([char | rest], width, current_chunk, chunks) do
if length(current_chunk) >= width do
completed_chunk = Enum.join(Enum.reverse(current_chunk), "")
chunk_graphemes([char | rest], width, [], [completed_chunk | chunks])
else
chunk_graphemes(rest, width, [char | current_chunk], chunks)
end
end
defp calculate_visual_width(text) do
# Simple implementation: count graphemes, with wider characters counting as 2
graphemes = String.graphemes(text)
Enum.reduce(graphemes, 0, fn char, acc ->
# CJK characters and some symbols take 2 columns
codepoint = String.to_charlist(char) |> List.first()
cond do
# CJK Unified Ideographs
codepoint >= 0x4E00 and codepoint <= 0x9FFF -> acc + 2
# CJK Symbols and Punctuation
codepoint >= 0x3000 and codepoint <= 0x303F -> acc + 2
# Hiragana
codepoint >= 0x3040 and codepoint <= 0x309F -> acc + 2
# Katakana
codepoint >= 0x30A0 and codepoint <= 0x30FF -> acc + 2
# Fullwidth Forms
codepoint >= 0xFF00 and codepoint <= 0xFFEF -> acc + 2
# Most other characters take 1 column
true -> acc + 1
end
end)
end
defp estimate_char_width_from_pixels(pixel_width, font_manager) do
# Rough estimation: assume monospace font with ~8 pixels per character
char_pixel_width =
case font_manager do
%{size: size} when is_number(size) -> max(size * 0.6, 6)
_ -> 8
end
max(div(pixel_width, trunc(char_pixel_width)), 1)
end
end