Plushie.Subscription (Plushie v0.6.0)

Copy Markdown View Source

Declarative subscription specifications for Plushie apps.

Subscriptions are ongoing event sources. Return them from subscribe/1 and the runtime manages their lifecycle automatically -- starting new subscriptions and stopping removed ones by diffing the list each cycle.

Tag semantics -- two different roles

The event_tag parameter means different things depending on the subscription type. Understanding this distinction is essential.

Timer subscriptions (:every)

For every/2, the tag becomes part of the Timer struct. Your update/2 receives %Plushie.Event.TimerEvent{tag: tag, timestamp: timestamp}.

Plushie.Subscription.every(1000, :tick)
# update/2 receives: %Plushie.Event.TimerEvent{tag: :tick, timestamp: 1234567890}

Renderer subscriptions (all others)

For renderer subscriptions (on_key_press, on_key_release, on_window_close, etc.), the tag is management-only. It is sent to the renderer to register/unregister the listener, and it is used by the runtime to diff subscription lists. The tag does NOT appear in the event tuple delivered to update/2. Events arrive as fixed tuples documented on each constructor.

Plushie.Subscription.on_key_press(:my_keys)
# update/2 receives: %Plushie.Event.KeyEvent{type: :press, ...} -- NOT {:my_keys, ...}

Plushie.Subscription.on_window_resize(:win_resize)
# update/2 receives: %Plushie.Event.WindowEvent{type: :resized, ...}

Rate limiting

Renderer subscriptions accept a :max_rate option that tells the renderer to coalesce events beyond the given rate (events per second). This reduces wire traffic and host CPU usage for high-frequency events.

# Rate-limit mouse moves to 30 events per second:
Subscription.on_pointer_move(:mouse, max_rate: 30)

# Animation frames at 60fps (matches display refresh):
Subscription.on_animation_frame(:frame, max_rate: 60)

# Subscribe but never emit (capture tracking only):
Subscription.on_pointer_move(:mouse, max_rate: 0)

The rate can also be set via the max_rate/2 setter for pipeline style:

Subscription.on_pointer_move(:mouse) |> Subscription.max_rate(30)

Timer subscriptions (every/2) do not support max_rate -- they are host-side timers, not renderer events.

Example

def subscribe(model) do
  subs = []
  if model.timer_running do
    subs = [Plushie.Subscription.every(1000, :tick) | subs]
  end
  subs
end

def update(model, %Plushie.Event.TimerEvent{tag: :tick}) do
  # Timer events are Timer structs with tag and timestamp fields.
  %{model | ticks: model.ticks + 1}
end

def update(model, %Plushie.Event.KeyEvent{type: :press, key: :escape}) do
  # Renderer subscription tag is NOT in the event -- match on struct type.
  %{model | menu_open: false}
end

Summary

Types

t()

A subscription specification. Every subscription has a :type atom identifying the kind (:every, :on_key_press, etc.) and a :tag atom used for subscription management. For timer subscriptions, the tag is also part of the Timer event struct in update/2 (e.g. %Plushie.Event.TimerEvent{tag: tag, timestamp: timestamp}). For renderer subscriptions (keyboard, window, pointer, etc.), the tag is sent to the renderer to register/unregister the listener but is not included in the event struct -- those use typed event structs like %Plushie.Event.KeyEvent{}, %Plushie.Event.WindowEvent{}, or %Plushie.Event.WidgetEvent{} (for pointer subscriptions).

Functions

Combines a list of subscriptions. Validates that all elements are %Subscription{} structs and returns the list.

Timer that fires every interval_ms milliseconds.

Scope a list of subscriptions to a specific window.

Returns a key that uniquely identifies this subscription spec. Two specs with the same key are considered the same subscription.

Transforms the tag of a subscription spec.

Sets the maximum event rate (events per second) for a renderer subscription.

Fires on each animation frame (vsync tick).

Fires on any renderer event (catch-all).

Fires when a file is dropped on a window.

