All user interactions, system responses, and asynchronous results are delivered to your Plushie.App.update/2 callback as typed structs under the Plushie.Event namespace.

This page is a comprehensive reference. For a gentler introduction, see the Events guide.

Event union type

Plushie.Event.t/0 is a union of every event struct the runtime can produce. Plushie.Event.delivered_t/0 is the narrower subset that guarantees fields like window_id are non-nil (events that came from the renderer rather than being hand-constructed in tests).

Use the union type when writing generic event-handling helpers. For update/2 clauses, pattern-match on the concrete struct directly.

Event taxonomy

CategoryStructSource
Widget interactionPlushie.Event.WidgetEventRenderer (widget callbacks)
KeyboardPlushie.Event.KeyEventSubscription (key press/release)
Modifier statePlushie.Event.ModifiersEventSubscription (modifier change)
Pointer (mouse)Plushie.Event.WidgetEventSubscription (global pointer)
Pointer (touch)Plushie.Event.WidgetEventSubscription (touchscreen)
IMEPlushie.Event.ImeEventSubscription (input method editor)
Window lifecyclePlushie.Event.WindowEventRenderer (open, close, resize, etc.)
SystemPlushie.Event.SystemEventRenderer (queries, theme, diagnostics)
TimerPlushie.Event.TimerEventSubscription (every/2)
Async resultPlushie.Event.AsyncEventCommand (async/2)
Stream valuePlushie.Event.StreamEventCommand (stream/2)
Effect responsePlushie.Event.EffectEventRenderer (file dialogs, clipboard, etc.)
Widget cmd errorPlushie.Event.WidgetCommandErrorRenderer (native widget command failure)

WidgetEvent built-in types

Every built-in :type atom, its payload carrier, and a short description. Carrier indicates where data lands in the Plushie.Event.WidgetEvent struct: none (no payload), value (value field), or data (data field as an atom-keyed map).

Standard widget events

TypeCarrierDescription
:clicknoneButton pressed
:inputvalue (string)Text input changed
:submitvalue (string)Text input submitted (Enter)
:togglevalue (boolean)Toggler/checkbox toggled
:selectvalue (any)Pick list / combo box selection
:slidevalue (number)Slider moved
:slide_releasevalue (number)Slider released at final value
:pastevalue (string)Paste action on a text input
:opennoneExpandable opened
:closenoneExpandable closed
:option_hoveredvalue (any)Pick list option hovered
:key_bindingdataKey binding activated (empty data map)
:sortdataTable column sort requested (column)
:scrolleddataScrollable viewport offset changed (absolute_x, absolute_y, relative_x, relative_y, bounds, content_bounds)
:pane_focus_cyclenonePane focus cycle requested
:transition_completevalue (any)Emitted when a renderer-side transition completes (requires on_complete: tag)

Pointer events

Unified pointer events for canvas-level, pointer area, and sensor interactions. These replace the previous canvas_*, mouse_*, and sensor_* families with a device-agnostic model. The pointer field identifies the input device (:mouse, :touch, :pen) and button identifies which button was involved.

TypeCarrierFields
:pressdatax, y, button, pointer, finger, modifiers
:releasedatax, y, button, pointer, finger, modifiers
:movedatax, y, pointer, finger, modifiers
:scrolldatax, y, delta_x, delta_y, pointer, modifiers
:enternone
:exitnone
:double_clickdatax, y, pointer, modifiers
:resizedatawidth, height

The button field is one of :left, :right, :middle, :back, :forward. The pointer field is one of :mouse, :touch, :pen. The finger field is an integer for touch events, nil otherwise. modifiers is a Plushie.KeyModifiers struct.

Note: :scroll is pointer input (wheel delta at coordinates). :scrolled (in the standard widget events table above) is container state: a scrollable widget reporting its viewport offset changed.

Generic element events

