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:
- Configuration experience via the ReflectOS Console web ui.
- 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
Callbacks
@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
@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.
@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.
@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.
@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.
@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
: TheEcto.Schema
struct defined in your layout module populated with the user's configuration.sections
: AMap
where each key corresponds to a one of the locations defined inlayout_definition/0
and values are a list of theReflectOS.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.
@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.