# Canvas

The canvas system provides 2D drawing with typed shape structs,
transforms, interactive groups, and accessibility support. Unlike
layout widgets that compose children, canvas draws shapes on a
surface: rectangles, circles, lines, paths, text, images, and SVG.

For a narrative introduction, see the [Canvas guide](../guides/12-canvas.md).

## Canvas widget

`Plushie.Widget.Canvas`

The canvas is a widget that contains named layers of shapes. It
participates in the layout system like any other widget (has width,
height, background) but its content is drawn shapes, not child widgets.

### Props

| Prop | Type | Default | Purpose |
|---|---|---|---|
| `width` | Length | `:fill` | Canvas width |
| `height` | Length | `200` | Canvas height (pixels) |
| `background` | Color | *n/a* | Canvas background colour |
| `on_press` | boolean | `false` | Emit `:press` events |
| `on_release` | boolean | `false` | Emit `:release` events |
| `on_move` | boolean | `false` | Emit `:move` events |
| `on_scroll` | boolean | `false` | Emit `:scroll` events |
| `event_rate` | integer | *n/a* | Max events/sec for canvas-level events |
| `a11y` | map | *n/a* | Accessibility overrides. See [Accessibility](accessibility.md). |

## Layers

Canvas content is organised into named layers. Each layer is drawn
independently:

```elixir
canvas "chart", width: 400, height: 200 do
  layer "background" do
    rect(0, 0, 400, 200, fill: "#f5f5f5")
  end

  layer "data" do
    rect(10, 50, 80, 150, fill: "#3b82f6")
    rect(110, 100, 80, 100, fill: "#22c55e")
  end

  layer "labels" do
    text(50, 190, "A", fill: "#333", size: 12)
    text(150, 190, "B", fill: "#333", size: 12)
  end
end
```

**Drawing order**: layers are drawn in alphabetical order by name.
`"background"` draws before `"data"` which draws before `"labels"`.
This is the z-ordering mechanism. Name your layers to control which
appears on top.

**Independent caching**: each layer maps to a separate cache on the
renderer side. When a layer's shapes change, only that layer is
re-tessellated. Unchanged layers are drawn from cache. For canvases
with many shapes, splitting into layers (static background, dynamic
data, interactive controls) significantly reduces rendering work.

## Shape catalog

All shapes are builder functions in `Plushie.Canvas.Shape` that return
typed structs. Use them inside `layer` or `group` blocks.

| Function | Struct | Required args | Key options |
|---|---|---|---|
| `rect/4` | `Rect` | x, y, w, h | `fill`, `stroke`, `opacity`, `radius` |
| `circle/3` | `Circle` | x, y, r | `fill`, `stroke`, `opacity` |
| `line/4` | `Line` | x1, y1, x2, y2 | `stroke`, `opacity` |
| `text/3` | `CanvasText` | x, y, content | `fill`, `size`, `font`, `align_x`, `align_y`, `opacity` |
| `path/1` | `Path` | commands | `fill`, `stroke`, `opacity`, `fill_rule` |
| `image/5` | `CanvasImage` | source, x, y, w, h | `rotation`, `opacity` |
| `svg/5` | `CanvasSvg` | source, x, y, w, h | *n/a* |

Common shape options:

- **`fill`**: hex colour string, named atom, or gradient
  (`linear_gradient`). Fills the shape interior.
