Events
Events are Data types delivered to your update method. Each event family
has its own type under Plushie::Event::*.
Widget events
These are generated by user interaction with widgets. The renderer maps
widget interactions to Event::Widget using the node's id.
Click
Event::Widget[type: :click, id: "save"]
Generated by button widgets when pressed.
def update(model, event)
case event
in Event::Widget[type: :click, id: "save"]
save(model)
in Event::Widget[type: :click, id: "cancel"]
revert(model)
else
model
end
end
Input
Event::Widget[type: :input, id: "search", value:]
Generated by text_input on every keystroke (when on_input is set,
which is the default).
in Event::Widget[type: :input, id: "search", value:]
model.with(search_query: value)
Submit
Event::Widget[type: :submit, id: "search", value:]
Generated by text_input when the user presses Enter (when on_submit
is set).
in Event::Widget[type: :submit, id: "search", value:]
[model.with(search_query: value), search_command(value)]
Toggle
Event::Widget[type: :toggle, id: "dark_mode", value:]
Generated by checkbox and toggler. value is true or false.
in Event::Widget[type: :toggle, id: "dark_mode", value:]
model.with(dark_mode: value)
Select
Event::Widget[type: :select, id: "theme_picker", value:]
Generated by pick_list and combo_box when an option is selected.
in Event::Widget[type: :select, id: "theme_picker", value:]
model.with(theme: value)
Slide
Event::Widget[type: :slide, id: "volume", value:]
Event::Widget[type: :slide_release, id: "volume", value:]
Generated by slider and vertical_slider. slide fires continuously
during dragging. slide_release fires once when the user releases the
slider.
in Event::Widget[type: :slide, id: "volume", value:]
model.with(volume: value)
in Event::Widget[type: :slide_release, id: "volume", value:]
[model.with(volume: value), save_preference(:volume, value)]
Text editor content change
Event::Widget[type: :input, id: "notes", value:]
Generated by text_editor on content changes. The value string
contains the full editor text after each edit. This is the same event
type as text_input.
Key binding
Event::Widget[type: :key_binding, id: "editor", value: "save"]
Generated by text_editor widgets when a declarative key binding rule
with a "custom" action matches. The id is the text editor's node ID.
The value is the custom tag string from the binding rule.
in Event::Widget[type: :key_binding, id: "editor", value: "save"]
[model, save_file(model)]
in Event::Widget[type: :key_binding, id: "editor", value: "format"]
model.with(content: format_code(model.content))
Scroll
Event::Widget[type: :scroll, id: "log_view", data:]
Generated by scrollable when the scroll position changes (when
on_scroll: true is set). The data hash contains:
data = {
"absolute_x" => Float, # pixel offset from left
"absolute_y" => Float, # pixel offset from top
"relative_x" => Float, # 0.0-1.0 horizontal scroll position
"relative_y" => Float, # 0.0-1.0 vertical scroll position
"bounds_width" => Float, # visible viewport width
"bounds_height" => Float, # visible viewport height
"content_width" => Float, # total content width
"content_height" => Float # total content height
}
in Event::Widget[type: :scroll, id: "log_view", data:]
at_bottom = data["relative_y"] >= 0.99
model.with(auto_scroll: at_bottom)
Paste
Event::Widget[type: :paste, id: "url_input", value:]
Generated by text_input when the user pastes text (when on_paste: true
is set). The value field contains the pasted string.
Option hovered
Event::Widget[type: :option_hovered, id: "search", value:]
Generated by combo_box when the user hovers over an option in the
dropdown (when on_option_hovered: true is set).
Open / Close
Event::Widget[type: :open, id: "country_picker"]
Event::Widget[type: :close, id: "country_picker"]
Generated by pick_list and combo_box when the dropdown menu opens or
closes (when on_open: true and/or on_close: true are set).
Sort
Event::Widget[type: :sort, id: "users", value:]
Generated by table when a sortable column header is clicked. The
value is the string key from the column descriptor.
Mouse area events
Mouse area events use the Event::MouseArea type:
Event::MouseArea[type: :right_press, id: "canvas"]
Event::MouseArea[type: :enter, id: "tooltip-target"]
Event::MouseArea[type: :move, id: "drag-zone", x:, y:]
Event::MouseArea[type: :scroll, id: "scroll-zone", delta_x:, delta_y:]
Available types: :right_press, :right_release, :middle_press,
:middle_release, :double_click, :enter, :exit, :move, :scroll.
Each event requires its corresponding boolean prop to be set on the mouse_area widget. Without the prop, the event is not emitted.
Note: left press/release events from mouse_area are delivered as
Event::Widget[type: :click] events.
in Event::MouseArea[type: :enter, id: "hover_zone"]
model.with(hovered: true)
in Event::MouseArea[type: :move, id: "canvas_area", x:, y:]
model.with(cursor: [x, y])
Canvas events
Generated by canvas widgets. Each event is opt-in via a boolean prop on
the canvas node. Canvas events use the Event::Canvas type.
Event::Canvas[type: :press, id: "draw_area", x:, y:, button: "left"]
Event::Canvas[type: :release, id: "draw_area", x:, y:, button: "left"]
Event::Canvas[type: :move, id: "draw_area", x:, y:]
Event::Canvas[type: :scroll, id: "draw_area", x:, y:, delta_x:, delta_y:]
The button field is a string ("left", "right", "middle"). The
x/y coordinates are relative to the canvas origin.
in Event::Canvas[type: :press, id: "draw_area", x:, y:, button: "left"]
model.with(drawing: true, last_point: [x, y])
in Event::Canvas[type: :move, id: "draw_area", x:, y:]
if model.drawing
model.with(last_point: [x, y], strokes: [[x, y]] + model.strokes)
else
model
end
Canvas element events
When a canvas contains elements with an interactive field, the renderer
handles hit testing locally and emits semantic element events. These arrive
as Event::Widget (not Event::Canvas). The id is the canvas widget
ID; data["element_id"] identifies the element.
# Cursor entered an element's bounds
Event::Widget[type: :canvas_element_enter, id: "chart", data: {"element_id" => "bar-jan", **}]
# Click on an element
Event::Widget[type: :canvas_element_click, id: "chart", data: {"element_id" => "bar-jan", **}]
# Drag on a draggable element
Event::Widget[type: :canvas_element_drag, id: "chart", data: {"element_id" => "handle", **}]
in Event::Widget[type: :canvas_element_click, id: "chart", data: {"element_id" => element_id, **}]
model.with(selected_bar: element_id)
Canvas element blurred
Event::Widget[type: :canvas_element_blurred, id: "chart", data: {"element_id" => "bar-jan"}]
Generated when a canvas element loses focus.
Canvas focused / blurred
Event::Widget[type: :canvas_focused, id: "draw_area"]
Event::Widget[type: :canvas_blurred, id: "draw_area"]
Generated when the canvas widget itself gains or loses focus.
Canvas group focused / blurred
Event::Widget[type: :canvas_group_focused, id: "chart", data: {"group_id" => "bar-group"}]
Event::Widget[type: :canvas_group_blurred, id: "chart", data: {"group_id" => "bar-group"}]
Generated when a group within a canvas gains or loses focus.
Diagnostic
Event::System[type: :diagnostic, data: {"level" => "warn", "element_id" => "broken", "code" => "missing_a11y", "message" => "..."}]
Generated by the renderer when it detects a configuration issue with a canvas element (e.g. missing accessibility metadata). Useful for development-time warnings.
Sensor events
Generated by sensor widgets when the sensor detects a size change.
Event::Sensor[type: :resize, id: "content_area", width:, height:]
in Event::Sensor[type: :resize, id: "content_area", width:, height:]
model.with(content_size: [width, height])
PaneGrid events
Generated by pane_grid widgets during pane interactions.
Event::Pane[type: :resized, id: "editor", split:, ratio:]
Event::Pane[type: :dragged, id: "editor", pane:, target:]
Event::Pane[type: :clicked, id: "editor", pane:]
in Event::Pane[type: :clicked, id: "editor", pane:]
model.with(active_pane: pane)
Keyboard events
Delivered when keyboard subscriptions are active (see commands.md).
Event::Key[type: :press, key: "s", modifiers: {command: true}]
Event::Key[type: :release, key: :escape]
Key fields
Event::Key[
type:, # :press | :release
key:, # :enter | "a" | ... (named symbol or character string)
modified_key:, # key with modifiers applied (e.g. Shift+a = "A")
physical_key:, # physical key code (layout-independent)
location:, # :standard | :left | :right | :numpad
modifiers:, # KeyModifiers hash
text:, # text produced by key press (nil for non-printable)
repeat:, # true if auto-repeat event
captured:
]
KeyModifiers
{
shift: true/false,
ctrl: true/false,
alt: true/false,
logo: true/false,
command: true/false # platform-aware: ctrl on Linux/Windows, logo (Cmd) on macOS
}
Key values
Named keys are symbols:
:enter, :escape, :tab, :backspace, :delete,
:arrow_up, :arrow_down, :arrow_left, :arrow_right,
:home, :end, :page_up, :page_down,
:f1, :f2, ... :f12,
:space
Character keys are single-character strings:
"a", "b", "1", "/", " "
Keyboard event examples
in Event::Key[type: :press, key: "s", modifiers: {command: true, **}]
[model, save_command]
in Event::Key[type: :press, key: :escape]
model.with(modal_open: false)
in Event::Key[type: :press, key: "z", modifiers: {command: true, shift: false, **}]
undo(model)
in Event::Key[type: :press, key: "z", modifiers: {command: true, shift: true, **}]
redo(model)
# Use physical_key for layout-independent bindings (e.g. WASD on non-QWERTY)
in Event::Key[type: :press, physical_key: :key_w]
move_up(model)
IME events
Delivered when IME subscriptions are active. Input Method Editor events support CJK and other compose-based text input.
Event::Ime[type: :opened]
Event::Ime[type: :preedit, text:, cursor:]
Event::Ime[type: :commit, text:]
Event::Ime[type: :closed]
preedit fires during composition with the in-progress text and optional
cursor range. commit fires when the user finalises a composed string.
Mouse events (global)
Delivered when mouse subscriptions are active. These are global (not widget-scoped) events from the windowing system.
Event::Mouse[type: :moved, x:, y:]
Event::Mouse[type: :entered]
Event::Mouse[type: :left]
Event::Mouse[type: :button_pressed, button: :left]
Event::Mouse[type: :button_released, button: :right]
Event::Mouse[type: :wheel_scrolled, delta_x:, delta_y:, unit: :line]
The button field is a symbol (:left, :right, :middle, :back, :forward).
The unit field indicates scroll units (:line or :pixel).
Touch events
Delivered when touch subscriptions are active.
Event::Touch[type: :pressed, finger_id:, x:, y:]
Event::Touch[type: :moved, finger_id:, x:, y:]
Event::Touch[type: :lifted, finger_id:, x:, y:]
Event::Touch[type: :lost, finger_id:, x:, y:]
Modifier state events
Delivered when modifier key state changes (subscription-driven).
Event::Modifiers[modifiers: {shift: true, ctrl: false, alt: false, logo: false, command: false}]
Window events
Delivered when window subscriptions are active or for lifecycle events on windows the app manages.
Event::Window[type: :close_requested, window_id: "main"]
Event::Window[type: :opened, window_id: "main", position:, width:, height:]
Event::Window[type: :closed, window_id: "main"]
Event::Window[type: :moved, window_id: "main", x:, y:]
Event::Window[type: :resized, window_id: "main", width:, height:]
Event::Window[type: :focused, window_id: "main"]
Event::Window[type: :unfocused, window_id: "main"]
Event::Window[type: :rescaled, window_id: "main", scale_factor:]
Event::Window[type: :file_hovered, window_id: "main", path:]
Event::Window[type: :file_dropped, window_id: "main", path:]
Event::Window[type: :files_hovered_left, window_id: "main"]
File drag and drop
in Event::Window[type: :file_hovered, window_id: "main", path:]
model.with(drop_target_active: true, hovered_file: path)
in Event::Window[type: :file_dropped, window_id: "main", path:]
[model.with(drop_target_active: false), load_file(path)]
in Event::Window[type: :files_hovered_left, window_id: "main"]
model.with(drop_target_active: false)
System events
Event::System[type: :animation_frame, data: ]
Event::System[type: :theme_changed, data: mode]
Animation frame events are delivered on each frame when an animation
subscription is active. Theme changed events fire when the OS theme
switches. The data field holds the timestamp or the mode string
("light" or "dark").
Timer events
Delivered by timer subscriptions.
Event::Timer[tag: :tick, timestamp: ts]
Where timestamp is the monotonic time in milliseconds.
Command result events
Delivered when an async command completes.
Event::Async[tag: :data_loaded, result: [:ok, value]]
Event::Async[tag: :data_loaded, result: [:error, reason]]
Where tag is the symbol you passed to Command.async.
in Event::Widget[type: :click, id: "fetch"]
[model, Command.async(-> { HTTP.get!("/api/data") }, :data_loaded)]
in Event::Async[tag: :data_loaded, result: [:ok, body]]
model.with(data: body)
Effect result events
Delivered when a renderer effect completes.
Event::Effect[request_id: "ef_1234", result: [:ok, data]]
Event::Effect[request_id: "ef_1234", result: :cancelled]
Event::Effect[request_id: "ef_1234", result: [:error, reason]]
See effects.md.
Catch-all
Always include an else clause:
def update(model, event)
case event
# ... specific handlers ...
else
model
end
end
Unknown events are silently ignored. This is important for forward compatibility -- new widget types or renderer versions may emit events your app does not yet handle.
Pattern matching tips
Events are Data types, so Ruby's pattern matching works:
# Match any click
in Event::Widget[type: :click, id:]
handle_click(model, id)
# Match clicks with a prefix
in Event::Widget[type: :click, id: /\Anav:(.+)\z/ => id]
section = id.delete_prefix("nav:")
model.with(section: section.to_sym)
# Match toggle on any "setting:" prefixed checkbox
in Event::Widget[type: :toggle, id: /\Asetting:/ => id, value:]
key = id.delete_prefix("setting:")
model.with(settings: model.settings.merge(key => value))
Scope matching
Widget events include a scope field listing ancestor container IDs,
nearest first. You can pattern match on scope to distinguish events from
different contexts:
# Button inside the "sidebar" container
in Event::Widget[type: :click, id: "save", scope: ["sidebar", *]]
(model)
# Same button ID but inside "main" container
in Event::Widget[type: :click, id: "save", scope: ["main", *]]
save_main(model)
Accessibility action
in Event::Widget[type: :a11y_action, id:, data: {"action" => action}]
case action
when "Increment" then model.with(value: model.value + 1)
when "Decrement" then model.with(value: model.value - 1)
else model
end
Generated when an assistive technology triggers a non-standard action on a
widget. Standard AT actions are mapped to normal events: Click/Default
becomes Event::Widget[type: :click], SetValue becomes
Event::Widget[type: :input]. This event catches everything else.
The action is a string representation of the accesskit action (e.g.
"ScrollDown", "Increment", "Decrement").
Note: This event is only generated when the renderer is built with the
a11y feature flag.