View Source ReflectOS.Kernel.Layout behaviour (reflect_os_kernel v0.10.1)

Layouts define the arrangment of sections on the screen.

Overview

Layouts are a type of Scenic.Scene (which in turn is a type of GenServer) which defines the arrangement of sections on the users ReflectOS Dashboard. For example, the FourCorners layouts which ships with ReflectOS allows users to place sections in the top left, top right, bottom left, and bottom right corners of the dashboard. Layouts are passed their configuration, the screen size, and the sections the user has selected to display and are expected to render those sections to the screen.

Like all extensions to ReflectOS, developers are also required to implement a set of callbacks which are used to allow run time configuration. If you plan on publishing your extensions for use by others in the community, you can use these callbacks to create a thoughtful and intuitive configuration experience.

For the runtime UI behavior, Layouts follow many of the same paradigms as Scenic.Scene and therefore should be very familiar to for developers experienced building native user interfaces with the Scenic Framework. With Scenic, the UI is rendered natively (e.g. not via a webview) - this allows much better performance on the devices typically used for smart mirror projects (i.e. devices with a low-profile form factor and reduced processing power such as the Raspberry Pi Zero W and Zero 2 W).

This means that Layouts take on an important responsibility in the ReflecOS system since the typical tools available in webviews (e.g. flexbox) are not available.

Layout Locations

Layouts define a list of available "locations" in their ReflectOS.Kernel.Layout.Definition, which are essentially areas of the screen where users can place one or more sections. Layouts are responsible for adding each section to the layout's graph and using Scenic.Primitive.Transform.Translate to ensure the section is located in the right place on the screen.

Rendering Sections

Since a ReflectOS.Kernel.Section is just a wrapped Scenic.Component, Layouts can call Scenic.Component.add_to_graph/3 to render them to the the layouts graph. For example, your layout might contain the following function:

def render_section(%Scenic.Graph{} = graph, %Section{} = section, layout_tracker, x, y) do
  %{id: section_id, module: section_module} = section

  graph
  |> section_module.add_to_graph({layout_tracker, section_id}, t: {x, y})
end

Note that the add_to_graph function must be called with a two-part tuple in the format {layout_tracker, section_id} as the second argument, where layout_tracker is a unique id assigned by the layout used to identify the section and section_id is the id of the section being rendered.

Note that you should avoid using the section_id as the layout tracker, since users are permitted to add the same section multiple times to the same layout.

Implementing a Layout

Layouts are just modules which use ReflectOS.Kernel.Layout. The callbacks and other requirements for a Layout fall into two major categories:

  1. Configuration experience via the ReflectOS Console web ui.
  2. Runtime native display rendering on the smart mirror/display

Configuration Experience

In order to drive the ReflectOS console UI, layouts are required to contain an Ecto.Schema which represents the available configuration options. This is typically done using the embedded_schema/1 macro, as they are not persisted via Ecto.Repo.

Note that embedding schemas in your root schema (e.g. Ecto.Schema.embeds_many/4) is not currently supported.

Additionally, layouts must implement the following callbacks which are used by the console:

Runtime native display

In order to render the layout ReflectOS dashboard, modules are required to implement the following callbacks:

Layouts can also optionally implement the following callbacks:

See the documentation for each callback below for more details.

Example

For a complete example of a ReflectOS Layout, see the FourCorner layout from ReflectOS Core, which is shipped with the pre-built system firmware.

Summary

Callbacks

Used to cast and validate input from the user via the ReflectOS console web ui.

Optional callback for when a user updates a layouts's configuration while it's displayed on the ReflectOS dashboard.

Required callback to handle updates to sections rendered by the layout.

Optional callback for when a user updates the arrangement of sections in the layout locations.

Optional callback for when a user updates the screen size in the ReflectOS system settings.

Callback invoked during initialization of the layout.

Provides the ReflectOS.Kernel.Layout.Definition struct for the layout.

Provides the list of options which can be configured through the ReflectOS console.

Optional callback to validate the layout config at runtime.

Types

@type t() :: %ReflectOS.Kernel.Layout{
  config: map(),
  id: binary(),
  module: module(),
  name: binary(),
  sections: %{optional(atom()) => [ReflectOS.Kernel.Section.t()]}
}

Callbacks

Link to this callback

changeset(layout_config, params)

View Source
@callback changeset(
  layout_config :: Ecto.Schema.embedded_schema(),
  params :: %{required(binary()) => any()}
) :: Ecto.Changeset.t()

Used to cast and validate input from the user via the ReflectOS console web ui.

Can be used like a standard Ecto.Changeset. If we had a layout called SimpleLayout which defined an Ecto.Schema with a :spacing field, the changeset callback might look like this:

@impl true
def changeset(%SimpleLayout{} = section, params \\ %{}) do
  section
  |> cast(params, [:spacing])
  |> validate_required([:spacing])
  |> validate_number(:spacing, greater_than: 0)
end
Link to this callback

handle_config_update(scene, config)

View Source (optional)
@callback handle_config_update(scene :: Scenic.Scene.t(), config :: struct()) ::
  Scenic.Scene.t()

Optional callback for when a user updates a layouts's configuration while it's displayed on the ReflectOS dashboard.

The default behavior when layout configuration changes is to restart the layout process with the new configuration. This will likely work in most circumstances, but you can override this behavior if it is desirable to do so.

Link to this callback

handle_section_update(layout, layout_tracker, section_graph)

View Source
@callback handle_section_update(
  layout :: Scenic.Scene.t(),
  layout_tracker :: any(),
  section_graph :: Scenic.Graph.t()
) :: Scenic.Scene.t()

Required callback to handle updates to sections rendered by the layout.

