# Custom Widgets

As the pad has grown, the view function has gotten larger. The file list,
event log, and preview pane are each self-contained pieces of UI with their
own rendering logic. In this chapter we extract them into **custom widgets**
-- reusable modules that encapsulate UI and behaviour.

Plushie has two kinds of custom widgets: pure Elixir (compose existing
widgets or draw custom visuals with canvas and SVG) and native (Rust-backed,
for custom GPU rendering). This chapter covers pure Elixir widgets. Native
widgets get a brief section at the end, with full details in the
[Custom Widgets reference](../reference/custom-widgets.md).

## Stateless widgets

The simplest custom widget is stateless: it takes props, returns a UI tree,
and has no internal state. Events from widgets inside it pass through
transparently to the parent app.

```elixir
defmodule PlushiePad.LabeledInput do
  use Plushie.Widget

  widget :labeled_input

  field :label, :string
  field :value, :string
  field :placeholder, :string, default: ""

  def view(id, props) do
    import Plushie.UI

    column id: id, spacing: 4 do
      text(props.label, size: 12)
      text_input("input", props.value, placeholder: props.placeholder)
    end
  end
end
```

`use Plushie.Widget` generates a struct, a `new/2` constructor, setter
functions, and the protocol implementation that connects it to the rendering
pipeline.

`widget :labeled_input` declares the widget's type name. This must be unique
across your application. It is used for event namespacing and protocol
dispatch.

`field` declarations define typed properties with optional defaults.
Primitive shortcuts (`:integer`, `:float`, `:string`, `:boolean`,
`:atom`, `:any`, `:map`) and domain type modules (`Plushie.Type.Color`,
`Plushie.Type.Length`, etc.) are accepted. See `Plushie.Type` for
details.

`view/2` receives the widget ID and a props map. It returns a UI tree using
the same DSL as `view/1`. Events from widgets inside (like the `text_input`)
flow through to the parent app's `update/2` without any special handling.

Use it in a view:

```elixir
PlushiePad.LabeledInput.new("email",
  label: "Email",
  value: model.email,
  placeholder: "you@example.com"
)
```

### Applying it: extract FileList

The file list sidebar is a good candidate. It takes the files list and
active file, renders the sidebar, and events (select, delete) pass through
to the pad's `update/2`:

```elixir
defmodule PlushiePad.FileList do
  use Plushie.Widget

  widget :file_list
  field :files, :any        # list of filenames
  field :active_file, :any  # currently selected filename

  def view(id, props) do
    import Plushie.UI

    column id: id, width: 200, height: :fill, padding: 8, spacing: 8 do
      text("sidebar-title", "Experiments", size: 14)

      scrollable "file-scroll", height: :fill do
        keyed_column spacing: 2 do
          for file <- props.files do
            container file do
              row spacing: 4 do
                button("select", file,
                  width: :fill,
                  style: if(file == props.active_file, do: :primary, else: :text)
                )
                button("delete", "x")
              end
            end
          end
        end
      end
    end
  end
end
```

In the pad's view:

```elixir
PlushiePad.FileList.new("sidebar",
  files: model.files,
  active_file: model.active_file
)
```

The pad's `update/2` still handles the select and delete events via scoped
IDs. The widget is transparent to events.

## Stateful widgets

When a widget needs internal state that the parent app should not manage,
add `state` declarations and a three-argument `view/3`:

```elixir
defmodule PlushiePad.CollapsiblePanel do
  use Plushie.Widget

  widget :collapsible_panel, container: true

  field :title, :string

  state expanded: true

  @impl Plushie.Widget.Handler
  def view(id, props, state) do
    import Plushie.UI

    column id: id, spacing: 4 do
      button("toggle", if(state.expanded, do: "- #{props.title}", else: "+ #{props.title}"))

      if state.expanded do
        container "content", padding: 8 do
          props.children
        end
      end
    end
  end

  @impl Plushie.Widget.Handler
  def handle_event(%Plushie.Event.WidgetEvent{type: :click, id: "toggle"}, state) do
    {:update_state, %{state | expanded: not state.expanded}}
  end

  def handle_event(_event, _state), do: :ignored
end
```

