Plushie.Subscription (Plushie v0.7.2)

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.

Timer subscriptions

Timer subscriptions carry a tag that becomes part of the event struct. Your update/2 receives %Plushie.Event.TimerEvent{tag: tag, timestamp: ts}.

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

Renderer subscriptions

Renderer subscriptions (on_key_press, on_pointer_move, etc.) take no tag. Events arrive as typed structs (%KeyEvent{}, %WindowEvent{}, etc.) and are matched by struct type.

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

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

Renderer subs are keyed by {kind, window_id} for lifecycle diffing. Only one subscription of each kind per window (or globally when no window is specified).

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(max_rate: 30)

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

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

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

Subscription.on_pointer_move() |> 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 = [Plushie.Subscription.on_key_press()]

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

def update(model, %Plushie.Event.TimerEvent{tag: :tick}) do
  %{model | ticks: model.ticks + 1}
end

def update(model, %Plushie.Event.KeyEvent{type: :press, key: :escape}) do
  %{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 field. For timer subscriptions, the tag is the user-provided atom that appears in %Plushie.Event.TimerEvent{tag: tag}. For renderer subscriptions, the tag is nil (management is by {kind, window_id}).

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 on touch 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: non_neg_integer() | nil,
  tag: atom() | nil,
  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 field. For timer subscriptions, the tag is the user-provided atom that appears in %Plushie.Event.TimerEvent{tag: tag}. For renderer subscriptions, the tag is nil (management is by {kind, window_id}).

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(),
  Subscription.on_pointer_move(max_rate: 60)
])

key(subscription)

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

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

Timer subscriptions are keyed by {:every, interval, tag}. Renderer subscriptions are keyed by {type, window_id}.

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() |> Subscription.max_rate(30)

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

on_animation_frame(opts \\ [])

@spec on_animation_frame(opts :: keyword()) :: t()

Fires on each animation frame (vsync tick).

Delivers %SystemEvent{type: :animation_frame, value: timestamp} to update/2.

on_event(opts \\ [])

@spec on_event(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.

on_file_drop(opts \\ [])

@spec on_file_drop(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.

on_ime(opts \\ [])

@spec on_ime(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

on_key_press(opts \\ [])

@spec on_key_press(opts :: keyword()) :: t()

Fires on key press events from the renderer.

Delivers %Plushie.Event.KeyEvent{type: :press, ...} to update/2.

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

Example

Plushie.Subscription.on_key_press()

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

on_key_release(opts \\ [])

@spec on_key_release(opts :: keyword()) :: t()

Fires on key release events from the renderer.

Delivers %Plushie.Event.KeyEvent{type: :release, ...} to update/2.

Example

Plushie.Subscription.on_key_release()

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

on_modifiers_changed(opts \\ [])

@spec on_modifiers_changed(opts :: keyword()) :: t()

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

Delivers %Plushie.Event.ModifiersEvent{modifiers: %KeyModifiers{}, captured: bool} to update/2.

Example

Plushie.Subscription.on_modifiers_changed()

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

on_pointer_button(opts \\ [])

@spec on_pointer_button(opts :: keyword()) :: t()

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

Delivers %Plushie.Event.WidgetEvent{type: :press, ...} or %Plushie.Event.WidgetEvent{type: :release, ...} to update/2. The value map includes button, pointer, x, y, and modifiers.

on_pointer_move(opts \\ [])

@spec on_pointer_move(opts :: keyword()) :: t()

Fires on pointer movement (mouse or touch).

Delivers %Plushie.Event.WidgetEvent{type: :move, ...} to update/2. The value map includes pointer, x, y, and modifiers. Also delivers :enter and :exit events for cursor enter/leave.

on_pointer_scroll(opts \\ [])

@spec on_pointer_scroll(opts :: keyword()) :: t()

Fires on pointer scroll events.

Delivers %Plushie.Event.WidgetEvent{type: :scroll, ...} to update/2. The value map includes delta_x, delta_y, x, y, pointer, and modifiers.

on_pointer_touch(opts \\ [])

@spec on_pointer_touch(opts :: keyword()) :: t()

Fires on touch events.

Delivers %Plushie.Event.WidgetEvent{type: :press, ...}, %Plushie.Event.WidgetEvent{type: :move, ...}, or %Plushie.Event.WidgetEvent{type: :release, ...} to update/2. The value map includes pointer: :touch, finger, x, y.

on_theme_change(opts \\ [])

@spec on_theme_change(opts :: keyword()) :: t()

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

Delivers %SystemEvent{type: :theme_changed, value: mode} to update/2 where mode is a string like "light" or "dark".

on_window_close(opts \\ [])

@spec on_window_close(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.

Example

Plushie.Subscription.on_window_close()

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

on_window_event(opts \\ [])

@spec on_window_event(opts :: keyword()) :: t()

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

Delivers %Plushie.Event.WindowEvent{} structs depending on the event.

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(opts \\ [])

@spec on_window_focus(opts :: keyword()) :: t()

Fires when a window gains focus.

Delivers %Plushie.Event.WindowEvent{type: :focused, window_id: id} to update/2.

on_window_move(opts \\ [])

@spec on_window_move(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.

on_window_open(opts \\ [])

@spec on_window_open(opts :: keyword()) :: t()

Fires when a new window is opened.

Delivers %Plushie.Event.WindowEvent{type: :opened, window_id: id, ...} to update/2.

on_window_resize(opts \\ [])

@spec on_window_resize(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.

on_window_unfocus(opts \\ [])

@spec on_window_unfocus(opts :: keyword()) :: t()

Fires when a window loses focus.

Delivers %Plushie.Event.WindowEvent{type: :unfocused, window_id: id} to update/2.