Scenic.Component behaviour (Scenic v0.11.0-beta.0) View Source

A Component is Scene that is optimized to be used as a child of another scene.

These are typically controls that you want to define once and use in multiple places.

Standard Components

Scenic includes a several standard components that you can use in your scenes. These were chosen to be in the main library because:

  • They are used frequently
  • Their use promotes a certain amount of "common" look and feel

All of these components are typically added/modified via the helper functions in the Scenic.Components module.

HelperComponent ModuleDescription
button/3Scenic.Component.ButtonA simple button
checkbox/3Scenic.Component.Input.CheckboxA boolean checkbox control
dropdown/3Scenic.Component.Input.DropdownA menu-like dropdown control
radio_group/3Scenic.Component.Input.RadioGroupA group of radio controls
slider/3Scenic.Component.Input.SliderA slider ranging from one value to another
text_field/3Scenic.Component.Input.TextFieldA text input field.
toggle/3Scenic.Component.Input.ToggleA boolean toggle control.
defmodule MyApp.Scene.MyScene do
  use Scenic.Scene
  import Scenic.Components

  @impl Scenic.Scene
  def init(scene, text, opts) do
    graph =
      Scenic.Graph.build()
      |> button( "Press Me", id: :press_me )
      |> slider( {{0,100}, 0}, id: :slide_me )

    { :ok, push_graph(scene, graph) }
  end
end

Creating Custom Components

Creating a custom component that you can use in your scenes is just like creating a scene with an extra validation function. This validation function is used when the graph that uses your component is built in order to make sure it uses data that conforms to what your component expects.

  defmodule MyApp.Component.Fancy do
    use Scenic.Component

    @impl Scenic.Component
    def validate(data) when is_bitstring(data), do: {:ok, data}
    def validate(_), do: {:error, "Descriptive error message goes here."}

    @impl Scenic.Scene
    def init(scene, data, opts) do
      { :ok, scene }
    end
  end

Generating/Sending Events

Communication from a component to it's parent is usually done via event messages. Scenic knows how to route events to a component's parent. If that parent doesn't handle it, then it is automatically routed to the parent's parent. If it gets all the way to the ViewPort itself, then it is ignored.

  defmodule MyApp.Component.Fancy do
    
  # ... validate, and other setup ...

    @impl Scenic.Scene
    def init(scene, data, opts) do
      # setup and push a graph here...
      { :ok, assign(scene, id: opts[:id] }
    end

    @impl Scenic.Scene
    def handle_input( {:cursor_button, {0, :release, _, _}}, :btn,
          %Scene{assigns: %{id: id}} = scene
        ) do
      :ok = send_parent_event( scene, {:click, id}  )
      { :noreply, scene }
    end

  end

Notice how the component saved the original id that was passed in to the init function via the opts list. This is then used to identify the click to the parent. This is a common pattern.

Optional: Fetch/Put Handlers

If you would like the parent scene to be able to query your component's state without waiting for the component to send events, you can optionally implement the following handle_call functions.

This is an "informal" spec... You don't have to implement it, but it is nice when you do.

defmodule MyApp.Component.Fancy do
  use Scenic.Component
  
  # ... init, validate, and other functions ...

  def handle_call(:fetch, _, %{assigns: %{value: value}} = scene) do
    { :reply, {:ok, value}, scene }
  end

  def handle_call({:put, value}, _, scene) when is_bitstring(value) do
    { :reply, :ok, assign(scene, value: value) }
  end

  def handle_call({:put, _}, _, scene) do
    {:reply, {:error, :invalid}, scene}
  end
end

To make the above example more practical, you would probably also modify and push a graph when handling the :put message. See the code for the standard input components for deeper examples.

Optional: has_children: false

If you know for certain that your component will not itself use any components, you can set :has_children to false like this.

defmodule MyApp.Component.Fancy do
  use Scenic.Component, has_children: false
  # ...
end

When :has_children is set to false, no DynamicSupervisor is started to manage the scene's children, overall resource use is improved, and startup time is faster. You will not, however, be able to nested components in any scene where :has_children is false.

For example, the Scenic.Component.Button component sets :has_children to false.

This option is available for any Scene, not just components.

Link to this section Summary

Callbacks

Add this component to a Graph.

Compute the bounding box of the component.

Provide a default pin for this component.

Validate that the data for a component is correctly formed.

Link to this section Callbacks

Link to this callback

add_to_graph(graph, data, opts)

View Source

Specs

add_to_graph(graph :: Scenic.Graph.t(), data :: any(), opts :: Keyword.t()) ::
  Scenic.Graph.t()

Add this component to a Graph.

A standard add_to_graph/3 is automatically added to your component. Override this callback if you want to customize it.

Link to this callback

bounds(data, styles)

View Source (optional)

Specs

bounds(data :: any(), styles :: map()) :: Scenic.Graph.bounds()

Compute the bounding box of the component.

This function can be called outside of the context of a running component. The box should be computed as if it was running with the given data and styles.

Link to this callback

default_pin(data, styles)

View Source (optional)

Specs

default_pin(data :: any(), styles :: map()) :: Scenic.Math.vector_2()

Provide a default pin for this component.

If this callback is not implemented, then the default pin will be {0,0}.

Specs

validate(data :: any()) :: {:ok, data :: any()} | {:error, String.t()}

Validate that the data for a component is correctly formed.

This callback is required.