# Events

Events are structs delivered to your `update/2` callback. Each event family
has its own struct under `Plushie.Event.*`.

```elixir
alias Plushie.Event.{Widget, Key, Mouse, Touch, Ime, Window, Effect, System, Sensor, MouseArea, Canvas, Pane}
```

## Widget events

These are generated by user interaction with widgets. The renderer maps
widget interactions to `%Widget{}` structs using the node's `id`.

### Click

<!-- test: events_widget_click_construct_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :click, id: "save"}
```

Generated by `button` widgets when pressed.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :click, id: "save"}), do: save(model)
def update(model, %Widget{type: :click, id: "cancel"}), do: revert(model)
```

### Input

<!-- test: events_widget_input_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :input, id: "search", value: value}
```

Generated by `text_input` on every keystroke (when `on_input` is set,
which is the default).

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :input, id: "search", value: value}) do
  %{model | search_query: value}
end
```

### Submit

<!-- test: events_widget_submit_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :submit, id: "search", value: query}
```

Generated by `text_input` when the user presses Enter (when `on_submit`
is set).

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :submit, id: "search", value: query}) do
  {%{model | search_query: query}, search_command(query)}
end
```

### Toggle

<!-- test: events_widget_toggle_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :toggle, id: "dark_mode", value: enabled}
```

Generated by `checkbox` and `toggler`. `value` is `true` or `false`.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :toggle, id: "dark_mode", value: enabled}) do
  %{model | dark_mode: enabled}
end
```

### Select

<!-- test: events_widget_select_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :select, id: "theme_picker", value: theme}
```

Generated by `pick_list` and `combo_box` when an option is selected.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :select, id: "theme_picker", value: theme}) do
  %{model | theme: theme}
end
```

### Slide

<!-- test: events_widget_slide_match_test, events_widget_slide_release_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :slide, id: "volume", value: value}
%Widget{type: :slide_release, id: "volume", value: value}
```

Generated by `slider` and `vertical_slider`. `slide` fires continuously
during dragging. `slide_release` fires once when the user releases the
slider.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :slide, id: "volume", value: value}) do
  %{model | volume: value}
end

def update(model, %Widget{type: :slide_release, id: "volume", value: value}) do
  # Persist the final value
  {%{model | volume: value}, save_preference(:volume, value)}
end
```

### Text editor content change

```elixir
%Widget{type: :input, id: "notes", value: content}
```

Generated by `text_editor` on content changes. The `value` string
contains the full editor text after each edit. This is the same event
type as `text_input`.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :input, id: "notes", value: content}) do
  %{model | notes_content: content}
end
```

### Key binding

<!-- test: events_widget_key_binding_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :key_binding, id: "editor", value: "save"}
```

Generated by `text_editor` widgets when a declarative key binding rule
with a `"custom"` action matches. The `id` is the text editor's node ID.
The `value` is the custom tag string from the binding rule.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :key_binding, id: "editor", value: "save"}) do
  {model, save_file(model)}
end

def update(model, %Widget{type: :key_binding, id: "editor", value: "format"}) do
  %{model | content: format_code(model.content)}
end
```

### Scroll

<!-- test: events_widget_scroll_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :scroll, id: "log_view", data: %{
  absolute_x: float,
  absolute_y: float,
  relative_x: float,
  relative_y: float,
  bounds: {width, height},
  content_bounds: {width, height}
}}
```

Generated by `scrollable` when the scroll position changes (when
`on_scroll: true` is set). The viewport data includes both absolute pixel
offsets and relative (0.0-1.0) scroll positions, plus the visible bounds
and total content bounds.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :scroll, id: "log_view", data: viewport}) do
  at_bottom = viewport.relative_y >= 0.99
  %{model | auto_scroll: at_bottom}
end
```

### Paste

<!-- test: events_widget_paste_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :paste, id: "url_input", value: text}
```

Generated by `text_input` when the user pastes text (when `on_paste: true`
is set). The `value` field contains the pasted string.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :paste, id: "url_input", value: text}) do
  %{model | url: String.trim(text)}
end
```

### Option hovered

<!-- test: events_widget_option_hovered_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :option_hovered, id: "search", value: value}
```

Generated by `combo_box` when the user hovers over an option in the
dropdown (when `on_option_hovered: true` is set). The `value` is the
hovered option string.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :option_hovered, id: "search", value: value}) do
  %{model | preview: value}