Fires on IME (Input Method Editor) events.

Fires on key press events from the renderer.

Fires on key release events from the renderer.

Fires when keyboard modifier state changes (shift, ctrl, alt, etc.).

Fires on pointer button press/release (mouse or touch).

Fires on pointer movement (mouse or touch).

Fires on pointer scroll events.

Fires when the system theme changes (light/dark mode).

Fires when a window close is requested (e.g. user clicks the close button).

Fires on general window events (resize, move, focus, etc.).

Fires when a window gains focus.

Fires when a window is moved.

Fires when a new window is opened.

Fires when a window is resized.

Fires when a window loses focus.

Types

t()

@type t() :: %Plushie.Subscription{
  interval: pos_integer() | nil,
  max_rate: pos_integer() | nil,
  tag: atom(),
  type: atom(),
  window_id: String.t() | nil
}

A subscription specification. Every subscription has a :type atom identifying the kind (:every, :on_key_press, etc.) and a :tag atom used for subscription management. For timer subscriptions, the tag is also part of the Timer event struct in update/2 (e.g. %Plushie.Event.TimerEvent{tag: tag, timestamp: timestamp}). For renderer subscriptions (keyboard, window, pointer, etc.), the tag is sent to the renderer to register/unregister the listener but is not included in the event struct -- those use typed event structs like %Plushie.Event.KeyEvent{}, %Plushie.Event.WindowEvent{}, or %Plushie.Event.WidgetEvent{} (for pointer subscriptions).

Functions

batch(subscriptions)

@spec batch(subscriptions :: [t()]) :: [t()]

Combines a list of subscriptions. Validates that all elements are %Subscription{} structs and returns the list.

every(interval_ms, event_tag)

@spec every(interval_ms :: pos_integer(), event_tag :: atom()) :: t()

Timer that fires every interval_ms milliseconds.

The tag becomes part of the Timer event struct -- update/2 receives %Plushie.Event.TimerEvent{tag: event_tag, timestamp: timestamp} where timestamp is System.monotonic_time(:millisecond).

Example

Plushie.Subscription.every(1000, :tick)

# In update/2:
def update(model, %Plushie.Event.TimerEvent{tag: :tick}), do: %{model | count: model.count + 1}

for_window(window_id, subscriptions)

@spec for_window(window_id :: String.t(), subscriptions :: [t()]) :: [t()]

Scope a list of subscriptions to a specific window.

Window-scoped subscriptions tell the renderer to only deliver events from the given window. Without a window scope, subscriptions receive events from all windows.

Subscription.for_window("editor", [
  Subscription.on_key_press(:editor_keys),
  Subscription.on_pointer_move(:editor_mouse, max_rate: 60)
])

key(subscription)

@spec key(sub :: t()) :: {:every, pos_integer(), atom()} | {atom(), atom()}

Returns a key that uniquely identifies this subscription spec. Two specs with the same key are considered the same subscription.

map_tag(sub, mapper)

@spec map_tag(sub :: t(), mapper :: (term() -> term())) :: t()

Transforms the tag of a subscription spec.

Used by the runtime to namespace stateful widget subscription tags so timer events can be routed back to the correct widget.

max_rate(sub, rate)

@spec max_rate(sub :: t(), rate :: non_neg_integer()) :: t()

Sets the maximum event rate (events per second) for a renderer subscription.

The renderer coalesces events beyond this rate, delivering at most rate events per second. A rate of 0 means "subscribe but never emit" -- the subscription is active (affects capture tracking) but no events are sent.

Timer subscriptions (:every) do not support max_rate (they are host-side timers, not renderer events).

Examples

# Rate-limit mouse moves to 30 events per second:
Subscription.on_pointer_move(:mouse) |> Subscription.max_rate(30)

# Animation frames at 60fps:
Subscription.on_animation_frame(:frame, max_rate: 60)

on_animation_frame(event_tag, opts \\ [])

