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

Renders an animated spinner with an optional label.

The spinner frame is read from the monotonic clock at render time, one frame per
100 ms. An optional colour gradient cycles through the provided colours on its own
clock, independent of the spinner frame. While `:running` is false both the frame
and the gradient step are pinned to `0`.

Send `:start` or `:stop` events to control animation at runtime.

## Component tag

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

    loading_indicator(opts)

There is no positional argument; every prop comes from `opts`. The renderer stamps
the current monotonic time onto the props on each pass, which forces the widget to
re-render; the frame itself is read from the clock, not from the stamp.

## Options

  * `:text` - `t:String.t/0` label shown after the spinner character. Default
    `"Loading..."`. `nil` renders the spinner alone
  * `:spinner_type` - `:default` (default), `:dots`, `:line`, `:points`, `:arrow`
    or `:bounce`. `:arrow` and `:bounce` use built-in four-frame sets; the rest
    come from the character set, with `:default` mapping to its `:dots` frames,
    `:dots` to its `:braille` frames, and anything unrecognised to `:dots`
  * `:running` - `t:boolean/0`, whether the spinner animates. Default `true`
  * `:gradient_colors` - list of `{r, g, b}` tuples to cycle the spinner colour
    through. Default `nil`, leaving the spinner the computed theme colour. A
    one-colour list is used as a constant colour
  * `:gradient_speed` - milliseconds per gradient step. Default `50`
  * `:style` - `t:map/0` of style properties. Default `%{}`
  * `:class` - theme class atom or list of them, reaching `mount/1` as
    `:classes`. Default `[]`
  * `:app_module` - module supplying a per-app theme. Default `nil`

`update/2` re-reads every option. Through the component tree a re-render passes
only a fresh `:_render_timestamp`, so every other option is effectively mount-only
there.

## Widget value

`Drafter.get_widget_value/1` returns the indicator's `:text`, because the value
extractor reads the `:text` field.

## Usage

    loading_indicator(text: "Fetching data...")
    loading_indicator(spinner_type: :dots, gradient_colors: [{255, 0, 100}, {0, 100, 255}])

# `spinner_type`

```elixir
@type spinner_type() :: :default | :dots | :line | :points | :arrow | :bounce
```

# `t`

```elixir
@type t() :: %Drafter.Widget.LoadingIndicator{
  _render_timestamp: integer(),
  app_module: module() | nil,
  classes: [atom()],
  gradient_colors: [{0..255, 0..255, 0..255}] | nil,
  gradient_speed: pos_integer(),
  running: boolean(),
  spinner_type: spinner_type(),
  style: map(),
  text: String.t() | nil
}
```

# `component_tag`

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

The registry tag for this widget.

    iex> Drafter.Widget.LoadingIndicator.component_tag()
    :loading_indicator

# `focused`

# `from_component_opts`

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

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

The positional argument is ignored. `:class` is normalised into `:classes` and a
fresh `:_render_timestamp` is stamped from the monotonic clock.

    iex> props = Drafter.Widget.LoadingIndicator.from_component_opts(nil, text: "Wait")
    iex> {props.text, props.spinner_type, props.running, props.gradient_speed, props.classes}
    {"Wait", :default, true, 50, []}

# `get_render_key`

```elixir
@spec get_render_key(t()) :: integer()
```

The render cache key, the current monotonic millisecond.

It never repeats, so the widget is redrawn on every frame and the animation keeps
moving.

# `handle_event`

```elixir
@spec handle_event(Drafter.Event.t() | :start | :stop, t() | Drafter.Widget.props()) ::
  {:ok, t()} | {:noreply, t()}
```

Starts and stops the animation.

`:start` sets `:running` and `:stop` clears it, both returning `{:ok, state}`.
Everything else returns `{:noreply, state}`.

    iex> li = Drafter.Widget.LoadingIndicator.mount(%{})
    iex> {:ok, stopped} = Drafter.Widget.LoadingIndicator.handle_event(:stop, li)
    iex> stopped.running
    false

    iex> li = Drafter.Widget.LoadingIndicator.mount(%{})
    iex> Drafter.Widget.LoadingIndicator.handle_event({:key, :enter}, li) |> elem(0)
    :noreply

# `mount`

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

Builds the indicator state from `props`.

`:_render_timestamp` defaults to the current monotonic millisecond.

    iex> li = Drafter.Widget.LoadingIndicator.mount(%{text: "Fetching..."})
    iex> {li.text, li.spinner_type, li.running, li.gradient_speed, li.gradient_colors}
    {"Fetching...", :default, true, 50, nil}

    iex> li = Drafter.Widget.LoadingIndicator.mount(%{})
    iex> {li.text, li.style, li.classes}
    {"Loading...", %{}, []}

# `preferred_height`

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

Always `1`: the indicator occupies a single row.

# `render`

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

Draws the spinner and label as a single strip.

Accepts either a `t:t/0` or a raw props map, which is mounted first. The strip is
`" <frame> <text> "` with `:text` and `" <frame> "` without it. `rect` is not
consulted, so the strip is neither cropped nor padded to it.

# `unmount`

# `update`

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

Folds fresh props into `state`, re-reading every option.

`:_render_timestamp` is taken from `props` or re-stamped from the monotonic clock.

    iex> li = Drafter.Widget.LoadingIndicator.mount(%{})
    iex> Drafter.Widget.LoadingIndicator.update(%{text: "Almost there"}, li).text
    "Almost there"

# `update_props_from_mount`

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

Narrows the props a re-render feeds to `update/2` to a fresh
`:_render_timestamp`, so no other option changes after mount through the component
tree.

---

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