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/accessibility/high_contrast.ex
defmodule Raxol.UI.Accessibility.HighContrast do
@moduledoc """
High contrast mode system for improved visual accessibility in Raxol terminals.
This module provides comprehensive high contrast support including:
- Multiple high contrast themes (black/white, white/black, custom)
- WCAG 2.1 AA and AAA contrast ratio compliance
- Automatic contrast adjustment for different lighting conditions
- Color blindness accommodation (Deuteranopia, Protanopia, Tritanopia)
- Large text mode with scalable fonts
- Custom user-defined contrast schemes
- System integration with OS accessibility settings
- Dynamic contrast adjustment based on ambient light
- Color palette optimization for terminal interfaces
## Features
### Contrast Modes
- High Contrast Black (white text on black background)
- High Contrast White (black text on white background)
- High Contrast Custom (user-defined color schemes)
- Inverted Colors mode
- Grayscale mode with enhanced contrast
- Adaptive contrast based on content type
### Accessibility Compliance
- WCAG 2.1 Level AA (4.5:1 contrast ratio)
- WCAG 2.1 Level AAA (7:1 contrast ratio)
- Color blindness simulation and correction
- Focus indicator enhancement
- Text shadow and outline options for improved readability
## Usage
# Initialize high contrast system
{:ok, hc} = HighContrast.start_link(
default_theme: :high_contrast_black,
compliance_level: :wcag_aa,
auto_detect_system: true,
color_blind_support: true
)
# Apply high contrast theme
HighContrast.apply_theme(hc, :high_contrast_white)
# Create custom theme with WCAG compliance
custom_theme = %{
name: "custom_blue",
background: {0, 0, 51}, # Dark blue
foreground: {255, 255, 204}, # Light yellow
accent: {255, 102, 0}, # Orange
compliance_level: :wcag_aaa
}
HighContrast.register_theme(hc, custom_theme)
# Enable color blindness accommodation
HighContrast.configure_color_blindness(hc, :deuteranopia, %{
strength: 0.8,
enable_patterns: true,
alternative_indicators: [:shapes, :textures]
})
"""
use Raxol.Core.Behaviours.BaseManager
alias Raxol.Core.Platform
defstruct [
:config,
:current_theme,
:theme_registry,
:contrast_analyzer,
:color_blind_config,
:system_monitor,
:ambient_light_sensor,
:user_preferences,
:compliance_validator
]
@type rgb_color :: {0..255, 0..255, 0..255}
@type contrast_ratio :: float()
@type compliance_level :: :wcag_a | :wcag_aa | :wcag_aaa
@type color_blindness_type ::
:deuteranopia | :protanopia | :tritanopia | :achromatopsia
@type theme_name :: atom()
@type theme :: %{
name: theme_name(),
background: rgb_color(),
foreground: rgb_color(),
accent: rgb_color(),
secondary: rgb_color(),
success: rgb_color(),
warning: rgb_color(),
error: rgb_color(),
info: rgb_color(),
border: rgb_color(),
shadow: rgb_color(),
compliance_level: compliance_level(),
large_text_optimized: boolean()
}
@type config :: %{
default_theme: theme_name(),
compliance_level: compliance_level(),
auto_detect_system: boolean(),
color_blind_support: boolean(),
large_text_mode: boolean(),
adaptive_contrast: boolean(),
ambient_light_adjustment: boolean(),
focus_enhancement: boolean(),
text_shadow: boolean(),
invert_colors: boolean()
}
# Default configuration
@default_config %{
default_theme: :system,
compliance_level: :wcag_aa,
auto_detect_system: true,
color_blind_support: false,
large_text_mode: false,
adaptive_contrast: true,
ambient_light_adjustment: false,
focus_enhancement: true,
text_shadow: false,
invert_colors: false
}
# Pre-defined high contrast themes
@builtin_themes %{
high_contrast_black: %{
name: :high_contrast_black,
# Black
background: {0, 0, 0},
# White
foreground: {255, 255, 255},
# Yellow
accent: {255, 255, 0},
# Light gray
secondary: {192, 192, 192},
# Lime green
success: {0, 255, 0},
# Yellow
warning: {255, 255, 0},
# Red
error: {255, 0, 0},
# Cyan
info: {0, 255, 255},
# White
border: {255, 255, 255},
# Dark gray
shadow: {64, 64, 64},
compliance_level: :wcag_aaa,
large_text_optimized: true
},
high_contrast_white: %{
name: :high_contrast_white,
# White
background: {255, 255, 255},
# Black
foreground: {0, 0, 0},
# Blue
accent: {0, 0, 255},
# Dark gray
secondary: {64, 64, 64},
# Dark green
success: {0, 128, 0},
# Dark orange
warning: {255, 140, 0},
# Dark red
error: {139, 0, 0},
# Dark blue
info: {0, 0, 139},
# Black
border: {0, 0, 0},
# Light gray
shadow: {192, 192, 192},
compliance_level: :wcag_aaa,
large_text_optimized: true
},
high_contrast_blue: %{
name: :high_contrast_blue,
# Very dark blue
background: {0, 0, 51},
# Light yellow
foreground: {255, 255, 204},
# Orange
accent: {255, 102, 0},
# Light blue
secondary: {153, 153, 255},
# Light green
success: {102, 255, 102},
# Gold
warning: {255, 204, 0},
# Light red
error: {255, 102, 102},
# Light blue
info: {102, 204, 255},
# Light yellow
border: {255, 255, 204},
# Darker blue
shadow: {25, 25, 76},
compliance_level: :wcag_aa,
large_text_optimized: false
},
grayscale_high_contrast: %{
name: :grayscale_high_contrast,
# Black
background: {0, 0, 0},
# White
foreground: {255, 255, 255},
# Light gray
accent: {192, 192, 192},
# Medium gray
secondary: {128, 128, 128},
# Very light gray
success: {224, 224, 224},
# Medium-light gray
warning: {160, 160, 160},
# Medium-dark gray
error: {96, 96, 96},
# Light gray
info: {192, 192, 192},
# White
border: {255, 255, 255},
# Dark gray
shadow: {64, 64, 64},
compliance_level: :wcag_aaa,
large_text_optimized: true
}
}
# WCAG contrast ratio requirements
@wcag_ratios %{
wcag_a: 3.0,
wcag_aa: 4.5,
wcag_aaa: 7.0
}
@large_text_ratios %{
wcag_a: 2.0,
wcag_aa: 3.0,
wcag_aaa: 4.5
}
## Public API
# BaseManager provides start_link/1 which handles GenServer initialization
# Usage: Raxol.UI.Accessibility.HighContrast.start_link(name: __MODULE__, config: custom_config)
# Options:
# - `:default_theme` - Initial theme to apply
# - `:compliance_level` - WCAG compliance level (:wcag_aa, :wcag_aaa)
# - `:auto_detect_system` - Detect system high contrast settings
# - `:color_blind_support` - Enable color blindness accommodations
@doc """
Applies a high contrast theme.
"""
def apply_theme(hc \\ __MODULE__, theme_name) do
GenServer.call(hc, {:apply_theme, theme_name})
end
@doc """
Registers a custom high contrast theme.
"""
def register_theme(hc \\ __MODULE__, theme) do
GenServer.call(hc, {:register_theme, theme})
end
@doc """
Gets the current active theme.
"""
def get_current_theme(hc \\ __MODULE__) do
GenServer.call(hc, :get_current_theme)
end
@doc """
Lists all available themes.
"""
def list_themes(hc \\ __MODULE__) do
GenServer.call(hc, :list_themes)
end
@doc """
Configures color blindness accommodation.
"""
def configure_color_blindness(hc \\ __MODULE__, type, options \\ %{}) do
GenServer.call(hc, {:configure_color_blindness, type, options})
end
@doc """
Enables or disables large text mode.
"""
def set_large_text_mode(hc \\ __MODULE__, enabled) do
GenServer.call(hc, {:set_large_text_mode, enabled})
end
@doc """
Toggles color inversion.
"""
def toggle_invert_colors(hc \\ __MODULE__) do
GenServer.call(hc, :toggle_invert_colors)
end
@doc """
Validates contrast ratio compliance for a color pair.
"""
def validate_contrast(
hc \\ __MODULE__,
foreground,
background,
options \\ %{}
) do
GenServer.call(hc, {:validate_contrast, foreground, background, options})
end
@doc """
Suggests improved colors for better contrast.
"""
def suggest_contrast_improvement(hc \\ __MODULE__, foreground, background) do
GenServer.call(hc, {:suggest_contrast_improvement, foreground, background})
end
@doc """
Gets accessibility information for the current theme.
"""
def get_accessibility_info(hc \\ __MODULE__) do
GenServer.call(hc, :get_accessibility_info)
end
## GenServer Implementation
@impl Raxol.Core.Behaviours.BaseManager
def init_manager(opts) do
config = Map.merge(@default_config, Keyword.get(opts, :config, %{}))
# Initialize theme registry with built-in themes
theme_registry = @builtin_themes
# Detect system high contrast settings if enabled
{initial_theme, updated_config} = get_initial_theme(config)
# Initialize contrast analyzer
contrast_analyzer = init_contrast_analyzer()
# Initialize color blindness support
color_blind_config = init_color_blind_config(config)
# Initialize system monitoring
system_monitor = init_system_monitor(config)
state = %__MODULE__{
config: updated_config,
current_theme: nil,
theme_registry: theme_registry,
contrast_analyzer: contrast_analyzer,
color_blind_config: color_blind_config,
system_monitor: system_monitor,
ambient_light_sensor: init_ambient_light_sensor(config),
user_preferences: %{},
compliance_validator: init_compliance_validator(config.compliance_level)
}
# Apply initial theme
{:ok, final_state} = apply_theme_internal(state, initial_theme)
Log.info("High contrast system initialized with theme: #{initial_theme}")
{:ok, final_state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call({:apply_theme, theme_name}, _from, state) do
case apply_theme_internal(state, theme_name) do
{:ok, new_state} ->
{:reply, :ok, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call({:register_theme, theme}, _from, state) do
# Validate theme structure
case validate_theme(theme) do
{:ok, validated_theme} ->
# Check contrast compliance
case validate_theme_compliance(
validated_theme,
state.config.compliance_level
) do
{:ok, compliance_info} ->
new_registry =
Map.put(
state.theme_registry,
validated_theme.name,
validated_theme
)
new_state = %{state | theme_registry: new_registry}
Log.info(
"Custom theme registered: #{validated_theme.name} (#{compliance_info.level})"
)
{:reply, {:ok, compliance_info}, new_state}
{:warning, issues} ->
new_registry =
Map.put(
state.theme_registry,
validated_theme.name,
validated_theme
)
new_state = %{state | theme_registry: new_registry}
Log.warning(
"Theme registered with compliance issues: #{inspect(issues)}"
)
{:reply, {:warning, issues}, new_state}
{:error, reason} ->
{:reply, {:error, reason}, state}
end
{:error, reason} ->
{:reply, {:error, reason}, state}
end
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call(:get_current_theme, _from, state) do
{:reply, state.current_theme, state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call(:list_themes, _from, state) do
themes =
state.theme_registry
|> Map.keys()
|> Enum.map(fn theme_name ->
theme = Map.get(state.theme_registry, theme_name)
%{
name: theme_name,
compliance_level: theme.compliance_level,
large_text_optimized: theme.large_text_optimized
}
end)
{:reply, themes, state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call(
{:configure_color_blindness, type, options},
_from,
state
) do
configure_color_blindness_internal(state, type, options)
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call({:set_large_text_mode, enabled}, _from, state) do
new_config = %{state.config | large_text_mode: enabled}
new_state = %{state | config: new_config}
# Re-apply current theme to adjust for large text
{:ok, updated_state} = reapply_current_theme(new_state)
Log.info("Large text mode #{format_enabled_status(enabled)}")
{:reply, :ok, updated_state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call(:toggle_invert_colors, _from, state) do
new_invert = not state.config.invert_colors
new_config = %{state.config | invert_colors: new_invert}
new_state = %{state | config: new_config}
# Re-apply current theme with inversion
{:ok, updated_state} = reapply_current_theme(new_state)
Log.info("Color inversion #{format_enabled_status(new_invert)}")
{:reply, :ok, updated_state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call(
{:validate_contrast, foreground, background, options},
_from,
state
) do
large_text = Map.get(options, :large_text, state.config.large_text_mode)
compliance_level =
Map.get(options, :compliance_level, state.config.compliance_level)
contrast_ratio = calculate_contrast_ratio(foreground, background)
required_ratio = get_required_ratio(compliance_level, large_text)
result = %{
contrast_ratio: contrast_ratio,
required_ratio: required_ratio,
compliant: contrast_ratio >= required_ratio,
level_achieved: get_achieved_compliance_level(contrast_ratio, large_text),
recommendations:
generate_contrast_recommendations(contrast_ratio, required_ratio)
}
{:reply, result, state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call(
{:suggest_contrast_improvement, foreground, background},
_from,
state
) do
current_ratio = calculate_contrast_ratio(foreground, background)
target_ratio =
get_required_ratio(
state.config.compliance_level,
state.config.large_text_mode
)
suggestions =
build_contrast_suggestions(
current_ratio,
target_ratio,
foreground,
background
)
result = %{
current_ratio: current_ratio,
target_ratio: target_ratio,
suggestions: suggestions
}
{:reply, result, state}
end
@impl Raxol.Core.Behaviours.BaseManager
def handle_manager_call(:get_accessibility_info, _from, state) do
info = build_accessibility_info(state)
{:reply, info, state}
end
## Private Implementation
defp apply_theme_internal(state, theme_name) do
case Map.get(state.theme_registry, theme_name) do
nil ->
{:error, :theme_not_found}
theme ->
# Apply color blindness corrections if enabled
adjusted_theme = apply_color_adjustments(theme, state)
# Apply color inversion if enabled
final_theme = apply_color_inversion(adjusted_theme, state.config)
# Apply the theme to the UI system
apply_theme_to_system(final_theme, state.config)
new_state = %{state | current_theme: final_theme}
{:ok, new_state}
end
end
defp validate_theme(theme) do
required_keys = [
:name,
:background,
:foreground,
:accent,
:secondary,
:success,
:warning,
:error,
:info,
:border,
:shadow
]
missing_keys =
Enum.filter(required_keys, fn key -> not Map.has_key?(theme, key) end)
validate_theme_keys(missing_keys, theme)
end
defp validate_theme_compliance(theme, required_level) do
required_ratio = @wcag_ratios[required_level]
# Check key color pairs
critical_pairs = [
{:foreground_background, theme.foreground, theme.background},
{:accent_background, theme.accent, theme.background}
]
issues = collect_compliance_issues(critical_pairs, required_ratio)
case issues do
[] ->
{:ok, %{level: required_level, all_compliant: true}}
issues when length(issues) < length(critical_pairs) ->
{:warning, issues}
_all_failed ->
{:error, :insufficient_contrast}
end
end
defp calculate_contrast_ratio({r1, g1, b1}, {r2, g2, b2}) do
# Calculate relative luminance for each color
l1 = relative_luminance(r1, g1, b1)
l2 = relative_luminance(r2, g2, b2)
# Ensure lighter color is in numerator
{lighter, darker} =
case l1 > l2 do
true -> {l1, l2}
false -> {l2, l1}
end
# Calculate contrast ratio
(lighter + 0.05) / (darker + 0.05)
end
defp relative_luminance(r, g, b) do
# Convert RGB to relative luminance using sRGB formula
[r, g, b]
|> Enum.map(fn c ->
# Convert to [0, 1] range
c_norm = c / 255.0
# Apply gamma correction
apply_gamma_correction(c_norm)
end)
|> then(fn [r_lin, g_lin, b_lin] ->
# Calculate relative luminance
0.2126 * r_lin + 0.7152 * g_lin + 0.0722 * b_lin
end)
end
defp get_required_ratio(compliance_level, large_text) do
get_ratio_for_text_size(large_text, compliance_level)
end
defp get_ratio_for_text_size(true, compliance_level),
do: @large_text_ratios[compliance_level]
defp get_ratio_for_text_size(false, compliance_level),
do: @wcag_ratios[compliance_level]
defp get_achieved_compliance_level(ratio, large_text) do
ratios = get_ratios_for_text_size(large_text)
determine_compliance_from_ratio(ratio, ratios)
end
defp get_ratios_for_text_size(true), do: @large_text_ratios
defp get_ratios_for_text_size(false), do: @wcag_ratios
defp generate_contrast_recommendations(current_ratio, required_ratio) do
improvement_needed = required_ratio / current_ratio
build_recommendation_list([], improvement_needed, current_ratio)
end
defp build_contrast_suggestions(
current_ratio,
target_ratio,
_foreground,
_background
)
when current_ratio >= target_ratio,
do: []
defp build_contrast_suggestions(
current_ratio,
target_ratio,
foreground,
background
) do
improvement_factor = target_ratio / current_ratio
# Suggest darker background
darker_bg = darken_color(background, improvement_factor)
darker_bg_ratio = calculate_contrast_ratio(foreground, darker_bg)
# Suggest lighter foreground
lighter_fg = lighten_color(foreground, improvement_factor)
lighter_fg_ratio = calculate_contrast_ratio(lighter_fg, background)
suggestions =
build_suggestion_list(
[],
darker_bg,
darker_bg_ratio,
lighter_fg,
lighter_fg_ratio,
target_ratio
)
# Suggest high contrast alternatives
high_contrast_suggestions = [
%{
type: :both,
foreground: {255, 255, 255},
background: {0, 0, 0},
resulting_ratio: calculate_contrast_ratio({255, 255, 255}, {0, 0, 0})
},
%{
type: :both,
foreground: {0, 0, 0},
background: {255, 255, 255},
resulting_ratio: calculate_contrast_ratio({0, 0, 0}, {255, 255, 255})
}
]
suggestions ++ high_contrast_suggestions
end
defp darken_color({r, g, b}, factor) do
adjustment = 1.0 / factor
{
round(r * adjustment) |> max(0) |> min(255),
round(g * adjustment) |> max(0) |> min(255),
round(b * adjustment) |> max(0) |> min(255)
}
end
defp lighten_color({r, g, b}, factor) do
# Cap the lightening
adjustment = min(factor, 2.0)
{
round(r * adjustment) |> max(0) |> min(255),
round(g * adjustment) |> max(0) |> min(255),
round(b * adjustment) |> max(0) |> min(255)
}
end
defp apply_color_blind_corrections(theme, color_blind_config) do
correction_fn =
case color_blind_config.type do
:deuteranopia -> &correct_deuteranopia/2
:protanopia -> &correct_protanopia/2
:tritanopia -> &correct_tritanopia/2
:achromatopsia -> &correct_achromatopsia/2
end
%{
theme
| foreground:
correction_fn.(theme.foreground, color_blind_config.strength),
accent: correction_fn.(theme.accent, color_blind_config.strength),
success: correction_fn.(theme.success, color_blind_config.strength),
warning: correction_fn.(theme.warning, color_blind_config.strength),
error: correction_fn.(theme.error, color_blind_config.strength),
info: correction_fn.(theme.info, color_blind_config.strength)
}
end
defp correct_deuteranopia({r, g, b}, strength) do
# Deuteranopia correction matrix (simplified)
# This would use proper color transformation matrices in practice
corrected_r = r
corrected_g = round(g * (1.0 - strength) + r * strength * 0.3)
corrected_b = b
{corrected_r, corrected_g, corrected_b}
end
defp correct_protanopia({r, g, b}, strength) do
# Protanopia correction matrix (simplified)
corrected_r = round(r * (1.0 - strength) + g * strength * 0.7)
corrected_g = g
corrected_b = b
{corrected_r, corrected_g, corrected_b}
end
defp correct_tritanopia({r, g, b}, strength) do
# Tritanopia correction matrix (simplified)
corrected_r = r
corrected_g = g
corrected_b = round(b * (1.0 - strength) + g * strength * 0.5)
{corrected_r, corrected_g, corrected_b}
end
defp correct_achromatopsia({r, g, b}, strength) do
# Convert to grayscale
gray = round(0.299 * r + 0.587 * g + 0.114 * b)
corrected_r = round(r * (1.0 - strength) + gray * strength)
corrected_g = round(g * (1.0 - strength) + gray * strength)
corrected_b = round(b * (1.0 - strength) + gray * strength)
{corrected_r, corrected_g, corrected_b}
end
defp invert_theme_colors(theme) do
%{
theme
| background: invert_color(theme.background),
foreground: invert_color(theme.foreground),
accent: invert_color(theme.accent),
secondary: invert_color(theme.secondary),
success: invert_color(theme.success),
warning: invert_color(theme.warning),
error: invert_color(theme.error),
info: invert_color(theme.info),
border: invert_color(theme.border),
shadow: invert_color(theme.shadow)
}
end
defp invert_color({r, g, b}) do
{255 - r, 255 - g, 255 - b}
end
defp apply_theme_to_system(theme, _config) do
# Apply the theme to the UI theme system
Log.info("Applying high contrast theme: #{theme.name}")
# Update the theme manager palette with the high contrast theme colors
palette = %{
background: theme.background,
foreground: theme.foreground,
accent: theme.accent,
secondary: theme.secondary,
success: theme.success,
warning: theme.warning,
error: theme.error,
info: theme.info,
border: theme.border,
shadow: theme.shadow
}
case Raxol.UI.Theming.ThemeManager.update_palette(palette) do
:ok ->
:ok
{:error, reason} ->
Log.warning("Failed to update theme palette: #{inspect(reason)}")
:ok
end
end
defp detect_system_high_contrast do
case Platform.os_type() do
:windows ->
# Check Windows high contrast settings
detect_windows_high_contrast()
:macos ->
# Check macOS accessibility settings
detect_macos_high_contrast()
:linux ->
# Check Linux accessibility settings
detect_linux_high_contrast()
_ ->
{:error, :unsupported_platform}
end
end
defp detect_windows_high_contrast do
# Would use Windows API to detect high contrast mode
# For now, return a reasonable default
{:ok, :high_contrast_black}
end
defp detect_macos_high_contrast do
# Would use macOS Accessibility APIs
{:ok, :high_contrast_white}
end
defp detect_linux_high_contrast do
# Would check GNOME/KDE accessibility settings
{:ok, :high_contrast_black}
end
## Initialization helpers
defp init_contrast_analyzer do
%{
cache: %{},
last_analysis: nil,
compliance_checks_enabled: true
}
end
defp init_color_blind_support do
%{
type: nil,
strength: 1.0,
enable_patterns: false,
alternative_indicators: [],
correction_matrix: nil
}
end
defp start_system_monitor do
# Would start a process to monitor system accessibility changes
%{enabled: true, last_check: System.monotonic_time(:millisecond)}
end
defp init_ambient_light_sensor(config) do
init_ambient_light_sensor_with_config(config.ambient_light_adjustment)
end
defp init_ambient_light_sensor_with_config(true) do
%{enabled: true, last_reading: 0.5, adjustment_factor: 1.0}
end
defp init_ambient_light_sensor_with_config(false) do
%{enabled: false}
end
defp init_compliance_validator(level) do
%{
level: level,
strict_mode: level == :wcag_aaa,
cache: %{}
}
end
defp generate_color_correction_matrix(type) do
# Generate color correction matrix for color blindness type
# These would be actual transformation matrices in practice
case type do
:deuteranopia ->
[[1.0, 0.0, 0.0], [0.7, 0.3, 0.0], [0.0, 0.0, 1.0]]
:protanopia ->
[[0.3, 0.7, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]
:tritanopia ->
[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.5, 0.5]]
:achromatopsia ->
[[0.299, 0.587, 0.114], [0.299, 0.587, 0.114], [0.299, 0.587, 0.114]]
end
end
defp color_blind_friendly?(_theme) do
# Check if theme uses patterns/shapes in addition to color
# This would analyze the theme for color-blind accessibility
# Simplified for now
true
end
defp determine_overall_compliance(contrast_analysis) do
compliant_pairs = Enum.count(contrast_analysis, & &1.wcag_aa_compliant)
total_pairs = length(contrast_analysis)
calculate_compliance_level(compliant_pairs, total_pairs)
end
defp determine_compliance_from_ratio(ratio, ratios)
when ratio >= ratios.wcag_aaa,
do: :wcag_aaa
defp determine_compliance_from_ratio(ratio, ratios)
when ratio >= ratios.wcag_aa,
do: :wcag_aa
defp determine_compliance_from_ratio(ratio, ratios)
when ratio >= ratios.wcag_a,
do: :wcag_a
defp determine_compliance_from_ratio(_ratio, _ratios), do: :non_compliant
defp calculate_compliance_level(compliant_pairs, total_pairs)
when compliant_pairs == total_pairs,
do: :fully_compliant
defp calculate_compliance_level(compliant_pairs, total_pairs)
when compliant_pairs > total_pairs * 0.8,
do: :mostly_compliant
defp calculate_compliance_level(compliant_pairs, total_pairs)
when compliant_pairs > total_pairs * 0.5,
do: :partially_compliant
defp calculate_compliance_level(_compliant_pairs, _total_pairs),
do: :non_compliant
defp get_enabled_accessibility_features(state) do
build_feature_list(state)
end
## Public Utility Functions
@doc """
Creates a custom theme builder with WCAG validation.
"""
def create_theme_builder(base_colors, target_compliance \\ :wcag_aa) do
%{
base_colors: base_colors,
target_compliance: target_compliance,
build: fn ->
# Build theme with automatic contrast adjustments
build_compliant_theme(base_colors, target_compliance)
end
}
end
defp build_compliant_theme(base_colors, target_compliance) do
# Build a theme that meets the target compliance level
# This would automatically adjust colors to meet contrast requirements
target_ratio = @wcag_ratios[target_compliance]
background = Map.get(base_colors, :background, {0, 0, 0})
foreground = Map.get(base_colors, :foreground, {255, 255, 255})
# Ensure foreground/background meet contrast requirements
current_ratio = calculate_contrast_ratio(foreground, background)
{final_foreground, final_background} =
ensure_contrast_compliance(
foreground,
background,
current_ratio,
target_ratio
)
%{
name: :custom_compliant,
background: final_background,
foreground: final_foreground,
accent: Map.get(base_colors, :accent, {255, 255, 0}),
secondary: Map.get(base_colors, :secondary, {192, 192, 192}),
success: Map.get(base_colors, :success, {0, 255, 0}),
warning: Map.get(base_colors, :warning, {255, 255, 0}),
error: Map.get(base_colors, :error, {255, 0, 0}),
info: Map.get(base_colors, :info, {0, 255, 255}),
border: final_foreground,
shadow: darken_color(final_background, 1.5),
compliance_level: target_compliance,
large_text_optimized: false
}
end
defp adjust_colors_for_contrast(foreground, background, target_ratio) do
# Try darkening background first
darker_bg = darken_color(background, 1.5)
adjust_colors_for_contrast_internal(
foreground,
background,
darker_bg,
target_ratio
)
end
## Helper functions for refactored code
defp get_initial_theme(%{auto_detect_system: true} = config) do
case detect_system_high_contrast() do
{:ok, system_theme} -> {system_theme, config}
{:error, _reason} -> {config.default_theme, config}
end
end
defp get_initial_theme(config) do
{config.default_theme, config}
end
defp init_color_blind_config(%{color_blind_support: true}) do
init_color_blind_support()
end
defp init_color_blind_config(_config) do
nil
end
defp init_system_monitor(%{auto_detect_system: true}) do
start_system_monitor()
end
defp init_system_monitor(_config) do
nil
end
defp configure_color_blindness_internal(
%{config: %{color_blind_support: false}} = state,
_type,
_options
) do
{:reply, {:error, :color_blind_support_disabled}, state}
end
defp configure_color_blindness_internal(state, type, options) do
new_color_blind_config = %{
type: type,
strength: Map.get(options, :strength, 1.0),
enable_patterns: Map.get(options, :enable_patterns, false),
alternative_indicators: Map.get(options, :alternative_indicators, []),
correction_matrix: generate_color_correction_matrix(type)
}
new_state = %{state | color_blind_config: new_color_blind_config}
# Re-apply current theme with color blindness corrections
{:ok, updated_state} = reapply_current_theme(new_state)
Log.info("Color blindness support configured: #{type}")
{:reply, :ok, updated_state}
end
defp reapply_current_theme(%{current_theme: nil} = state) do
{:ok, state}
end
defp reapply_current_theme(%{current_theme: current_theme} = state) do
apply_theme_internal(state, current_theme.name)
end
defp format_enabled_status(true), do: "enabled"
defp format_enabled_status(false), do: "disabled"
defp build_accessibility_info(%{current_theme: nil}) do
%{error: :no_theme_active}
end
defp build_accessibility_info(%{current_theme: theme} = state) do
# Analyze all color pairs in the theme
color_pairs = [
{:foreground_background, theme.foreground, theme.background},
{:accent_background, theme.accent, theme.background},
{:success_background, theme.success, theme.background},
{:warning_background, theme.warning, theme.background},
{:error_background, theme.error, theme.background},
{:info_background, theme.info, theme.background}
]
contrast_analysis =
Enum.map(color_pairs, fn {pair_name, fg, bg} ->
ratio = calculate_contrast_ratio(fg, bg)
%{
pair: pair_name,
foreground: fg,
background: bg,
contrast_ratio: ratio,
# wcag_aa ratio
wcag_aa_compliant: ratio >= 4.5,
# wcag_aaa ratio
wcag_aaa_compliant: ratio >= 7.0
}
end)
%{
theme_name: theme.name,
compliance_level: theme.compliance_level,
large_text_optimized: theme.large_text_optimized,
color_blind_friendly: color_blind_friendly?(theme),
contrast_analysis: contrast_analysis,
overall_compliance: determine_overall_compliance(contrast_analysis),
accessibility_features: get_enabled_accessibility_features(state)
}
end
defp apply_color_adjustments(theme, %{color_blind_config: nil}) do
theme
end
defp apply_color_adjustments(theme, %{color_blind_config: config}) do
apply_color_blind_corrections(theme, config)
end
defp apply_color_inversion(theme, %{invert_colors: true}) do
invert_theme_colors(theme)
end
defp apply_color_inversion(theme, _config) do
theme
end
defp validate_theme_keys([], theme) do
# Set defaults for optional keys
validated =
Map.merge(
%{
compliance_level: :wcag_aa,
large_text_optimized: false
},
theme
)
{:ok, validated}
end
defp validate_theme_keys(missing_keys, _theme) do
{:error, {:missing_keys, missing_keys}}
end
defp collect_compliance_issues(critical_pairs, required_ratio) do
Enum.reduce(critical_pairs, [], fn {pair_name, fg, bg}, acc ->
ratio = calculate_contrast_ratio(fg, bg)
case ratio < required_ratio do
true ->
[%{pair: pair_name, ratio: ratio, required: required_ratio} | acc]
false ->
acc
end
end)
end
defp apply_gamma_correction(c_norm) when c_norm <= 0.03928 do
c_norm / 12.92
end
defp apply_gamma_correction(c_norm) do
:math.pow((c_norm + 0.055) / 1.055, 2.4)
end
defp build_recommendation_list(
recommendations,
improvement_needed,
current_ratio
) do
recommendations
|> add_major_improvement_recommendation(improvement_needed)
|> add_minor_improvement_recommendation(improvement_needed)
|> add_critical_contrast_recommendation(current_ratio)
end
defp add_major_improvement_recommendation(recommendations, improvement_needed)
when improvement_needed > 1.5 do
[
"Consider using darker background or lighter foreground colors"
| recommendations
]
end
defp add_major_improvement_recommendation(recommendations, _),
do: recommendations
defp add_minor_improvement_recommendation(recommendations, improvement_needed)
when improvement_needed > 1.2 do
[
"Adjust color saturation or brightness for better contrast"
| recommendations
]
end
defp add_minor_improvement_recommendation(recommendations, _),
do: recommendations
defp add_critical_contrast_recommendation(recommendations, current_ratio)
when current_ratio < 3.0 do
[
"Current contrast is very low - significant changes needed"
| recommendations
]
end
defp add_critical_contrast_recommendation(recommendations, _),
do: recommendations
defp build_suggestion_list(
suggestions,
darker_bg,
darker_bg_ratio,
lighter_fg,
lighter_fg_ratio,
target_ratio
) do
suggestions
|> add_background_suggestion(darker_bg, darker_bg_ratio, target_ratio)
|> add_foreground_suggestion(lighter_fg, lighter_fg_ratio, target_ratio)
end
defp add_background_suggestion(
suggestions,
darker_bg,
darker_bg_ratio,
target_ratio
)
when darker_bg_ratio >= target_ratio do
[
%{
type: :background,
color: darker_bg,
resulting_ratio: darker_bg_ratio
}
| suggestions
]
end
defp add_background_suggestion(suggestions, _, _, _), do: suggestions
defp add_foreground_suggestion(
suggestions,
lighter_fg,
lighter_fg_ratio,
target_ratio
)
when lighter_fg_ratio >= target_ratio do
[
%{
type: :foreground,
color: lighter_fg,
resulting_ratio: lighter_fg_ratio
}
| suggestions
]
end
defp add_foreground_suggestion(suggestions, _, _, _), do: suggestions
defp build_feature_list(state) do
[]
|> add_feature("Large text mode", state.config.large_text_mode)
|> add_feature("Color inversion", state.config.invert_colors)
|> add_feature("Enhanced focus indicators", state.config.focus_enhancement)
|> add_feature("Text shadows", state.config.text_shadow)
|> add_color_blind_feature(state.color_blind_config)
end
defp add_feature(features, _name, false), do: features
defp add_feature(features, name, true), do: [name | features]
defp add_color_blind_feature(features, %{type: type}) when not is_nil(type) do
["Color blindness support" | features]
end
defp add_color_blind_feature(features, _), do: features
defp ensure_contrast_compliance(
foreground,
background,
current_ratio,
target_ratio
)
when current_ratio >= target_ratio do
{foreground, background}
end
defp ensure_contrast_compliance(
foreground,
background,
_current_ratio,
target_ratio
) do
adjust_colors_for_contrast(foreground, background, target_ratio)
end
defp adjust_colors_for_contrast_internal(
foreground,
background,
darker_bg,
target_ratio
) do
case calculate_contrast_ratio(foreground, darker_bg) >= target_ratio do
true ->
{foreground, darker_bg}
false ->
# Try lightening foreground
lighter_fg = lighten_color(foreground, 1.5)
case calculate_contrast_ratio(lighter_fg, background) >= target_ratio do
true ->
{lighter_fg, background}
false ->
# Use high contrast pair as last resort
{r, g, b} = background
luminance = relative_luminance(r, g, b)
case luminance > 0.5 do
true ->
# Dark on light
{{0, 0, 0}, background}
false ->
# Light on dark
{{255, 255, 255}, background}
end
end
end
end
end