`state expanded: true` declares internal state with a default value. The
runtime manages this state. It persists across re-renders as long as the
widget remains in the tree.

`view/3` receives the ID, props, and current state.

`container: true` on the widget declaration allows the widget to accept
children via `props.children`.

## Handling events

The `handle_event/2` callback intercepts events before they reach the parent
app. All clauses must return one of:

| Return value | Effect |
|---|---|
| `{:emit, family, data}` | Emit a new event to the parent. Original is replaced. |
| `{:emit, family, data, new_state}` | Emit to parent and update internal state. |
| `{:update_state, new_state}` | Update internal state. No event reaches the parent. |
| `:consumed` | Suppress the event. Nothing reaches the parent. |
| `:ignored` | Pass through. The event continues to the parent unchanged. |

Events walk up the widget scope chain. If a widget returns `:ignored`, the
next widget handler in the chain gets a chance. If no handler captures the
event, it reaches `update/2`.

### Applying it: extract EventLog

The event log can track its own expanded/collapsed state:

```elixir
defmodule PlushiePad.EventLog do
  use Plushie.Widget

  widget :event_log

  field :events, :any  # list of event description strings

  state expanded: true

  @impl Plushie.Widget.Handler
  def view(id, props, state) do
    import Plushie.UI

    column id: id, spacing: 4 do
      row spacing: 8 do
        button("toggle-log", if(state.expanded, do: "Hide Log", else: "Show Log"))
        text("count", "#{length(props.events)} events", size: 12)
      end

      if state.expanded do
        scrollable "log-scroll", height: 120 do
          column spacing: 2, padding: 4 do
            for {entry, i} <- Enum.with_index(props.events) do
              text("log-#{i}", entry, size: 12, font: :monospace)
            end
          end
        end
      end
    end
  end

  @impl Plushie.Widget.Handler
  def handle_event(%Plushie.Event.WidgetEvent{type: :click, id: "toggle-log"}, state) do
    {:update_state, %{state | expanded: not state.expanded}}
  end

  def handle_event(_event, _state), do: :ignored
end
```

The toggle button updates the widget's internal state. All other events
(from the log entries, if any were interactive) pass through via `:ignored`.

## Declaring events

When a widget should emit semantic events to the parent, declare them with
`event`:

```elixir
defmodule PlushiePad.RatingWidget do
  use Plushie.Widget

  widget :rating

  field :value, :float, default: 0

  event :change, value: :float

  @impl Plushie.Widget.Handler
  def view(id, props, _state) do
    import Plushie.UI

    row id: id, spacing: 4 do
      for i <- 1..5 do
        button("star-#{i}", if(i <= props.value, do: "★", else: "☆"))
      end
    end
  end

  @impl Plushie.Widget.Handler
  def handle_event(%Plushie.Event.WidgetEvent{type: :click, id: "star-" <> n}, _state) do
    {:emit, :change, String.to_integer(n)}
  end

  def handle_event(_event, _state), do: :ignored
end
```

The `event :change, value: :float` declaration tells the framework that
this widget emits `:change` events with a numeric value. In the parent app,
these arrive as:

```elixir
%WidgetEvent{type: {:rating, :change}, id: "my-rating", value: 3}
```

Custom widget event types are tuples: `{widget_type, event_name}`. This
distinguishes them from built-in events.

For events with multiple fields, use `fields:` instead of `value:`:

```elixir
event :change, fields: [hue: :float, saturation: :float, value: :float]
```

These arrive in `WidgetEvent.value` as an atom-keyed map.

## Widget subscriptions

Widgets can declare their own subscriptions via the optional `subscribe/2`
callback:

```elixir
@impl Plushie.Widget.Handler
def subscribe(_props, state) do
  if state.animating do
    [Plushie.Subscription.every(16, :animate)]
  end
end
```

Widget subscriptions are automatically namespaced per instance. Timer events
are routed through the widget's `handle_event/2`, not the app's `update/2`.
Multiple instances of the same widget each get independent subscriptions.

## Canvas-based widgets

A widget's `view/2` can return a canvas instead of layout widgets:

