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

lib/raxol/ui/theme_resolver.ex

defmodule Raxol.UI.ThemeResolver do
@moduledoc """
Consolidated theme resolution, color processing, and theme-related utilities.
This module provides theme resolution with optional caching for high-performance applications.
## Configuration
Set the default caching strategy in application config:
config :raxol, :theme_resolver,
cache_enabled: true
Or control per-call with options:
ThemeResolver.resolve_styles(attrs, component_type, theme, cache: true)
## Migration from ThemeResolverCached
Replace:
Raxol.UI.ThemeResolverCached.resolve_styles(attrs, component_type, theme)
Raxol.UI.ThemeResolverCached.get_default_theme()
With:
Raxol.UI.ThemeResolver.resolve_styles(attrs, component_type, theme, cache: true)
Raxol.UI.ThemeResolver.get_default_theme(cache: true)
## Examples
# Non-cached (simple operations)
theme = ThemeResolver.resolve_element_theme(element_theme, default_theme)
# Cached (performance-critical operations)
{fg, bg, attrs} = ThemeResolver.resolve_styles(attrs, :button, theme, cache: true)
"""
# Caching configuration and helpers
defp should_use_cache?(opts) do
cache_from_opts = Keyword.get(opts, :cache)
cache_from_config =
Application.get_env(:raxol, :theme_resolver, [])[:cache_enabled]
# opts override config, default to false
case {cache_from_opts, cache_from_config} do
{nil, nil} -> false
{nil, config} -> !!config
{opt, _} -> !!opt
end and cache_available?()
end
defp cache_available? do
Code.ensure_loaded?(Raxol.Performance.ETSCacheManager) and
Process.whereis(Raxol.Performance.ETSCacheManager) != nil
end
@doc """
Resolves an element's theme, handling string themes and providing fallbacks.
## Options
- `cache: boolean()` - Enable caching for this operation (default: false)
"""
def resolve_element_theme(element_theme, default_theme, opts \\ [])
def resolve_element_theme(element_theme, default_theme, opts) do
if should_use_cache?(opts) do
resolve_element_theme_cached(element_theme, default_theme)
else
resolve_element_theme_direct(element_theme, default_theme)
end
end
# Cached implementation
defp resolve_element_theme_cached(element_theme, default_theme) do
# For string themes, cache the lookup
case element_theme do
theme_name when is_binary(theme_name) ->
cache_key = {:theme_lookup, theme_name}
case get_cached_theme(cache_key) do
{:ok, theme} ->
theme
:miss ->
theme = resolve_element_theme_direct(element_theme, default_theme)
cache_theme(cache_key, theme)
theme
end
_ ->
# For non-string themes, use direct resolver
resolve_element_theme_direct(element_theme, default_theme)
end
end
# Direct implementation (original logic)
defp resolve_element_theme_direct(element_theme, default_theme) do
case element_theme do
nil ->
default_theme
theme when is_binary(theme) ->
# Try to get theme by name, fallback to default
case Raxol.UI.Theming.Theme.get(theme) do
nil -> default_theme
found_theme -> found_theme
end
theme when is_map(theme) ->
theme
_ ->
default_theme
end
end
@doc """
Resolves an element's theme with inheritance support.
## Options
- `cache: boolean()` - Enable caching for this operation (default: false)
"""
def resolve_element_theme_with_inheritance(element, default_theme, opts \\ [])
def resolve_element_theme_with_inheritance(element, default_theme, opts) do
if should_use_cache?(opts) do
resolve_element_theme_with_inheritance_cached(element, default_theme)
else
resolve_element_theme_with_inheritance_direct(element, default_theme)
end
end
# Cached implementation
defp resolve_element_theme_with_inheritance_cached(element, default_theme) do
# Create cache key based on element's theme configuration
cache_key = build_inheritance_cache_key(element)
case get_cached_theme(cache_key) do
{:ok, theme} ->
theme
:miss ->
theme =
resolve_element_theme_with_inheritance_direct(element, default_theme)
cache_theme(cache_key, theme)
theme
end
end
# Direct implementation (original logic)
defp resolve_element_theme_with_inheritance_direct(element, default_theme) do
# Get the main theme
main_theme =
resolve_element_theme_direct(Map.get(element, :theme), default_theme)
# Check for parent theme inheritance
parent_theme = Map.get(element, :parent_theme)
merge_parent_theme_if_present(parent_theme, main_theme)
end
@doc """
Merges themes for inheritance (parent theme as base, child theme overrides).
"""
def merge_themes_for_inheritance(parent_theme, child_theme) do
# Merge colors (child overrides parent)
merged_colors =
Map.merge(
Map.get(parent_theme, :colors, %{}),
Map.get(child_theme, :colors, %{})
)
# Merge component styles (child overrides parent)
merged_component_styles =
Map.merge(
Map.get(parent_theme, :component_styles, %{}),
Map.get(child_theme, :component_styles, %{})
)
# Merge variants (child overrides parent)
merged_variants =
Map.merge(
Map.get(parent_theme, :variants, %{}),
Map.get(child_theme, :variants, %{})
)
# Create merged theme
Map.merge(parent_theme, %{
colors: merged_colors,
component_styles: merged_component_styles,
variants: merged_variants
})
end
@doc """
Gets the default theme with fallback creation.
## Options
- `cache: boolean()` - Enable caching for this operation (default: false)
"""
def get_default_theme(opts \\ [])
def get_default_theme(opts) do
if should_use_cache?(opts) do
get_default_theme_cached()
else
get_default_theme_direct()
end
end
# Cached implementation
defp get_default_theme_cached do
cache_key = :default_theme
case get_cached_theme(cache_key) do
{:ok, theme} ->
theme
:miss ->
theme = get_default_theme_direct()
cache_theme(cache_key, theme)
theme
end
end
# Direct implementation (original logic)
defp get_default_theme_direct do
case Raxol.UI.Theming.Theme.get(:default) do
nil -> create_fallback_theme()
theme -> theme
end
end
@doc """
Creates a fallback theme when no default theme is available.
"""
def create_fallback_theme do
%{
colors: %{
foreground: :white,
background: :black
},
component_styles: %{},
variants: %{}
}
end
@doc """
Resolves foreground and background colors with proper fallbacks.
Returns {fg_color, bg_color, style_attrs}.
## Options
- `cache: boolean()` - Enable caching for this operation (default: false)
"""
def resolve_styles(attrs, component_type, theme, opts \\ [])
def resolve_styles(attrs, component_type, theme, opts) do
if should_use_cache?(opts) do
resolve_styles_cached(attrs, component_type, theme)
else
resolve_styles_direct(attrs, component_type, theme)
end
end
# Cached implementation
defp resolve_styles_cached(attrs, component_type, theme) do
# Generate cache key
theme_id = get_theme_id(theme)
attrs_hash = hash_attrs(attrs)
# Check cache first
case get_cached_style(theme_id, component_type, attrs_hash) do
{:ok, cached_result} ->
cached_result
:miss ->
# Compute and cache the result
result = resolve_styles_direct(attrs, component_type, theme)
cache_style(theme_id, component_type, attrs_hash, result)
result
end
end
# Direct implementation (original logic)
defp resolve_styles_direct(attrs, component_type, theme) do
component_styles = get_component_styles(component_type, theme)
fg_color = resolve_fg_color(attrs, component_styles, theme)
bg_color = resolve_bg_color(attrs, component_styles, theme)
style_attrs = resolve_style_attrs(attrs, component_styles)
{fg_color, bg_color, style_attrs}
end
@doc """
Resolves foreground color with proper fallbacks.
"""
def resolve_fg_color(attrs, _component_styles, theme) do
attrs
|> get_explicit_color([:fg, :foreground])
|> fallback_to_variant_color(attrs, theme, :foreground)
|> fallback_to_theme_color(theme, :foreground, :white)
|> convert_color_to_atom()
end
@doc """
Resolves background color with proper fallbacks.
"""
def resolve_bg_color(attrs, _component_styles, theme) do
attrs
|> get_explicit_color([:bg, :background])
|> fallback_to_variant_color(attrs, theme, :background)
|> fallback_to_theme_color(theme, :background, :black)
|> convert_color_to_atom()
end
@doc """
Resolves style attributes from explicit attrs and component styles.
"""
def resolve_style_attrs(attrs, component_styles) do
explicit_attrs = Map.get(attrs, :style, []) |> ensure_list()
component_attrs = Map.get(component_styles, :style, []) |> ensure_list()
(explicit_attrs ++ component_attrs) |> Enum.uniq()
end
@doc """
Resolves color from theme variant.
"""
def resolve_variant_color(attrs, theme, color_type) do
variant_name = Map.get(attrs, :variant)
get_variant_color_if_valid(variant_name, theme, color_type)
end
@doc """
Gets component styles from theme.
"""
def get_component_styles(component_type, theme) do
get_component_styles_if_valid(component_type, theme)
end
defp get_component_styles_from_map(component_styles, component_type)
when is_map(component_styles) do
Map.get(component_styles, component_type, %{})
end
defp get_component_styles_from_map(_, _), do: %{}
# Convert color values to atoms for test compatibility
defp convert_color_to_atom(color) when is_atom(color), do: color
defp convert_color_to_atom(color) when is_binary(color) do
hex_to_color_atom(String.downcase(color))
end
defp convert_color_to_atom(%{r: r, g: g, b: b}) do
# Convert RGB color struct to hex and then to atom
hex =
"##{Integer.to_string(r, 16) |> String.pad_leading(2, "0")}#{Integer.to_string(g, 16) |> String.pad_leading(2, "0")}#{Integer.to_string(b, 16) |> String.pad_leading(2, "0")}"
convert_color_to_atom(hex)
end
# Default fallback
defp convert_color_to_atom(_), do: :white
defp hex_to_color_atom("#ffffff"), do: :white
defp hex_to_color_atom("#000000"), do: :black
defp hex_to_color_atom("#ff0000"), do: :red
defp hex_to_color_atom("#00ff00"), do: :green
defp hex_to_color_atom("#0000ff"), do: :blue
defp hex_to_color_atom("#ffff00"), do: :yellow
defp hex_to_color_atom("#ff00ff"), do: :magenta
defp hex_to_color_atom("#00ffff"), do: :cyan
# Default fallback
defp hex_to_color_atom(_), do: :white
defp ensure_list(value) when is_list(value), do: value
defp ensure_list(value), do: [value]
# Helper functions to reduce complexity
defp get_explicit_color(attrs, color_keys) do
Enum.find_value(color_keys, fn key ->
get_attr_value_if_present(attrs, key)
end)
end
defp fallback_to_variant_color(nil, attrs, theme, color_type) do
resolve_variant_color(attrs, theme, color_type)
end
defp fallback_to_variant_color(color, _attrs, _theme, _color_type), do: color
defp fallback_to_theme_color(nil, theme, color_type, default) do
get_theme_color(theme, color_type, default)
end
defp fallback_to_theme_color(color, _theme, _color_type, _default), do: color
defp get_theme_color(nil, _color_type, default), do: default
defp get_theme_color(theme, _color_type, default) when not is_map(theme),
do: default
defp get_theme_color(theme, color_type, default) do
case Map.get(theme, :colors) do
nil -> default
colors when is_map(colors) -> Map.get(colors, color_type, default)
_ -> default
end
end
## Pattern matching helper functions for if statement elimination
defp merge_parent_theme_if_present(parent_theme, main_theme)
when is_map(parent_theme) do
# Merge parent theme with main theme (main theme overrides parent)
merge_themes_for_inheritance(parent_theme, main_theme)
end
defp merge_parent_theme_if_present(_parent_theme, main_theme), do: main_theme
defp get_variant_color_if_valid(nil, _theme, _color_type), do: nil
defp get_variant_color_if_valid(_variant_name, nil, _color_type), do: nil
defp get_variant_color_if_valid(_variant_name, theme, _color_type)
when not is_map(theme),
do: nil
defp get_variant_color_if_valid(variant_name, theme, color_type) do
variants = Map.get(theme, :variants, %{})
variant = Map.get(variants, variant_name)
get_color_from_variant(variant, color_type)
end
defp get_color_from_variant(variant, color_type) when is_map(variant) do
Map.get(variant, color_type)
end
defp get_color_from_variant(_variant, _color_type), do: nil
defp get_component_styles_if_valid(nil, _theme), do: %{}
defp get_component_styles_if_valid(_component_type, theme)
when not is_map(theme),
do: %{}
defp get_component_styles_if_valid(component_type, theme) do
theme
|> Map.get(:component_styles, %{})
|> get_component_styles_from_map(component_type)
end
defp get_attr_value_if_present(attrs, key) do
case Map.get(attrs, key) do
nil -> nil
value -> value
end
end
# Cache Management Functions
@doc """
Clear all theme/style caches.
"""
def clear_cache do
if cache_available?() do
require Raxol.Performance.ETSCacheManager
Raxol.Performance.ETSCacheManager.clear_cache(:style)
end
end
@doc """
Invalidate cache entries for a specific theme.
"""
def invalidate_theme(_theme_id) do
# This would require more sophisticated cache management
# For now, clear all style cache when a theme changes
clear_cache()
end
# Private cache operation helpers
defp get_cached_style(theme_id, component_type, attrs_hash) do
if cache_available?() do
Raxol.Performance.ETSCacheManager.get_style(
theme_id,
component_type,
attrs_hash
)
else
:miss
end
end
defp cache_style(theme_id, component_type, attrs_hash, result) do
if cache_available?() do
Raxol.Performance.ETSCacheManager.cache_style(
theme_id,
component_type,
attrs_hash,
result
)
end
end
defp get_cached_theme(key) do
if cache_available?() do
# Use the style cache table for theme data
Raxol.Performance.ETSCacheManager.get_style(
:theme_cache,
nil,
:erlang.phash2(key)
)
else
:miss
end
end
defp cache_theme(key, theme) do
if cache_available?() do
Raxol.Performance.ETSCacheManager.cache_style(
:theme_cache,
nil,
:erlang.phash2(key),
theme
)
end
end
# Cache key generation helpers
defp get_theme_id(nil), do: :no_theme
defp get_theme_id(theme) when is_atom(theme), do: theme
defp get_theme_id(theme) when is_binary(theme), do: theme
defp get_theme_id(theme) when is_map(theme) do
# Use theme name if available, otherwise hash the theme
Map.get(theme, :name, :erlang.phash2(theme))
end
defp get_theme_id(_), do: :unknown_theme
defp hash_attrs(nil), do: 0
defp hash_attrs(attrs) when is_map(attrs) do
# Create a stable hash of relevant style attributes
relevant_keys = [
:fg,
:foreground,
:bg,
:background,
:variant,
:style,
:bold,
:italic,
:underline,
:blink,
:reverse
]
attrs
|> Map.take(relevant_keys)
|> :erlang.phash2()
end
defp hash_attrs(_), do: 0
defp hash_theme(nil), do: 0
defp hash_theme(theme) when is_map(theme) do
# Hash only the parts of theme that affect style resolution
%{
colors: Map.get(theme, :colors, %{}),
component_styles: Map.get(theme, :component_styles, %{}),
variants: Map.get(theme, :variants, %{})
}
|> :erlang.phash2()
end
defp hash_theme(_), do: 0
defp build_inheritance_cache_key(element) do
element_theme = Map.get(element, :theme)
parent_theme = Map.get(element, :parent_theme)
{:inheritance, get_theme_id(element_theme), hash_theme(parent_theme)}
end
end