# Graphics
Deft's graphics surface is layered by concern, with **one vocabulary
that draws everywhere**. All namespaces are **TUI-only** (`retro` /
`retra` / `retrs` builds) except where noted.
| Namespace | Role |
|---|---|
| `media/*` | Pixel **data**: decode, stable-pointer buffers, rotates/flips, Image converters |
| `draw/*` | ALL 2D raster output — **polymorphic on target** |
| `surface/*` | SDL surface **lifecycle**: open/close/resize/events/flush |
| `tui/*` | Structure: panels/focus/widgets + geometry + capability probes |
| `gl/*` | 3D scenes (render through a canvas). GL-only |
| `scene/*` | Cross-panel world layer. GL-only |
The core idea: every `draw/*` pixel function accepts as its first
argument a **panel `DrawContext`**, a **surface id** (number), or a
**canvas** element (`draw/canvas`). Learn one set of primitives, draw on any
surface.
[draw/line $ctx 10 20 90 20 "#ff0000"] # panel (component-local px) [draw/line $sid 10 20 90 20 "#ff0000" 2] # surface — same call [draw/clear $canvas "#101018"] # retained canvas
## `draw/*` — Cell and Pixel Primitives
The primary rendering API, called inside a `Panel.draw` method. The
framework passes a `DrawContext`; cell-mode functions write characters
into the panel's grid, pixel-mode functions record vector commands
composited over it.
defimpl MyApp Panel { draw {|ctx| [draw/fill-bg $ctx "#141420"] [draw/text $ctx 0 0 "Hello" "#ffffff"] [draw/fill-rect $ctx 10 10 100 50 "#ff0000"] } }
### Cell-mode functions (panel contexts only)
| Function | Purpose |
|---|---|
| `draw/text $ctx $col $row $text ?fg ?bg ?bold` | Put text at cell coords |
| `draw/cell $ctx $col $row $char ?fg ?bg ?bold` | Set a single cell |
| `draw/fill-bg $ctx $color` | Fill bounds with a background color |
| `draw/fill-cells $ctx $col $row $w $h $char $fg $bg` | Fill a cell rect with one char |
| `draw/scrollbar $ctx $scroll-pos $total $visible ?col` | Vertical scrollbar glyphs |
| `draw/push-clip $ctx $x $y $w $h` / `draw/pop-clip $ctx` | Clip rect stack (cells) |
### Pixel-mode functions (polymorphic on target)
Target is a panel `DrawContext`, a surface id, or a canvas.
Coordinates are component-local pixels on panels, surface-local pixels
on surfaces, canvas-local pixels on canvases.
| Function | Purpose |
|---|---|
| `draw/clear $target $color` | Panel: background cmd. Surface: fill staging frame. Canvas: set bg + clear list |
| `draw/pixel $target $x $y $color` | Single pixel (surface: writes the staging buffer) |
| `draw/rect $target $x $y $w $h $color ?width` | Rectangle outline |
| `draw/fill-rect $target $x $y $w $h $color` | Filled rectangle |
| `draw/line $target $x1 $y1 $x2 $y2 $color ?width` | Line |
| `draw/path $target $points $color ?width ?closed` | Polyline (`$points` = vector of `[x y]` pairs) |
| `draw/bezier $target $x0 $y0 … $x3 $y3 $color ?width ?segments` | Cubic Bezier |
| `draw/circle $target $cx $cy $r $color` | Filled circle |
| `draw/circle-outline $target $cx $cy $r $color ?width` | Circle outline |
| `draw/text-px $target $x $y $text ?color` | Freeform text (font atlas) |
| `draw/frame $sid $bytes $w $h` | Replace a surface's whole staging frame with RGBA bytes (the decode/video fast path) |
| `draw/image $target $col $row $w $h $src ?opts` | Draw an image (see below) |
### Metadata
| Function | Purpose |
|---|---|
| `draw/bounds $ctx` | Component bounds → `%{ :x :y :w :h }` (cells, local origin) |
| `draw/theme $ctx` | Current theme colors as a map |
| `draw/image-preview $ctx $col $row $w $h $path ?opts` | Half-block cells (CPU-only, thread-safe). `opts`: `:zoom`, `:pan-x`/`:pan-y` |
| `draw/image-dims $path` | Image dimensions → `%{ :w :h }` (no GL, any thread) |
### `draw/image` — the one image blit
`$src` is a **path** string, a **`PixelBuffer`** (`media/*`), or an
**`Image`** (`image/*`). GL hosts draw a deferred texture; ANSI hosts
fall back to half-block cells automatically — no app-level branching.
Panels take the rect in cells; surfaces in pixels.
[draw/image $ctx 0 0 $w $h "icon.svg"] # path (SVG rasterizes on GL) [draw/image $ctx 0 0 $w $h $buf :refresh true] # per-frame re-upload (video)
`opts`: `:refresh true` re-uploads buffer contents every frame (the
video pattern); `:zoom`/`:pan-x`/`:pan-y` control the ANSI half-block
fallback view.
## Colors — one form everywhere
Every color argument in every `draw/*`, `gl/*` and `scene/*`
call accepts:
- hex strings: `"#ff0000"`, `"#ff000080"` (with alpha), `"#f80"` (shorthand)
- named: `"teal"`, `"red"`, `"gray"` …
- ANSI-256 index: `196` (number or decimal string)
- component maps: `%{ :r 255 :g 0 :b 0 :a 128 }` (0-255)
Helpers: `color/parse` (any form → `%{ :r :g :b :a }`, nil on garbage),
`color/rgb`, `color/hex`, `color/names`.
Tweaks: `color/lighten` / `color/darken` (HSL lightness ±amount),
`color/saturate` / `color/desaturate`, `color/mix`, `color/invert`,
`color/complement`, `color/rotate-hue`, `color/grayscale`, `color/alpha`
(get/set 0-255), `color/fade`, `color/luminance`, `color/contrast-text`.
They take any color form and return a `"#rrggbb"` (or `"#rrggbbaa"`)
hex string, so tweaks chain and paste straight into theme files:
[color/lighten "#101018"] # "#242437" [color/darken [color/mix "#89b4fa" "teal"] 0.2] [color/contrast-text "#89b4fa"] # readable text on accent
## `surface/*` — SDL Surface Lifecycle
SDL surfaces run on `retrs` (own OS window) and `retro` (embedded in
a panel, video-overlay style). The same script works on both hosts.
`surface/*` owns lifecycle only — **drawing goes through `draw/*`
with the surface id as target**. On the ANSI host (`retra`)
`surface/open` errors cleanly.
| Function | Purpose |
|---|---|
| `surface/open %{:title "app" :w 640 :h 480}` | Open (auto-sized when `:w`/`:h` omitted) → surface id |
| `surface/close $sid` / `surface/set-title $sid $text` | Close / rename |
| `surface/resize $sid $w $h` / `surface/size $sid` | Resize / current size `%{ :w :h }` |
| `surface/events $sid` | Drain pending event maps |
| `surface/flush $sid` | Present the staged frame (low-level path) |
| `surface/list` | Open surfaces as `%{ :id :title :w :h :closed }` |
| `surface/read $sid` | Snapshot the staging frame → `%{ :data RGBA-bytes :w :h }` (test/screenshot path) |
| `surface/quit` / `surface/keep-alive` | Quit semantics (retrs process exit) |
The declarative face is the **`Surface` protocol**: `render {|ctx|}`
(ctx = `%{ :sid :w :h }`), `on-event {|ev|}`, `on-close {}` (return
`:block` to veto the X button). The host drives render/flush — see
`packages/surface-demo`.
## `media/*` — Pixel Data
CPU-side RGBA pixels, safe from any thread. Producers and transforms
only — rendering is `draw/image`'s job.
| Function | Purpose |
|---|---|
| `media/image-load $path` | Decode file → `PixelBuffer` |
| `media/pixel-buffer $w $h` | Zero-init buffer with a **stable pixel pointer** |
| `media/pixel-write $buf $bytes ?offset` | `memcpy` bytes into a buffer in place |
| `media/image-dims $buf` / `media/image-free $buf` | Dims `%{ :w :h }` / early release |
| `media/rotate-90 \| -180 \| -270 $buf` / `media/flip-h \| -v $buf` | New transformed buffer |
| `media/to-image $buf` / `media/from-image $img` | Convert between `PixelBuffer` and `Image` |
The **video pattern** — decode on a worker, write into one stable
buffer, refresh per frame:
def buf [media/pixel-buffer 320 240] [media/pixel-write $buf $frame-bytes] [draw/image $ctx 0 0 $w $h $buf %{ :refresh true }]
### Working with `image/*`
The `image/*` pipeline (open → resize/rotate/flip → format → encode)
composes directly with surfaces: every `image/*` op returns a new
`Image` value, and `draw/image` accepts `Image` sources on any
target. No temp files, no manual conversion:
set sid [surface/open %{ :title "viewer" :w 640 :h 480}] set img [image/open "photo.jpg"] set img [image/resize $img 320 240] # transform pipeline [draw/image $sid 0 0 320 240 $img] # straight onto the surface [draw/image $sid 320 0 240 320 [image/rotate $img 90]] [surface/flush $sid]
Convert between the two pixel-value types when a consumer needs the
other one: `[media/from-image $img]` (Image → PixelBuffer, e.g. to
`media/pixel-write` over it) and `[media/to-image $buf]`
(PixelBuffer → Image, e.g. to `image/write` it out).
## Retained Canvases (`draw/canvas`)
A retained pixel-space canvas that renders as a per-app layer,
independent of the `Panel.draw` lifecycle (charts, diagrams, badges —
and the viewport `gl/*` 3D scenes render through). Create and place
with `draw/canvas` / `draw/set-bounds`; **draw into it with the same
polymorphic `draw/*` functions** — the canvas element is the target.
Workspace hosts only (`retro`/`retra`) — standalone `retrs` apps draw
on surfaces instead.
def cv [draw/canvas %{ :width 80 :height 25 :bg "#000000" }] # in draw, position it over the panel (cell coords): [draw/set-bounds $cv $bounds~x $bounds~y $bounds~w $bounds~h] [draw/clear $cv "#000000"] [draw/fill-rect $cv 10 5 30 10 "#ff0000"] [draw/line $cv 0 0 80 25 "#00ff00"] [draw/circle $cv 40 12 5 "#0000ff"]
| Function | Purpose |
|---|---|
| `draw/canvas %{:width … :height … :bg …}` | Create a canvas element |
| `draw/set-bounds $canvas $x $y $w $h` | Reposition (cell coords) |
## `gl/*` — 3D Scene
A small immediate-mode 3D API rendered through a canvas (`draw/canvas`).
GL-only. `gl/reset` clears the geometry buffer for the next frame
(`gl/bg` paints the background).
def cv [draw/canvas %{ :width 80 :height 40 }] [gl/scene $cv] # attach 3D scene to canvas [gl/reset] # clear vertex buffer [gl/perspective 60.0 1.5 0.1 100.0] [gl/lookat 0 0 5 0 0 0 0 1 0] [gl/identity] [gl/translate -1.5 0 0] [gl/triangle 0.0 1.0 0.0 -1.0 -1.0 0.0 1.0 -1.0 0.0 "#ff0000"] [gl/present $cv]
| Function | Purpose |
|---|---|
| `gl/scene $canvas` | Attach 3D scene to canvas |
| `gl/reset` | Clear the vertex buffer (not the background — see `gl/bg`) |
| `gl/perspective $fov $aspect $near $far` | Perspective projection |
| `gl/lookat $ex $ey $ez $cx $cy $cz $ux $uy $uz` | View matrix |
| `gl/identity` / `gl/translate` / `gl/rotate-x\|-y\|-z` / `gl/scale` | Model matrix stack |
| `gl/triangle … $color` / `gl/quad … $color` | Push geometry |
| `gl/line $x0 $y0 $z0 $x1 $y1 $z1 $color ?width` | 3D line |
| `gl/bg $color` / `gl/depth $bool` | Clear color / depth test |
| `gl/present $canvas` | Mark scene+canvas dirty |
## `scene/*` — Cross-Panel World Layer
A workspace-wide overlay shared across all panels. Nodes have
workspace-pixel coordinates and can be dragged between panels.
GL-only — no-op on ANSI backends.
def id [scene/add :rect :at @{100 100} :size @{80 40} :color "#ff0000" :filled true :z 1] [scene/node-set $id :at @{120 120} :color "#00ff00"] echo [scene/node-get $id] # => %{ :id 1 :kind :rect :x 120 :y 120 :w 80 :h 40 :opacity 1.0 :z 1 :visible true } [scene/select $id] # draw halo handles echo [scene/selected] # => $id echo [scene/hit-test $x $y] # node id at (x, y) or 0 [scene/remove $id] [scene/clear]
| Function | Purpose |
|---|---|
| `scene/add :kind :at @{x y} :size @{w h} :color … :width … :filled … :text … :z … :opacity … :tag …` | Add a node |
| `scene/remove $id` / `scene/clear` / `scene/count` | Remove / clear / count |
| `scene/node-set $id :at … :to … :color … :width … :filled … :opacity … :z … :visible … :tag …` | Mutate |
| `scene/node-get $id` | Describe a node |
| `scene/hit-test $x $y` | Topmost node at coords |
| `scene/select $id` / `scene/selected` | Selection |
Cross-panel drag-and-drop: `tui/begin-scene-drag` starts a drag; the
main thread renders a ghost that follows the cursor; on mouse-up the
target panel receives a `:scene-drop` event and the source receives
`:scene-drag-end`. GL-only.
[tui/begin-scene-drag %{ :kind :text :text "hello" :color "#ff0" }]
## Geometry and Capability Probes
| Function | Purpose |
|---|---|
| `tui/cell-size` | Font cell size → `%{ :w :h }` (the single source of truth) |
| `tui/px->cell $x $y` / `tui/cell->px $col $row` | Convert `@[x y]` ↔ `@[col row]` |
| `tui/backend` | `:gl` or `:ansi` — the canonical host probe |
| `tui/gl?` | True on GL hosts (pixels, textures, SDL surfaces) |
All size/rect maps use `:w`/`:h`: `draw/bounds`, `tui/cell-size`,
`surface/size`, `draw/image-dims`, `media/image-dims`, `image/dims`.
Bounds-shaped maps add `:x`/`:y`.
## Choosing The Right Tool
| You want to... | Use |
|---|---|
| Render text and UI in a panel | `draw/*` cell mode |
| Draw lines/shapes over a panel | `draw/*` pixel mode |
| Show an image (any host) | `draw/image` — ANSI fallback is built in |
| Play decoded video | `media/pixel-buffer` + `media/pixel-write` + `draw/image :refresh` |
| Own OS window(s) / in-panel surface | `surface/open` + `draw/*` on the surface id |
| A retained chart/diagram overlay | `draw/canvas` + `draw/*` on the canvas |
| Transient badges across panels | `scene/*` |
| Render a 3D model | `gl/*` |
| Encode/transform an image | `image/*` (via `media/to-image` / `from-image`) |
## Performance Notes
- `draw/*` calls are queued and batched per frame; panel cell buffers
and pixel command lists are double-buffered across threads.
- Scene and GL state persist across frames — call `gl/reset` at the
start of each redraw.
- GL textures load through deferred per-thread queues (`draw/image`
returns false on the panel's first frame while the texture uploads,
then re-renders). The stable-pointer `media/pixel-buffer` avoids
per-frame texture allocation for video.
- ANSI backends (`retra`) get half-block fallbacks for images and
cell-mode rendering; `scene/*`, `gl/*` and SDL surfaces are
GL-only.
## Reference
- `packages/surface-demo` — `Surface` protocol + `draw/*` on surface ids
- `packages/cube` — `draw/canvas` + `gl/*` 3D
- `packages/ants`, `packages/tetris` — cell-mode animation
- `packages/image` — `draw/image` with `Image` sources and pan/zoom
- `packages/stdext/src/ffmpeg` — the video pattern end-to-end