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
@type color() :: non_neg_integer()
@type renderer() :: reference() | EmergeSkia.Macos.Renderer.t()
@type video_target() :: EmergeSkia.VideoTarget.t()
Functions
@spec input_mask_all() :: non_neg_integer()
Returns the input mask for all events.
@spec input_mask_codepoint() :: non_neg_integer()
Returns the input mask for text input events.
@spec input_mask_cursor_button() :: non_neg_integer()
Returns the input mask for cursor button events.
@spec input_mask_cursor_enter() :: non_neg_integer()
Returns the input mask for cursor enter/exit events.
@spec input_mask_cursor_pos() :: non_neg_integer()
Returns the input mask for cursor position events.
@spec input_mask_cursor_scroll() :: non_neg_integer()
Returns the input mask for cursor scroll events.
@spec input_mask_focus() :: non_neg_integer()
Returns the input mask for window focus events.
@spec input_mask_key() :: non_neg_integer()
Returns the input mask for key events.
@spec input_mask_resize() :: non_neg_integer()
Returns the input mask for window resize events.
@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 variantpath- 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 dimensions for layout purposes.
Returns {width, line_height, ascent, descent} where:
width- Horizontal extent of the textline_height- Total line height (ascent + descent)ascent- Distance from baseline to top (positive)descent- Distance from baseline to bottom (positive)
@spec patch_tree(renderer(), Emerge.Engine.diff_state(), Emerge.tree()) :: {Emerge.Engine.diff_state(), Emerge.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).
@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 itsprivdir (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 asstart/1)asset_mode-:awaitto block for asset resolution, or:snapshotto capture the current placeholder state (default::await)asset_timeout_ms- Maximum wait time forasset_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
@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 itsprivdir (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 asstart/1)asset_mode-:awaitto block for asset resolution, or:snapshotto capture the current placeholder state (default::await)asset_timeout_ms- Maximum wait time forasset_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)
@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
@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
Check if the renderer window is still open.
@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 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:
buttonis an atom like:left,:right,:middleactionis 0 for release, 1 for pressmodsis a list of modifier atoms like[:shift, :ctrl]keyis 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 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.
@spec start() :: no_return()
@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 itsprivdir (required)backend- Backend selection (:wayland,:drm, or:macos). Defaults to:waylandfor Linux desktop builds,:macoson Darwin, and:drmfor Nerves-style builds. The requested backend must also be present inconfig :emerge, compiled_backends: [...].macos_backend- macOS surface backend selection (:auto,:metal, or:raster). Defaults to:autoand is only supported withbackend: :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 fordefault,text, andpointerinput_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>/privor%Emerge.Assets.Ref{})weight(default:400)italic(default:false)
Each drm_cursor entry supports:
source(required,.pngor.svg; logical path under<otp_app>/priv,%Emerge.Assets.Ref{}, or an absolute runtime path allowed byassets.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].
@spec start(String.t(), non_neg_integer()) :: no_return()
@spec start(String.t(), non_neg_integer(), non_neg_integer()) :: no_return()
@spec stop(renderer()) :: :ok
Stop the renderer and close the window.
@spec submit_prime(video_target(), map()) :: :ok | {:error, term()}
Submit a DRM Prime descriptor to a video target.
@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).
@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).