Skip to content

Commit 73ca1ee

Browse files
committed
add graph docs. cleanup
1 parent d39e317 commit 73ca1ee

File tree

3 files changed

+239
-42
lines changed

3 files changed

+239
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
* Much improved testing
55
* Simplify rules around user-closing a viewport
66
* Fix bug where captured positional inputs were not casting the transformed location
7+
* Deprecate Graph.get_root(). Use Graph.get!(graph, :\_root\_) instead.
78

89
## 0.8.0
910

lib/scenic/graph.ex

Lines changed: 235 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,128 @@
44
#
55

66
defmodule Scenic.Graph do
7-
@moduledoc false
7+
8+
@moduledoc """
9+
Please see [`Graph Overview`](overview_graph.html) for a high-level description.
10+
11+
## What is a Graph
12+
13+
There are many types of graphs in the world of computer science. There are graphs that
14+
show data to a user. There are graphs that give access to databases. Graphs that link
15+
people to a social network.
16+
17+
In Scenic, a Graph is a graph in same way that the DOM in HTML is a graph. It is a
18+
hierarchical tree of data that describes a set of things to draw on the screen.
19+
20+
You build a graph by appending primitives (individual things to draw) to the current
21+
node in the tree. Nodes are represented by the Group primitive in Scenic.
22+
23+
The following example builds a simple graph that displays some text, creates a group,
24+
then adds more text and a rounded rectangle to it.
25+
26+
@graph Scenic.Graph.build()
27+
|> text( "This is some text", translate: {20, 20} )
28+
|> group( fn(graph) ->
29+
graph
30+
|> text( "This text is in a group", translate: {200, 24} )
31+
|> rounded_rectangle( {400, 30}, stroke: {2, :blue})
32+
end, translate: {20, 100}, text_align: :center)
33+
34+
There is a fair amount going on in the example above, we we will break it down.
35+
The first line
36+
37+
@graph Scenic.Graph.build()
38+
39+
builds an empty graph with only one group as the root node. By assigning it to the
40+
compile directive @group, we know that this group will be built once at compile
41+
time and will be very fast to access later during runtime.
42+
43+
The empty graph that is returned from `build()` is then passed to `text(...)`, which
44+
adds a text primitive to the root group. The resulting graph from that call is then
45+
passed again into the `group(...)` call. This creates a new group and has calls an
46+
anonymous function that you can use to add primitives to the newly created group.
47+
48+
Notice that the anonymous "builder" function receives a graph as its only parameter.
49+
This is the same graph that we are building, except that it has a marker indicating
50+
that new primitives added to it are inserted into the new group instead of the
51+
root of the graph.
52+
53+
Finally, when the group is finished, a translation matrix and a `:text_align` style
54+
are added to it. These properties are _inherited_ by the primitives in the group.
55+
56+
## Inheritance
57+
58+
An important concept to understand is that both [styles](overview_styles.html) and
59+
[transforms](overview_styles.html) are inherited down the graph. This means that if
60+
you apply a style or transform to any group (including the root), then all primitives
61+
contained by that group will have those properties applied to them too. This is true
62+
even if the primitive is nested in several groups at the same time.
63+
64+
@graph Scenic.Graph.build(font: :roboto_mono)
65+
|> text( "This text inherits the font", translate: {20, 20} )
66+
|> group( fn(graph) ->
67+
graph
68+
|> text( "This text also inherits the font", translate: {200, 24} )
69+
|> text( "This text overrides the font", font: :roboto )
70+
end, translate: {20, 100}, text_align: :center)
71+
72+
Transforms, such as translate, rotate, scale, also inherit down the graph, but do
73+
so slightly differently than the styles. With a style, when you set a specific value
74+
on a primitive, that overrides the inherited value of the same type.
75+
76+
With a transform, the values multiply together. This allows you to position items
77+
within a group relative to the origin {0,0}, then move the group as a whole, keeping
78+
the interior positions unchanged.
79+
80+
## Modifying a Graph
81+
82+
Scenic was written specifically for Erlang/Elixir, which is a functional programming
83+
model with immutable data.
84+
85+
As such, once you make a graph, it stays in memory unchanged - until you transform it
86+
via `Graph.modify/3`. Technically you never change it (that's the immutable part),
87+
instead Graph.modify returns a new graph with different data in it.
88+
89+
[Graph.modify/3](Scenic.Graph.html#modify/3) is the single Graph function that you
90+
will use the most.
91+
92+
For example, lets go back to our graph with the two text items in it.
93+
94+
@graph Graph.build(font: :roboto, font_size: 24, rotate: 0.4)
95+
|> text("Hello World", translate: {300, 300}, id: :small_text)
96+
|> text("Bigger Hello", font_size: 40, scale: 1.5, id: :big_text)
97+
98+
This time, we've assigned ids to both of the text primitives. This makes it easy to
99+
find and modify that primitive in the graph.
100+
101+
graph =
102+
@graph
103+
|> Graph.modify( :small_text, &text(&1, "Smaller Hello", font_size: 16))
104+
|> Graph.modify( :big_text, &text(&1, "Bigger Hello", font_size: 60))
105+
|> push_graph()
106+
107+
Notice that the graph is modified multiple times in the pipeline. The `push_graph/1`
108+
function is relatively heavy when the graph references other scenes. The recommended
109+
pattern is to make multiple changes to the graph and then push once at the end.
110+
111+
## Accessing Primitives
112+
113+
When using a Graph, it is extremely common to access and modify primitives. They way
114+
you do this is by putting an id on the primitives you care about in a graph.
115+
116+
@graph Graph.build()
117+
|> text("small text", id: :small_text)
118+
|> text("bit text", id: :big_text)
119+
120+
When you get primitives, or modify a graph, you specify them by id. This happens
121+
quickly, but at a cost of using a little memory. If you aren't going to access
122+
a primitive, then don't assign an id to them.
123+
124+
One interesting note: There is nothing saying you have to use an atom as the id.
125+
In fact you can use any Erlang term you want. This can be very powerful, especially
126+
when used to identify components...
127+
"""
128+
8129

