Search documentation of Plushie

# 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

batch(subscriptions)

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

every(interval_ms, event_tag)

Timer that fires every `interval_ms` milliseconds.

for_window(window_id, subscriptions)

Scope a list of subscriptions to a specific window.

key(subscription)

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

map_tag(sub, mapper)

Transforms the tag of a subscription spec.

max_rate(sub, rate)

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

on_animation_frame(opts \\ [])

Fires on each animation frame (vsync tick).

on_event(opts \\ [])

Fires on any renderer event (catch-all).

on_file_drop(opts \\ [])

Fires when a file is dropped on a window.

on_ime(opts \\ [])

Fires on IME (Input Method Editor) events.

on_key_press(opts \\ [])

Fires on key press events from the renderer.

on_key_release(opts \\ [])

Fires on key release events from the renderer.

on_modifiers_changed(opts \\ [])

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

on_pointer_button(opts \\ [])

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

on_pointer_move(opts \\ [])

Fires on pointer movement (mouse or touch).

on_pointer_scroll(opts \\ [])

Fires on pointer scroll events.

on_pointer_touch(opts \\ [])

Fires on touch events.

on_theme_change(opts \\ [])

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

on_window_close(opts \\ [])

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

on_window_event(opts \\ [])

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

on_window_focus(opts \\ [])

Fires when a window gains focus.

on_window_move(opts \\ [])

Fires when a window is moved.

on_window_open(opts \\ [])

Fires when a new window is opened.

on_window_resize(opts \\ [])

Fires when a window is resized.

on_window_unfocus(opts \\ [])

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`.