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

Layout Managers are responsible for determining which layout should be rendered to the dashboard.

Overview

Layout Managers are specialized GenServers which control which layout should be shown on the dashboard at any given time. Layout Managers are a unique feature of ReflectOS, allowing developers to build managers which can change the layout in response to just about anything (e.g. a fixed schedule, weather conditions, etc). Using Layout Managers, users can ensure their ReflectOS dashhboard shows just what they need, when they need it.

Layouts Managers follow the same pattern of Scenic.Scene, and utilize a dedicated struct (see ReflectOS.Kernel.LayoutManager.State) with an assigns property as their state. You can use the assigns property to hold any data in state that you need. Layout Managers are passed their configuration as an init argument.

There are few requirements for LayoutManagers, which allows a high degree of flexibility for building custom logic around which layout is displayed. LayoutManagers simple use the push_layout/2 function to notify the ReflectOS system that a new layout should be displayed.

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.

Implementing a Layout Manager

Layouts are just modules which use ReflectOS.Kernel.LayoutManager and implement it's behavior. The callbacks and other requirements for a Layout fall into two major categories:

  1. Configuration experience via the ReflectOS Console web ui.
  2. Managing the logic around which layout should be displayed

Configuration Experience

In order to drive the ReflectOS console UI, layout managers 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:

Layout Display Logic

In order to determine which layout renders to the ReflectOS dashboard, modules are on required to implement a single callback:

Sections can also optionally implement the following callbacks:

See the documentation for each callback below for more details.

Example

Here is the Static layout manager which ships with the pre-built ReflectOS firmware. It simply allows the user the select a single layout via the console UI and sets it as the active layout.

defmodule ReflectOS.Core.LayoutManagers.Static do
  use ReflectOS.Kernel.LayoutManager

  import Phoenix.Component, only: [sigil_H: 2]

  alias ReflectOS.Kernel.Option
  alias ReflectOS.Kernel.LayoutManager
  alias ReflectOS.Kernel.LayoutManager.Definition
  alias ReflectOS.Kernel.LayoutManager.State
  alias ReflectOS.Kernel.Settings.LayoutStore

  @impl true
  def layout_manager_definition() do
    %Definition{
      name: "Static Layout",
      icon: """
        <svg class="text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
          <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.757 12h8.486M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
        </svg>
      """,
      description: fn assigns ->
        ~H"""
        Allows selecting a single, static layout.
        """
      end
    }
  end

  embedded_schema do
    field(:layout, :string)
  end

  @impl true
  def changeset(%__MODULE__{} = layout_manager, params \ %{}) do
    layout_manager
    |> cast(params, [:layout])
    |> validate_required([:layout])
  end

  @impl true
  def layout_manager_options(),
    do: [
      %Option{
        key: :layout,
        label: "Layout",
        config: %{
          type: "select",
          prompt: "--Select Layout--",
          options:
            LayoutStore.list()
            |> Enum.map(fn %{name: name, id: id} ->
              {name, id}
            end)
        }
      }
    ]

  @impl LayoutManager
  def init_layout_manager(%State{} = state, %{layout: layout_id}) do
    state =
      state
      |> push_layout(layout_id)

    {:ok, state}
  end
end

Summary

Callbacks

Used to cast and validate input from the user via the ReflectOS Console UI.

Optional callback for when users update a layout manager's configuration.

Callback invoked during initialization of the layout manager.

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

Functions

Convenience function to assign a list or map of values into a ReflectOS.Kernel.LayoutManager.State struct.

Convenience function to assign a value into a ReflectOS.Kernel.LayoutManager.State struct.

Convenience function to assign a list of new values into a ReflectOS.Kernel.LayoutManager.State struct.

Convenience function to assign a new values into a ReflectOS.Kernel.LayoutManager.State struct.

Convenience function to fetch an assigned value out of a ReflectOS.Kernel.LayoutManager.State struct.

Convenience function to get an assigned value out of a ReflectOS.Kernel.LayoutManager.State struct.

Pushes the layout with the provided id to the dashboard.

Types

@type response_opts() :: [timeout() | :hibernate | {:continue, term()}]
@type t() :: %ReflectOS.Kernel.LayoutManager{
  config: map(),
  id: binary(),
  module: module(),
  name: binary()
}

Callbacks

Link to this callback

changeset(layout_manager, params)

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

Used to cast and validate input from the user via the ReflectOS Console UI.

Can be used like a standard Ecto.Changeset. Here is the callback from the example given above:

@impl true
def changeset(%__MODULE__{} = layout_manager, params \ %{}) do
  layout_manager
  |> cast(params, [:layout])
  |> validate_required([:layout])
end
Link to this callback

handle_config_update(scene, config)

View Source (optional)
@callback handle_config_update(
  scene :: ReflectOS.Kernel.LayoutManager.State.t(),
  config :: struct()
) ::
  ReflectOS.Kernel.LayoutManager.State.t()

Optional callback for when users update a layout manager's configuration.

The default behavior when the layout manager's configuration changes is to restart the layout manager process with the new configuration. This will likely work in most circumstances, but you can override this behavior if it is desirable (e.g. your layout manager has a lengthy start up time).

Link to this callback

init_layout_manager(state, config)

View Source
@callback init_layout_manager(
  state :: ReflectOS.Kernel.LayoutManager.State.t(),
  config :: term()
) ::
  {:ok, state}
  | {:ok, state, timeout() | :hibernate | {:continue, continue_arg :: term()}}
  | :ignore
  | {:stop, reason :: any()}
when state: ReflectOS.Kernel.LayoutManager.State.t()

