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

A clickable button widget that triggers a callback when pressed or activated via keyboard.

The button renders with a 3-line layout: a top border highlight, a centred label, and a
bottom shadow. Visual state changes (hover, active, focused, disabled) are reflected
through colour adjustments.

## Component tag

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

    button(text, opts)

The positional argument becomes `:text`. `from_component_opts/2` wraps
`:on_click` with `Drafter.Widget.Callback`, so it may be given as an atom event
name, and drops it entirely when `disabled: true`.

## Options

  * `:text` - `t:String.t/0` button label. Default `""`. Supplied positionally
    through the `button/2` element
  * `:on_click` - atom event name or zero-arity function called when the button
    is activated. Default `nil`
  * `:variant` - visual style atom: `:default` (default), `:primary`, `:success`,
    `:warning`, `:error`. Any value other than `:default` is also prepended to the
    theme classes. `:type` is accepted as an alias by the element, and
    `:button_type` is accepted directly by `mount/1`
  * `:disabled` - `t:boolean/0`. Default `false`. A disabled button consumes
    interaction without firing `:on_click` and gains the `:disabled` theme class;
    the element also drops `:on_click` entirely
  * `:compact` - `t:boolean/0`. Default `false`. Renders the label row only,
    without the highlight and shadow rows
  * `:style` - `t:map/0` of style overrides applied on top of theme defaults.
    Default `%{}`
  * `:class` - theme class atom or list of them, reaching `mount/1` as
    `:classes`. Default `[]`
  * `:focused` - `t:boolean/0` initial focus flag. Default `false`
  * `:app_module` - module supplying a per-app theme, passed by the renderer as
    `:__app_module__`. Default `nil`

`:active` and `:hovered` are state the widget owns; `mount/1` always starts them
at `false` and ignores props of those names. Every other option, `:focused`
included, is live-updatable through `update/2`.

## Widget value

`Drafter.get_widget_value/1` returns the button's `:text` as a `t:String.t/0`,
because the value extractor reads the `:text` field. Activation itself is
reported through `:on_click`.

## Key bindings and events

`:enter` and `:" "` activate the button; every other key bubbles. A mouse release
anywhere in the rect activates it. Activation sets `:active`, fires `:on_click`,
and schedules a `:deactivate` message to the owning process 200 ms later, which
clears `:active` again. `handle_custom_event/2` also accepts `:activate`,
`{:mouse, %{type: :press}}`, `:hover` and `:unhover`.

## Usage

    button("Submit", on_click: fn -> :submit end, variant: :primary)

# `action`

```elixir
@type action() ::
  {:pop, term()}
  | {:push, module(), map()}
  | {:replace, module(), map()}
  | {:app_callback, atom(), term()}
```

# `t`

```elixir
@type t() :: %Drafter.Widget.Button{
  active: boolean(),
  app_module: module() | nil,
  button_type: variant(),
  classes: [atom()],
  compact: boolean(),
  disabled: boolean(),
  focused: boolean(),
  hovered: boolean(),
  on_click: (-&gt; any()) | nil,
  style: map(),
  text: String.t()
}
```

# `variant`

```elixir
@type variant() :: :default | :primary | :success | :warning | :error
```

# `component_tag`

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

The registry tag for this widget.

    iex> Drafter.Widget.Button.component_tag()
    :button

# `focused`

# `from_component_opts`

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

Turns the `{:button, text, opts}` element into a props map for `mount/1`.

`text` is the positional argument. The variant is read from `:variant`, falling
back to `:type` and then `:default`, and is emitted as `:button_type`. `:class` is
normalised into `:classes`. `:on_click` is wrapped by
`Drafter.Widget.Callback.wrap_0/1`, and is forced to `nil` when `disabled: true`.

    iex> props = Drafter.Widget.Button.from_component_opts("Go", type: :success, on_click: :go)
    iex> {props.text, props.button_type, props.disabled, props.compact, props.classes}
    {"Go", :success, false, false, []}

    iex> Drafter.Widget.Button.from_component_opts("Go", disabled: true, on_click: :go).on_click
    nil

# `handle_custom_event`

```elixir
@spec handle_custom_event(term(), t() | Drafter.Widget.props()) ::
  {:ok, t()} | {:ok, t(), [action()]} | {:bubble, t()}
```

