Plushie.Widget behaviour (Plushie v0.6.0)

Copy Markdown View Source

Macro-based DSL for declaring Plushie widgets.

Supports two kinds of widget:

  • :native_widget -- backed by a Rust crate implementing the WidgetExtension trait. Requires rust_crate and rust_constructor declarations.
  • :widget -- pure Elixir widget. Features are detected at compile time based on what callbacks are defined:
    • Has state declarations -> stateful (deferred view, state persistence via the runtime).
    • No state -> stateless (immediate view in new/2).
    • Has handle_event/2 -> participates in event dispatch.
    • Has subscribe/2 -> widget-scoped subscriptions.

Usage

defmodule MyApp.Gauge do
  use Plushie.Widget, :native_widget

  widget :gauge

  prop :value, :number
  prop :min, :number, default: 0
  prop :max, :number, default: 100
  prop :color, :color, default: :blue
  prop :width, :length
  prop :height, :length

  rust_crate "native/my_gauge"
  rust_constructor "my_gauge::GaugeWidget::new()"

  event :value_changed, data: [value: :number]
  command :set_value, value: :number
end

Generated code

The macro generates:

  • type_names/0 -- returns [:gauge] (from the widget declaration)
  • native_crate/0 -- returns the rust_crate path (native_widget only)
  • rust_constructor/0 -- returns the Rust expression (native_widget only)
  • new/2 -- creates a %Module{} struct ()
  • Setter functions per prop for pipeline composition
  • with_options/2 -- applies keyword options via setters
  • build/1 -- converts the struct to a ui_node() map
  • @type t, @type option -- typespecs for dialyzer
  • Plushie.Widget protocol implementation
  • __event_specs__/0, __event_spec__/1 -- typed event metadata
  • Command functions (native_widget only) that wrap Plushie.Command.widget_command/3

Prop types

Supported prop types. Values are stored raw; Tree.normalize/1 handles wire encoding in a single pass.

  • :number, :string, :boolean -- pass through
  • :color -- normalized via Plushie.Type.Color.cast/1 (input casting)
  • :length -- pass through (encoded by Tree.normalize)
  • :padding -- pass through (encoded by Tree.normalize)
  • :alignment -- pass through (encoded by Tree.normalize)
  • :font -- pass through
  • :style -- pass through (atom or StyleMap)
  • :atom -- pass through (encoded by Tree.normalize)
  • :map, :any -- pass through
  • {:list, _} -- pass through

Composite widgets

If the using module defines view/2 (leaf) or view/3 (container), new/2 delegates to it after resolving props:

defmodule MyApp.LabeledInput do
  use Plushie.Widget

  widget :labeled_input

  prop :label, :string

  def view(id, props) do
    import Plushie.UI
    column id: id do
      text(props.label)
    end
  end
end

view/2 vs view/3

Use view/2 for simple widgets:

def view(id, props) do
  %{id: id, type: "text", props: %{content: props.label}, children: []}
end

Use view/3 when the widget has state (declared via state). The third argument is the widget's internal state map:

def view(id, props, state) do
  fill = if state.hover, do: "#ff0", else: "#ccc"
  ...
end

Special options

All widgets automatically support:

  • :a11y -- accessibility overrides (see Plushie.Type.A11y)
  • :event_rate -- maximum events per second for coalescable events from this widget (see the event throttling design doc)

These do not need to be declared via prop -- they are always available on new/2.

Summary

Types

A child element: either an already-resolved node map or a widget struct.

A UI tree node map. Every widget builder returns this shape.

Callbacks

Path to the Rust crate relative to the package root.

Full Rust constructor expression for the widget.

Node type atoms this widget handles.

Functions

Declares a command (native_widget only) with optional typed params.

Declares a typed event emitted by a native or canvas widget.

Declares a prop with name, type, and optional default.

Declares the Rust constructor expression (native_widget only).

Declares the path to the Rust crate (native_widget only).

Declares internal state fields for a stateful widget.

Converts a widget struct to a ui_node() map via the WidgetProtocol.

Declares the widget type name. Pass container: true for container widgets.

Types

child()

@type child() :: ui_node() | struct()

A child element: either an already-resolved node map or a widget struct.

ui_node()

A UI tree node map. Every widget builder returns this shape.

Callbacks

native_crate()

(optional)
@callback native_crate() :: String.t()

Path to the Rust crate relative to the package root.

rust_constructor()

(optional)
@callback rust_constructor() :: String.t()

Full Rust constructor expression for the widget.

type_names()

@callback type_names() :: [atom()]

Node type atoms this widget handles.

Functions

command(name, params \\ [])

(macro)

Declares a command (native_widget only) with optional typed params.

event(name, opts_or_block \\ [])

(macro)

Declares a typed event emitted by a native or canvas widget.

Supports three forms:

No payload

event :cleared

Typed value (goes in WidgetEvent.value)

event :select, value: :number

Structured data (goes in WidgetEvent.data with atom keys)

event :change, data: [hue: :number, saturation: :number]

Block form

event :change do
  data do
    field :hue, :number
    field :saturation, :number
  end
end

value: and data: are mutually exclusive.

Type identifiers can be built-in atoms (:number, :string, :boolean, :any) or modules implementing Plushie.Event.EventType.

prop(name, type, opts \\ [])

(macro)

Declares a prop with name, type, and optional default.

rust_constructor(expr)

(macro)

Declares the Rust constructor expression (native_widget only).

rust_crate(path)

(macro)

Declares the path to the Rust crate (native_widget only).

state(fields)

(macro)

Declares internal state fields for a stateful widget.

State fields are managed by the runtime, not the app model. They persist across renders and are passed to view/3 and handle_event/2. Declaring state fields makes the widget stateful: the view is deferred to tree normalization and the WidgetHandler behaviour is injected automatically.

state hover: nil, drag: :none, animation_progress: 0.0

to_node(widget)

@spec to_node(struct()) :: ui_node()

Converts a widget struct to a ui_node() map via the WidgetProtocol.

widget(type_name, opts \\ [])

(macro)

Declares the widget type name. Pass container: true for container widgets.