The numeric and duration editor (Spec 019): 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.
Summary
Functions
Clamps a would-be endpoint so the thumbs can't cross — the lower may equal the upper, but never pass it.
At least one endpoint is required. Both open would mean "don't filter", which is what removing the facet is for.
Delegates to the facet's own casting, so a dial can never produce a token the parser rejects.
A range as a range literal, keeping an open end open; a scalar bare.
A thumb's position as a percentage of the facet's bounds, clamped to 0..100.
Functions
@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
@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
@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
@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"
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