Documentation menu
Built-in

Gui — native windows

Gui is the built-in backend for real native windows: a pixel framebuffer with shapes, real Unicode text, images, and full input. It is what serez-ui draws on — most apps use the library, but the raw primitives are here when you want to draw yourself.

Use serez-ui for apps. These are low-level drawing primitives (immediate mode: you clear and redraw every frame). For components, layout, focus and CSS, build on serez-ui instead. Everything here needs the Gui permission.

Under the hood: a cross-platform window with input and IME, a CPU framebuffer (software rendering — no GPU required), real font rasterization so accents and Unicode render properly, PNG/JPG decoding and clipboard access. Colors are 0xRRGGBB integers. Methods marked new arrived in Serez-Code 7.2.0.

The loop

Open a window, then loop while it is open: clear, draw, and present() (which shows the frame and polls input). idleWait(ms) sleeps until the next event (or a timeout) so an idle window costs ~0 CPU.

use permissions { Gui }

Gui.open("Demo", 480, 320)
while (Gui.isOpen()) {
    Gui.clear(0x0f172a)
    Gui.fillCircle(240, 160, 60, 0x3b82f6)
    Gui.drawText(20, 20, "hello", 2, 0xffffff)
    Gui.present()          // show the frame + read input
    Gui.idleWait(100)      // sleep until an event (CPU ~0 at rest)
}
Gui.close()
MethodReturnsNotes
Gui.open(title, w, h)Open the window
Gui.isOpen()boolStill open? (false after Esc / close button)
Gui.clear(color)Fill the whole canvas
Gui.present()Blit the frame + poll events
Gui.idleWait(maxMs)Sleep until input or timeout (idle = ~0 CPU)
Gui.close()Close the window
Gui.size()[w, h]Current inner size (px)
Gui.time()intms since the window opened (for animation)

Drawing

Filled and outlined shapes, gradients, a box blur, and — new — vector primitives for charts and custom graphics (thick anti-aliased lines, polylines, filled polygons). Coordinates are integers; a polyline/polygon takes a flat [x0, y0, x1, y1, ...] array.

MethodNotes
Gui.fillRect(x, y, w, h, color)Filled rectangle
Gui.fillRectAlpha(x, y, w, h, color, alpha)Filled rect blended at alpha 0–255 (scrims, highlights)
Gui.drawRect(x, y, w, h, color)1px outline
Gui.fillRoundRect(x, y, w, h, radius, color)Rounded filled rect (anti-aliased corners)
Gui.drawLine(x0, y0, x1, y1, color)1px line
Gui.fillCircle(cx, cy, r, color)Anti-aliased filled disc
Gui.setPixel(x, y, color)Single pixel
Gui.fillGradient(x, y, w, h, c1, c2, vertical)Linear gradient (vertical = bool)
Gui.blur(x, y, w, h, radius)Box blur a region in place (frosted panels/shadows)
Gui.drawLineThick(x0, y0, x1, y1, width, color)new — thick line, rounded ends/joins
Gui.drawPolyline(points, width, color)new — connected segments (line charts)
Gui.fillPolygon(points, color)new — filled polygon (even-odd; concave OK)
Gui.drawCircle(cx, cy, r, color)new — 1px circle outline
Gui.pushClip(x, y, w, h)Restricts every following draw call to that rectangle (nests)
Gui.popClip()Restores the previous clipping region

pushClip / popClip are the immediate-mode clipping pair — anything drawn between them is cut to the rectangle, which is how scrollable panels keep their content inside their box. They nest, so an inner clip intersects the outer one. The retained scene has its own equivalent in Gui.nodeClipPush / Gui.nodeClipPop.

Gui.drawPolyline([20, 160, 80, 110, 140, 140, 200, 80], 3, 0xd32f2f)   // line chart
Gui.fillPolygon([300, 280, 380, 120, 460, 280], 0x2e7d32)              // filled triangle
Gui.drawLineThick(20, 40, 220, 40, 6, 0x1565c0)

Text

drawText rasterizes real glyphs. The optional style is a bitfield: 1 bold, 2 italic, and — new 4 underline, 8 strikethrough (combine them, e.g. 5 = bold + underline). A seventh argument sets letter-spacing (new). Load a .ttf/.otf with loadFont and select it with setFont.