end
```

### Open / Close

<!-- test: events_widget_open_close_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :open, id: "country_picker"}
%Widget{type: :close, id: "country_picker"}
```

Generated by `pick_list` and `combo_box` when the dropdown menu opens or
closes (when `on_open: true` and/or `on_close: true` are set).

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :open, id: "country_picker"}) do
  %{model | picker_open: true}
end

def update(model, %Widget{type: :close, id: "country_picker"}) do
  %{model | picker_open: false}
end
```

### Sort

<!-- test: events_widget_sort_match_test -- keep this code block in sync with the test -->
```elixir
%Widget{type: :sort, id: "users", value: column_key}
```

Generated by `table` when a sortable column header is clicked. The
`value` is the string key from the column descriptor.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :sort, id: "users", value: column_key}) do
  order = if model.sort_by == column_key, do: flip(model.sort_order), else: "asc"
  %{model | sort_by: column_key, sort_order: order}
end
```

### Mouse area events

Mouse area events use the `%MouseArea{}` struct:

<!-- test: events_mouse_area_enter_match_test, events_mouse_area_move_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.MouseArea

%MouseArea{type: :right_press, id: "canvas"}
%MouseArea{type: :enter, id: "tooltip-target"}
%MouseArea{type: :move, id: "drag-zone", x: x, y: y}
%MouseArea{type: :scroll, id: "scroll-zone", delta_x: dx, delta_y: dy}
```

Available types: `:right_press`, `:right_release`, `:middle_press`,
`:middle_release`, `:double_click`, `:enter`, `:exit`, `:move`, `:scroll`.

Each event requires its corresponding boolean prop to be set on the
mouse_area widget. Without the prop, the event is not emitted.

Note: left press/release events from mouse_area are delivered as
`%Widget{type: :click}` events.

```elixir
alias Plushie.Event.MouseArea

def update(model, %MouseArea{type: :enter, id: "hover_zone"}) do
  %{model | hovered: true}
end

def update(model, %MouseArea{type: :move, id: "canvas_area", x: x, y: y}) do
  %{model | cursor: {x, y}}
end
```

### Canvas events

Generated by `canvas` widgets. Each event is opt-in via a boolean prop on
the canvas node. Canvas events use the `%Canvas{}` struct.

<!-- test: events_canvas_press_match_test, events_canvas_move_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Canvas

%Canvas{type: :press, id: "draw_area", x: x, y: y, button: "left"}
%Canvas{type: :release, id: "draw_area", x: x, y: y, button: "left"}
%Canvas{type: :move, id: "draw_area", x: x, y: y}
%Canvas{type: :scroll, id: "draw_area", x: x, y: y, delta_x: dx, delta_y: dy}
```

The `button` field is a string (`"left"`, `"right"`, `"middle"`). The
`x`/`y` coordinates are relative to the canvas origin.

```elixir
alias Plushie.Event.Canvas

def update(model, %Canvas{type: :press, id: "draw_area", x: x, y: y, button: "left"}) do
  %{model | drawing: true, last_point: {x, y}}
end

def update(model, %Canvas{type: :move, id: "draw_area", x: x, y: y}) do
  if model.drawing do
    %{model | last_point: {x, y}, strokes: [{x, y} | model.strokes]}
  else
    model
  end
end
```

### Canvas shape events