9130
alias Scenic.Primitive
10131
alias Scenic.Primitive.Group
@@ -46,17 +167,26 @@ defmodule Scenic.Graph do
46167
@err_msg_put "Graph.put can only update existing items."
47168
@err_msg_get_id_one "Graph.get! expected to find one and only one element"
48169

49-
# ============================================================================
50-
# access to the raw graph fields
51-
# concentrate all knowledge of the internal structure of the graph tuple here
52-
170+
# ============================================================================
171+
@doc """
172+
Returns the root group of a graph as a primitive.
173+
Deprecated. Use `Graph.get!(graph, :_root_)` instead.
174+
"""
175+
@deprecated "Use Graph.get!(graph, :_root_) instead"
53176
@spec get_root(graph :: t()) :: Primitive.t()
54177
def get_root(%__MODULE__{} = graph) do
55-
Map.delete(graph.primitives[@root_uid], :styles)
178+
get!(graph, :_root_)
179+
# Map.delete(graph.primitives[@root_uid], :styles)
56180
end
57181

58182
# ============================================================================
59183
# build a new graph, starting with the given element
184+
@doc """
185+
Builds and returns an empty graph.
186+
187+
Just like any primitive, you can pass in an option list of styles and transforms.
188+
These will be applied to the otherwise empty root group in the new graph.
189+
"""
60190
@spec build(opts :: keyword) :: t()
61191
def build(opts \\ []) do
62192
opts = handle_options(opts)
@@ -70,6 +200,16 @@ defmodule Scenic.Graph do
70200

71201
# ============================================================================
72202
# add a pre-built primitive
203+
@doc """
204+
Add a pre-built primitive to the current group in the graph.
205+
206+
This is usually called during graph construction. When add a new Group primitive
207+
to a Graph, it marks the new group as the current one before calling the group's
208+
builder function. This is what allows you to add primitives to the right place
209+
in the new Group.
210+
211+
Note that all primitives added to a group are appended to the draw order.
212+
"""
73213
@spec add(graph :: t(), primitive :: Primitive.t()) :: t()
74214
def add(graph, primitive)
75215

