Skip to content

Commit c6a6d6a

Browse files
committed
Merge branch 'master' into boyd
2 parents c6eecc3 + 5c374ab commit c6a6d6a

File tree

5 files changed

+493
-0
lines changed

5 files changed

+493
-0
lines changed

changelist.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
cover the space with a clear rect, or capture the input.
1414
* Add the `:direction` option to the `Dropdown` component so can can go either up or down.
1515
* Add specs to functions in components and primitives
16+
* Add the Toggle component. Thank you to Eric Watson. @wasnotrice
1617

1718
### 0.7.0
1819
* First public release
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
defmodule Scenic.Component.Input.Toggle do
2+
@moduledoc """
3+
An on/off toggle component
4+
5+
See the [Components](Scenic.Toggle.Components.html#toggle/2) module for usage
6+
"""
7+
8+
use Scenic.Component, has_children: false
9+
10+
alias Scenic.Graph
11+
alias Scenic.Primitive
12+
alias Scenic.Primitive.Group
13+
alias Scenic.Primitive.Style.Theme
14+
alias Scenic.ViewPort
15+
16+
import Scenic.Primitives
17+
18+
@default_thumb_pressed_color :gainsboro
19+
@default_thumb_radius 10
20+
@default_padding 2
21+
@default_border_width 2
22+
23+
defmodule State do
24+
@moduledoc false
25+
26+
defstruct graph: nil,
27+
contained?: false,
28+
id: nil,
29+
on?: false,
30+
pressed?: false,
31+
theme: nil,
32+
thumb_translate: nil,
33+
color: nil
34+
35+
@type t :: %__MODULE__{
36+
graph: Graph.t(),
37+
contained?: boolean,
38+
id: atom,
39+
on?: boolean,
40+
pressed?: boolean,
41+
theme: map,
42+
thumb_translate: %{on: {number, number}, off: {number, number}},
43+
color: %{
44+
thumb: %{default: term, active: term},
45+
border: term,
46+
track: %{off: term, on: term}
47+
}
48+
}
49+
end
50+
51+
# #--------------------------------------------------------
52+
def info(data) do
53+
"""
54+
#{IO.ANSI.red()}Toggle data must be: on?
55+
#{IO.ANSI.yellow()}Received: #{inspect(data)}
56+
#{IO.ANSI.default_color()}
57+
"""
58+
end
59+
60+
# --------------------------------------------------------
61+
@spec verify(any) :: {:ok, boolean} | :invalid_data
62+
def verify(on? = data) when is_boolean(on?) do
63+
{:ok, data}
64+
end
65+
66+
def verify(_), do: :invalid_data
67+
68+
# --------------------------------------------------------
69+
@spec init(any, Keyword.t() | map | nil) :: {:ok, State.t()}
70+
def init(on?, opts) do
71+
id = opts[:id]
72+
73+
styles = opts[:styles]
74+
75+
# theme is passed in as an inherited style
76+
theme =
77+
(styles[:theme] || Theme.preset(:primary))
78+
|> Theme.normalize()
79+
80+
# get toggle specific styles
81+
thumb_radius = Map.get(styles, :thumb_radius, @default_thumb_radius)
82+
padding = Map.get(styles, :padding, @default_padding)
83+
border_width = Map.get(styles, :border_width, @default_border_width)
84+
85+
# calculate the dimensions of the track
86+
track_height = thumb_radius * 2 + 2 * padding + 2 * border_width
87+
track_width = thumb_radius * 4 + 2 * padding + 2 * border_width
88+
track_border_radius = thumb_radius + padding + border_width
89+
90+
color = %{
91+
thumb: %{
92+
default: theme.text,
93+
pressed: Map.get(theme, :thumb_pressed, @default_thumb_pressed_color)
94+
},
95+
border: theme.border,
96+
track: %{
97+
off: theme.background,
98+
on: theme.thumb
99+
}
100+
}
101+
102+
thumb_translate = %{
103+
off: {thumb_radius + padding + border_width, thumb_radius + padding + border_width},
104+
on: {thumb_radius * 3 + padding + border_width, thumb_radius + padding + border_width}
105+
}
106+
107+
{initial_track_fill, initial_thumb_translate} =
108+
case on? do
109+
true -> {color.track.on, thumb_translate.on}
110+
false -> {color.track.off, thumb_translate.off}
111+
end
112+
113+
graph =
114+
Graph.build()
115+
|> Group.add_to_graph(
116+
fn graph ->
117+
graph
118+
|> rrect({track_width, track_height, track_border_radius},
119+
fill: initial_track_fill,
120+
stroke: {border_width, theme.border},
121+
id: :track
122+
)
123+
|> circle(thumb_radius,
124+
fill: color.thumb.default,
125+
id: :thumb,
126+
translate: initial_thumb_translate
127+
)
128+
end,
129+
translate: {border_width, -(thumb_radius + padding + border_width)}
130+
)
131+
132+
# |> text(text, fill: theme.text, translate: {20, 0})
133+
134+
state = %State{
135+
contained?: false,
136+
id: id,
137+
graph: graph,
138+
on?: on?,
139+
pressed?: false,
140+
theme: theme,
141+
thumb_translate: thumb_translate,
142+
color: color
143+
}
144+
145+
push_graph(graph)
146+
147+
{:ok, state}
148+
end
149+
150+
# --------------------------------------------------------
151+
def handle_input({:cursor_enter, _uid}, _, %{pressed?: true} = state) do
152+
state = Map.put(state, :contained?, true)
153+
graph = update_graph(state)
154+
{:noreply, %{state | graph: graph}}
155+
end
156+
157+
# --------------------------------------------------------
158+
def handle_input({:cursor_exit, _uid}, _, %{pressed?: true} = state) do
159+
state = Map.put(state, :contained?, false)
160+
graph = update_graph(state)
161+
{:noreply, %{state | graph: graph}}
162+
end
163+
164+
# --------------------------------------------------------
165+
def handle_input({:cursor_button, {:left, :press, _, _}}, context, state) do
166+
state =
167+
state
168+
|> Map.put(:pressed?, true)
169+
|> Map.put(:contained?, true)
170+
171+
graph = update_graph(state)
172+
173+
ViewPort.capture_input(context, [:cursor_button, :cursor_pos])
174+
175+
{:noreply, %{state | graph: graph}}
176+
end
177+
178+
# --------------------------------------------------------
179+
def handle_input(
180+
{:cursor_button, {:left, :release, _, _}},
181+
context,
182+
%{contained?: contained?, id: id, on?: on?, pressed?: pressed?} = state
183+
) do
184+
state = Map.put(state, :pressed?, false)
185+
186+
ViewPort.release_input(context, [:cursor_button, :cursor_pos])
187+
188+
# only do the action if the cursor is still contained in the target
189+
state =
190+
case pressed? && contained? do
191+
true ->
192+
on? = !on?
193+
send_event({:value_changed, id, on?})
194+
Map.put(state, :on?, on?)
195+
196+
false ->
197+
state
198+
end
199+
200+
graph = update_graph(state)
201+
202+
{:noreply, %{state | graph: graph}}
203+
end
204+
205+
# --------------------------------------------------------
206+
def handle_input(_event, _context, state) do
207+
{:noreply, state}
208+
end
209+
210+
@spec update_graph(State.t()) :: Graph.t()
211+
defp update_graph(%{
212+
color: color,
213+
contained?: contained?,
214+
graph: graph,
215+
on?: on?,
216+
pressed?: pressed?,
217+
thumb_translate: thumb_translate
218+
}) do
219+
graph =
220+
case pressed? && contained? do
221+
true ->
222+
Graph.modify(graph, :thumb, &Primitive.put_style(&1, :fill, color.thumb.pressed))
223+
224+
false ->
225+
Graph.modify(graph, :thumb, &Primitive.put_style(&1, :fill, color.thumb.default))
226+
end
227+
228+
graph =
229+
case on? do
230+
true ->
231+
graph
232+
|> Graph.modify(:track, &Primitive.put_style(&1, :fill, color.track.on))
233+
|> Graph.modify(:thumb, &Primitive.put_transform(&1, :translate, thumb_translate.on))
234+
235+
false ->
236+
graph
237+
|> Graph.modify(:track, &Primitive.put_style(&1, :fill, color.track.off))
238+
|> Graph.modify(:thumb, &Primitive.put_transform(&1, :translate, thumb_translate.off))
239+
end
240+
241+
push_graph(graph)
242+
end
243+
end

