Mob.UI (mob v0.7.39)

Copy Markdown View Source

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

camera_preview(props \\ [])

@spec camera_preview(keyword() | map()) :: map()

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

canvas(props)

@spec canvas(keyword() | map()) :: map()

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 via Mob.Canvas.line/5, Mob.Canvas.circle/4, etc., or as raw maps with an :op key

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.

gpu_view(props)

@spec gpu_view(keyword() | map()) :: map()

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's Uniforms struct in declaration order. Each element is one of:
    • a number — float (or uint if integer-typed at the BEAM level)
    • a 2-element list [a, b]float2
    • a 4-element list [a, b, c, d]float4 (float3 deliberately not supported in v1 — its 16-byte alignment with 12-byte size makes the layout API messier than it's worth here.)

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)]]) { ... }

native_view(module, props \\ [])

@spec native_view(module(), keyword() | map()) :: map()

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)

sheet(children, opts \\ [])

@spec sheet(map() | [map()], keyword() | map()) :: map()

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]. :medium alone rejects expansion to full height; :large alone 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/:large when the sheet's body is itself scrollable. And the sheet only knows its content height once it is on screen, so it presents at :medium for the first frame and resizes to the measured height immediately after.

    Invalid :detents raise. Mob.Renderer re-validates at the encode boundary through normalize_sheet_detents!/1, so a hand-built or ~MOB sigil node cannot bypass this by skipping sheet/2 — such a node now raises during render rather than silently degrading to [:medium, :large] on the native side.

  • :on_dismiss{pid, tag}, delivered as handle_info({:dismiss, tag}, socket) exactly once when the sheet is dismissed (swipe-down, back gesture, or outside tap) — the same {atom, tag} wire shape as on_focus, on_blur, on_submit, and on_select elsewhere in Mob.UI.

  • :background — container color: a theme token atom or a 0x00000000..0xFFFFFFFF ARGB 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 :detents or :on_dismiss); see Mob.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.

text(props)

@spec text(keyword() | map()) :: map()

Returns a :text leaf node.

Props

  • :text — the string to display (required)
  • :text_color — color value passed to set_text_color/2 in the NIF
  • :text_size — font size in sp passed to set_text_size/2 in 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: []}

webview(props \\ [])

@spec webview(keyword() | map()) :: map()

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} in handle_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