View Source Scenic.Scene behaviour (Scenic v0.11.2)
overview
Overview
Scenes are the core of the UI model.
A Scene is a type of GenServer that maintains state, handles input, events and other messages, and plays a role managing the supervision of components/controls such as buttons and other input.
a-brief-aside
A brief aside
Before saying anything else I want to emphasize that the rest of your application, meaning device control logic, sensor reading / writing, services, whatever, does not need to have anything to do with Scenes.
In many cases I recommend treating those as separate GenServers in their own supervision trees that you maintain. Then your Scenes would query or send information to/from them via cast or call messages.
Part of the point of using Elixir/Erlang/OTP is separating this sort of logic into independent trees. That way, an error in one part of your application does not mean the rest of it will fail.
scenes
Scenes
Scenes are the core of the UI model. A scene consists of one or more graphs, and a set of event handlers and filters to deal with user input and other messages.
Think of a scene as being a little like an HTML page. HTML pages have:
- Structure (the DOM)
- Logic (Javascript)
- Links to other pages.
A Scene has:
- Structure (graphs),
- Logic (event handlers and filters)
- Transitions to other scenes. Well... it can request the
ViewPort
to go to a different scene.
Your application is a collection of scenes that are in use at different times. There
is only ever one scene showing in a ViewPort
at a given
time. However, scenes can instantiate components, effectively embedding their graphs
inside the main one. More on that below.
graphs
Graphs
Each scene should maintain at least one graph. You can build graphs at compile time, or dynamically while your scene is running. Building them at compile time has two advantages
- Performance: It is clearly faster to build the graph once during build time than to build it repeatedly during runtime.
- Error checking: If your graph has an error in it, it is much better to have it stop compilation than cause an error during runtime.
Example of building a graph during compile time:
@graph Scenic.Graph.build(font_size: 24)
|> button({"Press Me", :button_id}, translate: {20, 20})
Rather than having a single scene maintain a massive graph of UI, graphs can reference graphs in other scenes.
On a typical screen of UI, there is one scene
that is the root. Each control, is its own scene process with
its own state. These child scenes can in turn contain other
child scenes. This allows for strong code reuse, isolates knowledge
and logic to just the pieces that need it, and keeps the size of any
given graph to a reasonable size. For example, the
handlers of a check-box scene don't need to know anything about
how a slider works, even though they are both used in the same
parent scene. At best, they only need to know that they
both conform to the Component.Input
behavior, and can thus
query or set each others value. Though it is usually
the parent scene that does that.
The application developer is responsible for building and
maintaining the scene's graph. It only enters the world of the
ViewPort
when you call push_graph
. Once you have called
push_graph
, that graph is sent to the drivers and is out of your
immediate control. You update that graph by calling push_graph
again.
scene-structure
Scene Structure
The overall structure of a scene has several parts. It is a GenServer
, which means you
have an input function and can implement the GenServer callbacks such as
handle_info/2
, handle_cast/2
, handle_call/3
and any others. The terms
you return from those callbacks is pretty much what you expect with the requirement that
the state is always a scene structure.
The init/3
callback takes 3 parameters. They are the scene structure (state), params that
the parent scene set up, and opts, which includes things like the id, theme, and some styles
that were set on the scene.
There are several additional callbacks that your scene can support. The main ones are
handle_input/2
, and handle_event/3
. If you are making a Component (a reusable scene), then
there are additional callbacks that allow it to play nicely with others.
scene-example
Scene Example
This example shows a simple scene that contains a button. When the button is clicked, the scene increments a counter and displays the number of clicks it has received.
defmodule MySimpleScene do
use Scenic.Scene
alias Scenic.Graph
import Scenic.Primitives
import Scenic.Components
@initial_count 0
# This graph is built at compile time so it doesn't do any work at runtime.
# It could also be built in the init function (or any other) if you want
# it to be dynamic based on the params or whatever.
@graph Graph.build()
|> group( fn graph ->
graph
|> text( "Count: " <> inspect(@initial_count), id: :count )
|> button( "Click Me", id: :btn, translate: {0, 30} )
end,
translate: {100, 100}
)
# Simple function to return @graph.
# @graph is built at compile time and stored directly in the BEAM file every
# time it is used. A simple accessor function will cause it to be stored only once.
# Do this when you build graphs at compile time to save space in your file.
defp graph(), do: @graph
# The Scenic.Scene init function
@impl Scenic.Scene
def init(scene, _params, _opts) do
scene =
scene
|> assign( count: @initial_count )
|> push_graph( graph() )
{:ok, scene}
end
@impl Scenic.Scene
def handle_event( {:click, :btn}, _, %{assigns: %{count: count}} = scene ) do
count = count + 1
# modify the graph to show the current click count
graph =
graph()
|> Graph.modify( :count, &text(&1, "Count: " <> inspect(count)) )
# update the count and push the modified graph
scene =
scene
|> assign( count: count )
|> push_graph( graph )
# return the updated scene
{ :noreply, scene }
end
end
scene-state
Scene State
Scenes are just a specialized form of GenServer
. This means they can react to messages
and can have state. However, scene state is a bit like socket state in Phoenix
in that
It has a strict format, but you can add anything you want into its :assigns
map.
Multiple Graphs
Any given scene can maintain multiple graphs and multiple draw scripts. They are identified from each other with an id that you attach.
Normally when you use push_graph, you don't attach an ID. In that case the scene's id is used as the graph id.
The act of pushing a graph to the ViewPort causes it to be compiled into
a script, which is stored in an ETS table so that the drivers can quickly
access it. To use a second, or third, graph that you scene has pushed, refer
to it using a Scenic.Primitive.Script
primitive.
def init( scene, param, opts ) do
second_graph = Scenic.Graph.build()
|> text( "Text in the second graph" )
main_graph = Scenic.Graph.build()
|> script( "my_fancy_id" )
scene =
scene
|> push_graph( main_graph )
|> push_graph( second_graph, "my_fancy_id" )
{ :ok, scene }
end
Note that it doesn't matter which graph you push first. They will link to each other via the string id that you supply.
communications
Communications
Scenes are specialized GenServers. As such, they communicate with each other
(and the rest of your application) through messages. You can receive
messages with the standard handle_info
, handle_cast
, handle_call
callbacks just
like any other scene.
Scenes have two new event handling callbacks that you can optionally implement. These are about user input vs UI events.
input-vs-events
Input vs. Events
Input is data generated by the drivers and sent up to the scenes
through Scenic.ViewPort
. There is a limited set of input types and
they are standardized so that the drivers can be built independently
of the scenes. Input follows certain rules about which scene receives them.
Events are messages that one scene generates for consumption
by other scenes. For example, a Scenic.Component.Button
scene would
generate a {:click, msg}
event that is sent to its parent
scene.
You can generate any message you want, however, the standard component libraries follow certain patterns to keep things sensible.
input-handling
Input Handling
You handle incoming input events by adding handle_input/3
callback
functions to your scene. Each handle_input/3
call passes in the input
message itself, an input context struct, and your scene's state. You can then
take the appropriate actions, including generating events (below) in response.
Under normal operation, input that is not position dependent (keys, window events, more...) is sent to the root scene. Input that does have a screen position (cursor_pos, cursor button presses, etc.) is sent to the scene that contains the graph that was hit.
Your scene can "capture" all input of a given type so that it is sent to itself instead of the default scene for that type. this is how a text input field receives the key input. First, the user selects that field by clicking on it. In response to the cursor input, the text field captures text input (and maybe transforms its graph to show that it is selected).
Captured input types should be released when no longer needed so that normal operation can resume.
The input messages are passed on to a scene's parent if not processed.
event-filtering
Event Filtering
In response to input, (or anything else... a timer perhaps?), a scene can generate an event (any term), which is sent backwards up the tree of scenes that make up the current aggregate graph.
In this way, a Scenic.Component.Button
scene can generate a {:click, msg}
event that is sent to its parent. If the parent doesn't
handle it, it is sent to that scene's parent. And so on until the
event reaches the root scene. If the root scene also doesn't handle
it then the event is dropped.
To handle events, you add handle_event/3
functions to your scene.
This function handles the event, and stops its progress backwards
up the graph. It can handle it and allow it to continue up the
graph. Or it can transform the event and pass the transformed
version up the graph.
You choose the behavior by returning either
{:cont, msg, state}
or
{:halt, state}
Parameters passed in to handle_event/3
are the event itself, a
reference to the originating scene (which you can to communicate
back to it), and your scene's state.
A pattern I'm using is to handle an event at the filter and stop its progression. It also generates and sends a new event to its parent. I do this instead of transforming and continuing when I want to change the originating scene.
no-children
No children
There is an optimization you can use. If you know for certain that your component
will not attempt to use any components, you can set has_children
to false
like this.
use Scenic.Component, has_children: false
Setting has_children
to false
means the scene won't create
a dynamic supervisor for this scene, which saves some resources and imporoves startup
time.
For example, the Button component sets has_children
to false
.
Link to this section Summary
Callbacks
Invoked when the Scene
receives an event from another scene.
Retrieve the current data associated with the scene and return it to the caller.
Get the current "value" associated with the scene and return it to the caller.
Invoked when the Scene
receives input from a driver.
Put the current "value" associated with the scene .
Update the data and options of a scene. Usually implemented by Components.
Invoked when the Scene
is started.
Functions
Convenience function to assign a list or map of values into a scene struct.
Convenience function to assign a value into a scene struct.
Convenience function to assign a list of new values into a scene struct.
Convenience function to assign a new values into a scene struct.
Request one or more types of input that a scene would otherwise not receive if not captured. This is rarely used by scenes and even then mostly for things like key events outside of a text field.
Cast a message to a scene's parent
Get the pid
of the child with the specified id.
Get a list of {id, pid}
pairs for all the scene's children.
Convenience function to fetch an assigned value out of a scene struct.
Fetch a list of input captured by the given scene.
Fetch the "data" of the child with the specified id.
Fetch a list of input requested by the given scene.
Fetch the "parent" matrix that positions this scene.
Convenience function to get an assigned value out of a scene struct.
Get the "value" of the child with the specified id.
Get the "parent" matrix that positions this scene.
Convert a point in global coordinates to scene local coordinates.
Convert a point in scene local coordinates to global coordinates.
Get the pid
of the scene's parent.
Push a graph to the scene's ViewPort.
Push a named script to the scene's ViewPort.
Put the "value" of the child with the specified id.
release all currently requested input.
Request one or more types of input that a scene would otherwise not receive if not captured. This is rarely used by scenes and even then mostly for things like key events outside of a text field.
Cast a message to a scene's children
Send an event message to a specific scene
Send a message to a scene's parent
Send an event message to a scene's parent
Cleanly stop a scene from running
release all currently requested input.
Update the "data" of the child with the specified id.
Link to this section Types
Link to this section Callbacks
@callback handle_event(event :: term(), from :: pid(), scene :: t()) :: {:noreply, scene} | {:noreply, scene} | {:noreply, scene, timeout()} | {:noreply, scene, :hibernate} | {:noreply, scene, opts :: response_opts()} | {:halt, scene} | {:halt, scene, timeout()} | {:halt, scene, :hibernate} | {:halt, scene, opts :: response_opts()} | {:cont, event, scene} | {:cont, event, scene, timeout()} | {:cont, event, scene, :hibernate} | {:cont, event, scene, opts :: response_opts()} | {:stop, reason, scene} when scene: t(), reason: term(), event: term()
Invoked when the Scene
receives an event from another scene.
Events are messages generated by a scene, that are passed backwards up the ViewPort's scene supervision tree. This is opposed to "input", which comes directly from the drivers.
When an event arrives at a scene, you can consume it, or pass it along to the scene above you in the ViewPort's supervision structure.
To consume the input and have processing stop afterward, return either a {:halt, ...}
or
{:noreply, ...}
value. They are effectively the same thing.
To allow the scene's parent to process the input, return {:cont, event, state, ...}
. Note
that you can pass along the event unchanged or transform it in the process if you wish.
The callback supports all the return values of the
init
callback in Genserver
.
In addition to the normal return values defined by GenServer, a Scene
can
add an optional {push: graph}
term, which pushes the graph to the viewport.
This has replaced push_graph() as the preferred way to push a graph.
@callback handle_fetch(from :: GenServer.from(), scene :: t()) :: {:reply, reply, scene} | {:reply, reply, scene, timeout() | :hibernate | {:continue, term()}} when reply: term(), scene: t()
Retrieve the current data associated with the scene and return it to the caller.
If this callback is not implemented, the caller with get an {:error, :not_implemented}.
@callback handle_get(from :: GenServer.from(), scene :: t()) :: {:reply, reply, scene} | {:reply, reply, scene, timeout() | :hibernate | {:continue, term()}} when reply: term(), scene: t()
Get the current "value" associated with the scene and return it to the caller.
If this callback is not implemented, the caller with receive nil.
@callback handle_input(input :: Scenic.ViewPort.Input.t(), id :: any(), scene :: t()) :: {:noreply, scene} | {:noreply, scene} | {:noreply, scene, timeout()} | {:noreply, scene, :hibernate} | {:noreply, scene, opts :: response_opts()} | {:halt, scene} | {:halt, scene, timeout()} | {:halt, scene, :hibernate} | {:halt, scene, opts :: response_opts()} | {:cont, input, scene} | {:cont, input, scene, timeout()} | {:cont, input, scene, :hibernate} | {:cont, input, scene, opts :: response_opts()} | {:stop, reason, scene} when scene: t(), reason: term(), input: term()
Invoked when the Scene
receives input from a driver.
Input is messages sent directly from a driver, usually based on some action by the user. This is opposed to "events", which are generated by other scenes.
When input arrives at a scene, you can consume it, or pass it along to the scene above you in the ViewPort's supervision structure.
To consume the input and have processing stop afterward, return either a {:halt, ...}
or
{:noreply, ...}
value. They are effectively the same thing.
To allow the scene's parent to process the input, return {:cont, input, state, ...}
. Note
that you can pass along the input unchanged or transform it in the process if you wish.
The callback supports all the return values of the
init
callback in Genserver
.
In addition to the normal return values defined by GenServer, a Scene
can
add an optional {push: graph}
term, which pushes the graph to the viewport.
This has replaced push_graph() as the preferred way to push a graph.
@callback handle_put(value :: any(), scene :: t()) :: {:noreply, scene} | {:noreply, scene, timeout() | :hibernate | {:continue, term()}} when scene: t()
Put the current "value" associated with the scene .
Does nothing if this callback is not implemented.
@callback handle_update(data :: any(), opts :: Keyword.t(), scene :: t()) :: {:noreply, scene} | {:noreply, scene, timeout() | :hibernate | {:continue, term()}} when scene: t()
Update the data and options of a scene. Usually implemented by Components.
If this callback is not implemented, then changes to the component in the parent's graph will have no affect.
@callback init(scene :: t(), args :: term(), options :: Keyword.t()) :: {:ok, scene} | {:ok, scene, timeout :: non_neg_integer()} | {:ok, scene, :hibernate} | {:ok, scene, opts :: response_opts()} | :ignore | {:stop, reason} when scene: t(), reason: term()
Invoked when the Scene
is started.
args
is the argument term you passed in via config or ViewPort.set_root.
options
is a list of information giving you context about the environment
the scene is running in. If an option is not in the list, then it should be
treated as nil.
:viewport
- This is the pid of the ViewPort that is managing this dynamic scene. It will be not set, or nil, if you are managing the Scene in a static supervisor yourself.:styles
- This is the map of styles that your scene can choose to inherit (or not) from its parent scene. This is typically used by a child control that wants to visually fit into its parent's look.:id
- This is the :id term that the parent set a component when it was invoked.
The callback supports all the return values of the
init
callback in Genserver
.
In addition to the normal return values defined by GenServer, a Scene
can
return two new ones that push a graph to the viewport
Returning {:ok, state, push: graph}
will push the indicated graph
to the ViewPort. This is preferable to the old push_graph() function.
Link to this section Functions
Convenience function to assign a list or map of values into a scene struct.
Convenience function to assign a value into a scene struct.
Convenience function to assign a list of new values into a scene struct.
Only values that do not already exist will be assigned
Convenience function to assign a new values into a scene struct.
The value will only be assigned if it does not already exist in the struct.
@spec capture_input( scene :: t(), input_class :: Scenic.ViewPort.Input.class() | [Scenic.ViewPort.Input.class()] ) :: :ok | {:error, atom()}
Request one or more types of input that a scene would otherwise not receive if not captured. This is rarely used by scenes and even then mostly for things like key events outside of a text field.
Any input types that were previously requested that are no longer in the request list are dropped. Request [] to cancel all input requests.
returns :ok or an error
This is intended be called by a Scene process, but doesn't need to be.
Cast a message to a scene's parent
Get the pid
of the child with the specified id.
You can specify the same ID to more than one child. This is why the return is a list.
Get a list of {id, pid}
pairs for all the scene's children.
Convenience function to fetch an assigned value out of a scene struct.
@spec fetch_captures(scene :: t()) :: {:ok, [Scenic.ViewPort.Input.class()]} | {:error, atom()}
Fetch a list of input captured by the given scene.
This is intended be called by a Scene process, but doesn't need to be.
@spec fetch_child(scene :: t(), id :: any()) :: {:ok, [child_pid :: pid()]} | {:error, :no_children}
Fetch the "data" of the child with the specified id.
This function is intended to be used to query the current data of a component.
The component must have implemented the Scenic.Scene.handle_fetch/2
callback.
All of the built-in components support this.
Unlike get_child
, fetch_child
returns the full data associated with
component, not just the current value. For example a checkbox component
might fetch the value {"My Checkbox", true}
You can specify the same ID to more than one child. This is why the return is a list.
@spec fetch_requests(scene :: t()) :: {:ok, [Scenic.ViewPort.Input.class()]} | {:error, atom()}
Fetch a list of input requested by the given scene.
This is intended be called by a Scene process, but doesn't need to be.
@spec fetch_transform(scene :: t()) :: {:ok, Scenic.Math.matrix()} | {:error, :not_found}
Fetch the "parent" matrix that positions this scene.
This matrix can be used to move from scene "local" coordinates to global coordinates.
Convenience function to get an assigned value out of a scene struct.
Get the "value" of the child with the specified id.
This function is intended to be used to query the current value of a component.
The component must have implemented the Scenic.Scene.handle_get/2
callback.
All of the built-in components support this.
For example, you could use this to query the current value of a checkbox.
You can specify the same ID to more than one child. This is why the return is a list.
@spec get_transform(scene :: t()) :: Scenic.Math.matrix()
Get the "parent" matrix that positions this scene.
This matrix can be used to move from scene "local" coordinates to global coordinates.
@spec global_to_local(scene :: t(), Scenic.Math.point()) :: Scenic.Math.point()
Convert a point in global coordinates to scene local coordinates.
@spec local_to_global(scene :: t(), Scenic.Math.point()) :: Scenic.Math.point()
Convert a point in scene local coordinates to global coordinates.
Get the pid
of the scene's parent.
@spec push_graph(scene :: t(), graph :: Scenic.Graph.t(), name :: String.t() | nil) :: t()
Push a graph to the scene's ViewPort.
This function compiles a graph into a script, registers any requested inputs and stores it all in the ViewPort's ETS tables.
Any components that are created or removed from the scene are started/stopped/updated as appropriate.
@spec push_script( scene :: t(), script :: Scenic.Script.t(), name :: String.t(), opts :: Keyword.t() ) :: t()
Push a named script to the scene's ViewPort.
Put the "value" of the child with the specified id.
This function is intended to be used to change the current value of a component.
The component must have implemented the Scenic.Scene.handle_put/2
callback.
All of the built-in components support this.
For example, you could use this to change the current value of a checkbox.
In this case, the returned value would be true
or false
.
You can specify the same ID to more than one child. This will cause all the components with that id to receive the handle_put call.
@spec release_input( scene :: t(), input_class :: Scenic.ViewPort.Input.class() | [Scenic.ViewPort.Input.class()] | :all ) :: :ok | {:error, atom()}
release all currently requested input.
This is intended be called by a Scene process, but doesn't need to be.
@spec request_input( scene :: t(), input_class :: Scenic.ViewPort.Input.class() | [Scenic.ViewPort.Input.class()] ) :: :ok | {:error, atom()}
Request one or more types of input that a scene would otherwise not receive if not captured. This is rarely used by scenes and even then mostly for things like key events outside of a text field.
Any input types that were previously requested that are no longer in the request list are dropped. Request [] to cancel all input requests.
returns :ok or an error
This is intended be called by a Scene process, but doesn't need to be.
Cast a message to a scene's children
Send an event message to a specific scene
Send a message to a scene's parent
Send an event message to a scene's parent
@spec stop(scene :: t()) :: :ok
Cleanly stop a scene from running
@spec unrequest_input( scene :: t(), input_class :: Scenic.ViewPort.Input.class() | [Scenic.ViewPort.Input.class()] | :all ) :: :ok | {:error, atom()}
release all currently requested input.
This is intended be called by a Scene process, but doesn't need to be.
Update the "data" of the child with the specified id.
This function is intended to be used to update the current data of a component.
The component must have implemented the Scenic.Scene.handle_update/3
callback.
All of the built-in components support this.
Unlike put_child
, update_child
effectively re-initializes the component with the
new data. This would be the same data format you have provided when you created the
component in the first place. For example, you might update a checkbox component
with the value {"New Label", true}
You can specify the same ID to more than one child. This will cause all the components with that id to receive the handle_put call.