lib/scenic/components.ex

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -585,6 +585,61 @@ defmodule Scenic.Components do
585585
modify(p, Component.Input.TextField, data, options)
586586
end
587587

588+
@doc """
589+
Add toggle to a Scenic graph.
590+
591+
You must pass the initial state, `on?`. Pass `true` if the toggle is on, pass `false` if not.
592+
593+
### Styles
594+
595+
Toggles honor the following styles. The `:light` and `:dark` styles look nice. The other bundled themes...not so much. You can also [supply your own theme](Scenic.Toggle.Components.html#toggle/3-theme).
596+
597+
* `:hidden` - If `false` the toggle is rendered. If true, it is skipped. The default
598+
is `false`.
599+
* `:theme` - The color set used to draw. See below. The default is `:dark`
600+
601+
### Additional Styles
602+
603+
Toggles also honor the following additional styles.
604+
605+
* `:border_width` - the border width. Defaults to `2`.
606+
* `:padding` - the space between the border and the thumb. Defaults to `2`
607+
* `:thumb_radius` - the radius of the thumb. This determines the size of the entire toggle. Defaults to `10`.
608+
609+
## Theme
610+
611+
To pass in a custom theme, supply a map with at least the following entries:
612+
613+
* `:border` - the color of the border around the toggle
614+
* `:background` - the color of the track when the toggle is `off`.
615+
* `:text` - the color of the thumb.
616+
* `:thumb` - the color of the track when the toggle is `on`.
617+
618+
Optionally, you can supply the following entries:
619+
620+
* `:thumb_pressed` - the color of the thumb when pressed. Defaults to `:gainsboro`.
621+
622+
### Examples
623+
624+
The following example creates a toggle.
625+
graph
626+
|> toggle(true, translate: {20, 20})
627+
628+
The next example makes a larger toggle.
629+
graph
630+
|> toggle(true, translate: {20, 20}, thumb_radius: 14)
631+
"""
632+
@spec toggle(Graph.t() | Primitive.t(), boolean, Keyword.t() | nil) :: Graph.t()
633+
def toggle(graph, data, options \\ [])
634+
635+
def toggle(%Graph{} = g, data, options) do
636+
add_to_graph(g, Component.Input.Toggle, data, options)
637+
end
638+
639+
def toggle(%Primitive{module: SceneRef} = p, data, options) do
640+
modify(p, Component.Input.Toggle, data, options)
641+
end
642+
588643
# ============================================================================
589644
# generic workhorse versions
590645

0 commit comments

Comments
 (0)