defmodule Raxol.Core.Events.Subscription do @moduledoc """ Provides helpers for managing event subscriptions. This module makes it easy to: * Subscribe to specific event types * Filter events based on criteria * Handle event cleanup * Manage multiple subscriptions """ alias Raxol.Core.Events.EventManager, as: Manager @type subscription_ref :: reference() @type subscription_opts :: keyword() @doc """ Subscribes to keyboard events with optional key filters. ## Options * `:keys` - List of specific keys to match * `:exclude_keys` - List of keys to ignore ## Example subscribe_keyboard(keys: [:enter, :esc]) subscribe_keyboard(exclude_keys: [:tab]) """ def subscribe_keyboard(opts \\ []) do keys = Keyword.get(opts, :keys) _exclude_keys = Keyword.get(opts, :exclude_keys, []) filter_opts = build_key_filter(keys) Manager.subscribe([:key], filter_opts) end @spec build_key_filter(any()) :: any() defp build_key_filter(nil), do: [] @spec build_key_filter(any()) :: any() defp build_key_filter(keys), do: [key: keys] @doc """ Subscribes to mouse events with optional button and position filters. ## Options * `:buttons` - List of mouse buttons to match * `:drag_only` - Only match drag events * `:click_only` - Only match click events * `:area` - Tuple of {x, y, width, height} to match position ## Example subscribe_mouse(buttons: [:left, :right]) subscribe_mouse(area: {0, 0, 10, 10}) """ def subscribe_mouse(opts \\ []) do buttons = Keyword.get(opts, :buttons) drag_only = Keyword.get(opts, :drag_only, false) click_only = Keyword.get(opts, :click_only, false) area = Keyword.get(opts, :area) filter_opts = [] |> add_filter(:mouse_button, buttons) |> add_filter(:drag, if(drag_only, do: true)) |> add_filter(:click, if(click_only, do: true)) |> add_filter(:area, area) Manager.subscribe([:mouse], filter_opts) end @doc """ Subscribes to window events with optional action filters. ## Options * `:actions` - List of window actions to match (:resize, :focus, :blur) ## Example subscribe_window(actions: [:resize]) """ def subscribe_window(opts \\ []) do actions = Keyword.get(opts, :actions) filter_opts = build_window_filter(actions) Manager.subscribe([:window], filter_opts) end @spec build_window_filter(any()) :: any() defp build_window_filter(nil), do: [] @spec build_window_filter(any()) :: any() defp build_window_filter(actions), do: [window_action: actions] @doc """ Subscribes to timer events with optional data matching. ## Options * `:match` - Pattern to match against timer data ## Example subscribe_timer(match: :tick) """ def subscribe_timer(opts \\ []) do match = Keyword.get(opts, :match) filter_opts = build_match_filter(match) Manager.subscribe([:timer], filter_opts) end @spec build_match_filter(any()) :: any() defp build_match_filter(nil), do: [] @spec build_match_filter(any()) :: any() defp build_match_filter(match), do: [data: match] @doc """ Subscribes to custom events with data matching. ## Options * `:match` - Pattern to match against custom event data ## Example subscribe_custom(match: {:user_action, _}) """ def subscribe_custom(opts \\ []) do match = Keyword.get(opts, :match) filter_opts = build_match_filter(match) Manager.subscribe([:custom], filter_opts) end @doc """ Unsubscribes from events using the subscription reference. """ def unsubscribe(ref) when is_reference(ref) do Manager.unsubscribe(ref) end @doc """ Unsubscribes from multiple subscriptions. """ def unsubscribe_all(refs) when is_list(refs) do Enum.each(refs, &unsubscribe/1) end @doc """ Subscribes to a list of event types. ## Parameters - `event_types` - A list of event type atoms (e.g., `[:key, :mouse, :window]`). Can also include tuples like `{:key, opts}` or `{:mouse, opts}` to pass specific options to the individual subscription functions. ## Returns - `{:ok, list(subscription_ref())}` on success, containing a list of references for each subscription. - `{:error, reason}` if any subscription fails. Successfully created subscriptions before the failure will be automatically unsubscribed. """ def events(event_types) when is_list(event_types) do do_subscribe_events(event_types, []) end # Helper function to recursively subscribe and handle cleanup on error @spec do_subscribe_events(any(), reference()) :: any() defp do_subscribe_events([], successful_refs) do {:ok, Enum.reverse(successful_refs)} end @spec do_subscribe_events(any(), reference()) :: any() defp do_subscribe_events([event_spec | rest], successful_refs) do case subscribe_single_event(event_spec) do {:ok, ref} -> do_subscribe_events(rest, [ref | successful_refs]) {:error, _reason} = error -> # Unsubscribe already successful subscriptions unsubscribe_all(successful_refs) error end end # Helper to call the correct specific subscribe function @spec subscribe_single_event(any()) :: any() defp subscribe_single_event(:key), do: subscribe_keyboard() @spec subscribe_single_event(any()) :: any() defp subscribe_single_event({:key, opts}), do: subscribe_keyboard(opts) @spec subscribe_single_event(any()) :: any() defp subscribe_single_event(:mouse), do: subscribe_mouse() @spec subscribe_single_event(any()) :: any() defp subscribe_single_event({:mouse, opts}), do: subscribe_mouse(opts) @spec subscribe_single_event(any()) :: any() defp subscribe_single_event(:window), do: subscribe_window() @spec subscribe_single_event(any()) :: any() defp subscribe_single_event({:window, opts}), do: subscribe_window(opts) @spec subscribe_single_event(any()) :: any() defp subscribe_single_event(:timer), do: subscribe_timer() @spec subscribe_single_event(any()) :: any() defp subscribe_single_event({:timer, opts}), do: subscribe_timer(opts) @spec subscribe_single_event(any()) :: any() defp subscribe_single_event(:custom), do: subscribe_custom() @spec subscribe_single_event(any()) :: any() defp subscribe_single_event({:custom, opts}), do: subscribe_custom(opts) @spec subscribe_single_event(any()) :: any() defp subscribe_single_event(other), do: {:error, {:invalid_event_type, other}} # Private Helpers @spec add_filter(keyword(), any(), any()) :: any() defp add_filter(opts, _key, nil), do: opts @spec add_filter(keyword(), any(), any()) :: any() defp add_filter(opts, key, value) do Keyword.put(opts, key, value) end end