defmodule Raxol.Core.Accessibility.Preferences do @moduledoc """ Manages accessibility preferences and settings. """ alias Raxol.Core.Events.EventManager, as: EventManager alias Raxol.Core.UserPreferences require Raxol.Core.Runtime.Log # Default UserPreferences name @default_prefs_name Raxol.Core.UserPreferences @doc """ Get the default preferences name. ## Examples iex> Preferences.default_prefs_name() Raxol.Core.UserPreferences """ def default_prefs_name, do: @default_prefs_name # Helper function to get preference key as a path list @spec pref_key(term()) :: list(atom()) defp pref_key(key), do: [:accessibility, key] # Helper to get preference using pid_or_name or default @spec get_pref(any(), any(), GenServer.server() | nil) :: any() defp get_pref(key, default, pid_or_name) do target_pid_or_name = pid_or_name || @default_prefs_name # Pass the list path directly value = UserPreferences.get(pref_key(key), target_pid_or_name) # Explicitly check for nil before applying default, to handle false values case is_nil(value) do true -> default false -> # If the value is a process name, return the default instead case value do pid_or_name when is_atom(pid_or_name) or is_pid(pid_or_name) -> default _ -> value end end end # Helper to set preference using pid_or_name or default @spec set_pref(atom(), any(), GenServer.server() | nil) :: :ok defp set_pref(key, value, pid_or_name) do target_pid_or_name = pid_or_name || @default_prefs_name :ok = UserPreferences.set(pref_key(key), value, target_pid_or_name) :ok end @doc """ Get an accessibility option value. ## Parameters * `option_name` - The atom representing the accessibility option (e.g., `:high_contrast`). * `user_preferences_pid_or_name` - The PID or registered name of the UserPreferences process to use (optional). * `default` - The default value to return if the option is not set (optional). ## Examples iex> Preferences.get_option(:high_contrast) false """ def get_option(key, user_preferences_pid_or_name \\ nil, default \\ nil) do case (Code.ensure_loaded?(Mix) and Mix.env()) || :prod do :test -> get_option_test(key, user_preferences_pid_or_name, default) _ -> get_pref(key, default, user_preferences_pid_or_name) end end @spec get_option_test(any(), GenServer.server() | nil, any()) :: any() defp get_option_test(key, user_preferences_pid_or_name, default) do target_pid_or_name = user_preferences_pid_or_name || @default_prefs_name case target_pid_or_name == self() do true -> default false -> value = UserPreferences.get(pref_key(key), target_pid_or_name) case value do nil -> default _ -> value end end end @doc """ Set an accessibility option value. ## Parameters * `key` - The option key to set * `value` - The value to set * `user_preferences_pid_or_name` - The PID or registered name of the UserPreferences process to use (optional). ## Examples iex> Preferences.set_option(:high_contrast, true) :ok """ def set_option(key, value, user_preferences_pid_or_name \\ nil) when is_atom(key) do # Use our existing functions for specific settings when available case key do # Pass the pid_or_name down to the specific setters :high_contrast -> set_high_contrast(value, user_preferences_pid_or_name) :reduced_motion -> set_reduced_motion(value, user_preferences_pid_or_name) :large_text -> set_large_text(value, user_preferences_pid_or_name) _ -> # For other settings, save directly to preferences using the pid_or_name set_pref(key, value, user_preferences_pid_or_name) end end @doc """ Enable or disable high contrast mode. ## Parameters * `enabled` - `true` to enable high contrast, `false` to disable. * `user_preferences_pid_or_name` - The PID or registered name of the UserPreferences process to use (optional). ## Examples iex> Preferences.set_high_contrast(true) :ok """ def set_high_contrast(enabled, user_preferences_pid_or_name \\ nil) when is_boolean(enabled) do target_pid_or_name = user_preferences_pid_or_name || @default_prefs_name set_pref(:high_contrast, enabled, target_pid_or_name) # Dispatch the preference_changed event key_path = pref_key(:high_contrast) :ok = EventManager.dispatch(:preference_changed, %{ key: key_path, value: enabled }) # Dispatch the event that ColorSystem is listening for :ok = EventManager.dispatch(:accessibility_high_contrast, %{enabled: enabled}) :ok end @doc """ Enable or disable reduced motion. ## Parameters * `enabled` - `true` to enable reduced motion, `false` to disable. * `user_preferences_pid_or_name` - The PID or registered name of the UserPreferences process to use (optional). ## Examples iex> Preferences.set_reduced_motion(true) :ok """ def set_reduced_motion(enabled, user_preferences_pid_or_name \\ nil) when is_boolean(enabled) do target_pid_or_name = user_preferences_pid_or_name || @default_prefs_name :ok = set_pref(:reduced_motion, enabled, target_pid_or_name) # Dispatch the preference_changed event key_path = pref_key(:reduced_motion) :ok = EventManager.dispatch(:preference_changed, %{ key: key_path, value: enabled }) # Trigger potential side effects using the correct format for handle_preference_changed :ok = handle_preference_changed( {key_path, enabled}, user_preferences_pid_or_name ) :ok end @doc """ Enable or disable large text mode. ## Parameters * `enabled` - `true` to enable large text, `false` to disable. * `user_preferences_pid_or_name` - The PID or registered name of the UserPreferences process to use (optional). ## Examples iex> Preferences.set_large_text(true) :ok """ def set_large_text(enabled, user_preferences_pid_or_name \\ nil) when is_boolean(enabled) do target_pid_or_name = user_preferences_pid_or_name || @default_prefs_name :ok = set_pref(:large_text, enabled, target_pid_or_name) # Dispatch the preference_changed event key_path = pref_key(:large_text) :ok = EventManager.dispatch(:preference_changed, %{ key: key_path, value: enabled }) # Trigger potential side effects using the correct format for handle_preference_changed :ok = handle_preference_changed( {key_path, enabled}, user_preferences_pid_or_name ) # Send text scale updated event scale = case enabled do true -> 1.5 false -> 1.0 end send(self(), {:text_scale_updated, user_preferences_pid_or_name, scale}) :ok end @doc """ Get the current text scale factor based on the large text setting. ## Parameters * `user_preferences_pid_or_name` - The PID or registered name of the UserPreferences process to use (optional). ## Examples iex> Preferences.get_text_scale() 1.0 # or 1.5 if large_text is enabled """ def get_text_scale(user_preferences_pid_or_name \\ nil) do # Calculate based on the :large_text preference directly # Explicitly handle test environment to ensure consistent behavior case (Code.ensure_loaded?(Mix) and Mix.env()) || :prod do :test -> target_pid_or_name = user_preferences_pid_or_name || @default_prefs_name case UserPreferences.get(pref_key(:large_text), target_pid_or_name) do # Always return 1.5 when explicitly true true -> 1.5 # Default to 1.0 for any other value _ -> 1.0 end _ -> large_text_enabled = get_option(:large_text, user_preferences_pid_or_name) case large_text_enabled do true -> 1.5 _ -> 1.0 end end end # --- Private Functions --- def handle_preference_changed( {key_path, value}, user_preferences_pid_or_name ) do # When a preference changes, dispatch a general event and specific events # This allows different parts of the application to react to specific changes # e.g., the color system reacting to high_contrast changes # Note: We don't dispatch :preference_changed events here to avoid infinite loops # since this function is also a handler for :preference_changed events case key_path do [:accessibility, :high_contrast] -> :ok = EventManager.dispatch({:accessibility_high_contrast, value}) :ok [:accessibility, :reduced_motion] -> # Example of dispatching an event for reduced motion :ok = EventManager.dispatch({:accessibility_reduced_motion, value}) :ok [:accessibility, :large_text] -> # Example of dispatching an event for large text scale = case value do true -> 1.5 false -> 1.0 end :ok = EventManager.dispatch({:text_scale_updated, user_preferences_pid_or_name, scale}) :ok _ -> :ok end end end