@@ -79,6 +219,16 @@ defmodule Scenic.Graph do
79219
end
80220

81221
# build and add new primitives
222+
@doc """
223+
Build and add a primitive to the current group in the graph.
224+
225+
This is usually called during graph construction. When add a new Group primitive
226+
to a Graph, it marks the new group as the current one before calling the group's
227+
builder function. This is what allows you to add primitives to the right place
228+
in the new Group.
229+
230+
Note that all primitives added to a group are appended to the draw order.
231+
"""
82232
@spec add(graph :: t(), module :: atom, data :: any, opts :: keyword) :: t()
83233
def add(graph, primitive_module, primitive_data, opts \\ [])
84234

@@ -103,6 +253,15 @@ defmodule Scenic.Graph do
103253

104254
# ============================================================================
105255
# delete a primitive/s from a graph
256+
@doc """
257+
Permanently delete a primitive from a group by id.
258+
259+
This will remove a primitive (or many if they have the same id) from a graph. It
260+
then returns the modified graph.
261+
262+
If you delete a group from a graph, then all primitives contained by that
263+
group are deleted as well.
264+
"""
106265
@spec delete(graph :: t(), id :: any) :: t()
107266
def delete(%__MODULE__{primitives: primitives, ids: ids} = graph, id) do
108267
# resolve the id into a list of uids
@@ -141,6 +300,8 @@ defmodule Scenic.Graph do
141300
end
142301

143302
# ============================================================================
303+
# KEEP THIS AROUND FOR NOW
304+
# might want to put it back in...
144305
# add a pre-built primitive to an existing group in a graph
145306
# def add_to( graph, id, primitive )
146307

@@ -242,24 +403,6 @@ defmodule Scenic.Graph do
242403
Map.put(ids, id, uid_list)
243404
end
244405

245-
# ============================================================================
246-
# remove an entry in the ids
247-
# defp unmap_id_to_uid(graph, id, uid)
248-
# defp unmap_id_to_uid(graph, nil, _uid), do: graph
249-
# defp unmap_id_to_uid(graph, _id, nil), do: graph
250-
# defp unmap_id_to_uid(%Graph{ids: ids} = graph, id, uid) when is_integer(uid) do
251-
252-
# uid_list = Map.get(ids, id, [])
253-
# |> Enum.reject(fn(mapped_uid)-> mapped_uid == uid end)
254-
255-
# ids = case uid_list do
256-
# [] -> Map.delete(ids, id)
257-
# uids -> Map.put(ids, id, uids)
258-
# end
259-
260-
# %{graph | ids: ids}
261-
# end
262-
263406
# ============================================================================
264407
defp resolve_id(graph, id)
265408

@@ -270,6 +413,12 @@ defmodule Scenic.Graph do
270413
# ============================================================================
271414
# --------------------------------------------------------
272415
# count all the nodes in a graph.
416+
@doc """
417+
Returns a count of all the primitives in a graph.
418+
419+
The root Group counts as a primitive, so an empty graph should have a count
420+
of 1.
421+
"""
273422
@spec count(graph :: t()) :: integer
274423
def count(graph)
275424

@@ -279,6 +428,9 @@ defmodule Scenic.Graph do
279428

280429
# --------------------------------------------------------
281430
# count the nodes associated with an id.
431+
@doc """
432+
Returns a count of all the primitives in a graph with a specific id.
433+
"""
282434
@spec count(graph :: t(), id :: any) :: integer
283435
def count(graph, id)
284436

@@ -306,6 +458,9 @@ defmodule Scenic.Graph do
306458

307459
# ============================================================================
308460
# get a list of primitives by id
461+
@doc """
462+
Returns a list of primitives from a graph with a specific id.
463+
"""
309464
@spec get(graph :: t(), id :: any) :: list(Primitive.t())
310465
def get(%__MODULE__{} = graph, id) do
311466
graph
@@ -316,6 +471,12 @@ defmodule Scenic.Graph do
316471

