Scenic v0.9.0 Scenic.Scene behaviour View Source

Overview

Scenes are the core of the UI model.

A Scene has three jobs.

  1. Maintain any state specific to that part of your application.
  2. Build and maintain a graph of UI primitives that gets drawn to the screen.
  3. Handle input and events.

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 seperate 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 Erlang/Elixir/OTP is seperating this sort of logic into independant trees. That way, an error in one part of your application does not mean the rest of it will fail.

Scenes

So.. scenes are the core of the UI model. A scene consists of one ore 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 various times. There is only ever one scene showing in a ViewPort at a given time. However, scenes can reference other scenes, effectively embedding their graphs inside the main one. More on that below.

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

  1. Performance: It is clearly faster to build the graph once during build time to to build it repeatedly during runtime.
  2. 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 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 graph hand handlers of a checkbox 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 behaviour, 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 either calling push_graph again, or cleaning it up via release_graph.

This does mean you could maintain two seperate graphs and rapidly switch back and forth between them via push_graph. I have not yet hit a good usecase for that.

Graph ID.

This is an advanced feature…

Each scene has a default graph id of nil. When you send a graph to the ViewPort by calling push_graph(graph), you are really sending it with a sub-id of nil. This encourages thinking that scenes and graphs have a 1:1 relationship. There are, however, times that you want a single scene to host multiple graphs that can refer to each other via sub-ids. You would push them like this: push_graph(graph, id). Where the id is any term you would like to use as a key.

There are several use cases where this makes sense. 1) You have a complex tree (perhaps a lot of text) that you want to animate. Rather than re-rendering the text (relatively expensive) every time you simply transform a rotation matrix, you could place the text into its own static sub-graph and then refer to it from the primary. This will save energy as you animate.

2) Both the Remote and Recording clients make heavy use of sub-ids to make sense of the graph being replayed.

The downside of using graph_ids comes with input handling. because you have a single scene handling the input events for multiple graphs, you will need to take extra care to correctly handle position-dependent events, which may not be projected into the coordinate space you think they are. The Remote and Recording clients deal with this by rendering a single, completely transparent rect above all the sub scenes. This invisible, yet present rect hits all the position dependent events and makes sure they are sent to the scene projected in to the main (id is nil) graph’s coordinate space.

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 is data generated by the drivers and sent up to the scenes through the ViewPort. There is a limited set of input types and they are standardized so that the drivers can be built independantly 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 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

You handle incoming input events by adding handle_input/3 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 are should be released when no longer needed so that normal operation can resume.

The input messages re not passed on to other scene if the first one doesn’t handle it.

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 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 util the event reaches the root scene. If the root scene doesn’t handle it either then the event is dropped.

To handle events, you add filter_event/3 functions to your scene. This function handle the event, and stop 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

{:continue, msg, state}

or

{:stop, state}

Parameters passed in to filter_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 and event at the filter and stop its progression. It also generates and sends new event to its parent. I do this instead of transforming and continuing when I want to change the originating scene.

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 this will do two things. First, it won’t create a dynamic supervisor for this scene, which saves some resources. Second, push_graph/1 goes through a fast pass that doesn’t scan the graph for dynamic children.

For example, the Button component sets has_children to false.

Link to this section Summary

Link to this section Types

Link to this section Functions

Link to this function cast(scene_or_graph_key, msg) View Source
Link to this function cast_to_refs(graph_key_or_id, msg) View Source
Link to this function handle_call(msg, from, state) View Source
Link to this function send_event(scene_server, event) View Source
send_event(scene_server :: GenServer.server(), event :: Scenic.ViewPort.event()) ::
  :ok

send a filterable event to a scene.

This is very similar in feel to casting a message to a GenServer. However, This message will be handled by the Scene’s filter_event3 function. If the Scene returns {:continue, msg, state} from filter_event3, then the event will also be sent to the scene’s parent. This will continue until the message reaches the root scene or some other permananently supervised scene.

Typically, when a scene wants to initiate an event, it will call send_event/1 which is a private function injected into the scene during use Scenic.Scene

This private version of send_event will take care of the housekeeping of tracking the parent’s pid. It will, in turn, call this function on the main Scene module to send the event on its way.

def handle_input( {:cursor_button, {:left, :release, _, _}}, _, %{msg: msg} = state ) do
  send_event( {:click, msg} )
  {:noreply, state}
end

On the other hand, if you know you want to send the event to a named scene that you supervise yourself, then you would use Scenic.Scene.send_event/2

def handle_input( {:cursor_button, {:left, :release, _, _}}, _, %{msg: msg} = state ) do
  Scenic.Scene.send_event( :some_scene, {:click, msg} )
  {:noreply, state}
end

Be aware that named scenes that your supervise yourself are unable to continue the event to their parent in the graph becuase they could be referenced by multiple graphs making the continuation ambiguous.

Link to this section Callbacks

Link to this callback filter_event(any, any, any) View Source
filter_event(any(), any(), any()) ::
  {:continue, any(), any()} | {:stop, any()}
Link to this callback handle_input(input, context, state) View Source
handle_input(
  input :: any(),
  context :: Scenic.ViewPort.Context.t(),
  state :: any()
) :: {:noreply, state :: any()}
Link to this callback init(args, otps) View Source
init(args :: any(), otps :: list()) :: {:ok, any()}