MethodReturnsNotes
Gui.drawText(x, y, text, scale, color)Draw text (also …, style and …, style, letterSpacing)
Gui.measureText(text, scale)[w]Pixel width (index [0])
Gui.textAdvances(text, scale)[x0, x1, …]Cumulative x per character (caret placement)
Gui.loadFont(path)Load a .ttf/.otf
Gui.setFont(name)Select a family ("" = default mono grid)
Gui.setImePosition(x, y)Where the OS IME popup appears
Gui.drawText(20, 20, "Bold underline", 2, 0x111111, 1 + 4)   // style bits
Gui.drawText(20, 60, "S p a c e d", 2, 0x111111, 0, 6)      // letterSpacing = 6

Images

Load an image to a handle, then draw it (optionally scaled, with global alpha). Load from a file, or — new — from bytes in memory (a fetched image, or one read with File.read_asBinary).

MethodReturnsNotes
Gui.loadImage(path)handleDecode a PNG/JPG file
Gui.loadImageBytes(bytes)handlenew — decode from an in-memory byte array
Gui.drawImage(x, y, handle)Also …, w, h (scale) and …, w, h, alpha
Gui.imageSize(handle)[w, h]Natural size

Input

After each present(), read the current input. Alongside mouse/keyboard, 7.2.0 added window focus, IME composition, file drag-drop, extra mouse buttons, and touch/pinch — all new.

MethodReturnsNotes
Gui.mouse()[x, y]Cursor position
Gui.mouseDown() / mouseRightDown() / mouseMiddleDown()boolButton held
Gui.scroll()[dx, dy]Wheel delta this frame
Gui.keyDown(name)boolKey held (e.g. "Shift", "a")
Gui.keysPressed() / keysRepeated() / keysReleased()[names]Key edges this frame
Gui.charsTyped()stringText typed this frame (respects OS layout/IME)
Gui.focused()boolnew — window has OS focus
Gui.mouseInWindow()boolnew — cursor is over the window
Gui.mouseBackDown() / mouseForwardDown()boolnew — side buttons
Gui.imePreedit()stringnew — CJK composition in progress ("" if none)
Gui.droppedFiles()[paths]new — files dropped this frame (needs File to read)
Gui.hoveredFiles()[paths]new — files dragged over the window (before dropping)
Gui.touches()[id, phase, x, y, …]new — flat touch points (phase 0=start/1=move/2=end/3=cancel)
Gui.pinchDelta()decimalnew — trackpad pinch/zoom this frame

Window control

Retitle, resize, move, and decorate the window. Most of the ops below (max size, minimize, always-on-top, taskbar attention, hide cursor, borderless drag, custom icon) are new in 7.2.0.

MethodNotes
Gui.setTitle(text)Change the title bar text
Gui.setMinSize(w, h) / setMaxSize(w, h)Size limits (setMaxSize is new; 0,0 = none)
Gui.setResizable(b) / setDecorations(b)Toggle resize / the title bar + border
Gui.setFullscreen(b) / maximize(b) / minimize(b)Window state (minimize is new)
Gui.setPosition(x, y)Move the window
Gui.dragWindow()new — start an OS window drag (custom title bars); call on mousedown
Gui.setAlwaysOnTop(b)new — keep above other windows
Gui.requestAttention(b)new — flash the taskbar entry
Gui.setCursorVisible(b)new — hide/show the cursor
Gui.setWindowIcon(path)new — app icon ("" removes it)

Cursor, clipboard & dialogs

MethodReturnsNotes
Gui.setCursor(name)Named cursor ("hand", "text", "crosshair", …)
Gui.setCursorImage(path, hotspotX, hotspotY)new — custom cursor image ("" restores default)
Gui.clipboardGet() / clipboardSet(text)string / —Text clipboard
Gui.clipboardGetImage()handlenew — read an image from the clipboard (0 if none)
Gui.clipboardSetImage(handle)new — copy an image handle to the clipboard
Gui.openFileDialog(filterName, exts)pathNative open dialog (exts = "json,txt"); "" if cancelled
Gui.saveFileDialog(filterName, exts, defaultName)pathNative save dialog

