UI component constructors for the Mob framework.
Each function returns a node map compatible with Mob.Renderer. These can
be used directly, via the ~MOB sigil, or mixed freely — they produce the
same map format.
# Native map literal
%{type: :text, props: %{text: "Hello"}, children: []}
# Component function (keyword list or map)
Mob.UI.text(text: "Hello")
# Sigil (import Mob.Sigil or use Mob.Screen)
~MOB(<Text text="Hello" />)All three forms produce identical output and are accepted by Mob.Renderer.
Summary
Functions
Returns a :camera_preview component node. Renders a live camera feed inline.
Returns a :canvas leaf node — declarative 2D drawing surface backed
by SwiftUI Canvas on iOS and Jetpack Compose Canvas on Android.
Returns a :gpu_view leaf node — a fragment-shader-driven GPU surface
backed by MTKView + Metal on iOS. The native side compiles the
supplied shader (Metal Shading Language) into a render pipeline, binds
the supplied uniforms in declaration order at fragment buffer slot 0,
and renders a full-screen quad at the display refresh rate.
Returns a :native_view node that renders a platform-native component.
Returns a :sheet node — a native modal bottom sheet (iOS .sheet,
Android Material 3 ModalBottomSheet) that composes ordinary Mob nodes
as its content.
Returns a :text leaf node.
Returns a :webview component node. Renders a native web view inline.
Functions
Returns a :camera_preview component node. Renders a live camera feed inline.
Call MobCamera.start_preview/2 (the mob_camera plugin) before mounting this
component, and MobCamera.stop_preview/1 when done.
Props:
:facing—:back(default) or:front:width,:height— dimensions in dp/pts; omit to fill parent
Returns a :canvas leaf node — declarative 2D drawing surface backed
by SwiftUI Canvas on iOS and Jetpack Compose Canvas on Android.
Coordinates are canvas-local in points/dp, top-left origin.
Props
:width— canvas width in pt/dp (required):height— canvas height in pt/dp (required):draw— list of op maps (required); construct viaMob.Canvas.line/5,Mob.Canvas.circle/4, etc., or as raw maps with an:opkey
Color tokens inside draw ops are resolved against the active theme
by Mob.Renderer before serialisation, exactly like top-level color
props on text/button/etc.
Example
import Mob.UI
import Mob.Canvas
canvas(width: 240, height: 240, draw: [
circle(120, 120, 115, color: :surface_outline, width: 2),
line(60, 60, 60, 180, color: :primary, width: 8, cap: :round),
line(60, 180, 180, 180, color: :primary, width: 8, cap: :round),
line(60, 60, 180, 180, color: :primary, width: 8, cap: :round)
])See Mob.Canvas for the full op list and modifier reference.
Returns a :gpu_view leaf node — a fragment-shader-driven GPU surface
backed by MTKView + Metal on iOS. The native side compiles the
supplied shader (Metal Shading Language) into a render pipeline, binds
the supplied uniforms in declaration order at fragment buffer slot 0,
and renders a full-screen quad at the display refresh rate.
Android support (GLSurfaceView + GLES 3.0) is not in v1.
Props
:id— required atom that identifies the GPU view across re-renders (so the native side keeps the same Metal pipeline / texture cache).:width/:height— pt/dp, required.:shader— either a string of Metal Shading Language source (iOS), or a map%{ios: "...MSL..."}(escape hatch — same as the string form; the map form exists so future platforms can be added without breaking the API).:uniforms— an ordered list of values packed into the shader'sUniformsstruct in declaration order. Each element is one of:- a number —
float(oruintif integer-typed at the BEAM level) - a 2-element list
[a, b]—float2 - a 4-element list
[a, b, c, d]—float4(float3deliberately not supported in v1 — its 16-byte alignment with 12-byte size makes the layout API messier than it's worth here.)
- a number —
Shader compile errors are caught natively and surfaced as a translucent overlay on top of the GpuView with the error message.
Why a list, not a map
Elixir map iteration order is not stable across runtimes or map
sizes — %{a: 1, b: 2, c: 3} can iterate in any order. The natural
MSL layout for a Uniforms struct is positional, so we mirror that
on the BEAM side. List position 0 → first struct member, etc.
A map form is still accepted as a backward-compat fallback but will pack in whatever order the runtime decides, so the shader-side struct has to match an unstable order — not recommended.
Example — Mandelbrot at the display's refresh rate
@shader File.read!("priv/shaders/mandelbrot.metal")
Mob.UI.gpu_view(
id: :mandelbrot,
width: 350,
height: 350,
shader: @shader,
# MSL: struct Uniforms { float2 center; float zoom; uint max_iter; };
uniforms: [[cx, cy], zoom, max_iter]
)What the framework auto-provides
The host emits a built-in vertex shader that draws a full-screen quad
and produces a VertexOut { float4 position [[position]]; float2 uv; }.
Your fragment shader receives that as [[stage_in]] and reads
in.uv (0..1 across the view) plus the user uniforms at buffer slot 0.
Don't redeclare VertexOut, vertex_main, or the metal_stdlib include
in your shader — the host prepends them.
Required fragment entry point
Your shader must export fragment_main:
fragment half4 fragment_main(VertexOut in [[stage_in]],
constant Uniforms& u [[buffer(0)]]) { ... }
Returns a :native_view node that renders a platform-native component.
module must implement the Mob.Component behaviour and be registered
on the native side via MobNativeViewRegistry. The :id must be unique
per screen — a duplicate raises at render time.
All other props are passed to mount/2 and update/2 on the component.
Example
Mob.UI.native_view(MyApp.ChartComponent, id: :revenue_chart, data: @points)
Returns a :sheet node — a native modal bottom sheet (iOS .sheet,
Android Material 3 ModalBottomSheet) that composes ordinary Mob nodes
as its content.
children is one child node or a list of them.
Props
:detents— nonempty, duplicate-free subset of[:medium, :large], or the exclusive content-height detent[:content]/[{:content, max_height: number}]. Defaults to[:medium, :large].:mediumalone rejects expansion to full height;:largealone skips the half-height stop. A content detent wraps intrinsic content and caps overflow in an internally scrolling body.Two things to know about content detents. They size from the content's intrinsic height, so a scrollable child (
scroll,lazy_list) reports its full content height rather than a viewport height — it will expand inside the sheet and the sheet's own scroll takes the gesture, instead of the child scrolling independently. Use:medium/:largewhen the sheet's body is itself scrollable. And the sheet only knows its content height once it is on screen, so it presents at:mediumfor the first frame and resizes to the measured height immediately after.Invalid
:detentsraise.Mob.Rendererre-validates at the encode boundary throughnormalize_sheet_detents!/1, so a hand-built or~MOBsigil node cannot bypass this by skippingsheet/2— such a node now raises during render rather than silently degrading to[:medium, :large]on the native side.:on_dismiss—{pid, tag}, delivered ashandle_info({:dismiss, tag}, socket)exactly once when the sheet is dismissed (swipe-down, back gesture, or outside tap) — the same{atom, tag}wire shape ason_focus,on_blur,on_submit, andon_selectelsewhere inMob.UI.:background— container color: a theme token atom or a0x00000000..0xFFFFFFFFARGB integer.:scrim— dimming-layer color, same value shape as:background. iOS cannot honor this exactly — see the note below.:corner_radius— top-corner radius: a theme radius token atom or a non-negative number.:drag_indicator_color,:drag_indicator_width,:drag_indicator_height,:drag_indicator_rail_height— a custom drag-indicator capsule. All four are required together, or omit all four for the platform default indicator. Width and height must be positive; rail height must be at least the indicator height (the rail is the invisible touch target the visible capsule sits inside).:ios/:android— per-platform overrides. Each accepts only the style keys above (not:detentsor:on_dismiss); seeMob.Renderer's "Platform blocks" section for the general override mechanism.
Example
Mob.UI.sheet(
Mob.UI.text(text: "Hello from the sheet"),
detents: [:medium, :large],
on_dismiss: {self(), :dismiss_sheet},
background: :surface,
scrim: 0x33000000,
corner_radius: 10,
drag_indicator_color: :muted,
drag_indicator_width: 36,
drag_indicator_height: 5,
drag_indicator_rail_height: 22,
ios: %{corner_radius: 10},
android: %{corner_radius: 28}
)Platform limitation: scrim opacity on iOS
Android applies :scrim exactly — the sheet's dimming layer is drawn
with the requested color, alpha included. iOS's .sheet presentation
owns its dimming layer and does not expose an API to configure its
opacity; supported SwiftUI APIs leave it system-black at a fixed,
non-configurable alpha. There is no workaround that doesn't involve
private view-hierarchy manipulation, which this framework does not do.
If your design depends on exact scrim opacity, treat it as
Android-only and expect iOS to look slightly different.
Returns a :text leaf node.
Props
:text— the string to display (required):text_color— color value passed toset_text_color/2in the NIF:text_size— font size in sp passed toset_text_size/2in the NIF
Examples
Mob.UI.text(text: "Hello")
#=> %{type: :text, props: %{text: "Hello"}, children: []}
Mob.UI.text(text: "Hello", text_color: "#ffffff", text_size: 18)
#=> %{type: :text, props: %{text: "Hello", text_color: "#ffffff", text_size: 18}, children: []}
Returns a :webview component node. Renders a native web view inline.
The JS bridge is injected automatically — the page can call window.mob.send(data)
to deliver messages to handle_info({:webview, :message, data}, socket), and
Elixir can push to JS via Mob.WebView.post_message/2.
Props:
:url— URL to load (required):allow— list of URL prefixes that navigation is permitted to (default: allow all). Blocked attempts arrive as{:webview, :blocked, url}inhandle_info.:show_url— show a native URL label above the WebView (default: false):title— static title label above the WebView; overrides:show_url:width,:height— dimensions in dp/pts; omit to fill parent