- **`stroke`**: a stroke descriptor (see [Strokes](#strokes)). Draws
  the shape outline.
- **`opacity`**: 0.0 (transparent) to 1.0 (opaque).

## Path commands

Functions that return path command data for use with `path/1`:

| Function | Arguments | Description |
|---|---|---|
| `move_to/2` | x, y | Move pen without drawing |
| `line_to/2` | x, y | Straight line to point |
| `bezier_to/6` | cp1x, cp1y, cp2x, cp2y, x, y | Cubic bezier curve |
| `quadratic_to/4` | cpx, cpy, x, y | Quadratic bezier curve |
| `arc/5` | cx, cy, r, start_angle, end_angle | Arc by centre |
| `arc_to/5` | x1, y1, x2, y2, radius | Tangent arc |
| `ellipse/7` | cx, cy, rx, ry, rotation, start, end | Ellipse arc |
| `rounded_rect/5` | x, y, w, h, radius | Rounded rectangle path |
| `close/0` | *n/a* | Close the current subpath |

```elixir
path([
  move_to(10, 0),
  line_to(20, 20),
  line_to(0, 20),
  close()
], fill: "#22c55e")
```

## Strokes

`stroke/3` creates a stroke descriptor:

```elixir
stroke("#333", 2)                        # colour and width
stroke("#333", 2, cap: :round)           # with line cap
stroke("#333", 2, dash: {[5, 3], 0})     # dashed line
```

| Option | Values | Default | Purpose |
|---|---|---|---|
| `cap:` | `:butt`, `:round`, `:square` | `:butt` | Line end style |
| `join:` | `:miter`, `:round`, `:bevel` | `:miter` | Corner join style |
| `dash:` | `{segments, offset}` | *n/a* | Dash pattern (segment lengths + initial offset) |

Strokes support the Buildable do-block syntax:

```elixir
rect(0, 0, 100, 50) do
  fill "#3b82f6"
  stroke do
    color "#333"
    width 2
    cap :round
  end
end
```

See `Plushie.Canvas.Shape.Stroke` and `Plushie.Canvas.Shape.Dash`.

## Gradients (canvas)

`linear_gradient/3` creates a gradient for use as a fill in canvas
shapes. This is different from `Plushie.Type.Gradient.linear/2` (which
uses an angle for widget backgrounds). Canvas gradients use coordinate
pairs:

```elixir
rect(0, 0, 200, 50,
  fill: linear_gradient({0, 0}, {200, 0}, [
    {0.0, "#3b82f6"},
    {1.0, "#1d4ed8"}
  ])
)
```

The first argument is the start point `{x, y}`, the second is the end
point `{x, y}`. Stops are `{offset, colour}` tuples where offset is
0.0-1.0.

See `Plushie.Canvas.Shape.LinearGradient`.

## SVG and images

Embed external visuals in canvas layers:

```elixir
layer "icons" do
  svg(File.read!("priv/icons/save.svg"), 10, 8, 20, 20)
  image("priv/images/logo.png", 50, 8, 32, 32, opacity: 0.8)
end
```

`svg/5` takes an SVG source string (not a file path; read the file
first). `image/5` takes a file path string.

Combined with interactive groups, SVG content can be made clickable,
hoverable, and keyboard-accessible:

```elixir
group "save", on_click: true, cursor: :pointer,
  focusable: true, a11y: %{role: :button, label: "Save"} do
  svg(File.read!("priv/icons/save.svg"), 0, 0, 36, 36)
end
```

## Transforms

Transforms apply to **groups only**, not individual shapes. They are
applied in declaration order.

| Function | Arguments | Description |
|---|---|---|
| `translate/2` | x, y | Move the group |
| `rotate/1` | angle | Rotate around origin (degrees by default) |
| `rotate/1` | `degrees: n` or `radians: n` | Explicit unit |
| `scale/1` | factor | Uniform scale |
| `scale/2` | x, y | Non-uniform scale |

```elixir
group x: 100, y: 50 do
  rotate(45)                    # 45 degrees
  rotate(radians: 0.785)        # explicit radians
  rect(0, 0, 40, 40, fill: "#ef4444")
end
```

The `x:` and `y:` keyword options on groups desugar to a leading
`translate`.

## Clips

`clip/4` restricts drawing to a rectangular region. One clip per group.

```elixir
group do
  clip(0, 0, 80, 80)
  circle(40, 40, 60, fill: "#3b82f6")  # clipped to 80x80 square
end
```

See `Plushie.Canvas.Shape.Clip`.

## Interactive groups

`Plushie.Canvas.Shape.Group` is the only shape type that supports
interactivity. Any collection of shapes can become clickable, hoverable,
draggable, or keyboard-focusable by wrapping them in an interactive
group.

### Interaction props

| Prop | Type | Default | Purpose |
|---|---|---|---|
| `on_click` | boolean | `false` | Enable `:click` events (scoped under canvas ID) |
| `on_hover` | boolean | `false` | Enable `:enter`/`:exit` events |
| `draggable` | boolean | `false` | Enable `:drag`/`:drag_end` events |
| `drag_axis` | `"x"` / `"y"` / `"both"` | *n/a* | Constrain drag direction (unconstrained when not set) |
| `drag_bounds` | DragBounds | *n/a* | Limit drag region (`%{min_x, max_x, min_y, max_y}`) |
| `focusable` | boolean | `false` | Add to Tab order for keyboard navigation |
| `cursor` | atom/string | *n/a* | Cursor style on hover (`:pointer`, `:grab`, etc.) |
| `tooltip` | string | *n/a* | Tooltip text on hover |
| `hit_rect` | HitRect | *n/a* | Custom hit testing region (`%{x, y, w, h}`) |

### Visual feedback props

| Prop | Type | Purpose |
|---|---|---|
| `hover_style` | ShapeStyle | Override `fill`, `stroke`, `opacity` on hover |
| `pressed_style` | ShapeStyle | Override while pressed |
| `focus_style` | ShapeStyle | Override when keyboard-focused |
| `show_focus_ring` | boolean | Show/hide the default focus indicator |
| `focus_ring_radius` | number | Corner radius for the focus ring |

ShapeStyle accepts: `fill` (colour or gradient), `stroke` (colour or
stroke descriptor), `opacity` (0.0-1.0). Only specified fields are
overridden; others inherit from the shape's base values.

### Accessibility

Canvas is a raw drawing surface with no inherent semantic knowledge.
Interactive groups need explicit `a11y` annotations for screen reader
and keyboard support:

```elixir
group "hue-ring",
  on_click: true,
  focusable: true,
  a11y: %{role: :slider, label: "Hue", value: "#{round(hue)} degrees"} do
  # ... shapes
end
```

Without `a11y` annotations, interactive groups are invisible to
assistive technology. See the [Accessibility reference](accessibility.md)
for the full set of fields and roles.

## Canvas events

All canvas events arrive as `Plushie.Event.WidgetEvent` structs.

### Canvas-level events

Require `on_press`/`on_release`/`on_move`/`on_scroll` props on the
canvas widget. These use the unified pointer event model with device
type and modifier information. Mouse, touch, and pen input all produce
the same event types with full hit testing, drag, and click support.
The `pointer` field identifies the device, `finger` carries the touch
finger ID (nil for mouse), and `modifiers` carries the current modifier
key state:

| Event type | Data fields |
|---|---|
| `:press` | `x`, `y`, `button`, `pointer`, `finger`, `modifiers` |
| `:release` | `x`, `y`, `button`, `pointer`, `finger`, `modifiers` |
| `:move` | `x`, `y`, `pointer`, `finger`, `modifiers` |
| `:scroll` | `x`, `y`, `delta_x`, `delta_y`, `pointer`, `modifiers` |

Touch events use `pointer: :touch` with `button: :left` and include
a `finger` integer identifying the touch point:

```elixir
# Handle touch press on canvas
def update(model, %WidgetEvent{type: :press, id: "drawing",
    data: %{x: x, y: y, pointer: :touch, finger: 0}}) do
  start_stroke(model, x, y)
end

# Handle touch drag
def update(model, %WidgetEvent{type: :move, id: "drawing",
    data: %{x: x, y: y, pointer: :touch, finger: 0}}) do
  continue_stroke(model, x, y)
end
```

### Element-level events

Require interaction props on interactive groups:

| Event type | Trigger | Data fields |
|---|---|---|
| `:click` | `on_click: true` | Scoped under canvas ID (see below) |
| `:enter` | `on_hover: true` | *n/a* |
| `:exit` | `on_hover: true` | *n/a* |
| `:drag` | `draggable: true` | `x`, `y`, `delta_x`, `delta_y` |
| `:drag_end` | `draggable: true` | `x`, `y` |
| `:key_press` | `focusable: true` | `key`, `modifiers`, `text` |
| `:key_release` | `focusable: true` | `key`, `modifiers` |
| `:focused` | `focusable: true` | *n/a* |
| `:blurred` | `focusable: true` | *n/a* |

Canvas element clicks are regular `:click` events. The renderer
emits them with the element's scoped ID (e.g., `"my-canvas/handle"`),
and the SDK's scoped ID system splits this into `id: "handle"` with
`scope: ["my-canvas", window_id]`. Match them like any scoped click:

```elixir
def update(model, %WidgetEvent{type: :click, id: "handle", scope: ["my-canvas" | _]}) do
  # handle canvas element click
end
```

Other element events (enter, leave, drag, key, focus) use standard
generic event families shared across all widget types.

### Pointer events in custom widgets

Canvas-level pointer events (`:press`, `:release`, `:move`,
`:scroll`) are delivered through the widget handler pipeline
like any other event. If a custom widget's `handle_event/2` does not
intercept them, they reach the parent app's `update/2`.

To transform a pointer event before it reaches `update/2`, handle it in
`handle_event/2` and emit a new event via `{:emit, family, data}`.

## Element scoping

Canvas element IDs participate in the standard
[scoped ID](scoped-ids.md) system. The canvas widget's ID creates a
scope, and interactive group IDs within it are scoped under it:

```
canvas "drawing"              ->  "drawing"
  group "handle" ...          ->  "drawing/handle"
```

Events arrive with the group's local ID, the canvas in the scope, and
the window ID at the end:

```elixir
%WidgetEvent{type: :click, id: "handle", scope: ["drawing", "main"], window_id: "main"}
```

## Examples

### Toggle switch

An interactive group with state-driven thumb position and accessibility
annotations:

```elixir
canvas "switch", width: 64, height: 32 do
  layer "track" do
    group "toggle", on_click: true, cursor: :pointer,
      a11y: %{role: :switch, label: "Dark mode", toggled: model.dark} do
      rect(0, 0, 64, 32, fill: if(model.dark, do: "#3b82f6", else: "#ddd"), radius: 16)
      circle(if(model.dark, do: 44, else: 20), 16, 12, fill: "#fff")
    end
  end
end
```

### Bar chart

Focusable bars with accessibility annotations for screen reader
navigation. Each bar reports its position in the data set:

```elixir
canvas "chart", width: 300, height: 200 do
  layer "bars" do
    for {value, i} <- Enum.with_index(model.data) do
      x = i * 40 + 10
      h = value * 2

      group "bar-#{i}", x: x, y: 200 - h, focusable: true,
        tooltip: "#{value}",
        a11y: %{role: :image, label: "Value: #{value}",
               position_in_set: i + 1, size_of_set: length(model.data)} do
        rect(0, 0, 30, h, fill: "#3b82f6")
      end
    end
  end
end
```

### Canvas with widget overlay

Layer a canvas (custom visuals) under a text_input (standard widget)
using `stack`. The transparent background on the input lets the canvas
decoration show through:

```elixir
stack width: 200, height: 40 do
  canvas "bg", width: 200, height: 40 do
    layer "decor" do
      rect(0, 0, 200, 40, fill: "#f5f5f5", radius: 8)
    end
  end

  text_input("input", model.value,
    placeholder: "Search...",
    style: StyleMap.new() |> StyleMap.background(:transparent)
  )
end
```

## See also

- `Plushie.Canvas.Shape` - all builder functions
- `Plushie.Widget.Canvas` - canvas widget props
- [Canvas guide](../guides/12-canvas.md) - building a canvas button
  for the pad
- [Custom Widgets guide](../guides/13-custom-widgets.md) - canvas-based
  custom widgets
- [Accessibility reference](accessibility.md) - canvas accessibility
  annotations
- [Styling reference](themes-and-styling.md) - `Plushie.Type.Gradient.linear/2`
  for widget background gradients (different from canvas
  `linear_gradient/3`)