HiDPI & monitors

MethodReturnsNotes
Gui.scaleFactor()decimalMonitor scale (1.0 = 96 dpi)
Gui.windowPosition()[x, y]new — window position (physical px)
Gui.monitors()[dict, …]new — one dict per monitor: {x, y, width, height, scale, name}

Multi-window — one selection at a time

A program can drive several OS windows at once. The part worth understanding before writing any code is that there is no window argument anywhere: no drawText(windowId, …), no keyDown(windowId, …). Instead one window is selected at any moment, and every other call in the whole Gui namespace acts on that one. Gui.selectWindow(id) is what moves the selection.

What gets swapped when you select is not just a drawing target. Each window owns its canvas, its input snapshot (keys, mouse, typed characters, dropped files), its retained scene graph, its clip stack, and its own size and HiDPI scale. So Gui.size(), Gui.mouse(), Gui.keysPressed() and Gui.isOpen() all answer about the selected window — a fact that is easy to read past and is the source of almost every multi-window bug.

Window IDs

  • 0 is the primary window, the one Gui.open creates. It always exists and it is the selection your program starts with.
  • Gui.openWindow hands back 1, 2, 3… and requires the primary to be open already — calling it first fails with GuiError: open the primary window first. It blocks until the OS window actually exists, so the ID it returns is immediately usable.
  • Gui.closeWindow only accepts 1 or higher. The primary is closed with Gui.close(), which ends the GUI session. If you close the window that was selected, the selection falls back to 0 on its own, so you are never left pointing at something that is gone.

The loop, written correctly

Because Gui.isOpen() reports on the selected window, the loop condition depends on whatever you happened to select last. Draw the secondary window at the bottom of the body and while (Gui.isOpen()) silently becomes “while the secondarywindow is open”: closing the little panel kills the app, and closing the main one is not even noticed. Select the window you mean before you ask.

use permissions { Gui }

Gui.open("Main Window", 800, 600)                          // this one is ID 0
let panelId = Gui.openWindow("Secondary Panel", 400, 300)   // → 1
let panelAlive = true

// Ask about window 0 explicitly, so the loop follows the MAIN window.
Gui.selectWindow(0)
while (Gui.isOpen()) {
    // ── main window ──────────────────────────────────────────────
    Gui.selectWindow(0)
    Gui.clear(0x0f172a)
    Gui.drawText(20, 20, "Main window", 2, 0xffffff)
    if (Gui.keysPressed().includes("Space")) {   // input of window 0
        out "space in the main window"
    }
    Gui.present()

    // ── secondary window ─────────────────────────────────────────
    if (panelAlive) {
        Gui.selectWindow(panelId)
        if (Gui.isOpen()) {                      // did the user click its × ?
            Gui.clear(0x1e293b)
            Gui.drawText(20, 20, "Panel", 2, 0xffffff)
            Gui.present()
        } else {
            panelAlive = false                   // stop drawing to a dead window
        }
    }

    // Re-select the main window BEFORE the condition is evaluated again.
    Gui.selectWindow(0)
    Gui.idleWait(16)
}

if (panelAlive) { Gui.closeWindow(panelId) }
Gui.close()

The same rule decides where a click lands. Reading Gui.mouse() while the panel is selected gives you coordinates inside the panel, not the desktop — each window accumulates its own events while it is in the background, so nothing is lost between selections.

MethodReturnsNotes
Gui.openWindow(title, w, h)intOpens a secondary window and returns its ID (≥ 1). Blocks until it exists. Errors if the primary is not open yet, or if the OS refuses to create it
Gui.selectWindow(id)Moves the selection. Swaps canvas, input, scene graph, clip stack, size and scale in one step. Errors on an unknown ID
Gui.currentWindow()intID of the selected window — useful in a helper that must restore the previous selection
Gui.closeWindow(id)Closes a secondary window (ID ≥ 1 only). If it was selected, the selection returns to 0
Gui.isOpen()boolWhether the selected window is still open — not whether any window is
Retained mode across windows. Scene graphs are per-window, but node IDs are global. A node you create while window 0 is selected belongs to window 0's scene; calling Gui.nodeSet on that ID while the panel is selected will not find it, and Gui.sceneClear()only clears the scene of the window you are pointing at. Keep each window's node IDs in its own list, and select before you touch them.
Building an app, not a canvas?If you are drawing components rather than raw primitives, use serez-ui's panels (openPanel / closePanel), which wrap all of the above: the selection dance, per-panel input and focus, and removing a panel when the user closes its window. Reach for the raw Gui calls on this page when you own the pixels.