Handles the button's out-of-band messages.

`:activate` and `{:mouse, %{type: :press}}` activate the button unless it is
disabled. `:deactivate` clears `:active`, `:hover` sets `:hovered` and `:unhover`
clears it, all returning `{:ok, state}`. Anything else returns `{:bubble, state}`,
including `:activate` on a disabled button.

    iex> b = Drafter.Widget.Button.mount(%{text: "Go"})
    iex> {:ok, hovered} = Drafter.Widget.Button.handle_custom_event(:hover, b)
    iex> hovered.hovered
    true

    iex> b = Drafter.Widget.Button.mount(%{text: "Go", disabled: true})
    iex> Drafter.Widget.Button.handle_custom_event(:activate, b) |> elem(0)
    :bubble

# `handle_event`

# `handle_key`

```elixir
@spec handle_key(Drafter.Widget.key(), t() | Drafter.Widget.props()) ::
  {:ok, t()} | {:ok, t(), [action()]} | {:bubble, t()}
```

Activates the button on `:enter` or `:" "`; bubbles every other key.

A disabled button still consumes `:enter` and `:" "`, returning `{:ok, state}`
without firing `:on_click`.

    iex> b = Drafter.Widget.Button.mount(%{text: "Go"})
    iex> {tag, ^b} = Drafter.Widget.Button.handle_key(:tab, b)
    iex> tag
    :bubble

    iex> b = Drafter.Widget.Button.mount(%{text: "Go", disabled: true})
    iex> {tag, unchanged} = Drafter.Widget.Button.handle_key(:enter, b)
    iex> {tag, unchanged.active}
    {:ok, false}

# `handle_mouse_up`

```elixir
@spec handle_mouse_up(integer(), integer(), t() | Drafter.Widget.props()) ::
  {:ok, t()} | {:ok, t(), [action()]}
```

Activates the button on mouse release, wherever in the rect it lands.

Returns `{:ok, state}` unchanged when `:disabled`, otherwise
`{:ok, active_state, actions}`. Accepts a raw props map as `state`.

# `mount`

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

Builds the button state from `props`.

A `:variant` other than `:default` is prepended to `:classes`, and `:disabled`
prepends `:disabled` on top of that. `:active` and `:hovered` always start at
`false`.

    iex> b = Drafter.Widget.Button.mount(%{text: "Save", variant: :primary})
    iex> {b.text, b.button_type, b.classes, b.disabled, b.compact}
    {"Save", :primary, [:primary], false, false}

    iex> b = Drafter.Widget.Button.mount(%{})
    iex> {b.text, b.button_type, b.classes, b.active, b.hovered, b.focused}
    {"", :default, [], false, false, false}

    iex> Drafter.Widget.Button.mount(%{variant: :error, disabled: true}).classes
    [:disabled, :error]

# `preferred_height`

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

`1` when `opts[:compact]` is true, otherwise `3`.

    iex> Drafter.Widget.Button.preferred_height("Go", [])
    3

    iex> Drafter.Widget.Button.preferred_height("Go", compact: true)
    1

# `render`

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

Draws the button into `rect`.

Accepts either a `t:t/0` or a raw props map, which is mounted first. Produces one
strip when `:compact` is set and three otherwise, then centres those rows
vertically in `rect.height`, truncating from the bottom when the rect is shorter.
A label wider than `rect.width` is cut, not wrapped.

# `unmount`

# `update`

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

Folds fresh props into `state`.

Re-reads `:text`, `:style`, `:focused`, `:on_click`, `:variant` (or
`:button_type`), `:classes`, `:app_module`, `:disabled` and `:compact`, then
rebuilds the theme class list from the variant and disabled flag. `:active` and
`:hovered` are left as they are.

    iex> b = Drafter.Widget.Button.mount(%{text: "Save"})
    iex> updated = Drafter.Widget.Button.update(%{text: "Saved", disabled: true}, b)
    iex> {updated.text, updated.disabled, updated.classes}
    {"Saved", true, [:disabled]}

# `update_props_from_mount`

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

Returns `mount_props` unchanged, so a re-render passes every option through to
`update/2`.

---

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