GPUI keeps application state in ordinary Elixir processes and uses a display to
present serializable snapshots. A local application combines one or more
GPUI.View modules, a GPUI.Application, and an OTP-supervised GPUI.Runtime.
Prerequisites
GPUI requires Elixir 1.20 or later. Native Linux builds require Rust and the XKB development packages:
sudo apt-get install libxkbcommon-dev libxkbcommon-x11-dev
Enable the native display outside tests in your application's configuration:
# config/config.exs
config :gpui_native, build_native: config_env() != :test
config :gpui_native, GPUI.Native, host: :gpui_componentThis keeps renderer-independent tests free of Rust and native-library
requirements. If fontconfig.pc is unavailable for a native build, compile
with:
RUST_FONTCONFIG_DLOPEN=1 mix compile
Run the examples
The getting-started examples form a short progression:
RUST_FONTCONFIG_DLOPEN=1 mix run apps/gpui/examples/getting_started/01_hello_window.exs
RUST_FONTCONFIG_DLOPEN=1 mix run apps/gpui/examples/getting_started/02_events.exs
RUST_FONTCONFIG_DLOPEN=1 mix run apps/gpui/examples/getting_started/03_supervised_updates.exs
RUST_FONTCONFIG_DLOPEN=1 mix run apps/gpui/examples/getting_started/04_controlled_form.exs
RUST_FONTCONFIG_DLOPEN=1 mix run apps/gpui/examples/getting_started/05_multiple_windows.exs
- Hello Window introduces a view, an application, and supervision.
- Focus Timer combines controlled events with periodic OTP messages.
- Settings Form demonstrates native controls, validation state, dynamic styling, and a controlled dialog.
Their application modules live under examples/getting_started/support/, so
they can also be loaded without starting a native display and tested through
GPUI.Test.
Define a view
A view renders a %GPUI.Element{} tree from assigns. The ~GPUI sigil accepts
HEEx-shaped tags, expressions, aliases, native components, and named slots.
defmodule MyApp.WelcomeView do
use GPUI.View
@impl GPUI.View
def render(_assigns) do
~GPUI"""
<div class="flex flex-col items-center justify-center gap-4 p-8 bg-slate-900">
<text class="text-white text-3xl font-semibold">Hello from the BEAM</text>
<text class="text-green-500">● Runtime connected</text>
</div>
"""
end
endDefine an application
An application's mount/1 callback returns its initial windows. Each root view
owns its own assigns.
defmodule MyApp.Desktop do
use GPUI.Application
@impl GPUI.Application
def mount(_args) do
{:ok,
[
window "My application" do
size(520, 320)
root(MyApp.WelcomeView)
end
]}
end
endUsing the application module as a child starts a GPUI.Runtime with the native
display by default:
Supervisor.start_link([MyApp.Desktop], strategy: :one_for_one)Pass runtime options through the normal {module, options} child form when the
application needs mount arguments, a registered runtime, or a custom display:
children = [
{MyApp.Desktop,
name: MyApp.Runtime,
args: %{account_id: account_id},
display: GPUI.Display.Native,
poll_interval: 16}
]
Supervisor.start_link(children, strategy: :one_for_one)Controlled events
Interactive native controls are controlled by root-view assigns:
<GPUI.UI.field
label="Display name"
required={true}
help="Used in shared workspace activity."
error={assigns.errors[:name]}
>
<GPUI.UI.input
id="display-name"
label="Display name"
value={assigns.name}
focus_request={assigns.name_focus_request}
phx-change="name_changed"
phx-submit="save"
/>
</GPUI.UI.field>@impl GPUI.View
def handle_event("name_changed", %{value: name}, assigns),
do: {:noreply, %{assigns | name: name}}phx-submit is optional and emits Enter activation with the input's current
string value. Increment focus_request when an application-owned transition,
such as failed validation, should move native focus back to the input.
Stable string IDs preserve native focus, editing state, popup state, and selection across snapshots. Duplicate IDs are rejected before reaching a display.
Updates from OTP processes
Views can also handle application messages independently of pointer and keyboard input:
@impl GPUI.View
def handle_info(:tick, assigns),
do: {:noreply, %{assigns | elapsed: assigns.elapsed + 1}}A supervised worker delivers the message through the runtime:
{:ok, _snapshot} = GPUI.Runtime.send_view(MyApp.Runtime, 1, :tick)send_view/3 updates the selected root view, synchronizes the display, and
publishes the same typed runtime update used by other state transitions. The
Focus Timer shows this pattern with a GenServer using Process.send_after/3.
Test without a native window
The examples use normal application modules, so the same behavior can be tested without a NIF or display server:
defmodule MyApp.TimerTest do
use GPUI.Test, async: true
test "advances from an OTP message" do
runtime = start_runtime!(GettingStarted.FocusTimer.App, args: %{seconds: 2})
click(runtime, "start")
send_view(runtime, :tick)
assert %{remaining: 1, status: :running} = assigns(runtime)
end
endContinue with UI components, Overlays and menus, Testing GPUI applications, and Sessions, snapshots, and displays.