Layouts must implement this function to handle updates to a section's graph. If a section's dimensions change, it may impact where it or other sections in the layout should be located (remember that all elements in Scenic.Scene are located on at fixed x,y location). This callback allows layouts to adjust the positioning of their sections based on the new section size.

The layout_tracker argument is the unique id assigned to the section by the layout when it calls Scenic.Component.add_to_graph/3, see the docs on rendering sections above.

Link to this callback

handle_sections_update(scene, sections)

View Source (optional)
@callback handle_sections_update(scene :: Scenic.Scene.t(), sections :: map()) ::
  Scenic.Scene.t()

Optional callback for when a user updates the arrangement of sections in the layout locations.

The default behavior when this changes is to restart the layout process with the new section arrangement. This will likely work in most circumstances, but you can override this behavior if it is desirable to do so.

Link to this callback

handle_viewport_update(scene, viewport_size)

View Source (optional)
@callback handle_viewport_update(scene :: Scenic.Scene.t(), viewport_size :: tuple()) ::
  Scenic.Scene.t()

Optional callback for when a user updates the screen size in the ReflectOS system settings.

The default behavior when this changes is to restart the layout process with the new viewport size. This will likely work in most circumstances, but you can override this behavior if it is desirable to do so.

Link to this callback

init_layout(scene, args, options)

View Source
@callback init_layout(
  scene :: Scenic.Scene.t(),
  args :: %{
    config: Ecto.Schema.embedded_schema(),
    sections: %{required(atom()) => [ReflectOS.Kernel.Section.t()]},
    viewport_size: {integer(), integer()}
  },
  options :: Keyword.t()
) ::
  {:ok, scene}
  | {:ok, scene, timeout :: non_neg_integer()}
  | {:ok, scene, :hibernate}
  | {:ok, scene, opts :: Scenic.Scene.response_opts()}
  | :ignore
  | {:stop, reason}
when scene: Scenic.Scene.t(), reason: term()

Callback invoked during initialization of the layout.

Wraps the Scenic.Scene.init/3 callback, and allows the same return values.

The first argument is the Scenic.Scene.

The second argument is a map containing three fields:

  • config: The Ecto.Schema struct defined in your layout module populated with the user's configuration.
  • sections: A Map where each key corresponds to a one of the locations defined in layout_definition/0 and values are a list of the ReflectOS.Kernel.Section struct.
  • viewport_size: A tuple representing the screen size in {width, height} format. The ReflectOS default is {1080, 1920} but can be adjusted by user.

The last argument is a list of options which maybe passed into the layout by the system. These are not currently used but are included to ensure consistency with Scenic.Scene.

@callback layout_definition() :: ReflectOS.Kernel.Layout.Definition.t()

Provides the ReflectOS.Kernel.Layout.Definition struct for the layout.

This is used to show your layout in the Console UI. See below for an example adapted From the FourCorner layout, which ships with the pre-built ReflectOS system:

@doc false
@impl ReflectOS.Kernel.Layout
def layout_definition(),
  do: %Definition{
    name: "Four Corner",
    description: fn assigns ->
      ~H"""
      Allows placing sections in each of the four corners of the screen, with options for
      stacking (vertical vs. horizontal) and spacing.
      """
    end,
    icon: """
      <svg class="text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
        <path fill-rule="evenodd" d="M4.857 3A1.857 1.857 0 0 0 3 4.857v4.286C3 10.169 3.831 11 4.857 11h4.286A1.857 1.857 0 0 0 11 9.143V4.857A1.857 1.857 0 0 0 9.143 3H4.857Zm10 0A1.857 1.857 0 0 0 13 4.857v4.286c0 1.026.831 1.857 1.857 1.857h4.286A1.857 1.857 0 0 0 21 9.143V4.857A1.857 1.857 0 0 0 19.143 3h-4.286Zm-10 10A1.857 1.857 0 0 0 3 14.857v4.286C3 20.169 3.831 21 4.857 21h4.286A1.857 1.857 0 0 0 11 19.143v-4.286A1.857 1.857 0 0 0 9.143 13H4.857Zm10 0A1.857 1.857 0 0 0 13 14.857v4.286c0 1.026.831 1.857 1.857 1.857h4.286A1.857 1.857 0 0 0 21 19.143v-4.286A1.857 1.857 0 0 0 19.143 13h-4.286Z" clip-rule="evenodd"/>
      </svg>
    """,
    locations: [
      %{key: :top_left, label: "Top Left"},
      %{key: :top_right, label: "Top Right"},
      %{key: :bottom_left, label: "Bottom Left"},
      %{key: :bottom_right, label: "Bottom Right"}
    ]
  }

Note that the icon property is passed in as simple string, while the description property takes a function which can be passed to Phoenix.LiveView. This allows you to use html tags in your description. Additionally, note that the argument must be called assigns.

The locations property must be a list of maps with a key and a label property.

See ReflectOS.Kernel.Layout.Definition for more details.

@callback layout_options() :: [
  ReflectOS.Kernel.Option.t() | ReflectOS.Kernel.OptionGroup.t()
]

Provides the list of options which can be configured through the ReflectOS console.

General guidelines are to use ReflectOS.Kernel.OptionGroup to present configuration related to a specific layout location together.

Link to this callback

validate_layout(config)

View Source (optional)
@callback validate_layout(config :: Ecto.Schema.embedded_schema()) ::
  :ok | {:error, [{:error, any()}]}

Optional callback to validate the layout config at runtime.

This is likely to be rarely used, as layouts use the changeset/2 callback to validate the configuration from the user, but is provided as it can be useful during development to ensure the configuration your layout is receiving matches what you expect.