@spec on_animation_frame(event_tag :: atom(), opts :: keyword()) :: t()

Fires on each animation frame (vsync tick).

Delivers %SystemEvent{type: :animation_frame, data: timestamp} to update/2. The event_tag is for subscription management only.

on_event(event_tag, opts \\ [])

@spec on_event(event_tag :: atom(), opts :: keyword()) :: t()

Fires on any renderer event (catch-all).

Use this to receive all event types that the renderer emits. The event struct type varies by event family. The event_tag is for subscription management only.

on_file_drop(event_tag, opts \\ [])

@spec on_file_drop(event_tag :: atom(), opts :: keyword()) :: t()

Fires when a file is dropped on a window.

Delivers %WindowEvent{type: :file_dropped, window_id: id, path: path} to update/2. Also fires %WindowEvent{type: :file_hovered, ...} while hovering and %WindowEvent{type: :files_hovered_left, ...} when the hover exits. The event_tag is for subscription management only.

on_ime(event_tag, opts \\ [])

@spec on_ime(event_tag :: atom(), opts :: keyword()) :: t()

Fires on IME (Input Method Editor) events.

Delivers one of:

  • %ImeEvent{type: :opened, captured: bool} -- the IME session started
  • %ImeEvent{type: :preedit, text: str, cursor: {start, end_pos} | nil, captured: bool}

  • %ImeEvent{type: :commit, text: str, captured: bool} -- final text committed
  • %ImeEvent{type: :closed, captured: bool} -- the IME session ended

The event_tag is for subscription management only.

on_key_press(event_tag, opts \\ [])

@spec on_key_press(event_tag :: atom(), opts :: keyword()) :: t()

Fires on key press events from the renderer.

Delivers %Plushie.Event.KeyEvent{type: :press, ...} to update/2. The event_tag is used only for subscription management (registration and diffing). It does NOT appear in the event struct.

See Plushie.Event.KeyEvent and Plushie.KeyModifiers for struct definitions.

Example

Plushie.Subscription.on_key_press(:my_keys)

# In update/2 -- match on the struct, NOT the tag:
def update(model, %Plushie.Event.KeyEvent{type: :press, key: :enter}), do: ...

on_key_release(event_tag, opts \\ [])

@spec on_key_release(event_tag :: atom(), opts :: keyword()) :: t()

Fires on key release events from the renderer.

Delivers %Plushie.Event.KeyEvent{type: :release, ...} to update/2. Same format as on_key_press/1. The event_tag is used for subscription management only -- it does NOT appear in the event struct.

Example

Plushie.Subscription.on_key_release(:keys)

# In update/2:
def update(model, %Plushie.Event.KeyEvent{type: :release, key: key}), do: ...

on_modifiers_changed(event_tag, opts \\ [])

@spec on_modifiers_changed(event_tag :: atom(), opts :: keyword()) :: t()

Fires when keyboard modifier state changes (shift, ctrl, alt, etc.).

Delivers %Plushie.Event.ModifiersEvent{modifiers: %KeyModifiers{}, captured: bool} to update/2. The event_tag is for subscription management only.

Example

Plushie.Subscription.on_modifiers_changed(:mods)

def update(model, %Plushie.Event.ModifiersEvent{modifiers: %{shift: true}}), do: ...

on_pointer_button(event_tag, opts \\ [])

@spec on_pointer_button(event_tag :: atom(), opts :: keyword()) :: t()

Fires on pointer button press/release (mouse or touch).

Delivers %WidgetEvent{type: :press, id: window_id, scope: [], ...} or %WidgetEvent{type: :release, ...} to update/2. The data map includes button (:left, :right, :middle), pointer, and modifiers. The event_tag is for subscription management only.

on_pointer_move(event_tag, opts \\ [])

@spec on_pointer_move(event_tag :: atom(), opts :: keyword()) :: t()

Fires on pointer movement (mouse or touch).