```elixir
defmodule PlushiePad.Gauge do
  use Plushie.Widget

  widget :gauge

  field :value, :float, default: 0
  field :max, :float, default: 100

  @impl Plushie.Widget.Handler
  def view(id, props, _state) do
    import Plushie.UI

    pct = min(props.value / props.max, 1.0)
    angle = pct * 180

    canvas id, width: 120, height: 70 do
      layer "gauge" do
        # Background arc
        path([arc(60, 60, 50, 180, 0)],
          stroke: stroke("#ddd", 8, cap: :round)
        )
        # Value arc
        path([arc(60, 60, 50, 180, 180 + angle)],
          stroke: stroke("#3b82f6", 8, cap: :round)
        )
        # Value text
        text(40, 55, "#{round(pct * 100)}%", fill: "#333", size: 16)
      end
    end
  end
end
```

Canvas-based widgets with interactivity combine `handle_event/2` with
canvas events (`:press`, `:enter`, `:drag`, etc.) to build
rich custom controls like colour pickers, drawing tools, and data
visualisations. You can also embed SVG content in canvas layers (as
shown in [chapter 12](12-canvas.md)). Design your visuals in a vector
editor and use them as interactive widget elements.

## The widget lifecycle

Understanding the lifecycle helps when debugging:

1. Your view calls `MyWidget.new(id, opts)`, which returns a widget struct.
2. During tree normalization, the struct is converted to a placeholder node
   tagged with the widget module and props.
3. The runtime detects the placeholder, looks up stored state (or uses
   initial defaults for new widgets), and calls `view/3`.
4. The rendered output replaces the placeholder in the final tree.
5. Widget metadata (module, state, event handlers) is accumulated in the
   `NormalizeCtx` during normalization, not attached to tree nodes.
6. The runtime derives a handler registry from the context for event dispatch.

There are no explicit mount or unmount callbacks. **Tree presence is the
lifecycle.** When a widget appears in the tree, it is "mounted" with initial
state. When it disappears, its state is cleaned up. This is why widget IDs
must be stable. A changing ID looks like a removal and re-creation.

## Native widgets

When you need rendering capabilities beyond what built-in widgets offer --
custom GPU drawing, new input types, or performance-critical visuals, you
can build a **native widget** backed by Rust:

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

  widget :gauge
  field :value, :float
  rust_crate "path/to/gauge_crate"
  rust_constructor "gauge::new()"
end
```

On the Rust side, you implement the `PlushieWidget` trait with `render()`,
and optionally `init()`, `prepare()`, `handle_message()`, and `cleanup()`.

`mix plushie.build` auto-detects native widgets via protocol consolidation,
generates a Cargo workspace that includes them, and builds the renderer
binary with your widgets registered. If you later remove a native widget
dependency, run `mix clean` to clear stale BEAM files. Without this,
protocol consolidation may still detect the removed widget.

Native widgets are an escape hatch for when pure Elixir composition is not
enough. Most apps will never need them. See the
[Custom Widgets reference](../reference/custom-widgets.md) for the full Rust
integration guide.

## Verify it

Test the EventLog widget using `Plushie.Test.WidgetCase`:

```elixir
defmodule PlushiePad.EventLogTest do
  use Plushie.Test.WidgetCase, widget: PlushiePad.EventLog

  setup do
    init_widget("log", events: ["click on btn", "input on name"])
  end

  test "shows event entries" do
    element = find!({:text, "click on btn"})
    assert element.type == "text"
  end
end
```

`WidgetCase` hosts a single widget in a test harness. The `init_widget/2`
call creates the widget with the given props. This is covered in detail in
[chapter 15](15-testing.md).

## Try it

Build custom widgets in your pad experiments:

- Start with a stateless widget that composes a label and an input. Use it
  in another experiment.
- Add `state` and `handle_event/2` to make a collapsible section.
- Declare a custom event and match on the `{widget_type, :event_name}` tuple
  in the parent experiment.
- Build a canvas-based widget: a simple progress ring, a mini sparkline, or
  a colour swatch.
- Try a widget with `subscribe/2` that animates on a timer.

In the next chapter, we will enhance the pad with state management helpers
for undo, search, selection, and navigation.

---

Next: [State Management](14-state-management.md)