317472
# --------------------------------------------------------
318473
# get a single primitive by id. Raise error if it finds any count other than one
474+
@doc """
475+
Returns a single primitive from a graph with a specific id.
476+
477+
This will raise an error if either none or multiple primitives are found with
478+
the specified id.
479+
"""
319480
@spec get!(graph :: t(), id :: any) :: Primitive.t()
320481
def get!(%__MODULE__{} = graph, id) do
321482
case resolve_id(graph, id) do
@@ -519,6 +680,19 @@ defmodule Scenic.Graph do
519680
end
520681
end
521682

683+
684+
685+
@doc """
686+
Modify one or more primitives in a graph.
687+
688+
Retrieves the primitive (or primitives) specified by an id and passes them to
689+
a callback function. The result of the callback function is stored as the new
690+
version of that primitive in the graph.
691+
692+
If multiple primitives match the specified id, then each is passed, in turn,
693+
to the callback function.
694+
"""
695+
522696
@spec modify(graph :: t(), id :: any, action :: (... -> Primitive.t())) :: t()
523697
def modify(graph, id, action)
524698

@@ -531,13 +705,30 @@ defmodule Scenic.Graph do
531705

532706
# ============================================================================
533707
# map a graph via traversal from the root node
708+
@doc """
709+
Map all primitives in a graph into a new graph.
710+
711+
Crawls through the entire graph, passing each primitive to the callback function.
712+
The result of the callback replaces that primitive in the graph. The updated
713+
graph is returned.
714+
"""
715+
534716
@spec map(graph :: t(), action :: function) :: t()
535717
def map(%__MODULE__{} = graph, action) when is_function(action, 1) do
536718
do_map(graph, @root_uid, action)
537719
end
538720

539721
# ============================================================================
540-
# map a graph, but only those elements mapped to the id
722+
@doc """
723+
Map all primitives in a graph that match a specified id into a new graph.
724+
725+
Crawls through the entire graph, passing each primitive to the callback function.
726+
The result of the callback replaces that primitive in the graph. The updated
727+
graph is returned.
728+
729+
This is so similar to the modify function that it may be deprecated in the future.
730+
For now I recommend you use `Graph.modify/3` instead of this.
731+
"""
541732
@spec map(graph :: t(), id :: any, action :: function) :: t()
542733
def map(%__MODULE__{} = graph, id, action) when is_function(action, 1) do
543734
# resolve the id into a list of uids
@@ -589,14 +780,32 @@ defmodule Scenic.Graph do
589780
end
590781

591782
# ============================================================================
783+
@doc """
784+
Invokes action for each primitive in the graph with the accumulator.
785+
786+
Iterates over all primitives in a graph, passing each into the callback function
787+
with an accumulator. The return value of the callback is the new accumulator.
788+
789+
This is extremely similar in behaviour to Elixir's Enum.reduce function, except
790+
that it understands now to navigate the tree structure of a Graph.
791+
"""
792+
592793
# reduce a graph via traversal from the root node
593794
@spec reduce(graph :: t(), acc :: any, action :: function) :: any
594795
def reduce(%__MODULE__{} = graph, acc, action) when is_function(action, 2) do
595796
do_reduce(graph, @root_uid, acc, action)
596797
end
597798

598799
# ============================================================================
599-
# reduce a graph, but only for nodes mapped to the given id
800+
@doc """
801+
Invokes action for each primitive that matches an id in the graph with the accumulator.
802+
803+
Iterates over all primitives that match a specified id, passing each into the callback
804+
function with an accumulator.
805+
806+
This is extremely similar in behaviour to Elixir's Enum.reduce function, except
807+
that it understands now to navigate the tree structure of a Graph.
808+
"""
600809
@spec reduce(graph :: t(), id :: any, acc :: any, action :: function) :: any
601810
def reduce(%__MODULE__{} = graph, id, acc, action) when is_function(action, 2) do
602811
# resolve the id into a list of uids

0 commit comments

Comments
 (0)