Retained-mode (Scene Graph)

Instead of redrawing everything pixel-by-pixel in immediate mode (which executes slowly inside a script loop), you can use retained-mode. Define persistent drawing nodes in a scene graph, which the runtime renders natively. The scene graph is window-specific, and the engine updates only when necessary.

use permissions { Gui }

Gui.open("Retained Demo", 640, 480)

// Create persistent scene graph nodes (returns node IDs)
let rectId   = Gui.nodeRect(100, 100, 200, 150, 0x3b82f6)
let circleId = Gui.nodeCircle(400, 200, 50, 0xef4444)
let textId   = Gui.nodeText(100, 300, "Persistent Text", 2, 0xffffff)

// Update node properties dynamically without re-creating them
Gui.nodeSet(rectId, "color", 0x10b981) // Change color to green
Gui.nodeSet(rectId, "x", 120)          // Move the rectangle

while (Gui.isOpen()) {
    // Gui.renderScene(bg_color) redraws ONLY if a node changed (is dirty).
    // It returns true if it repainted, and false if it just re-presented the existing frame.
    let repainted = Gui.renderScene(0x0f172a)
    
    Gui.idleWait(16)
}

// Delete a node or clear the entire scene
Gui.nodeDelete(rectId)
Gui.sceneClear()

Retained Node Types & Properties

Create nodes with specific shape constructors, and modify their traits via Gui.nodeSet(id, property, value).

ConstructorNotes
Gui.nodeRect(x, y, w, h, color)Create a filled rectangle
Gui.nodeRectAlpha(x, y, w, h, color, alpha)Rectangle with transparency (alpha: 0-255)
Gui.nodeRectOutline(x, y, w, h, color)Outline rectangle
Gui.nodeRoundRect(x, y, w, h, radius, color)Rectangle with rounded corners
Gui.nodeRoundRectOutline(x, y, w, h, radius, color)Outline of a rounded rectangle (1px, anti-aliased corners) — v9.9
Gui.nodeCircle(cx, cy, r, color)Filled anti-aliased circle
Gui.nodeLine(x1, y1, x2, y2, color)Draw a line
Gui.nodePolyline(points, width, color)Polyline from flat array [x1, y1, x2, y2, ...]
Gui.nodePolygon(points, color)Filled polygon from points
Gui.nodeText(x, y, text, scale, color)Render text (scale multiplies the 8px grid)
Gui.nodeTextPx(x, y, text, px, color)Render text at a literal pixel size — any value, not only multiples of 8 — v9.10
Gui.nodeImage(x, y, handle[, w, h[, alpha[, radius]]])Image handle: native size, scaled, with global alpha (0-255), and AA-masked rounded corners — v9.7 / v9.9
Gui.nodeClipPush(x, y, w, h)Push a clipping region bounds
Gui.nodeClipPop()Pop the active clipping region
MethodReturnsDescription
Gui.nodeSet(id, property, value)Set property: x, y, w, h, r, x2, y2, color, z, visible, text, scale, px, image, font, style, spacing, radius, alpha, width, points
Gui.nodeTransform(id, rotDeg, scaleXmille, scaleYmille, origX, origY)Affine transform on a node: rotation in degrees, scale in thousandths (1000 = 1.0), around a canvas-px origin. Identity (0, 1000, 1000) clears it — v9.11
Gui.measureTextPx(text, px)arrayReturns [width_px, px] for text at a literal pixel size — v9.10
Gui.nodeDelete(id)Remove a specific node from the scene
Gui.sceneClear()Remove all nodes from the selected window's scene
Gui.nodeCount()intGet the total number of active nodes in the scene
Gui.renderScene(bg)boolRedraw the dirty scene graph. Returns true if repainted, false if re-presented

Primitives engine (HTML/CSS-like) — v9.2

