EmergeSkia (Emerge v0.2.1)

Copy Markdown View Source

Minimal Skia renderer for the Emerge layout engine.

This library renders retained Emerge trees through the native Rust layout, event, and Skia pipeline.

Example

# Start renderer
{:ok, renderer} =
  EmergeSkia.start(
    otp_app: :my_app,
    title: "My App",
    width: 800,
    height: 600
  )

import Emerge.UI
import Emerge.UI.Color
import Emerge.UI.Size
import Emerge.UI.Space

tree =
  el(
    [
      width(px(220)),
      height(px(80)),
      Emerge.UI.Background.color(color(:sky, 500)),
      Emerge.UI.Border.rounded(10),
      padding(16),
      Emerge.UI.Font.color(color(:white)),
      Emerge.UI.Font.size(24)
    ],
    text("Hello!")
  )

{_state, _assigned} = EmergeSkia.upload_tree(renderer, tree)

# Stop when done
EmergeSkia.stop(renderer)

Color Format

For Emerge.UI styling, prefer Emerge.UI.Color.color/1..3, Emerge.UI.Color.color_rgb/3, and Emerge.UI.Color.color_rgba/4.

EmergeSkia.rgb/3 and EmergeSkia.rgba/4 are still available when you need packed 32-bit unsigned integers in RGBA format: 0xRRGGBBAA

  • 0xFF0000FF = Red (fully opaque)
  • 0x00FF00FF = Green (fully opaque)
  • 0x0000FFFF = Blue (fully opaque)
  • 0x00000080 = Black at 50% opacity

When you register an input target with set_input_target/2, Wayland close requests are delivered separately as {:emerge_skia_close, :window_close_requested}. This lifecycle message bypasses the input mask so higher-level runtimes can shut down promptly.

Summary

Functions

Returns the input mask for all events.

Returns the input mask for text input events.

Returns the input mask for cursor button events.

Returns the input mask for cursor enter/exit events.

Returns the input mask for cursor position events.

Returns the input mask for cursor scroll events.

Returns the input mask for window focus events.

Returns the input mask for key events.

Returns the input mask for window resize events.

Load a font from a file path.

Measure text dimensions for layout purposes.

Apply patches for a new tree, run layout, and render.

Render a tree to an RGBA pixel buffer (synchronous, no window).

Render a tree to an encoded PNG binary (synchronous, no window).

Convert RGB values to a color integer.

Convert RGBA values to a color integer.

Check if the renderer window is still open.

Set the input event mask to filter which events are sent.

Set the target process to receive renderer events.

Set the target process to receive native renderer log messages.

Start a new renderer window.

Stop the renderer and close the window.

Submit a DRM Prime descriptor to a video target.

Upload a full EMRG tree, run layout, and render.

Create a renderer-owned video target.

Types

color()

@type color() :: non_neg_integer()

renderer()

@type renderer() :: reference() | EmergeSkia.Macos.Renderer.t()

video_target()

@type video_target() :: EmergeSkia.VideoTarget.t()

Functions

input_mask_all()

@spec input_mask_all() :: non_neg_integer()

Returns the input mask for all events.

input_mask_codepoint()

@spec input_mask_codepoint() :: non_neg_integer()

Returns the input mask for text input events.

input_mask_cursor_button()

@spec input_mask_cursor_button() :: non_neg_integer()

Returns the input mask for cursor button events.

input_mask_cursor_enter()

@spec input_mask_cursor_enter() :: non_neg_integer()

Returns the input mask for cursor enter/exit events.

input_mask_cursor_pos()

@spec input_mask_cursor_pos() :: non_neg_integer()

Returns the input mask for cursor position events.

input_mask_cursor_scroll()

@spec input_mask_cursor_scroll() :: non_neg_integer()

Returns the input mask for cursor scroll events.

input_mask_focus()

@spec input_mask_focus() :: non_neg_integer()

Returns the input mask for window focus events.

input_mask_key()

@spec input_mask_key() :: non_neg_integer()

