Plushie.Runtime.WidgetHandlers (Plushie v0.7.2)

Copy Markdown View Source

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

apply_widget_event_family_spec(event, widget_type, event_name, spec)

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

atomize_declared_fields(wire_data, declared_fields)

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

collect_subscriptions(registry)

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

derive_registry(tree)

@spec derive_registry(tree :: map() | nil) :: %{required(String.t()) => map()}

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.

dispatch_event(registry, event)

@spec dispatch_event(registry :: map(), event :: struct()) :: {struct() | nil, map()}

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.

extract_wire_value(v)

@spec extract_wire_value(wire_data :: map() | term()) :: term()

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.

maybe_handle_timer(registry, arg2)

@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

normalize_widget_event!(widget_events, event)

@spec normalize_widget_event!(widget_events :: map(), event :: term()) :: term()

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").

parse_widget_family!(family)

@spec parse_widget_family!(String.t()) :: {atom(), atom()}

Parses a wire event family string (e.g., "color_picker:select") into a {widget_type, event_name} tuple of existing atoms.