When a canvas contains shapes with an `interactive` field (see
[composition patterns](composition-patterns.md#canvas-interactive-shapes)),
the renderer handles hit testing locally and emits semantic shape
events. These arrive as `%Widget{}` structs (not `%Canvas{}`). The
`id` is the canvas widget ID; `data["element_id"]` identifies the shape.

<!-- test: events_canvas_element_event_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Widget

# Cursor entered a shape's bounds
%Widget{type: :canvas_element_enter, id: "chart", data: %{"element_id" => "bar-jan", "x" => 15.0, "y" => 70.0}}

# Cursor left a shape's bounds
%Widget{type: :canvas_element_leave, id: "chart", data: %{"element_id" => "bar-jan"}}

# Click on a shape
%Widget{type: :canvas_element_click, id: "chart", data: %{"element_id" => "bar-jan", "x" => 15.0, "y" => 70.0, "button" => "left"}}

# Drag on a draggable shape (rate-limited by event_rate)
%Widget{type: :canvas_element_drag, id: "chart", data: %{"element_id" => "handle", "x" => 50.0, "y" => 80.0, "delta_x" => 2.0, "delta_y" => -1.0}}

# Drag ended
%Widget{type: :canvas_element_drag_end, id: "chart", data: %{"element_id" => "handle", "x" => 52.0, "y" => 79.0}}

# Shape received keyboard focus (Tab/Arrow navigation)
%Widget{type: :canvas_element_focused, id: "chart", data: %{"element_id" => "bar-jan"}}
```

Hover styles, pressed styles, cursors, and tooltips on shapes are
handled by the renderer locally -- no round-trip needed. Shape events
give the host semantic actions (clicks, drags, focus changes) instead
of raw coordinates.

```elixir
def update(model, %Widget{type: :canvas_element_click, id: "chart", data: %{"element_id" => element_id}}) do
  %{model | selected_bar: element_id}
end
```

### Sensor events

Generated by `sensor` widgets when the sensor detects a size change.

<!-- test: events_sensor_resize_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Sensor

%Sensor{type: :resize, id: "content_area", width: w, height: h}
```

```elixir
alias Plushie.Event.Sensor

def update(model, %Sensor{type: :resize, id: "content_area", width: w, height: h}) do
  %{model | content_size: {w, h}}
end
```

### PaneGrid events

Generated by `pane_grid` widgets during pane interactions. Pane events
use the `%Pane{}` struct.

<!-- test: events_pane_resized_match_test, events_pane_clicked_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Pane

%Pane{type: :resized, id: "editor", split: split, ratio: ratio}
%Pane{type: :dragged, id: "editor", pane: source, target: target}
%Pane{type: :clicked, id: "editor", pane: pane}
```

```elixir
alias Plushie.Event.Pane

def update(model, %Pane{type: :resized, id: "editor", split: split, ratio: ratio}) do
  put_in(model, [:splits, split], ratio)
end

def update(model, %Pane{type: :clicked, id: "editor", pane: pane}) do
  %{model | active_pane: pane}
end
```

## Keyboard events

Delivered when keyboard subscriptions are active (see
[commands.md](commands.md)).

<!-- test: events_key_press_cmd_s_match_test, events_key_press_escape_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Key

%Key{type: :press, key: "s", modifiers: %{command: true}}
%Key{type: :release, key: :escape}
```

### Key struct

```elixir
%Plushie.Event.Key{
  type: :press | :release,
  key: :enter | "a" | ...,           # logical key (named atom or character string)
  modified_key: :enter | "A" | ...,  # key with modifiers applied (e.g. Shift+a = "A")
  physical_key: atom(),               # physical key code (layout-independent)
  location: :standard | :left | :right | :numpad,
  modifiers: %Plushie.KeyModifiers{},
  text: String.t() | nil,            # text produced by the key press (nil for non-printable)
  repeat: boolean(),                   # true if this is an auto-repeat event
  captured: boolean()
}
```

### KeyModifiers struct

<!-- test: events_modifiers_construct_test -- keep this code block in sync with the test -->
```elixir
%Plushie.KeyModifiers{
  shift: boolean(),
  ctrl: boolean(),
  alt: boolean(),
  logo: boolean(),
  command: boolean()    # platform-aware: ctrl on Linux/Windows, logo (Cmd) on macOS
}
```

### Key values

Named keys are atoms:

```elixir
:enter, :escape, :tab, :backspace, :delete,
:arrow_up, :arrow_down, :arrow_left, :arrow_right,
:home, :end, :page_up, :page_down,
:f1, :f2, ... :f12,
:space
```

Character keys are single-character strings:

```elixir
"a", "b", "1", "/", " "
```

### Keyboard event examples

<!-- test: events_key_press_physical_key_match_test, events_key_press_text_field_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Key

def update(model, %Key{type: :press, key: "s", modifiers: %{command: true}}) do
  {model, save_command()}
end

def update(model, %Key{type: :press, key: :escape}) do
  %{model | modal_open: false}
end

def update(model, %Key{type: :press, key: "z", modifiers: %{command: true, shift: false}}) do
  undo(model)
end

def update(model, %Key{type: :press, key: "z", modifiers: %{command: true, shift: true}}) do
  redo(model)
end

# Use physical_key for layout-independent bindings (e.g. WASD on non-QWERTY)
def update(model, %Key{type: :press, physical_key: :key_w}) do
  move_up(model)
end

# Use text field for text input handling
def update(model, %Key{type: :press, text: text}) when text != nil do
  append_char(model, text)
end
```

## IME events

Delivered when IME subscriptions are active (see
[commands.md](commands.md)). Input Method Editor events support CJK and
other compose-based text input.

<!-- test: events_ime_preedit_match_test, events_ime_commit_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Ime

%Ime{type: :opened}
%Ime{type: :preedit, text: text, cursor: {start, end_pos} | nil}
%Ime{type: :commit, text: text}
%Ime{type: :closed}
```

`preedit` fires during composition with the in-progress text and optional
cursor range. `commit` fires when the user finalises a composed string.

```elixir
alias Plushie.Event.Ime

def update(model, %Ime{type: :preedit, text: text}) do
  %{model | composing: text}
end

def update(model, %Ime{type: :commit, text: text}) do
  %{model | composing: nil, value: model.value <> text}
end
```

## Mouse events (global)

Delivered when mouse subscriptions are active. These are global
(not widget-scoped) events from the windowing system.

<!-- test: events_mouse_moved_match_test, events_mouse_button_pressed_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Mouse

%Mouse{type: :moved, x: x, y: y}
%Mouse{type: :entered}
%Mouse{type: :left}
%Mouse{type: :button_pressed, button: :left}
%Mouse{type: :button_released, button: :right}
%Mouse{type: :wheel_scrolled, delta_x: dx, delta_y: dy, unit: :line}
```

The `button` field is an atom (`:left`, `:right`, `:middle`, `:back`, `:forward`).
The `unit` field indicates scroll units (`:line` or `:pixel`).

```elixir
alias Plushie.Event.Mouse

def update(model, %Mouse{type: :moved, x: x, y: y}) do
  %{model | cursor: {x, y}}
end

def update(model, %Mouse{type: :button_pressed, button: :left}) do
  %{model | mouse_down: true}
end
```

## Touch events

Delivered when touch subscriptions are active.

<!-- test: events_touch_pressed_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Touch

%Touch{type: :pressed, finger_id: fid, x: x, y: y}
%Touch{type: :moved, finger_id: fid, x: x, y: y}
%Touch{type: :lifted, finger_id: fid, x: x, y: y}
%Touch{type: :lost, finger_id: fid, x: x, y: y}
```

```elixir
alias Plushie.Event.Touch

def update(model, %Touch{type: :pressed, x: x, y: y}) do
  %{model | touch_start: {x, y}}
end
```

## Modifier state events

Delivered when modifier key state changes (subscription-driven).

<!-- test: events_modifiers_changed_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Modifiers

%Modifiers{
  modifiers: %Plushie.KeyModifiers{
    shift: true, ctrl: false, alt: false, logo: false, command: false
  },
  captured: false
}
```

```elixir
def update(model, %Modifiers{modifiers: %{shift: true}}) do
  %{model | shift_held: true}
end
```

## Window events

Delivered when window subscriptions are active or for lifecycle events
on windows the app manages.

<!-- test: events_window_close_requested_match_test, events_window_resized_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Window

%Window{type: :close_requested, window_id: "main"}
%Window{type: :opened, window_id: "main", position: {x, y}, width: w, height: h}
%Window{type: :closed, window_id: "main"}
%Window{type: :moved, window_id: "main", x: x, y: y}
%Window{type: :resized, window_id: "main", width: w, height: h}
%Window{type: :focused, window_id: "main"}
%Window{type: :unfocused, window_id: "main"}
%Window{type: :rescaled, window_id: "main", scale_factor: factor}
%Window{type: :file_hovered, window_id: "main", path: path}
%Window{type: :file_dropped, window_id: "main", path: path}
%Window{type: :files_hovered_left, window_id: "main"}
```

### Handling window close

```elixir
alias Plushie.Event.Widget
alias Plushie.Event.Window

def update(model, %Window{type: :close_requested, window_id: "main"}) do
  if model.unsaved_changes do
    %{model | confirm_exit: true}
  else
    {model, Plushie.Command.close_window("main")}
  end
end
```

### File drag and drop

<!-- test: events_window_file_drag_drop_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Window

def update(model, %Window{type: :file_hovered, window_id: "main", path: path}) do
  %{model | drop_target_active: true, hovered_file: path}
end

def update(model, %Window{type: :file_dropped, window_id: "main", path: path}) do
  {%{model | drop_target_active: false}, load_file(path)}
end

def update(model, %Window{type: :files_hovered_left, window_id: "main"}) do
  %{model | drop_target_active: false}
end
```

## System events

<!-- test: events_animation_frame_construct_test, events_theme_changed_construct_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.System

%System{type: :animation_frame, data: timestamp}
%System{type: :theme_changed, data: mode}
```

Animation frame events are delivered on each frame when an animation
subscription is active. Theme changed events fire when the OS theme
switches. The `data` field holds the timestamp or the mode string
(`"light"` or `"dark"`).

## Timer events

Delivered by timer subscriptions.

<!-- test: events_timer_tick_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Timer

%Timer{tag: :tick, timestamp: ts}
```

Where `timestamp` is the monotonic time in milliseconds.

## Command result events

Delivered when an async command completes.

<!-- test: events_async_result_ok_match_test, events_async_result_error_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Async

%Async{tag: :data_loaded, result: {:ok, value}}
%Async{tag: :data_loaded, result: {:error, reason}}
```

Where `tag` is the atom you passed to `Plushie.Command.async/2`.

```elixir
alias Plushie.Event.Widget

def update(model, %Widget{type: :click, id: "fetch"}) do
  {model, Plushie.Command.async(fn -> HTTP.get!("/api/data") end, :data_loaded)}
end

def update(model, {:data_loaded, %{body: body}}) do
  %{model | data: body}
end
```

## Effect result events

Delivered when a renderer effect completes.

<!-- test: events_effect_response_ok_match_test, events_effect_response_cancelled_match_test, events_effect_response_error_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Effect

%Effect{request_id: "ef_1234", result: {:ok, data}}
%Effect{request_id: "ef_1234", result: :cancelled}
%Effect{request_id: "ef_1234", result: {:error, reason}}
```

The `request_id` is the auto-generated effect ID (e.g. `"ef_1234"`), not
the effect kind name.

```elixir
alias Plushie.Event.Effect

def update(model, %Effect{result: {:ok, path}}) do
  {model, load_file(path)}
end

def update(model, %Effect{result: {:error, reason}}) do
  %{model | error: reason}
end
```

See [effects.md](effects.md).

### Accessibility action

```elixir
{:a11y_action, id, action_name}
```

Generated when an assistive technology triggers a non-standard action on a
widget. Standard AT actions are mapped to normal events: Click/Default
becomes `%Widget{type: :click, id: id}`, SetValue becomes
`%Widget{type: :input, id: id, value: value}`. This event catches
everything else.

The `action_name` is a string representation of the accesskit action (e.g.
`"ScrollDown"`, `"Increment"`, `"Decrement"`).

```elixir
def update(model, {:a11y_action, "volume", "Increment"}) do
  %{model | volume: min(model.volume + 5, 100)}
end
```

Note: This event is only generated when the renderer is built with the
`a11y` feature flag.

## Catch-all

Always include a catch-all clause:

```elixir
def update(model, _event), do: model
```

Unknown events are silently ignored. This is important for forward
compatibility -- new widget types or renderer versions may emit events
your app does not yet handle.

## Pattern matching tips

Events are structs, so all of Elixir's pattern matching works:

<!-- test: events_pattern_prefix_match_test, events_pattern_toggle_prefix_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Widget

# Match any click
def update(model, %Widget{type: :click, id: id}) do
  IO.inspect(id, label: "clicked")
  handle_click(model, id)
end

# Match clicks with a prefix
def update(model, %Widget{type: :click, id: "nav:" <> section}) do
  %{model | section: String.to_existing_atom(section)}
end

# Match toggle on any "setting:" prefixed checkbox
def update(model, %Widget{type: :toggle, id: "setting:" <> key, value: value}) do
  put_in(model, [:settings, String.to_existing_atom(key)], value)
end
```

### Scope matching

Widget events include a `scope` field listing ancestor container IDs,
nearest first. You can pattern match on scope to distinguish events from
different contexts:

<!-- test: events_scope_sidebar_match_test, events_scope_main_match_test -- keep this code block in sync with the test -->
```elixir
alias Plushie.Event.Widget

# Button inside the "sidebar" container
def update(model, %Widget{type: :click, id: "save", scope: ["sidebar" | _]}) do
  save_sidebar(model)
end

# Same button ID but inside "main" container
def update(model, %Widget{type: :click, id: "save", scope: ["main" | _]}) do
  save_main(model)
end
```