Returns the input mask for key events.

input_mask_resize()

@spec input_mask_resize() :: non_neg_integer()

Returns the input mask for window resize events.

load_font_file(name, weight, italic, path)

@spec load_font_file(String.t(), non_neg_integer(), boolean(), Path.t()) ::
  :ok | {:error, term()}

Load a font from a file path.

The font is registered by name and can be used with Font.family/1 in elements. Load different variants (bold, italic) with separate calls using appropriate weight/italic params.

Parameters

  • name - Font family name to register (e.g., "my-font")
  • weight - Font weight (100-900, 400=normal, 700=bold)
  • italic - Whether this is an italic variant
  • path - Path to the TTF font file

Example

# Load font variants
:ok = EmergeSkia.load_font_file("my-font", 400, false, "priv/fonts/MyFont-Regular.ttf")
:ok = EmergeSkia.load_font_file("my-font", 700, false, "priv/fonts/MyFont-Bold.ttf")
:ok = EmergeSkia.load_font_file("my-font", 400, true, "priv/fonts/MyFont-Italic.ttf")

# Use in elements
el([Font.family("my-font"), Font.size(16)], text("Hello"))
el([Font.family("my-font"), Font.bold()], text("Bold text"))

measure_text(text, font_size)

@spec measure_text(String.t(), number()) :: {float(), float(), float(), float()}

Measure text dimensions for layout purposes.

Returns {width, line_height, ascent, descent} where:

  • width - Horizontal extent of the text
  • line_height - Total line height (ascent + descent)
  • ascent - Distance from baseline to top (positive)
  • descent - Distance from baseline to bottom (positive)

patch_tree(renderer, state, tree)

Apply patches for a new tree, run layout, and render.

Window dimensions come from the initial start config and are updated automatically when the window is resized (handled on the Rust side).

render_to_pixels(tree, opts)

@spec render_to_pixels(
  Emerge.tree(),
  keyword()
) :: binary()

Render a tree to an RGBA pixel buffer (synchronous, no window).

This is useful for testing, headless rendering, and image generation. Each call creates a fresh CPU surface, runs layout, renders the tree, and returns the pixels.

Options

  • otp_app - OTP application used to resolve logical assets from its priv dir (required)
  • width - Output width in pixels (required)
  • height - Output height in pixels (required)
  • scale - Layout scale factor (default: 1.0)
  • assets - Asset runtime policy options (same shape as start/1)
  • asset_mode - :await to block for asset resolution, or :snapshot to capture the current placeholder state (default: :await)
  • asset_timeout_ms - Maximum wait time for asset_mode: :await (default: 30000)

Returns a binary containing RGBA pixel data (4 bytes per pixel, row-major order). The binary size is width * height * 4 bytes.

Example

import Emerge.UI
import Emerge.UI.Color
import Emerge.UI.Size

pixels =
  EmergeSkia.render_to_pixels(
    el(
      [width(px(100)), height(px(100)), Emerge.UI.Background.color(color(:red, 500))],
      none()
    ),
    otp_app: :my_app,
    width: 100,
    height: 100
  )

# pixels is 100 * 100 * 4 = 40000 bytes

render_to_png(tree, opts)

@spec render_to_png(
  Emerge.tree(),
  keyword()
) :: binary()

Render a tree to an encoded PNG binary (synchronous, no window).

This is useful for generating screenshots and documentation assets. Each call creates a fresh CPU surface, runs layout, renders the tree, and returns PNG file bytes.

Options

  • otp_app - OTP application used to resolve logical assets from its priv dir (required)
  • width - Output width in pixels (required)
  • height - Output height in pixels (required)
  • scale - Layout scale factor (default: 1.0)
  • assets - Asset runtime policy options (same shape as start/1)
  • asset_mode - :await to block for asset resolution, or :snapshot to capture the current placeholder state (default: :await)
  • asset_timeout_ms - Maximum wait time for asset_mode: :await (default: 30000)

Returns a binary containing the full encoded PNG file.

