Runtime support for widget handler event dispatch and state management.
Maintains a registry of active widget_handlers (keyed by canonical scoped ID) derived from the normalized tree. Routes events through the scope chain of widget handlers, following iced's captured/ignored model.
Event dispatch
When an event arrives with a scope that contains one or more registered widget_handlers, the runtime builds a handler chain (innermost to outermost) and walks it:
:ignored- handler didn't capture; continue to next handler:consumed/{:update_state, ...}- captured, no output; stop{:emit, ...}- captured with output; replace event and continue
If no handler captures, the event reaches app.update/2 unchanged.
This mirrors iced's Status::Captured / Status::Ignored model.
Scoped ID reconstruction
Events carry a reversed ancestor scope list (e.g., ["picker", "form"]
for a widget at form/picker). Registry keys are forward-order scoped
IDs. The dispatch logic reconstructs scoped IDs from the scope array
to build the handler chain.
Summary
Functions
Applies an event spec to a native widget event, setting type tuple and routing data to value/data fields based on the spec.
Atomizes declared field keys from wire data and parses typed fields. Undeclared keys are dropped; only declared fields appear in the result.
Collects subscription specs from all registered widget_handlers.
Derives the widget handler registry from the normalized tree.
Dispatches an event through the widget handler chain.
Extracts a scalar value from wire event data. Wire data from the renderer is a string-keyed map; value events carry the value under "value". Falls back to the raw data for pre-parsed or nil values.
Checks if a timer event is for a widget handler subscription. If so, routes it through the widget's handle_event and returns the result. Timer-triggered emits are dispatched through the scope chain of parent widget_handlers.
Normalizes a widget event by resolving the wire family string
(e.g., "color_picker:select") into a {widget_type, event_name}
tuple and applying the event spec (scalar extraction, field atomization).
Parses a wire event family string (e.g., "color_picker:select") into
a {widget_type, event_name} tuple of existing atoms.
Functions
@spec apply_widget_event_family_spec( event :: Plushie.Event.WidgetEvent.t(), widget_type :: atom(), event_name :: atom(), spec :: Plushie.Event.BuiltinSpecs.t() | nil ) :: Plushie.Event.WidgetEvent.t()
Applies an event spec to a native widget event, setting type tuple and routing data to value/data fields based on the spec.
@spec atomize_declared_fields( wire_data :: map(), declared_fields :: [{atom(), Plushie.Event.BuiltinSpecs.field_type()}] ) :: map()
Atomizes declared field keys from wire data and parses typed fields. Undeclared keys are dropped; only declared fields appear in the result.
@spec collect_subscriptions(registry :: %{required(String.t()) => map()}) :: [ Plushie.Subscription.t() ]
Collects subscription specs from all registered widget_handlers.
Each widget's subscribe/2 callback (if defined) is called with
props and state from the registry. The returned specs are namespaced
with the widget ID so they don't collide with app subscriptions or
other widgets.
Derives the widget handler registry from the normalized tree.
The tree carries widget metadata (module, state, props) in each
stateful widget node's :meta field. This function extracts that
metadata into a flat map keyed by scoped ID for O(1) event
dispatch lookups.
Called after each render. The tree is the single source of truth -- new widgets appear with their initial state (set during normalization), existing widgets carry their updated state, and removed widgets are simply absent.
Dispatches an event through the widget handler chain.
Builds an ordered list of widget handlers from the event's scope (innermost to outermost), then walks the chain:
:ignored- not captured, continue to next handler:consumed/{:update_state, ...}- captured, stop{:emit, family, data}- captured, replace event, continue
Returns {event_or_nil, updated_registry}. If no handler captures,
returns the original event unchanged. If a handler consumes, returns
nil. This follows iced's captured/ignored model.
Extracts a scalar value from wire event data. Wire data from the renderer is a string-keyed map; value events carry the value under "value". Falls back to the raw data for pre-parsed or nil values.
@spec maybe_handle_timer(registry :: %{required(String.t()) => map()}, tag :: term()) :: {:handled, struct() | nil, map()} | :not_routed
Checks if a timer event is for a widget handler subscription. If so, routes it through the widget's handle_event and returns the result. Timer-triggered emits are dispatched through the scope chain of parent widget_handlers.
Returns:
{:handled, event_or_nil, new_registry}- timer was for a widget handler:not_routed- not a widget handler timer
Normalizes a widget event by resolving the wire family string
(e.g., "color_picker:select") into a {widget_type, event_name}
tuple and applying the event spec (scalar extraction, field atomization).
Non-widget events pass through unchanged.
Unknown event families raise Protocol.Error. This is intentional:
the SDK version pins 1:1 to the renderer version, so an unknown
family indicates a real bug (stale tree, wrong binary) rather than
a forward-compatibility scenario. Graceful degradation would hide
the mismatch.
The widget_events argument is the event registry derived from the tree
(keyed by canonical scoped ID, e.g. "main#form/email").
Parses a wire event family string (e.g., "color_picker:select") into
a {widget_type, event_name} tuple of existing atoms.