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.
Define the viewport
Create a module that uses Emerge:
defmodule MyApp.View.Counter do
use Emerge
@impl Viewport
def mount(opts) do
{:ok, %{count: 0}, Keyword.merge([title: "Counter"], opts)}
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 options
mount/1 receives the options passed when the viewport starts. This example
returns:
{:ok, %{count: 0}, Keyword.merge([title: "Counter"], opts)}The state is passed to render/1 and callback functions. It must be a map.
The third tuple item contains viewport configuration. Merging opts after the
default title lets callers override that title when starting the viewport.
A viewport without local state can instead return {:ok, viewport_options} and
implement render/0.
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 another call to render/1.
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",
width: 1024,
height: 768
)The Emerge.Runtime.Viewport module documentation lists viewport configuration
for DRM, headless output, raw input, and renderer diagnostics. You do not need
those options for this example.
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",
width: 1024,
height: 768
)
]Next
Continue with Describe your UI to learn how to build and style
the tree returned by render/0 or render/1.