Example

import Emerge.UI
import Emerge.UI.Color
import Emerge.UI.Size

png =
  EmergeSkia.render_to_png(
    el(
      [width(px(100)), height(px(100)), Emerge.UI.Background.color(color(:red, 500))],
      none()
    ),
    otp_app: :my_app,
    width: 100,
    height: 100
  )

File.write!("preview.png", png)

rgb(r, g, b)

@spec rgb(0..255, 0..255, 0..255) :: color()

Convert RGB values to a color integer.

Examples

iex> EmergeSkia.rgb(255, 0, 0)
0xFF0000FF

iex> EmergeSkia.rgb(0, 255, 0)
0x00FF00FF

rgba(r, g, b, a)

@spec rgba(0..255, 0..255, 0..255, 0..255) :: color()

Convert RGBA values to a color integer.

Examples

iex> EmergeSkia.rgba(255, 0, 0, 128)
0xFF000080

iex> EmergeSkia.rgba(0, 0, 0, 255)
0x000000FF

running?(renderer)

@spec running?(renderer()) :: boolean()

Check if the renderer window is still open.

set_input_mask(renderer, mask)

@spec set_input_mask(renderer(), non_neg_integer()) :: :ok

Set the input event mask to filter which events are sent.

Wayland close notifications are always delivered to the input target as {:emerge_skia_close, :window_close_requested} and are not filtered by this mask.

Example

# Only capture mouse button and key events
import Bitwise
mask = EmergeSkia.input_mask_cursor_button() ||| EmergeSkia.input_mask_key()
EmergeSkia.set_input_mask(renderer, mask)

set_input_target(renderer, pid)

@spec set_input_target(renderer(), pid() | nil) :: :ok

Set the target process to receive renderer events.

Events are sent directly to the target process as {:emerge_skia_event, event} messages.

Raw input event payloads include:

  • {:cursor_pos, {x, y}}
  • {:cursor_button, {button, action, mods, {x, y}}}
  • {:cursor_scroll, {{dx, dy}, {x, y}}}
  • {:key, {key, action, mods}}
  • {:codepoint, {char, mods}}
  • {:text_commit, {text, mods}}
  • {:text_preedit, {text, cursor}}
  • :text_preedit_clear
  • {:cursor_entered, entered}
  • {:resized, {width, height, scale}}
  • {:focused, focused}

On Wayland, close notifications are sent separately as:

  • {:emerge_skia_close, :window_close_requested}

This lifecycle message bypasses the input mask so close requests are still delivered when other raw input categories are disabled.

On DRM, raw {:cursor_pos, {x, y}} delivery is latest-wins under load so pointer motion does not stall rendering. Button, scroll, key, and text events remain ordered.

Element event payloads include:

  • {id_bin, event_type}
  • {id_bin, event_type, payload}

where id_bin is an opaque element id and event_type is an atom such as :press, :click, :swipe_up, :swipe_down, :swipe_left, :swipe_right, :change, :key_down, :key_up, or :key_press.

Routed :key_down, :key_up, and :key_press payloads currently carry an opaque binding route id used by higher-level runtimes.

Higher-level runtimes should route element events with Emerge.Engine.lookup_event/3 or Emerge.Engine.dispatch_event/3/4.

Where:

  • button is an atom like :left, :right, :middle
  • action is 0 for release, 1 for press
  • mods is a list of modifier atoms like [:shift, :ctrl]
  • key is a canonical atom like :escape, :enter, :a, :digit_1, :arrow_left, or :plus

Raw key events stay layout-independent. Text-producing input is delivered separately through text commit/preedit events. For example, Shift+= reports raw key :equal with [:shift] and still commits the text "+".

Example

EmergeSkia.set_input_target(renderer, self())

receive do
  {:emerge_skia_event, {:cursor_button, {button, 1, _mods, {x, y}}}} ->
    IO.puts("Clicked #{button} at #{x}, #{y}")

  {:emerge_skia_event, {:key, {key, 1, _mods}}} ->
    IO.puts("Key pressed: #{key}")