Focus, blur, drag, and key events emitted by interactive elements (canvas groups, widgets, etc.). Canvas element clicks are regular :click events with the canvas ID in scope. See the Canvas reference for details.

TypeCarrierFields
:focusednone
:blurrednone
:dragdatax, y, delta_x, delta_y
:drag_enddatax, y
:key_pressdatakey, modifiers, text
:key_releasedatakey, modifiers

Pane grid events

TypeCarrierFields
:pane_resizeddatasplit, ratio
:pane_draggeddatapane, target, action, region, edge
:pane_clickeddatapane

Custom widget event types

Custom widgets declare events with the event macro. Their :type field uses a {widget_type, event_name} tuple instead of a bare atom:

%WidgetEvent{type: {:color_picker, :change}, id: "picker", data: %{hue: 180}}

The carrier (value vs. data) and field types are defined by the widget's event declaration. See Plushie.Widget for details.

Struct reference

Plushie.Event.WidgetEvent

The workhorse event struct. Covers all widget interactions: buttons, inputs, sliders, canvas, pointer areas, sensors, panes, and custom widgets.

FieldTypeDescription
typeatom | {atom, atom}Event family (see tables above)
idString.t()Widget ID
scope[String.t()]Ancestor scope chain (nearest first, window last)
valueterm() | nilScalar payload
datamap() | nilStructured payload (atom keys)
window_idString.t() | nilSource window

Plushie.Event.KeyEvent

Keyboard press and release events from subscriptions.

FieldTypeDescription
type:press | :releaseKey action
keyatom() | String.t()Logical key
modified_keyatom() | String.t() | nilKey with modifiers applied
physical_keyatom() | String.t() | nilPhysical scan code
location:standard | :left | :right | :numpadKey location
modifiersPlushie.KeyModifiers.t()Modifier state
textString.t() | nilText produced by key
repeatboolean()Whether this is a repeat event
capturedboolean()Whether a subscription captured
window_idString.t() | nilSource window

KeyModifiers

The modifiers field is a Plushie.KeyModifiers struct with boolean fields:

FieldPurpose
ctrlControl key
shiftShift key
altAlt key (Option on macOS)
logoLogo/Super key (Windows key, Command on macOS)
commandPlatform-aware: Ctrl on Linux/Windows, Cmd on macOS

command is the one to use for cross-platform shortcuts. Match on command: true and it works on all platforms.

Helper functions ctrl?/1, shift?/1, alt?/1, logo?/1, command?/1 are available for readable conditionals.

Plushie.Event.ModifiersEvent

Modifier state change event. Fires when the set of held modifiers changes.

FieldTypeDescription
modifiersPlushie.KeyModifiers.t()Current modifier state
capturedboolean()Subscription captured
window_idString.t() | nilSource window

Subscription pointer events

Global mouse and touch subscription events are delivered as Plushie.Event.WidgetEvent structs where id is the window ID (or "__global__" when no window context) and scope is [].

Mouse move (cursor_moved): %WidgetEvent{type: :move, data: %{x, y, pointer: :mouse, captured, modifiers}}

Cursor enter/exit: %WidgetEvent{type: :enter | :exit, data: %{captured}}

Button press/release (button_pressed, button_released): %WidgetEvent{type: :press | :release, data: %{button, pointer: :mouse, x: nil, y: nil, captured, modifiers}}

Scroll (wheel_scrolled): %WidgetEvent{type: :scroll, data: %{delta_x, delta_y, unit, pointer: :mouse, captured, modifiers}}

Touch press (finger_pressed): %WidgetEvent{type: :press, data: %{pointer: :touch, finger, x, y, button: :left, captured, modifiers}}

Touch move (finger_moved): %WidgetEvent{type: :move, data: %{pointer: :touch, finger, x, y, captured, modifiers}}

Touch release (finger_lifted): %WidgetEvent{type: :release, data: %{pointer: :touch, finger, x, y, button: :left, captured, modifiers}}

