# `Flicker.FacetEditor.Dial`
[🔗](https://github.com/team-alembic/flicker/blob/v0.1.0/lib/flicker/facet_editor/dial.ex#L1)

The numeric and duration editor ([Spec 019](https://github.com/team-alembic/flicker/blob/main/docs/specs/spec-019-facet-editors.md)):
a dual-thumb slider when the facet is bounded, paired numeric inputs either
way.

The slider is for feel and the inputs are for exactness; they are two views of
one value. Both exist because either alone is worse — a slider can't express
`10..` and a text box can't show you where the data sits.

Which is the point of the **open bound**. A control that cannot say "over 100"
is worse than the two text boxes it replaced, so an endpoint left blank
serialises to `10..` or `..50` and is reachable from both views.

Unbounded facets get the inputs alone: a slider needs endpoints, and inventing
them would be a lie about the data.

# `clamp_endpoint`

```elixir
@spec clamp_endpoint(:from | :to, number(), Flicker.Facet.Range.t()) :: number()
```

Clamps a would-be endpoint so the thumbs can't cross — the lower may equal the
upper, but never pass it.

## Examples

    iex> Flicker.FacetEditor.Dial.clamp_endpoint(:from, 80, %Flicker.Facet.Range{from: 10, to: 50})
    50

    iex> Flicker.FacetEditor.Dial.clamp_endpoint(:to, 5, %Flicker.Facet.Range{from: 10, to: 50})
    10

    iex> Flicker.FacetEditor.Dial.clamp_endpoint(:from, 20, %Flicker.Facet.Range{from: 10, to: 50})
    20

    iex> Flicker.FacetEditor.Dial.clamp_endpoint(:from, 80, %Flicker.Facet.Range{to: nil})
    80

# `complete?`

```elixir
@spec complete?(term(), Flicker.Facet.t()) :: boolean()
```

At least one endpoint is required. Both open would mean "don't filter", which
is what removing the facet is for.

## Examples

    iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
    ...> Flicker.FacetEditor.Dial.complete?(%Flicker.Facet.Range{from: 10}, facet)
    true

    iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
    ...> Flicker.FacetEditor.Dial.complete?(%Flicker.Facet.Range{}, facet)
    false

    iex> facet = Flicker.Facet.new(key: :n, type: :integer)
    ...> Flicker.FacetEditor.Dial.complete?(5, facet)
    true

# `parse`

```elixir
@spec parse(String.t(), Flicker.Facet.t()) :: {:ok, term()} | :error
```

Delegates to the facet's own casting, so a dial can never produce a token the
parser rejects.

## Examples

    iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
    ...> {:ok, range} = Flicker.FacetEditor.Dial.parse("10..50", facet)
    ...> {range.from, range.to}
    {10, 50}

    iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
    ...> Flicker.FacetEditor.Dial.parse("nonsense", facet)
    :error

# `serialise`

```elixir
@spec serialise(term(), Flicker.Facet.t()) :: String.t()
```

A range as a range literal, keeping an open end open; a scalar bare.

## Examples

    iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
    ...> Flicker.FacetEditor.Dial.serialise(%Flicker.Facet.Range{from: 10, to: 50}, facet)
    "10..50"

    iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
    ...> Flicker.FacetEditor.Dial.serialise(%Flicker.Facet.Range{from: 10, to: nil}, facet)
    "10.."

    iex> facet = Flicker.Facet.new(key: :price, type: :number_range)
    ...> Flicker.FacetEditor.Dial.serialise(%Flicker.Facet.Range{from: nil, to: 50}, facet)
    "..50"

    iex> facet = Flicker.Facet.new(key: :length, type: :duration)
    ...> Flicker.FacetEditor.Dial.serialise(9000, facet)
    "9000s"

# `thumb_percent`

```elixir
@spec thumb_percent(number() | nil, map() | nil) :: float() | nil
```

A thumb's position as a percentage of the facet's bounds, clamped to `0..100`.

Returns `nil` for an open endpoint or an unbounded facet — there is no
meaningful position for either, and guessing one would misrepresent the value.

## Examples

    iex> Flicker.FacetEditor.Dial.thumb_percent(50, %{min: 0, max: 100})
    50.0

    iex> Flicker.FacetEditor.Dial.thumb_percent(150, %{min: 0, max: 100})
    100.0

    iex> Flicker.FacetEditor.Dial.thumb_percent(nil, %{min: 0, max: 100})
    nil

    iex> Flicker.FacetEditor.Dial.thumb_percent(50, nil)
    nil

---

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