defmodule Scenic.Component.Input.RadioGroup do
@moduledoc """
Add a radio group to a graph
The data format for RadioGroup has changed since v0.10!
## Data
`{radio_buttons, checked_id}`
* `radio_buttons` must be a list of radio button data. See below.
* `checked_id` Is the id of the currently selected radio from the list.
radio_buttons list:
`{text, radio_id}`
* `text` - must be a bitstring
* `button_id` - can be any term you want. It will be passed back to you as the
group's value.
* `checked?` - must be a boolean and indicates if the button is selected.
`checked?` is not required and will default to `false` if not supplied.
Example showing the full data format
```elixir
{[{"One", :one}, {"Two", :two}, {"Three", :three}], :two}
```
## Messages
When the state of the radio group changes, it sends an event message to the
host scene in the form of:
`{:value_changed, id, radio_id}`
## Options
Radio Buttons honor the following list of options.
* `:theme` - This sets the color scheme of the button. This can be one of
pre-defined button schemes `:light`, `:dark`, or it can be a completely custom
scheme like this: `{text_color, box_background, border_color, pressed_color,
checkmark_color}`.
## Styles
Radio Buttons honor the following styles
* `:hidden` - If `false` the component is rendered. If `true`, it is skipped.
The default is `false`.
* `:theme` - The color set used to draw. See below. The default is `:dark`
## Theme
Radio buttons work well with the following predefined themes: `:light`,
`:dark`
To pass in a custom theme, supply a map with at least the following entries:
* `:text` - the color of the text
* `:background` - the background of the component
* `:border` - the border of the component
* `:active` - the background of the circle while the button is pressed
* `:thumb` - the color of inner selected-mark
## Usage
You should add/modify components via the helper functions in
[`Scenic.Components`](Scenic.Components.html#radio_group/3)
## Examples
The following example creates a radio group and positions it on the screen.
graph
|> radio_group({[
{"Radio A", :radio_a},
{"Radio B", :radio_b},
{"Radio C", :radio_c},
], :radio_b},
id: :radio_group_id, translate: {20, 20})
"""
use Scenic.Component, has_children: true
alias Scenic.Graph
alias Scenic.Assets.Static
alias Scenic.Component.Input.RadioButton
import Scenic.Primitives, only: [{:group, 2}]
require Logger
# import IEx
@default_font :roboto
@default_font_size 20
@border_width 2
@line_height @default_font_size + @border_width + @border_width
# --------------------------------------------------------
@impl Scenic.Component
def validate({items, _} = data) when is_list(items) do
# confirm all the entries
Enum.reduce(items, {:ok, data}, fn
_, {:error, _} = error -> error
{text, _}, acc when is_bitstring(text) -> acc
item, _ -> err_bad_item(item, data)
end)
|> case do
{:error, _} = err ->
err
{:ok, {items, initial}} ->
# confirm that initial is in the items list
items
|> Enum.any?(fn {_, id} -> id == initial end)
|> case do
true -> {:ok, data}
false -> err_initial(data)
end
end
end
def validate(data) do
{
:error,
"""
#{IO.ANSI.red()}Invalid RadioGroup specification
Received: #{inspect(data)}
#{IO.ANSI.yellow()}
RadioGroup data must formed like: {[{text, id}], initial_id}
This is a list of text/id pairs, and the id of the pair that is initially selected.
NOTE: This has changed from v.10 and prior. You used to specify the selected radio inside
the list, but now the current id has moved out into a tuple.
#{IO.ANSI.default_color()}
"""
}
end
defp err_bad_item(item, data) do
{
:error,
"""
#{IO.ANSI.red()}Invalid RadioGroup specification
Received: #{inspect(data)}
Invalid Item: #{inspect(item)}
#{IO.ANSI.yellow()}
RadioGroup data must formed like: {[{text, id}], initial_id}
This is a list of text/id pairs, and the id of the pair that is initially selected.#{IO.ANSI.default_color()}
"""
}
end
defp err_initial({_, initial} = data) do
{
:error,
"""
#{IO.ANSI.red()}Invalid RadioGroup specification
Received: #{inspect(data)}
The initial id #{inspect(initial)} is not in the listed items
#{IO.ANSI.yellow()}
RadioGroup data must formed like: {[{text, id}], initial_id}
This is a list of text/id pairs, and the id of the pair that is initially selected.#{IO.ANSI.default_color()}
"""
}
end
# --------------------------------------------------------
@doc false
@impl Scenic.Scene
def init(scene, {items, initial_id}, opts) when is_list(items) do
id = opts[:id]
graph =
Graph.build()
|> group(fn graph ->
{graph, _} =
Enum.reduce(items, {graph, 0}, fn
{t, i}, {g, voffset} ->
g =
RadioButton.add_to_graph(
g,
{t, i, i == initial_id},
opts
|> Keyword.put(:translate, {0, voffset})
|> Keyword.put(:id, i)
)
{g, voffset + @line_height}
end)
graph
end)
scene =
scene
|> push_graph(graph)
|> assign(
value: initial_id,
items: items,
id: id
)
{:ok, scene}
end
@impl Scenic.Component
def bounds({items, _id}, _opts) do
{:ok, {Static.Font, fm}} = Static.meta(@default_font)
ascent = FontMetrics.ascent(@default_font_size, fm)
# find the longest text width
fm_width =
Enum.reduce(items, 0, fn {text, _}, w ->
tw = FontMetrics.width(text, @default_font_size, fm)
case tw > w do
true -> tw
false -> w
end
end)
space_width = FontMetrics.width(' ', @default_font_size, fm)
box_width = fm_width + ascent + space_width + @border_width
{0, 0, box_width, @line_height * Enum.count(items)}
end
# ============================================================================
@doc false
@impl Scenic.Scene
def handle_event({:click, btn_id}, _from, %{assigns: %{id: id}} = scene) do
:ok = cast_children(scene, {:set_to_msg, btn_id})
:ok = send_parent_event(scene, {:value_changed, id, btn_id})
{:halt, assign(scene, value: btn_id)}
end
# --------------------------------------------------------
@doc false
@impl Scenic.Scene
def handle_get(_, %{assigns: %{value: value}} = scene) do
{:reply, value, scene}
end
@doc false
@impl Scenic.Scene
def handle_put(v, %{assigns: %{value: value}} = scene) when v == value do
# no change
{:noreply, scene}
end
def handle_put(
value,
%{
assigns: %{
items: items,
id: id
}
} = scene
) do
# find the newly selected item's text
scene =
case Enum.find(items, fn {_, id} -> id == value end) do
nil ->
Logger.warn(
"Attempted to put an invalid value on Radio Group id: #{inspect(id)}, value: #{inspect(value)}"
)
scene
{_, _} ->
# send the value_changed message
send_parent_event(scene, {:value_changed, id, value})
:ok = cast_children(scene, {:set_to_msg, value})
assign(scene, value: value)
end
{:noreply, scene}
end
def handle_put(v, %{assigns: %{id: id}} = scene) do
Logger.warn(
"Attempted to put an invalid value on Dropdown id: #{inspect(id)}, value: #{inspect(v)}"
)
{:noreply, scene}
end
@doc false
@impl Scenic.Scene
def handle_fetch(_, %{assigns: %{items: items, value: value}} = scene) do
{:reply, {:ok, {items, value}}, scene}
end
end