Touch lost (finger_lost): %WidgetEvent{type: :release, data: %{pointer: :touch, finger, x, y, button: :left, lost: true, captured, modifiers}}

Plushie.Event.ImeEvent

Input Method Editor events from subscriptions. Lifecycle: :opened -> :preedit (repeated) -> :commit -> :closed.

FieldTypeDescription
type:opened | :preedit | :commit | :closedIME phase
idString.t() | nilTarget widget ID
scope[String.t()]Ancestor scope chain (nearest first, window last)
textString.t() | nilComposition/commit text
cursor{start, end} | nilByte offsets in preedit
capturedboolean()Subscription captured
window_idString.t() | nilSource window

Plushie.Event.WindowEvent

Window lifecycle events from the renderer.

FieldTypeDescription
typesee belowWindow event kind
window_idString.t()Window identifier
x, ynumber() | nilPosition (for :moved)
width, heightnumber() | nilSize (for :resized)
position{number(), number()} | nilWindow position tuple
pathString.t() | nilFile path (for file drop)
scale_factornumber() | nilDPI scale (for :rescaled)

Window event types: :opened, :closed, :close_requested, :moved, :resized, :focused, :unfocused, :rescaled, :file_hovered, :file_dropped, :files_hovered_left.

Plushie.Event.SystemEvent

System query responses and platform events.

FieldTypeDescription
typesee belowSystem event kind
tagString.t() | nilCorrelation tag from the query
datamap() | String.t() | number() | nilPayload (shape depends on type)

System event types: :system_info, :system_theme, :animation_frame, :theme_changed, :all_windows_closed, :image_list, :tree_hash, :find_focused, :diagnostic, :announce, :error.

Plushie.Event.TimerEvent

Timer tick events from Plushie.Subscription.every/2.

FieldTypeDescription
tagatom()User-defined tag from subscription
timestampinteger()Monotonic timestamp in ms

Plushie.Event.AsyncEvent

Results from Plushie.Command.async/2 tasks.

FieldTypeDescription
tagatom()User-defined tag
result{:ok, term()} | {:error, term()}Task result

Plushie.Event.StreamEvent

Intermediate values from Plushie.Command.stream/2 tasks.

FieldTypeDescription
tagatom()User-defined tag
valueterm()Emitted stream value

Plushie.Event.EffectEvent

Platform effect responses (file dialogs, clipboard, notifications).

FieldTypeDescription
tagatom()User-defined tag
result{:ok, term()} | :cancelled | {:error, term()}Effect result

The :cancelled result is a normal outcome (user dismissed a dialog), not an error.

Plushie.Event.WidgetCommandError

Renderer error for a native widget command.

FieldTypeDescription
reasonString.t()Machine-readable reason
node_idString.t() | nilTarget widget node ID
opString.t() | nilCommand operation name
extensionString.t() | nilNative widget type
messageString.t() | nilHuman-readable error text

Pattern matching cookbook

Match by widget ID

def update(model, %WidgetEvent{type: :click, id: "save"}) do
  save(model)
end

Match by type with payload

def update(model, %WidgetEvent{type: :input, id: "search", value: text}) do
  %{model | query: text}
end

def update(model, %WidgetEvent{type: :toggle, id: "dark_mode", value: on?}) do
  %{model | dark_mode: on?}
end

def update(model, %WidgetEvent{type: :slide, id: "volume", value: level}) do
  %{model | volume: level}
end

Match by scope (dynamic lists)

When items are rendered in a named container with a dynamic ID, the container's ID appears in the event's scope:

def update(model, %WidgetEvent{type: :click, id: "delete", scope: [item_id | _]}) do
  %{model | items: Map.delete(model.items, item_id)}
end

Match key with modifiers

def update(model, %KeyEvent{type: :press, key: "s", modifiers: %{command: true}}) do
  save(model)
end

def update(model, %KeyEvent{type: :press, key: :escape}) do
  close_dialog(model)
end

Match pointer event with device type

