A viewport is an Elixir process that displays an Emerge UI and receives its events. On macOS and Wayland, a viewport opens a window.
In this tutorial you will create a small counter viewport, start it, and add it to a supervision tree.
Viewport configuration
This example uses a desktop window: normal Linux defaults to Wayland/OpenGL and macOS uses its Metal window host. For DRM, headless output, Nerves/Raspberry Pi, rendering API choices or build settings, see the viewport documentation. It explains compile-time support versus runtime selection, target defaults, configuration examples, and inspecting/rebuilding native support.
Define the viewport
Create a module that uses Emerge:
defmodule MyApp.View.Counter do
use Emerge
@impl Viewport
def mount(opts) do
output = %Emerge.Config.Window{
title: Keyword.get(opts, :title, "Counter"),
size: Keyword.get(opts, :size, {800, 600})
}
{:ok, %{count: 0}, %Emerge.Config{output: output}}
end
@impl Viewport
def render(%{count: count}) do
row(
[
Background.color(color(:slate, 800)),
Font.color(color(:white)),
spacing(12),
padding(12)
],
[
button([Event.on_press(:decrement)], text("-")),
el([padding(10)], text("Count: #{count}")),
button([Event.on_press(:increment)], text("+"))
]
)
end
defp button(attrs, content) do
Input.button(
attrs ++
[
padding(10),
Background.color(color(:sky, 500)),
Border.rounded(8)
],
content
)
end
@impl Viewport
def handle_info(:increment, state) do
{:noreply, Viewport.rerender(%{state | count: state.count + 1})}
end
def handle_info(:decrement, state) do
{:noreply, Viewport.rerender(%{state | count: state.count - 1})}
end
enduse Emerge makes the UI helpers available and aliases Emerge as
Viewport inside the module.
Mount state and configuration
mount/1 receives your application-defined startup arguments as a keyword list.
It returns state and a typed configuration:
output = %Emerge.Config.Window{title: "Counter", size: {800, 600}}
{:ok, %{count: 0}, %Emerge.Config{output: output}}State must be a map and is passed to render/1 and event callbacks. The
:__emerge__ key is reserved for Emerge.
The third tuple item must be %Emerge.Config{}, not the startup arguments or a
renderer keyword list. The example deliberately translates its :title and
:size arguments into a window record. Callers cannot override arbitrary renderer
fields unless your mount/1 chooses to expose them.
A viewport without local state can return {:ok, %Emerge.Config{}} and implement
render/0. All config groups have defaults. To customize drawing, for example,
add renderer: %Emerge.Config.Renderer{api: :raster} to the root config.
Render UI
render/1 returns an Emerge UI tree. It runs once when the viewport starts and
again after you request a rerender.
This tutorial uses row/2, text/1, and Input.button/2. The next tutorial,
Describe your UI, explains elements, layout, and styling in
detail.
Handle events
The buttons declare messages with Event.on_press/1:
Input.button([Event.on_press(:increment)], text("+"))Passing only a message sends it back to the viewport process. Handle it with
handle_info/2:
@impl Viewport
def handle_info(:increment, state) do
{:noreply, Viewport.rerender(%{state | count: state.count + 1})}
endUpdate the state, pass it to Viewport.rerender/1, and return the resulting
state. rerender/1 schedules the UI update; it does not call render/1 immediately.
To send an event to another process, pass {pid, message}:
Event.on_press({worker_pid, :save})Start the viewport
From iex -S mix:
{:ok, viewport} = MyApp.View.Counter.start_link()On Linux desktop this opens a Wayland window. On macOS it opens a macOS window.
Override mounted defaults when needed:
{:ok, viewport} =
MyApp.View.Counter.start_link(
title: "Counter Example",
size: {1024, 768}
)Here title and size are arguments interpreted by this particular module's
mount/1, not globally recognized renderer options. GenServer startup options
such as name: MyApp.Counter remain separate from the returned configuration.
See configuration records for DRM,
headless output, live fields, assets and diagnostics.
Supervise the viewport
A viewport can be supervised like any other child:
defmodule MyApp.Application do
use Application
@impl true
def start(_type, _args) do
children = [
MyApp.View.Counter
]
Supervisor.start_link(children,
strategy: :one_for_one,
name: MyApp.Supervisor
)
end
endPass startup options through child_spec/1:
children = [
MyApp.View.Counter.child_spec(
title: "Counter In App",
size: {1024, 768}
)
]Optional: show the count in the window title
Add this callback to the counter module to make the title follow its state:
@impl Viewport
def config(previous, state) do
output = %{previous.output | title: "Count: #{state.count}"}
%{previous | output: output}
endIt runs before the first render and again when you request a rerender. Updating
previous.output preserves the window's size and backend. Supplying a new window
record instead would use defaults for fields you omit.
Use config(previous) when no local state is needed; implement only one arity.
Window titles can change while running, but settings such as the renderer API,
assets and initial size require a new viewport. See
configuration records for other settings.
Raw input is opt-in and separate from the buttons above. Continue with
Observe raw input when you need handle_input/2.
Next
Continue with Describe your UI to learn how to build and style
the tree returned by render/0 or render/1.