# `Plushie.UI`
[🔗](https://github.com/plushie-ui/plushie-elixir/blob/v0.6.0/lib/plushie/ui.ex#L1)

Ergonomic builder layer for Plushie UI trees.

Import this module in your `view/1` function to get concise widget builder
syntax with optional `do` block sugar for children.

## Usage

    def view(model) do
      import Plushie.UI

      window "main", title: "Counter" do
        column do
          text("Count: #{model.count}")
          row do
            button("increment", "+")
            button("decrement", "-")
          end
        end
      end
    end

## Node shape

Every builder produces:

    %{id: string, type: string, props: %{}, children: []}

Props use atom keys internally; string keys are used only at the wire encoding
boundary. Reserved opts keys (`:children`, `:id`, `:do`) are not treated as props.

## Two equivalent forms

The `do` block is sugar that compiles to the explicit `:children` form:

    column(padding: 8, children: [text("hello")])

    column padding: 8 do
      text("hello")
    end

Inside a `do` block:
- `for` comprehensions work (they return lists; one level is flattened)
- `if` without `else` works (returns nil; nils are filtered out)

## Auto-IDs

**WARNING: Auto-generated IDs are unstable.** Layout and display widgets
that do not receive an explicit `:id` option generate one from the call
site line number: `"auto:ModuleName:42"`. These IDs change whenever you
refactor code, add/remove lines above the call, or use conditional
rendering (`if`/`case`) that moves the call to a different branch.

When an ID changes between renders, the renderer treats it as a
removal + insertion, **losing all widget-local state** (scroll position,
text cursor, focus, editor content).

**Always supply explicit `:id` opts for stateful widgets:**
`text_editor`, `combo_box`, `pane_grid`, `scrollable`, `text_input`.

Auto-IDs are fine for purely visual widgets like `text`, `row`, `column`
where state loss is invisible.

## Formatter

Plushie exports formatter settings that keep layout blocks paren-free so
they read like declarative markup. Add `:plushie` to `import_deps` in
your `.formatter.exs`:

    # .formatter.exs
    [
      inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
      import_deps: [:plushie]
    ]

Layout blocks stay paren-free; leaf widgets keep parens for clarity:

    column padding: 8 do
      text("count", "Count: #{model.count}", size: 24)
      button("inc", "+1")
    end

## Container inline props

Container widgets (`column`, `row`, `container`, etc.) support option
declarations directly inside their do-blocks, mixed with children:

    column do
      spacing 8
      padding do
        top 16
        bottom 16
      end
      width :fill

      text("Hello")
      button("save", "Save")
    end

Options and children can be freely mixed. Options are validated at
compile time -- using an option that doesn't belong to the container
produces a helpful error.

Struct-typed options support nested do-blocks:

    container "hero" do
      border do
        width 1
        color "#ddd"
        rounded 4
      end
      shadow do
        color "#00000022"
        offset_y 2
        blur_radius 4
      end
      padding 20

      text("Welcome")
    end

All three forms are equivalent and can be mixed:

    column spacing: 8, padding: 16 do ... end    # keyword on call line
    column do spacing 8; padding 16; ... end      # inline in block
    column do padding do top 16 end; ... end      # nested do-block

## Block-form options

Leaf widgets accept an optional `do` block for setting props when the
keyword list gets long:

    button "save", "Save" do
      style(:primary)
    end

The keyword form is still valid and preferred for short option lists.

## Canvas shapes

Canvas shape functions (`rect`, `circle`, `line`, `path`, `stroke`,
`linear_gradient`, `move_to`, `line_to`, etc.) and canvas structure
macros (`group`, `layer`) are available directly via `import Plushie.UI`.
No separate `import Plushie.Canvas.Shape` is needed inside canvas blocks.

Inside `canvas`, `layer`, and `group` blocks, `text`, `image`, and
`svg` calls resolve automatically to their canvas shape variants.

Interactive fields go directly on the group via keyword opts:

    canvas "toggle", width: 52, height: 28 do
      layer "bg" do
        group "switch", on_click: true, cursor: :pointer do
          rect(0, 0, 52, 28, fill: "#4CAF50", radius: 14)
          circle(36, 14, 10, fill: "#fff")
        end
      end
    end

Import `Plushie.Canvas.Shape` directly only when building shapes in
helper functions outside canvas blocks.

## Prop override semantics

When both the keyword argument on the call line and a block
declaration specify the same option, the block value wins:

    column spacing: 8 do
      spacing 16         # overrides -- spacing is 16
      text("hello")
    end

This applies to all container and leaf widget do-blocks.

## Control flow preservation

All DSL blocks preserve every expression from control flow forms.
Multi-expression `if`/`for`/`case`/`cond`/`with` bodies contribute
all their expressions to the parent's children list, not just the
last one.

## Tree query

`find/2`, `find/3`, and `find_local/2` are re-exported from
`Plushie.Tree` for convenience.

`find/2` does exact scoped lookup. Use `find_local/2` when you
intentionally want a local ID search:

    Plushie.UI.find(tree, "form/save")
    Plushie.UI.find(tree, "save", "settings")
    Plushie.UI.find_local(tree, "save")

## Internals

For maintainer and widget author details on the macro architecture,
see `docs/reference/dsl.md`.

# `arc`

# `arc_to`

# `bezier_to`

# `button`
*macro* 

Clickable button.

Emits `%WidgetEvent{type: :click, id: id}` when clicked.

## Example

    button("save", "Save", style: :primary)

    button "save", "Save" do
      style :primary
    end

# `canvas`
*macro* 

Canvas for drawing shapes organized into named layers.

## Keyword form

    canvas("drawing",
      layers: %{"main" => [%{type: "circle", x: 50, y: 50, r: 20}]},
      width: 400,
      height: 300
    )

## Do-block form

Use `layer/2` to collect layers declaratively:

    canvas "chart", width: 400, height: 300 do
      layer "grid" do
        rect(0, 0, 400, 300, stroke: "#eee")
      end
      layer "data" do
        for bar <- bars do
          rect(bar.x, bar.y, bar.w, bar.h, fill: bar.color)
        end
      end
    end

## Options

- `:layers` -- map of layer names to shape descriptor lists
- `:width` / `:height` -- dimensions
- `:background` -- background color

# `checkbox`
*macro* 

Boolean checkbox toggle.

Emits `%WidgetEvent{type: :toggle, id: id, value: boolean}` when toggled.

## Example

    checkbox("agree", model.agreed, label: "I agree")

    checkbox "agree", model.agreed do
      label "I agree"
    end

# `circle`
*macro* 

Builds a circle shape. See `Plushie.Canvas.Shape.circle/4`.

# `clip`

# `close`

# `column`
*macro* 

Vertical flex layout.

## Options

- `:spacing` -- gap between children
- `:padding` -- padding around children
- `:width` / `:height` -- `:fill`, `:shrink`, or number
- `:align_x` -- `:left`, `:center`, `:right`
- `:id` -- explicit ID (otherwise auto-generated from call site)
- `:children` -- child nodes (function-form shorthand)

## Example

    column spacing: 8 do
      text("Hello")
      text("World")
    end

# `combo_box`
*macro* 

Combo box with free-text input and dropdown suggestions.

## Example

    combo_box("lang", ["Elixir", "Rust", "Go"], model.lang, placeholder: "Type...")

    combo_box "lang", ["Elixir", "Rust", "Go"], model.lang do
      placeholder "Type..."
    end

# `container`
*macro* 

Generic box with alignment and padding.

## Example

    container "hero", padding: 16 do
      text("Welcome")
    end

# `ellipse`

# `exists?`

```elixir
@spec exists?(tree :: Plushie.Widget.ui_node() | nil, id :: String.t()) :: boolean()
```

Returns true if a node with `id` exists in the tree.

# `find`

```elixir
@spec find(tree :: Plushie.Widget.ui_node(), id :: String.t()) ::
  Plushie.Widget.ui_node() | nil
```

Finds the first node in a tree whose `:id` matches `id`.

Delegates to `Plushie.Tree.find/2`. Returns the node map or `nil`.

## Example

    tree = MyApp.view(model)
    Plushie.UI.find(tree, "save_button")

# `find`

```elixir
@spec find(
  tree :: Plushie.Widget.ui_node(),
  id :: String.t(),
  window_id :: String.t()
) :: Plushie.Widget.ui_node() | nil
```

# `find_all`

```elixir
@spec find_all(
  tree :: Plushie.Widget.ui_node() | nil,
  id_or_pred :: String.t() | (Plushie.Widget.ui_node() -&gt; boolean())
) :: [Plushie.Widget.ui_node()]
```

Finds all nodes matching a predicate.

# `floating`
*macro* 

Floating overlay layout.

## Example

    floating "popup" do
      text("Floating content")
    end

# `grid`
*macro* 

Grid layout.

## Options

- `:columns` -- number of columns
- `:column_width` -- width of each column
- `:row_height` -- height of each row
- `:spacing` -- gap between cells
- `:padding` -- padding around grid
- `:width` / `:height` -- dimensions
- `:id` -- explicit ID (otherwise auto-generated from call site)

## Example

    grid columns: 3, spacing: 8 do
      for item <- items do
        text(item.name)
      end
    end

# `group`
*macro* 

Groups child shapes with optional positioning and interaction.

## Do-block form

    group "btn", x: 4, y: 4, on_click: true do
      rect(0, 0, 32, 32, radius: 4)
    end

## List form

    group([rect(0, 0, 100, 40)], x: 10, y: 50)

# `ids`

```elixir
@spec ids(tree :: Plushie.Widget.ui_node() | nil) :: [String.t()]
```

Returns all node IDs in the tree.

# `image`
*macro* 

Raster image display.

## Example

    image("logo", "/assets/logo.png", width: 200, content_fit: :cover)

    image "logo", "/assets/logo.png" do
      width 200
      content_fit :cover
    end

# `keyed_column`
*macro* 

Keyed column for efficient list diffing.

## Options

Same as `column/1`.

## Example

    keyed_column spacing: 8 do
      for item <- items do
        text(item.id, item.name)
      end
    end

# `layer`
*macro* 

Collects shapes into a named layer for use inside canvas blocks.

    canvas "chart", width: 400 do
      layer "grid" do
        rect(0, 0, 400, 300, stroke: "#eee")
      end
    end

# `line`
*macro* 

Builds a line shape. See `Plushie.Canvas.Shape.line/5`.

# `line_to`

# `linear_gradient`

# `loop`
*macro* 

Creates a looping transition descriptor.

Sets `repeat: :forever` and `auto_reverse: true` by default.
Requires `from:` and `to:`.

## Examples

    opacity: loop(800, to: 0.4, from: 1.0)
    rotation: loop(1000, to: 360, from: 0, auto_reverse: false)
    opacity: loop(to: 0.4, from: 1.0, duration: 800, cycles: 3)

# `loop`
*macro* 

# `markdown`
*macro* 

Markdown content renderer.

## Forms

- `markdown(content)` -- auto-generated ID
- `markdown(id, content)` -- explicit ID
- `markdown(id, content, opts)` -- explicit ID with options

## Example

    markdown("# Hello\n\nSome **bold** text")
    markdown("my_md", "# Hello", code_theme: "dracula")

# `move_to`

# `overlay`
*macro* 

Overlay container. First child is the anchor, second is the overlay content.

## Options

- `:position` -- `:below`, `:above`, `:left`, `:right`
- `:gap` -- space between anchor and overlay in pixels
- `:offset_x` -- horizontal offset in pixels
- `:offset_y` -- vertical offset in pixels

## Example

    overlay "popup", position: :below, gap: 4 do
      button("anchor", "Click me")
      container "dropdown" do
        text("dropdown_text", "Dropdown content")
      end
    end

# `pane_grid`
*macro* 

Pane grid for resizable tiled panes.

## Options

- `:spacing` -- gap between panes
- `:min_size` -- minimum pane size
- `:on_resize` -- resize event tag
- `:on_drag` -- drag event tag
- `:on_click` -- click event tag

Children are pane content keyed by ID.

## Example

    pane_grid "editor_panes", spacing: 2 do
      column id: "left" do
        text("Left pane")
      end
      column id: "right" do
        text("Right pane")
      end
    end

# `path`
*macro* 

Builds a path shape. See `Plushie.Canvas.Shape.path/2`.

# `pick_list`
*macro* 

Dropdown pick list for selecting from a list of options.

## Example

    pick_list("country", ["UK", "US", "DE"], model.country, placeholder: "Choose...")

    pick_list "country", ["UK", "US", "DE"], model.country do
      placeholder "Choose..."
    end

# `pin`
*macro* 

Pin layout for absolute positioning.

## Example

    pin "overlay" do
      text("Pinned content")
    end

# `pointer_area`
*macro* 

Pointer area for capturing mouse events on children.

## Options

- `:on_press`, `:on_release`, `:on_right_press`, `:on_middle_press`
- `:on_enter`, `:on_exit`

## Example

    pointer_area "clickable" do
      text("Click me")
    end

# `progress_bar`
*macro* 

Progress indicator.

## Forms

- `progress_bar(range, value)` -- auto-generated ID (sugar)
- `progress_bar(id, range, value)` -- explicit ID
- `progress_bar(id, range, value, opts)` -- explicit ID with options

## Arguments

- `range` -- `{min, max}` tuple defining the full range
- `value` -- current value within the range

## Example

    progress_bar({0, 100}, model.progress)
    progress_bar("dl_progress", {0, 100}, model.progress, height: 8)

# `qr_code`
*macro* 

QR code display. No children.

## Arguments

- `id` -- unique identifier
- `data` -- the string to encode

## Options

- `:cell_size` -- size of each QR module in pixels (default 4.0)
- `:cell_color` -- color of dark modules
- `:background` -- color of light modules
- `:error_correction` -- `:low`, `:medium` (default), `:quartile`, `:high`

## Example

    qr_code("my_qr", "https://example.com", cell_size: 6)

    qr_code "my_qr", "https://example.com" do
      cell_size 6
    end

# `quadratic_to`

# `radio`
*macro* 

Radio button for single-value selection from a group.

Use the `group` option so all radios in the same group emit select events
with the group name as the ID instead of each radio's individual ID.

## Example

    radio("size_sm", "small", model.size, label: "Small", group: "size")
    radio("size_lg", "large", model.size, label: "Large", group: "size")

    radio "size_sm", "small", model.size do
      label "Small"
      group "size"
    end

# `rect`
*macro* 

Builds a rectangle shape. See `Plushie.Canvas.Shape.rect/5`.

# `responsive`
*macro* 

Responsive layout that adapts to available size.

## Options

- `:width` / `:height` -- dimensions
- `:id` -- explicit ID (otherwise auto-generated from call site)

## Example

    responsive do
      column do
        text("Adapts to size")
      end
    end

# `rich_text`
*macro* 

Rich text display with styled spans.

## Options

- `:spans` -- list of span descriptors
- `:width` -- width

## Example

    rich_text("styled", spans: [%{text: "bold", weight: :bold}, %{text: " normal"}])

    rich_text "styled" do
      spans [%{text: "bold", weight: :bold}, %{text: " normal"}]
    end

# `rotate`

# `rounded_rect`

# `row`
*macro* 

Horizontal flex layout.

## Options

Same as `column/1`.

## Example

    row spacing: 4 do
      button("yes", "Yes")
      button("no", "No")
    end

# `rule`
*macro* 

Horizontal or vertical divider.

## Example

    rule(width: :fill)

    rule do
      direction :vertical
      style :weak
    end

# `scale`

# `scale`

# `scrollable`
*macro* 

Scrollable region.

## Example

    scrollable "feed" do
      for item <- items do
        text(item.title)
      end
    end

# `sensor`
*macro* 

Sensor for detecting layout changes on children.

## Options

- `:on_resize`, `:on_appear`

## Example

    sensor "tracked" do
      text("Monitored content")
    end

# `sequence`
*macro* 

Creates a sequential animation chain.

Steps execute one after another on the same prop. Accepts a
list of transition/spring descriptors.

## Examples

    opacity: sequence([
      transition(200, to: 1.0, from: 0.0),
      loop(800, to: 0.7, from: 1.0, cycles: 3),
      transition(300, to: 0.0)
    ])

    opacity: sequence do
      transition(200, to: 1.0, from: 0.0)
      transition(300, to: 0.0)
    end

# `slider`
*macro* 

Horizontal slider for numeric range input.

## Arguments

- `range` -- `{min, max}` tuple or `min..max` Range
- `value` -- current value

## Example

    slider("volume", {0, 100}, model.volume, step: 5)
    slider("volume", 0..100, model.volume, step: 5)

    slider "volume", {0, 100}, model.volume do
      step 5
    end

# `space`
*macro* 

Flexible spacer. No children.

## Options

- `:width` -- `:fill`, `:shrink`, or number
- `:height` -- `:fill`, `:shrink`, or number

## Example

    space(width: :fill)

    space do
      width :fill
    end

# `spring`
*macro* 

Creates a physics-based spring descriptor.

Springs have no fixed duration -- they settle naturally based
on stiffness and damping.

## Examples

    scale: spring(to: 1.05, preset: :bouncy)
    scale: spring(to: 1.05, stiffness: 200, damping: 20)

    scale: spring do
      to 1.05
      preset :bouncy
    end

# `stack`
*macro* 

Z-axis stacking layout (overlays).

## Example

    stack do
      image("bg", "/path/to/bg.png")
      container "overlay", padding: 16 do
        text("Overlaid text")
      end
    end

# `stroke`
*macro* 

Builds a stroke descriptor. See `Plushie.Canvas.Shape.stroke/3`.

# `svg`
*macro* 

SVG image display.

## Example

    svg("icon", "/assets/icon.svg", width: 24, height: 24)

    svg "icon", "/assets/icon.svg" do
      width 24
      height 24
    end

# `table`
*macro* 

Data table widget.

## Options

- `:columns` -- list of column descriptors (`%{key, label, width}`)
- `:rows` -- list of row data maps

The `do` block can contain row content templates (e.g. for custom cell
rendering). Children from the block are stored in `:children`.

## Examples

    table("users", columns: cols, rows: data)

    table "users", columns: cols, rows: data do
      text("custom footer")
    end

# `text`
*macro* 

Text label.

## Forms

- `text(content)` -- auto-generated ID (sugar for quick labels)
- `text(id, content)` -- explicit ID
- `text(id, content, opts)` -- explicit ID with options

## Example

    text("Hello, world!")
    text("greeting", "Hello, world!", size: 18)

# `text_editor`
*macro* 

Multi-line text editor.

## Example

    text_editor("notes", model.notes, width: :fill, height: 200)

    text_editor "notes", model.notes do
      width :fill
      height 200
    end

# `text_input`
*macro* 

Single-line text input.

Emits `%WidgetEvent{type: :input, id: id, value: value}` on change and `%WidgetEvent{type: :submit, id: id, value: value}` on Enter.

## Example

    text_input("name", model.name, placeholder: "Your name")

    text_input "name", model.name do
      placeholder "Your name"
    end

# `themer`
*macro* 

Per-subtree theme override.

## Options

- `:theme` -- theme name string or custom palette map

## Example

    themer "dark_section", theme: "Dark" do
      column do
        text("This subtree uses the dark theme")
      end
    end

# `toggler`
*macro* 

Toggle switch.

Emits `%WidgetEvent{type: :toggle, id: id, value: boolean}` when toggled.

## Example

    toggler("dark_mode", model.dark_mode, label: "Dark mode")

    toggler "dark_mode", model.dark_mode do
      label "Dark mode"
    end

# `tooltip`
*macro* 

Tooltip wrapper. Children are the content being tooltipped.

## Forms

- `tooltip(id, tip, do: block)` -- with children
- `tooltip(id, tip, opts, do: block)` -- with children and options

## Example

    tooltip "save_tip", "Save your work", position: :top do
      button("save", "Save")
    end

# `transition`
*macro* 

Creates a timed transition descriptor for animated prop values.

The renderer handles interpolation locally -- zero wire traffic
during animation. Duration can be a positional argument or a
keyword.

## Examples

    opacity: transition(300, to: 0.0)
    opacity: transition(300, to: 0.0, easing: :ease_out)
    opacity: transition(to: 0.0, duration: 300)

    # Enter animation
    opacity: transition(200, to: 1.0, from: 0.0)

    # Do-block
    opacity: transition 300 do
      to 0.0
      easing :ease_out
    end

# `transition`
*macro* 

# `translate`

# `vertical_slider`
*macro* 

Vertical slider for numeric range input.

Same as `slider/4` but oriented vertically. Accepts `{min, max}` or `min..max`.

## Example

    vertical_slider("brightness", {0, 100}, model.brightness)
    vertical_slider("brightness", 0..100, model.brightness)

    vertical_slider "brightness", {0, 100}, model.brightness do
      step 1
    end

# `window`
*macro* 

Top-level window container.

## Arguments

- `id`   -- stable string identifier for this window
- `opts` -- keyword list; common option: `:title`

## Example

    window "main", title: "My App" do
      column do
        text("Hello")
      end
    end

---

*Consult [api-reference.md](api-reference.md) for complete listing*