# Mouse click
def update(model, %WidgetEvent{type: :press, id: "area",
    data: %{pointer: :mouse, button: :left}}) do
  select(model)
end

# Touch press
def update(model, %WidgetEvent{type: :press, id: "area",
    data: %{pointer: :touch, finger: finger_id}}) do
  touch_start(model, finger_id)
end

Match pointer event with modifiers

# Shift-click for multi-select
def update(model, %WidgetEvent{type: :press, id: "item",
    data: %{modifiers: %{shift: true}}}) do
  add_to_selection(model)
end

# Ctrl-drag for special behaviour
def update(model, %WidgetEvent{type: :move, id: "canvas",
    data: %{x: x, y: y, modifiers: %{ctrl: true}}}) do
  pan(model, x, y)
end

Match custom widget event

def update(model, %WidgetEvent{type: {:color_picker, :change}, data: %{hue: h}}) do
  %{model | hue: h}
end

Match async result

def update(model, %AsyncEvent{tag: :fetch, result: {:ok, data}}) do
  %{model | items: data, loading: false}
end

def update(model, %AsyncEvent{tag: :fetch, result: {:error, reason}}) do
  %{model | error: reason, loading: false}
end

Match stream values

def update(model, %StreamEvent{tag: :download, value: %{progress: pct}}) do
  %{model | progress: pct}
end

Match effect result

def update(model, %EffectEvent{tag: :open_file, result: {:ok, %{path: path}}}) do
  load_file(model, path)
end

def update(model, %EffectEvent{tag: :open_file, result: :cancelled}) do
  model
end

Match timer tick

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

Match window events

def update(model, %WindowEvent{type: :close_requested, window_id: wid}) do
  close_window(model, wid)
end

def update(model, %WindowEvent{type: :resized, width: w, height: h}) do
  %{model | width: w, height: h}
end

Reconstruct the full scoped path

Plushie.Event.target/1 reconstructs the forward-order path from id and scope:

event = %WidgetEvent{type: :click, id: "save", scope: ["form", "sidebar", "main"], window_id: "main"}
Plushie.Event.target(event)
# => "sidebar/form/save"

Catch-all clause

Always include a catch-all as the last update/2 clause:

def update(model, _event), do: model

Event flow

Events travel through a fixed pipeline before reaching your update/2:

  1. Renderer - the renderer detects a user interaction and encodes an event message.
  2. Bridge - Plushie.Bridge receives the wire frame, decodes it via Plushie.Protocol, and forwards the struct to the runtime.
  3. Runtime - Plushie.Runtime receives the event. If the event targets a widget with a registered handle_event/2 callback, the runtime walks the scope chain (innermost widget handler first) before delivering to the app. Widget handlers can emit, transform, consume, or ignore events.
  4. App - your update/2 receives the event (unless a widget handler consumed it).

Coalescable events

High-frequency events (pointer moves with WidgetEvent type: :move, and resize events with WidgetEvent type: :resize) are coalescable. When multiple events of the same type arrive for the same source before the runtime processes them, only the latest is delivered. This prevents queue backup during rapid mouse movement or window resizing. A zero-delay timer ensures coalescable events are flushed before the next non-coalescable event, preserving relative ordering.

Widget handler interception

Custom widgets with handle_event/2 callbacks are registered in a handler registry derived from the current view tree. When an event arrives, the runtime checks the scope chain for registered handlers. Each handler can return:

  • {:emit, family, data} - transform and re-emit as a new event
  • {:update_state, new_state} - update widget state, suppress event
  • :ignored - pass through to the next handler
  • :consumed - suppress the event entirely

Render-only widgets (no events, no state) are skipped in the registry and have zero overhead in the event path.

See also

  • Events - events, pattern matching, and the event log
  • Subscriptions - keyboard, timer, and other event sources
  • Scoped IDs - how container scoping affects event IDs
  • Commands - the command structs that produce async/effect events