Plushie.UI (Plushie v0.6.0)

Copy Markdown View Source

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.

Summary

Functions

Canvas for drawing shapes organized into named layers.

Vertical flex layout.

Combo box with free-text input and dropdown suggestions.

Generic box with alignment and padding.

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

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

Finds all nodes matching a predicate.

Floating overlay layout.

Groups child shapes with optional positioning and interaction.

Returns all node IDs in the tree.

Keyed column for efficient list diffing.

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

Creates a looping transition descriptor.

Markdown content renderer.

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

Pane grid for resizable tiled panes.

Dropdown pick list for selecting from a list of options.

Pin layout for absolute positioning.

Pointer area for capturing mouse events on children.

Progress indicator.

QR code display. No children.

Radio button for single-value selection from a group.

Responsive layout that adapts to available size.

Rich text display with styled spans.

Horizontal flex layout.

Horizontal or vertical divider.

Sensor for detecting layout changes on children.

Creates a sequential animation chain.

Horizontal slider for numeric range input.

Flexible spacer. No children.

Creates a physics-based spring descriptor.

Z-axis stacking layout (overlays).

Data table widget.

Text label.

Per-subtree theme override.

Tooltip wrapper. Children are the content being tooltipped.

Creates a timed transition descriptor for animated prop values.

Vertical slider for numeric range input.

Top-level window container.

Functions

arc(cx, cy, r, start_angle, end_angle)

See Plushie.Canvas.Shape.arc/5.

arc_to(x1, y1, x2, y2, radius)

See Plushie.Canvas.Shape.arc_to/5.

bezier_to(cp1x, cp1y, cp2x, cp2y, x, y)

See Plushie.Canvas.Shape.bezier_to/6.

button(id, positional, opts_or_do \\ [])

(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(id, opts_or_do \\ [])

(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(id, positional, opts_or_do \\ [])

(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(x, y, r, opts_or_do \\ [])

(macro)

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

clip(x, y, w, h)

See Plushie.Canvas.Shape.clip/4.

close()

See Plushie.Canvas.Shape.close/0.

column(opts_or_block \\ [])

(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(id, options, value, opts_or_do \\ [])

(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(id, opts_or_do \\ [])

(macro)

Generic box with alignment and padding.

Example

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

ellipse(cx, cy, rx, ry, rotation, start_angle, end_angle)

See Plushie.Canvas.Shape.ellipse/7.

exists?(tree, id)

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

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

find(tree, id)

@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(tree, id, window_id)

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

See Plushie.Tree.find/3.

find_all(tree, id_or_pred)

@spec find_all(
  tree :: Plushie.Widget.ui_node() | nil,
  id_or_pred :: String.t() | (Plushie.Widget.ui_node() -> boolean())
) :: [Plushie.Widget.ui_node()]

Finds all nodes matching a predicate.

floating(id, opts_or_do \\ [])

(macro)

Floating overlay layout.

Example

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

grid(opts_or_block \\ [])

(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(opts_or_do \\ [])

(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(tree)

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

Returns all node IDs in the tree.

image(id, positional, opts_or_do \\ [])

(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(opts_or_block \\ [])

(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(name, list)

(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(x1, y1, x2, y2, opts_or_do \\ [])

(macro)

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

line_to(x, y)

See Plushie.Canvas.Shape.line_to/2.

linear_gradient(from, to, stops)

See Plushie.Canvas.Shape.linear_gradient/3.

loop(opts_or_do)

(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(duration, opts_or_do)

(macro)

markdown(content)

(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(x, y)

See Plushie.Canvas.Shape.move_to/2.

overlay(id, opts_or_do \\ [])

(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(id, opts_or_do \\ [])

(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(commands, opts_or_do \\ [])

(macro)

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

pick_list(id, options, selected, opts_or_do \\ [])

(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(id, opts_or_do \\ [])

(macro)

Pin layout for absolute positioning.

Example

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

pointer_area(id, opts_or_do \\ [])

(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(range, value)

(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(id, positional, opts_or_do \\ [])

(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(cpx, cpy, x, y)

See Plushie.Canvas.Shape.quadratic_to/4.

radio(id, value, selected, opts_or_do \\ [])

(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(x, y, w, h, opts_or_do \\ [])

(macro)

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

responsive(opts_or_block \\ [])

(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(id, opts_or_do \\ [])

(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(angle)

See Plushie.Canvas.Shape.rotate/1.

rounded_rect(x, y, w, h, radius)

See Plushie.Canvas.Shape.rounded_rect/5.

row(opts_or_block \\ [])

(macro)

Horizontal flex layout.

Options

Same as column/1.

Example

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

rule(opts_or_do \\ [])

(macro)

Horizontal or vertical divider.

Example

rule(width: :fill)

rule do
  direction :vertical
  style :weak
end

scale(factor)

See Plushie.Canvas.Shape.scale/1.

scale(x, y)

See Plushie.Canvas.Shape.scale/2.

scrollable(id, opts_or_do \\ [])

(macro)

Scrollable region.

Example

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

sensor(id, opts_or_do \\ [])

(macro)

Sensor for detecting layout changes on children.

Options

  • :on_resize, :on_appear

Example

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

sequence(list_or_do)

(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(id, range, value, opts_or_do \\ [])

(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(opts_or_do \\ [])

(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(opts_or_do)

(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(opts_or_block \\ [])

(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(color, width, opts_or_do \\ [])

(macro)

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

svg(id, positional, opts_or_do \\ [])

(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(id, opts_or_do \\ [])

(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(content)

(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(id, positional, opts_or_do \\ [])

(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(id, positional, opts_or_do \\ [])

(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(id, opts_or_do \\ [])

(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(id, positional, opts_or_do \\ [])

(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(id, tip_or_do)

(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(opts_or_do)

(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(duration, opts_or_do)

(macro)

translate(x, y)

See Plushie.Canvas.Shape.translate/2.

vertical_slider(id, range, value, opts_or_do \\ [])

(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(id, opts_or_do \\ [])

(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