Drafter.Widget.Slider (drafter v0.3.2)

Copy Markdown View Source

A draggable value slider: a track, a fill up to the current value, a thumb, and an optional label and value readout.

The value lives on the widget. Keys, the mouse and the scroll wheel all move it, :on_change reports it, and bind: keeps it in step with a key of the app state.

Component tag

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

slider(opts)
slider(value, opts)

The two-argument form puts value into opts under :value. :value and :on_change go through the binding layer, so bind: :some_key reads the value from that app-state key and writes each new one back.

Options

  • :value - number/0 the slider starts at, clamped into the range and snapped to :step. Default :min.
  • :min - number/0 low end of the range. Default 0.0.
  • :max - number/0 high end of the range. Default 1.0.
  • :step - number/0 the value moves in. Default nil, a hundredth of the range — whole numbers when :min and :max are both integers. A range whose bounds and step are all integers keeps integer values; any other range works in floats.
  • :bind - app-state key atom for two-way binding of the value. Default: none. Without it a later :value prop does not reach the mounted widget, which owns whatever the user set it to.
  • :label - String.t/0 drawn ahead of the track, or nil. Default nil.
  • :show_value - boolean/0, draw the value after the track. Default true. The readout reserves the width of the widest value in the range, so the track does not move as the value changes.
  • :format - (number() -> String.t()) for the readout. Default nil, which writes the number with :precision decimals.
  • :precision - decimals in the readout. Default: as many as :step needs.
  • :orientation - :horizontal | :vertical. Default :horizontal. A vertical slider runs bottom to top, with the label on its first row and the readout on its last.

  • :disabled - boolean/0. Default false. A disabled slider draws muted and bubbles every key and click.
  • :track_color / :fill_color / :thumb_color - {r, g, b} overrides for the three parts. Default nil, which takes them from the theme.
  • :renderer - :text (default) draws characters; :braille draws the shape through Drafter.Widget.Slider.Pixel; a graphics protocol atom (:pixel, :kitty, :iterm2, :sixel, :auto) transmits a picture, falling back to braille where the terminal has no protocol. Unset, the mode the app was run with applies; DRAFTER_MODE overrides both.
  • :class - theme class atom or list of them. Default [].
  • :style - inline style map merged over the theme. Default %{}.

update/2 accepts every option above except :class and :style, which are mount-only. Through the component tree update_props_from_mount/3 narrows that further, adding :value only when :bind is set and the bound value differs from the widget's own.

Key bindings

  • , - one step up; , - one step down
  • PageUp, PageDown - ten steps
  • Home, End - the ends of the range

A press or drag anywhere on the track moves the thumb there, and the scroll wheel moves one step.

Widget value

Drafter.get_widget_value/1 returns the number, and Drafter.set_widget_value/2 writes one, clamped and snapped like any other.

Usage

slider(value: 0.5, label: "Gain", on_change: :set_gain)
slider(min: 0, max: 11, step: 1, bind: :volume)
slider(value: 0.546, precision: 3, renderer: :auto)

Summary

Functions

The component tag this widget registers under.

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

Tracks :hover and :unhover; every other event bubbles.

Moves the thumb to the pointer while a button is held.

Moves the value by one step for and , down one for and , ten for the page keys, and to the ends of the range for Home and End.

Moves the thumb to the pointer and ends the drag gesture.

Moves the thumb to the pointer and starts a drag gesture, so later motion keeps tracking even once the pointer leaves the widget.

Moves the value one step per wheel notch.

Whether this slider is drawing a transmitted image rather than characters.

Builds the widget state from props.

Records the rect the layout gave the widget, so a click can be turned into a value.

The number of rows the element asks for: :height, defaulting to 8 for a vertical slider and 1 for a horizontal one.

Draws the slider into rect, returning exactly rect.height strips.

Callback implementation for Drafter.Widget.unmount/1.

Folds fresh props into state, keeping the current value for any key that is absent.

Narrows a re-render to the props that may change after mount.

Types

rgb()

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

t()

@type t() :: %Drafter.Widget.Slider{
  app_module: module() | nil,
  classes: [atom()],
  disabled: boolean(),
  dragging: boolean(),
  fill_color: rgb() | nil,
  focused: boolean(),
  format: (number() -> String.t()) | nil,
  height: pos_integer() | nil,
  hovered: boolean(),
  label: String.t() | nil,
  max: number(),
  min: number(),
  on_change: (number() -> term()) | nil,
  orientation: :horizontal | :vertical,
  precision: non_neg_integer() | nil,
  renderer: atom() | nil,
  show_value: boolean(),
  step: number() | nil,
  style: map(),
  thumb_color: rgb() | nil,
  track_color: rgb() | nil,
  value: number(),
  width: pos_integer() | nil
}

Functions

component_tag()

@spec component_tag() :: :slider

The component tag this widget registers under.

iex> Drafter.Widget.Slider.component_tag()
:slider

focused(state)

from_component_opts(args, opts)

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

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

The positional argument is ignored. :value is the bound value for that key, so bind: :key reads it from opts[:__app_state__], and :on_change is the binding's writer. :class is normalised into :classes and :__app_module__ into :app_module.

