Your first application

Copy Markdown View Source

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_component

This 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
end

Define 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
end

Using 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
end

Continue with UI components, Overlays and menus, Testing GPUI applications, and Sessions, snapshots, and displays.