# Events

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

```elixir
alias Toddy.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

```elixir
%Widget{type: :click, id: "save"}
```

Generated by `button` widgets when pressed.

```elixir
alias Toddy.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

```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 Toddy.Event.Widget

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

### Submit

```elixir
%Widget{type: :submit, id: "search", value: query}
```

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

```elixir
alias Toddy.Event.Widget

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

### Toggle

```elixir
%Widget{type: :toggle, id: "dark_mode", value: enabled}
```

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

```elixir
alias Toddy.Event.Widget

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

### Select

```elixir
%Widget{type: :select, id: "theme_picker", value: theme}
```

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

```elixir
alias Toddy.Event.Widget

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

### Slide

```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 Toddy.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 Toddy.Event.Widget

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

### Key binding

```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 Toddy.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

```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 Toddy.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

```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 Toddy.Event.Widget

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

### Option hovered

```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 Toddy.Event.Widget

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

### Open / Close

```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 Toddy.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

```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 Toddy.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:

```elixir
alias Toddy.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 Toddy.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.

```elixir
alias Toddy.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 Toddy.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
```

### Sensor events

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

```elixir
alias Toddy.Event.Sensor

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

```elixir
alias Toddy.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.

```elixir
alias Toddy.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 Toddy.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)).

```elixir
alias Toddy.Event.Key

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

### Key struct

```elixir
%Toddy.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: %Toddy.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

```elixir
%Toddy.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

```elixir
alias Toddy.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.

```elixir
alias Toddy.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 Toddy.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.

```elixir
alias Toddy.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 Toddy.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.

```elixir
alias Toddy.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 Toddy.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).

```elixir
alias Toddy.Event.Modifiers

%Modifiers{shift: true, ctrl: false, alt: false, logo: false, command: false}
```

```elixir
def update(model, %Toddy.Event.Modifiers{} = mods) do
  %{model | shift_held: mods.shift}
end
```

## Window events

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

```elixir
alias Toddy.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 Toddy.Event.Widget
alias Toddy.Event.Window

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

### File drag and drop

```elixir
alias Toddy.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

```elixir
alias Toddy.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.

```elixir
alias Toddy.Event.Timer

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

Where `timestamp` is the monotonic time in milliseconds.

## Command result events

Delivered when an async command completes.

```elixir
{event_tag, result}
```

Where `event_tag` is the atom or tuple you passed to
`Toddy.Command.async/2`.

```elixir
alias Toddy.Event.Widget

def update(model, %Widget{type: :click, id: "fetch"}) do
  {model, Toddy.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.

```elixir
alias Toddy.Event.Effect

%Effect{request_id: "ef_1234", result: {:ok, path}}
%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 Toddy.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:

```elixir
alias Toddy.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
```
