# `Drafter.Widget.Switch`
[🔗](https://github.com/jaman/drafter/blob/main/lib/drafter/widget/switch.ex#L1)

An animated toggle switch widget with on/off states.

The slider thumb animates between positions when the state changes: a toggle puts
the widget into `:animating_on` or `:animating_off` and schedules a `:tick`
message every 30 ms, each moving `:slider_position` by `0.25` until it reaches
`1.0` or `0.0` and the state settles at `:on` or `:off`. `:on_change` fires only
once the animation has finished.

## Component tag

Tag `:switch`, built by `Drafter.App` as `{:switch, opts}`:

    switch(opts)
    switch(value, opts)

The two-argument form puts `value` into `opts` under `:value`; there is no
positional prop. `:enabled` and `:on_change` go through the binding layer, so
passing `bind: :some_key` reads the current state from that app-state key and
writes the new one back when the switch settles. `:width` and `:height`
default to the rect the parent allocated.

## Options

  * `:enabled` - `t:boolean/0` initial state, `true` for on. Default `false`.
  * `:bind` - app-state key atom for two-way binding of the on/off state.
    Default: none. With it set, `:enabled` is read from that key of the app state
    instead of from `opts`.
  * `:label` - `t:String.t/0` drawn to the left of the switch track, or `nil`.
    Default `nil`.
  * `:on_change` - the app callback name fired once the animation settles, with
    the new boolean as its data. Default `nil`. Through the element this is set
    to the one-argument function the framework builds, which the switch passes on as a callback name rather than calling.
  * `:size` - `:normal | :small | :compact`. Default `:normal`, an 8-column track
    with a 4-column thumb; `:small` is 6 and 2, `:compact` is 4 and 2. Any other
    value is treated as `:normal`.
  * `:width` - `t:pos_integer/0`. Default `12` when mounting directly, and the
    width of `opts[:__rect__]` through the element. Held on the state and never
    read by `render/2`, which uses the rect it is given.
  * `:height` - `t:pos_integer/0`. Default `1` when mounting directly, and the
    height of `opts[:__rect__]` through the element. Held on the state and never
    read by `render/2`.
  * `:on_color` - `{r, g, b}` for the thumb when on. Default `nil`, which uses
    `{100, 200, 100}`. Read by `mount/1` only — the `switch/1` element does not
    forward it.
  * `:off_color` - `{r, g, b}` for the thumb when off. Default `nil`, which uses
    `{150, 150, 150}`. Read by `mount/1` only — the `switch/1` element does not
    forward it.
  * `:focused` - `t:boolean/0` read by `mount/1`. Default `false`.
  * `:hovered` - `t:boolean/0` read by `mount/1`. Default `false`.

`update/2` applies `:label`, `:on_change`, `:width`, `:height`, `:size` and
`:enabled`, and silently drops every other key, so `:focused`, `:hovered`,
`:on_color` and `:off_color` are mount-only. Through the component tree
`update_props_from_mount/3` narrows that further to `:on_change`, `:label` and
`:size`, plus `:enabled` only when `:bind` is set and the bound value differs
from the current one.

## Key bindings

  * `enter`, `space` - toggle
  * `right` - turn on
  * `left` - turn off

A mouse up toggles and also focuses the switch.

## Widget value

`Drafter.get_widget_value/1` returns `true` while the state is `:on` and `false`
while it is `:off`. Mid-animation the state is `:animating_on` or
`:animating_off`, which reads as `nil`.

## Usage

    switch(enabled: true, label: "Dark mode", on_change: :toggle_theme)

# `rgb`

```elixir
@type rgb() :: {0..255, 0..255, 0..255}
```

# `t`

```elixir
@type t() :: %Drafter.Widget.Switch{
  focused: boolean(),
  height: pos_integer(),
  hovered: boolean(),
  label: String.t() | nil,
  off_color: rgb() | nil,
  on_change: term(),
  on_color: rgb() | nil,
  size: :normal | :small | :compact,
  slider_position: float(),
  state: :on | :off | :animating_on | :animating_off,
  width: pos_integer()
}
```

# `component_tag`

```elixir
@spec component_tag() :: :switch
```

The component tag this widget registers under.

    iex> Drafter.Widget.Switch.component_tag()
    :switch

# `focused`

# `from_component_opts`

```elixir
@spec from_component_opts(
  term(),
  keyword()
) :: Drafter.Widget.props()
```

Builds the props map for a `{:switch, opts}` element.

The positional argument is ignored. `:enabled` is the bound value for that key, so `bind: :key` reads it from
`opts[:__app_state__]` and plain `enabled:` is used otherwise, defaulting to
`false`. `:on_change` is the binding's writer, a one-argument function or `nil`.
`:width` and `:height` fall back to `opts[:__rect__]`, itself defaulting to
`%{width: 12, height: 1}`. `:on_color` and `:off_color` are not forwarded.

    iex> props = Drafter.Widget.Switch.from_component_opts(nil, label: "Dark")
    iex> {props.enabled, props.label, props.size, props.width, props.height, props.on_change}
    {false, "Dark", :normal, 12, 1, nil}

    iex> opts = [bind: :dark_mode, __app_state__: %{dark_mode: true}]
    iex> Drafter.Widget.Switch.from_component_opts(nil, opts).enabled
    true

# `handle_event`

```elixir
@spec handle_event(term(), t()) :: {:ok, t()} | {:noreply, t()}
```

Handles the switch's own events, replacing the dispatch `use Drafter.Widget`
would otherwise generate.

Recognised events:

  * `:activate`, `{:key, :enter}`, `{:key, :" "}` - toggle
  * `{:key, :right}` - turn on; `{:key, :left}` - turn off
  * `{:mouse, %{type: :mouse_up}}` - focus and toggle
  * `:hover` / `:unhover` - set or clear `:hovered`
  * `{:focus}` / `{:blur}` - set or clear `:focused`
  * `:tick` - advance the animation

Starting an animation returns `{:ok, state}` and schedules the next `:tick` on
the calling process. A toggle that has nothing to do — turning on a switch that
is already on, or any event during an animation other than `:tick` — returns
`{:noreply, state}`, and so does every unrecognised event.

    iex> state = Drafter.Widget.Switch.mount(%{})
    iex> {:ok, toggled} = Drafter.Widget.Switch.handle_event({:key, :enter}, state)
    iex> {toggled.state, toggled.slider_position}
    {:animating_on, 0.0}

    iex> state = Drafter.Widget.Switch.mount(%{})
    iex> Drafter.Widget.Switch.handle_event({:key, :left}, state)
    ...> |> elem(0)
    :noreply

    iex> state = Drafter.Widget.Switch.mount(%{})
    iex> {:ok, hovered} = Drafter.Widget.Switch.handle_event(:hover, state)
    iex> hovered.hovered
    true

# `mount`

```elixir
@spec mount(Drafter.Widget.props()) :: t()
```

Builds the widget state from `props`.

`:enabled` decides both `:state` (`:on` or `:off`) and `:slider_position`
(`1.0` or `0.0`); there is no way to mount mid-animation.

    iex> state = Drafter.Widget.Switch.mount(%{})
    iex> {state.state, state.slider_position, state.size, state.width, state.height}
    {:off, 0.0, :normal, 12, 1}

    iex> state = Drafter.Widget.Switch.mount(%{enabled: true, label: "Dark mode"})
    iex> {state.state, state.slider_position, state.label}
    {:on, 1.0, "Dark mode"}

# `preferred_height`

```elixir
@spec preferred_height(
  term(),
  keyword()
) :: 3
```

The number of rows the element asks for: always `3`. There is no `:height`
override.

    iex> Drafter.Widget.Switch.preferred_height(nil, height: 1)
    3

# `render`

```elixir
@spec render(t() | Drafter.Widget.props(), Drafter.Widget.rect()) :: [
  Drafter.Draw.Strip.t()
]
```

Draws the switch into `rect`, returning exactly `rect.height` strips.

`state` may be a plain props map, in which case it is passed through `mount/1`
first. The first strip holds the label followed by the track, padded with spaces
to `rect.width`; the rest are blank. The thumb is lightened while hovered, and by
half as much while focused.

# `unmount`

# `update`

```elixir
@spec update(Drafter.Widget.props(), t()) :: t()
```

Folds `props` into `widget_state`, applying `:label`, `:on_change`, `:width`,
`:height` and `:size` in every case and dropping every other key.

`:enabled` is compared with the current state rather than assigned:

  * settled at `:on` or `:off` — an `:enabled` that differs jumps straight to the
    other state with no animation; one that matches changes nothing. Absent, it
    defaults to the current state, so the switch stays put.
  * mid-animation — an `:enabled` that agrees with where the animation is heading
    lets it finish; one that disagrees cancels it and snaps to that state. Absent,
    it defaults to the animation's destination, so the animation continues.

## Examples

    iex> state = Drafter.Widget.Switch.mount(%{})
    iex> updated = Drafter.Widget.Switch.update(%{enabled: true, label: "On"}, state)
    iex> {updated.state, updated.slider_position, updated.label}
    {:on, 1.0, "On"}

    iex> state = Drafter.Widget.Switch.mount(%{enabled: true})
    iex> updated = Drafter.Widget.Switch.update(%{label: "Kept"}, state)
    iex> {updated.state, updated.label}
    {:on, "Kept"}

    iex> state = %{Drafter.Widget.Switch.mount(%{}) | state: :animating_on, slider_position: 0.5}
    iex> updated = Drafter.Widget.Switch.update(%{enabled: false}, state)
    iex> {updated.state, updated.slider_position}
    {:off, 0.0}

# `update_props_from_mount`

```elixir
@spec update_props_from_mount(Drafter.Widget.props(), t(), keyword()) ::
  Drafter.Widget.props()
```

Narrows a re-render to `:on_change`, `:label` and `:size`.

`:enabled` is added only when `opts` carries `:bind` and the bound value differs
from the widget's current state, so an unbound switch keeps whatever the user
toggled it to and `:width` and `:height` are mount-only.

    iex> props = Drafter.Widget.Switch.from_component_opts(nil, label: "Dark")
    iex> state = Drafter.Widget.Switch.mount(props)
    iex> Drafter.Widget.Switch.update_props_from_mount(props, state, []) |> Map.keys() |> Enum.sort()
    [:label, :on_change, :size]

    iex> opts = [bind: :dark_mode, __app_state__: %{dark_mode: true}]
    iex> props = Drafter.Widget.Switch.from_component_opts(nil, opts)
    iex> state = Drafter.Widget.Switch.mount(%{enabled: false})
    iex> Drafter.Widget.Switch.update_props_from_mount(props, state, opts).enabled
    true

---

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