iex> props = Drafter.Widget.Slider.from_component_opts(nil, min: 0, max: 10, step: 1)
iex> {props.value, props.min, props.max, props.step}
{0, 0, 10, 1}

iex> opts = [bind: :gain, __app_state__: %{gain: 0.75}]
iex> Drafter.Widget.Slider.from_component_opts(nil, opts).value
0.75

handle_custom_event(arg1, state)

@spec handle_custom_event(Drafter.Event.t(), t()) :: {:ok, t()} | {:bubble, t()}

Tracks :hover and :unhover; every other event bubbles.

handle_drag(x, y, state)

@spec handle_drag(integer(), integer(), t()) :: {:ok, t()} | {:bubble, t()}

Moves the thumb to the pointer while a button is held.

handle_event(event, state)

Callback implementation for Drafter.Widget.handle_event/2.

handle_key(key, state)

@spec handle_key(Drafter.Widget.key(), t()) :: {:ok, t()} | {:bubble, t()}

Moves the value by one step for and , down one for and , ten for the page keys, and to the ends of the range for Home and End.

A key that does not move the value still returns {:ok, state}; every other key, and every key at all while :disabled, bubbles.

iex> state = Drafter.Widget.Slider.mount(%{value: 0.5, step: 0.1})
iex> {:ok, moved} = Drafter.Widget.Slider.handle_key(:right, state)
iex> moved.value
0.6

iex> state = Drafter.Widget.Slider.mount(%{value: 0.5})
iex> {:ok, moved} = Drafter.Widget.Slider.handle_key(:home, state)
iex> moved.value
0.0

handle_mouse_up(x, y, state)

@spec handle_mouse_up(integer(), integer(), t()) :: {:ok, t()} | {:bubble, t()}

Moves the thumb to the pointer and ends the drag gesture.

handle_press(x, y, state)

@spec handle_press(integer(), integer(), t()) :: {:ok, t()} | {:bubble, t()}

Moves the thumb to the pointer and starts a drag gesture, so later motion keeps tracking even once the pointer leaves the widget.

x and y are widget-relative cells.

handle_scroll(arg1, state)

@spec handle_scroll(Drafter.Widget.scroll_direction(), t()) ::
  {:ok, t()} | {:bubble, t()}

Moves the value one step per wheel notch.

image_active?(state)

@spec image_active?(t()) :: boolean()

Whether this slider is drawing a transmitted image rather than characters.

Only a :pixel mode on a terminal with a graphics protocol draws one; a :text or :braille slider costs nothing on the image path.

iex> Drafter.Widget.Slider.image_active?(Drafter.Widget.Slider.mount(%{renderer: :text}))
false

mount(props)

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

Builds the widget state from props.

The value is clamped into :min..:max and snapped to :step, so a slider can never mount off its own scale.

iex> state = Drafter.Widget.Slider.mount(%{})
iex> {state.value, state.min, state.max, state.orientation}
{0.0, 0.0, 1.0, :horizontal}

iex> Drafter.Widget.Slider.mount(%{value: 0.37, step: 0.25}).value
0.25

iex> Drafter.Widget.Slider.mount(%{value: 42, min: 0, max: 10, step: 1}).value
10

on_rect_change(rect, state)

@spec on_rect_change(Drafter.Widget.rect(), t()) :: t()

Records the rect the layout gave the widget, so a click can be turned into a value.

iex> state = Drafter.Widget.Slider.mount(%{})
iex> sized = Drafter.Widget.Slider.on_rect_change(%{x: 0, y: 0, width: 40, height: 1}, state)
iex> {sized.width, sized.height}
{40, 1}

preferred_height(args, opts)

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

The number of rows the element asks for: :height, defaulting to 8 for a vertical slider and 1 for a horizontal one.

iex> Drafter.Widget.Slider.preferred_height(nil, [])
1

iex> Drafter.Widget.Slider.preferred_height(nil, orientation: :vertical)
8

render(state, rect)

Draws the slider 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 renderer decides the track: a :text slider draws characters, a :braille one the braille shape, and a :pixel one leaves the track blank for the picture the widget server transmits. The label and the readout are characters in every mode. A rect with no width draws nothing.

unmount(state)

Callback implementation for Drafter.Widget.unmount/1.

update(props, state)

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

Folds fresh props into state, keeping the current value for any key that is absent.

The value is re-snapped against whichever range the props leave behind, so moving :min, :max or :step never leaves the thumb off its scale.

iex> state = Drafter.Widget.Slider.mount(%{value: 90, min: 0, max: 100})
iex> Drafter.Widget.Slider.update(%{max: 50}, state).value
50

update_props_from_mount(mount_props, existing_state, opts)

@spec update_props_from_mount(Drafter.Widget.props(), t() | map(), keyword()) ::
  Drafter.Widget.props()

Narrows a re-render to the props that may change after mount.

:value is added only when opts carries :bind and the bound value differs from the widget's own, so an unbound slider keeps whatever the user dragged it to. :class and :style are dropped, making them mount-only.