Instead of drawing rectangles yourself, hand the core a tree of HTML-like primitives plus a CSS stylesheet and let it do style resolution, layout and painting natively — the browser model. One call lays out the tree and rebuilds the retained scene; Gui.renderScene(bg)paints it. Layout + CSS for a real app-sized tree runs in ~0.05 ms — roughly 1000× fasterthan the same walk in interpreted code. This is the engine behind serez-ui's native renderer, and it is generic: the core knows tags, not widgets.

FunctionReturnsDescription
Gui.loadStylesheet(src)intParses CSS text; returns a stylesheet handle.
Gui.loadSvg(srcOrPath)intParses SVG markup (or reads an .svg file); the handle is used as src of svg/img nodes.
Gui.renderTree(root, sheet, w, h[, ctx])arrayResolves CSS, lays out, rebuilds the scene and returns the clickable regions [[tag, x, y, w, h, onClick], …] in pre-order. ctx is a dict evaluated by reactive CSS conditions.

Nodes are plain arrays — [tag, [[prop, value], …], [children…]], where children are nodes or strings. Tags: div, row, p, h1h6, span, b, i, hr, img, svg, circle, line, polyline, polygon and textbox (editable: caret, selection and line virtualization handled natively). The CSS covers the familiar web subset: full box model (per-side padding/margin, 1–4 value shorthands, borders offset content), border / border-radius, flexbox (justify-content, align-items, gap, flex weights, flex-shrink, text shrink-to-fit, flex-direction: column), basic display: grid (grid-template-columns with px / % / fr / repeat() + gaps), position: absolute + z-index overlays and position: relative, width/height in px / % / auto (% heights resolve against the parent; an absolute node without width shrink-wraps to its text, so right:-anchored badges just work), overflow: scroll, text alignment/line-height/letter-spacing/ white-space: nowrap/font-weight/text-decoration/font-family (with custom files declared in :font { alias: "path.ttf" } blocks of the sheet)/font-size — both color and font-size inherit down the tree like on the web — hex / named / rgb() / rgba() / hsl() / hsla() colors (the alpha channel makes backgrounds truly translucent), opacity applied to the whole subtree (text included), linear-gradient backgrounds, box-shadow, transform: translate and display: none. Selectors: tag, *, .class, #id, compounds (.a.b), descendants (.a .b), groups (h1, h2), the :focus/:hover/:active/:disabled pseudo-classes (matched against node state attributes; :active-focus is an alias of :focus) and reactive conditions evaluated against ctx(var == val) with the six comparison operators, or a bare (flag) for truthiness — last match wins. img takes a PNG/JPG file path (auto-sized, aspect-preserving, cached) or a Gui.loadSvg handle.

use permissions { Gui }

Gui.open("Primitives", 480, 220)
// Raw string (r"…") because CSS braces would otherwise trigger string interpolation
let sheet = Gui.loadStylesheet(r".card { background: #1e293b; padding: 14; border-radius: 10 } h2 { color: #f1c40f } .btn { background: #3b82f6; color: #ffffff; padding: 10; border-radius: 6; width: 130 }")

let clicks = 0
fn void onBtn() { clicks = clicks + 1 }

while (Gui.isOpen()) {
    let tree = ["div", [["class", "card"]], [
        ["h2", [], ["Native engine"]],
        ["p", [], ["Clicks: {clicks}"]],
        ["div", [["class", "btn"], ["onClick", onBtn]], ["Click me"]]
    ]]
    let regions = Gui.renderTree(tree, sheet, 480, 220)
    Gui.renderScene(0x0f172a)

    // Hit-test: route the click to the region under the mouse
    if (Gui.mousePressed()) {
        let m = Gui.mouse()
        let i = 0
        while (i < regions.length()) {
            let r = regions[i]
            if (r[5] != null && m[0] >= r[1] && m[0] <= r[1] + r[3] && m[1] >= r[2] && m[1] <= r[2] + r[4]) {
                r[5]()
            }
            i = i + 1
        }
    }
    Gui.idleWait(16)
}
Gui.close()
Building an actual app? Reach for serez-ui — its components, layout, focus, .szs styling and event hooks wrap all of the above. Drop down to raw Gui only for custom drawing (charts, canvases, games).