# 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