end

set_log_target(renderer, pid)

@spec set_log_target(renderer(), pid() | nil) :: :ok

Set the target process to receive native renderer log messages.

Native logs are sent directly to the target process as {:emerge_skia_log, level, source, message} messages.

start()

@spec start() :: no_return()

start(opts)

@spec start(keyword()) :: {:ok, renderer()} | {:error, term()}
@spec start(String.t()) :: no_return()

Start a new renderer window.

Options

  • otp_app - OTP application used to resolve logical assets from its priv dir (required)
  • backend - Backend selection (:wayland, :drm, or :macos). Defaults to :wayland for Linux desktop builds, :macos on Darwin, and :drm for Nerves-style builds. The requested backend must also be present in config :emerge, compiled_backends: [...].
  • macos_backend - macOS surface backend selection (:auto, :metal, or :raster). Defaults to :auto and is only supported with backend: :macos.
  • title - Window title (default: "Emerge")
  • width - Window width in pixels (default: 800)
  • height - Window height in pixels (default: 600)
  • scroll_line_pixels - Pixel distance used for each discrete mouse-wheel line step (default: 30.0)
  • drm_card - DRM device path (default: /dev/dri/card0)
  • hw_cursor - Enable hardware cursor when available (default: true)
  • drm_cursor - Optional DRM-only cursor overrides for default, text, and pointer
  • input_log - Log DRM input devices on startup (default: false)
  • render_log - Log DRM render/present diagnostics (default: false)
  • close_signal_log - Log detailed Wayland window-close diagnostics to stderr (default: false)
  • renderer_stats_log - Log renderer timing stats every 5 seconds, including frame rate and min/avg/max/count timing windows for layout, event resolve, and patch completion (default: false)
  • assets - Asset runtime policy options (optional)

Native renderer logs are delivered to the process that starts the renderer as {:emerge_skia_log, level, source, message} messages. Call set_log_target/2 to redirect them.

assets options:

  • runtime_paths.enabled (default: false)
  • runtime_paths.allowlist (default: [])
  • runtime_paths.follow_symlinks (default: false)
  • runtime_paths.max_file_size (default: 25_000_000)
  • runtime_paths.extensions (default image/SVG extension allowlist)
  • fonts (default: [])

Each assets.fonts entry supports:

  • family (required)
  • source (required, logical path under <otp_app>/priv or %Emerge.Assets.Ref{})
  • weight (default: 400)
  • italic (default: false)

Each drm_cursor entry supports:

  • source (required, .png or .svg; logical path under <otp_app>/priv, %Emerge.Assets.Ref{}, or an absolute runtime path allowed by assets.runtime_paths)
  • hotspot (required {x, y} tuple; integers and floats are allowed)

DRM cursor overrides are applied only on the :drm backend. Missing icons fall back to the built-in mocu-black-right theme.

Compile-time backend selection is configured separately with config :emerge, compiled_backends: [...]. If omitted, desktop builds assume [:wayland] and Nerves-style builds assume [:drm].

start(title, width)

@spec start(String.t(), non_neg_integer()) :: no_return()

start(title, width, height)

stop(renderer)

@spec stop(renderer()) :: :ok

Stop the renderer and close the window.

submit_prime(video_target, desc)

@spec submit_prime(video_target(), map()) :: :ok | {:error, term()}

Submit a DRM Prime descriptor to a video target.

upload_tree(renderer, tree)

@spec upload_tree(renderer(), Emerge.tree()) ::
  {Emerge.Engine.diff_state(), Emerge.tree()}

Upload a full EMRG tree, run layout, and render.

Window dimensions come from the initial start config and are updated automatically when the window is resized (handled on the Rust side).

video_target(renderer, opts)

@spec video_target(
  renderer(),
  keyword()
) :: {:ok, video_target()} | {:error, term()}

Create a renderer-owned video target.

V1 supports fixed-size :prime targets only on Prime-capable backends (:wayland and :drm).