Delivers %WidgetEvent{type: :move, id: window_id, scope: [], ...} to update/2. The data map includes pointer: :mouse, x, y, and modifiers. Also delivers :enter and :exit events for cursor enter/leave. The event_tag is for subscription management only.

on_pointer_scroll(event_tag, opts \\ [])

@spec on_pointer_scroll(event_tag :: atom(), opts :: keyword()) :: t()

Fires on pointer scroll events.

Delivers %WidgetEvent{type: :scroll, id: window_id, scope: [], ...} to update/2. The data map includes delta_x, delta_y, unit (:line or :pixel), pointer, and modifiers. The event_tag is for subscription management only.

on_pointer_touch(event_tag, opts \\ [])

@spec on_pointer_touch(event_tag :: atom(), opts :: keyword()) :: t()

Fires on touch events.

Delivers %WidgetEvent{type: :press, id: window_id, scope: [], ...}, %WidgetEvent{type: :move, ...}, or %WidgetEvent{type: :release, ...} to update/2. The data map includes pointer: :touch, finger, x, y. Touch :release events from a lost finger include lost: true in the data. The event_tag is for subscription management only.

on_theme_change(event_tag, opts \\ [])

@spec on_theme_change(event_tag :: atom(), opts :: keyword()) :: t()

Fires when the system theme changes (light/dark mode).

Delivers %SystemEvent{type: :theme_changed, data: mode} to update/2 where mode is a string like "light" or "dark". The event_tag is for subscription management only.

on_window_close(event_tag, opts \\ [])

@spec on_window_close(event_tag :: atom(), opts :: keyword()) :: t()

Fires when a window close is requested (e.g. user clicks the close button).

Delivers %Plushie.Event.WindowEvent{type: :close_requested, window_id: id} to update/2. The event_tag is for subscription management only.

Example

Plushie.Subscription.on_window_close(:win_close)

# In update/2:
def update(model, %Plushie.Event.WindowEvent{type: :close_requested, window_id: wid}), do: ...

on_window_event(event_tag, opts \\ [])

@spec on_window_event(event_tag :: atom(), opts :: keyword()) :: t()

Fires on general window events (resize, move, focus, etc.).

Delivers %Plushie.Event.WindowEvent{} structs depending on the event. The event_tag is for subscription management only.

Note: If both on_window_event and a specific subscription (e.g. on_window_resize) are registered, matching events will be delivered twice -- once from each subscription. Use either the aggregate or specific subscriptions, not both.

on_window_focus(event_tag, opts \\ [])

@spec on_window_focus(event_tag :: atom(), opts :: keyword()) :: t()

Fires when a window gains focus.

Delivers %Plushie.Event.WindowEvent{type: :focused, window_id: id} to update/2. The event_tag is for subscription management only.

on_window_move(event_tag, opts \\ [])

@spec on_window_move(event_tag :: atom(), opts :: keyword()) :: t()

Fires when a window is moved.

Delivers %Plushie.Event.WindowEvent{type: :moved, window_id: id, x: x, y: y} to update/2. The event_tag is for subscription management only.

on_window_open(event_tag, opts \\ [])

@spec on_window_open(event_tag :: atom(), opts :: keyword()) :: t()

Fires when a new window is opened.

Delivers %Plushie.Event.WindowEvent{type: :opened, window_id: id, ...} to update/2. The event_tag is for subscription management only.

on_window_resize(event_tag, opts \\ [])

@spec on_window_resize(event_tag :: atom(), opts :: keyword()) :: t()

Fires when a window is resized.

Delivers %Plushie.Event.WindowEvent{type: :resized, window_id: id, width: w, height: h} to update/2. The event_tag is for subscription management only.

on_window_unfocus(event_tag, opts \\ [])

@spec on_window_unfocus(event_tag :: atom(), opts :: keyword()) :: t()

Fires when a window loses focus.

Delivers %Plushie.Event.WindowEvent{type: :unfocused, window_id: id} to update/2. The event_tag is for subscription management only.