Callback invoked during initialization of the layout manager.

Wraps the GenServer.init/1 callback, and allows the same return values. The config argument will be the populated Ecto.Schema defined in your section module.

From the Static example above:

@impl LayoutManager
def init_layout_manager(%State{} = state, %{layout: layout_id}) do
  state =
    state
    |> push_layout(layout_id)

  {:ok, state}
end
Link to this callback

layout_manager_definition()

View Source
@callback layout_manager_definition() :: ReflectOS.Kernel.LayoutManager.Definition.t()

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

This is used to show your layout manager in the Console UI. Here is the callback from the example above:

@impl true
def layout_manager_definition() do
  %Definition{
    name: "Static Layout",
    icon: """
      <svg class="text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
        <path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7.757 12h8.486M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"/>
      </svg>
    """,
    description: fn assigns ->
      ~H"""
      Allows selecting a single, static layout.
      """
    end
  }
end

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.

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

Link to this callback

layout_manager_options()

View Source
@callback layout_manager_options() :: [
  ReflectOS.Kernel.Option.t() | ReflectOS.Kernel.OptionGroup.t()
]

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

From the example above:

@impl true
def layout_manager_options(),
  do: [
    %Option{
      key: :layout,
      label: "Layout",
      config: %{
        type: "select",
        prompt: "--Select Layout--",
        options:
          LayoutStore.list()
          |> Enum.map(fn %{name: name, id: id} ->
            {name, id}
          end)
      }
    }
  ]

Note that any properties in the config map will be passed directly to the HTML input in the Console UI configuration form.

See ReflectOS.Kernel.Option and ReflectOS.Kernel.OptionGroup for more information.

Functions

Convenience function to assign a list or map of values into a ReflectOS.Kernel.LayoutManager.State struct.

iex> state = %State{assigns: %{}}
iex> assign(state, some_field: "Some Field", another_field: "Another Field")
%State{assigns: %{some_field: "Some Field", another_field: "Another Field"}}

iex> state = %State{assigns: %{}}
iex> assign(state, %{some_field: "Some Field"})
%State{assigns: %{some_field: "Some Field"}}
Link to this function

assign(state, key, value)

View Source
@spec assign(
  state :: ReflectOS.Kernel.LayoutManager.State.t(),
  key :: any(),
  value :: any()
) ::
  ReflectOS.Kernel.LayoutManager.State.t()

Convenience function to assign a value into a ReflectOS.Kernel.LayoutManager.State struct.

iex> state = %State{assigns: %{}}
iex> assign(state, :some_field, "Some Field")
%State{assigns: %{some_field: "Some Field"}}
Link to this function

assign_new(state, key_list)

View Source
@spec assign_new(
  state :: ReflectOS.Kernel.LayoutManager.State.t(),
  key_list :: Keyword.t() | map()
) ::
  ReflectOS.Kernel.LayoutManager.State.t()

Convenience function to assign a list of new values into a ReflectOS.Kernel.LayoutManager.State struct.

Only values that do not already exist will be assigned.

iex> state = %State{assigns: %{existing_field: "Existing Field"}}
iex> assign_new(state, existing_field: "New Value", new_field: "New Field")
%State{assigns: %{existing_field: "Existing Field", new_field: "New Field"}}

iex> state = %State{assigns: %{existing_field: "Existing Field"}}
iex> assign_new(state, %{existing_field: "New Value", new_field: "New Field"})
%State{assigns: %{existing_field: "Existing Field", new_field: "New Field"}}
Link to this function

assign_new(state, key, value)

View Source
@spec assign_new(
  state :: ReflectOS.Kernel.LayoutManager.State.t(),
  key :: any(),
  value :: any()
) ::
  ReflectOS.Kernel.LayoutManager.State.t()

Convenience function to assign a new values into a ReflectOS.Kernel.LayoutManager.State struct.

The value will only be assigned if it does not already exist in the struct.

iex> state = %State{assigns: %{existing_field: "Existing Field"}}
iex> assign_new(state, :existing_field, "New Value")
%State{assigns: %{existing_field: "Existing Field"}}

iex> state = %State{assigns: %{}}
iex> assign_new(state, :new_field, "New Value")
%State{assigns: %{new_field: "New Value"}}
@spec fetch(state :: ReflectOS.Kernel.LayoutManager.State.t(), key :: any()) ::
  {:ok, any()} | :error

Convenience function to fetch an assigned value out of a ReflectOS.Kernel.LayoutManager.State struct.

iex> state = %State{assigns: %{some_field: "some value"}}
iex> fetch(state, :some_field)
{:ok, "some value"}

iex> state = %State{assigns: %{some_field: "some value"}}
iex> fetch(state, :invalid_key)
:error
Link to this function

get(state, key, default \\ nil)

View Source
@spec get(
  state :: ReflectOS.Kernel.LayoutManager.State.t(),
  key :: any(),
  default :: any()
) :: any()

Convenience function to get an assigned value out of a ReflectOS.Kernel.LayoutManager.State struct.

iex> state = %State{assigns: %{some_field: "some value"}}
iex> get(state, :some_field)
"some value"

iex> state = %State{assigns: %{some_field: "some value"}}
iex> get(state, :invalid_key, "default value")
"default value"
Link to this function

push_layout(layout_manager, new_layout_id)

View Source
@spec push_layout(
  layout_manager :: ReflectOS.Kernel.LayoutManager.State.t(),
  new_layout_id :: binary()
) ::
  ReflectOS.Kernel.LayoutManager.State.t()

Pushes the layout with the provided id to the dashboard.