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
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
@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
Combines a list of subscriptions. Validates that all elements are
%Subscription{} structs and returns the list.
@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}
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)
])
@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}.
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.
@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)
Fires on each animation frame (vsync tick).
Delivers %SystemEvent{type: :animation_frame, value: timestamp} to update/2.
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.
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.
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
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: ...
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: ...
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: ...
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.
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.
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.
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.
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".
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: ...
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.
Fires when a window gains focus.
Delivers %Plushie.Event.WindowEvent{type: :focused, window_id: id} to update/2.
Fires when a window is moved.
Delivers %Plushie.Event.WindowEvent{type: :moved, window_id: id, x: x, y: y} to update/2.
Fires when a new window is opened.
Delivers %Plushie.Event.WindowEvent{type: :opened, window_id: id, ...} to
update/2.
Fires when a window is resized.
Delivers %Plushie.Event.WindowEvent{type: :resized, window_id: id, width: w, height: h} to update/2.
Fires when a window loses focus.
Delivers %Plushie.Event.WindowEvent{type: :unfocused, window_id: id} to update/2.