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.
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()| Method | Returns | Notes |
|---|---|---|
Gui.open(title, w, h) | — | Open the window |
Gui.isOpen() | bool | Still 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() | int | ms 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.
| Method | Notes |
|---|---|
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.
| Method | Returns | Notes |
|---|---|---|
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 = 6Images
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).
| Method | Returns | Notes |
|---|---|---|
Gui.loadImage(path) | handle | Decode a PNG/JPG file |
Gui.loadImageBytes(bytes) | handle | new — 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.
| Method | Returns | Notes |
|---|---|---|
Gui.mouse() | [x, y] | Cursor position |
Gui.mouseDown() / mouseRightDown() / mouseMiddleDown() | bool | Button held |
Gui.scroll() | [dx, dy] | Wheel delta this frame |
Gui.keyDown(name) | bool | Key held (e.g. "Shift", "a") |
Gui.keysPressed() / keysRepeated() / keysReleased() | [names] | Key edges this frame |
Gui.charsTyped() | string | Text typed this frame (respects OS layout/IME) |
Gui.focused() | bool | new — window has OS focus |
Gui.mouseInWindow() | bool | new — cursor is over the window |
Gui.mouseBackDown() / mouseForwardDown() | bool | new — side buttons |
Gui.imePreedit() | string | new — 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() | decimal | new — 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.
| Method | Notes |
|---|---|
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
| Method | Returns | Notes |
|---|---|---|
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() | handle | new — read an image from the clipboard (0 if none) |
Gui.clipboardSetImage(handle) | — | new — copy an image handle to the clipboard |
Gui.openFileDialog(filterName, exts) | path | Native open dialog (exts = "json,txt"); "" if cancelled |
Gui.saveFileDialog(filterName, exts, defaultName) | path | Native save dialog |
HiDPI & monitors
| Method | Returns | Notes |
|---|---|---|
Gui.scaleFactor() | decimal | Monitor 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.opencreates. It always exists and it is the selection your program starts with. Gui.openWindowhands back 1, 2, 3… and requires the primary to be open already — calling it first fails withGuiError: open the primary window first. It blocks until the OS window actually exists, so the ID it returns is immediately usable.Gui.closeWindowonly accepts 1 or higher. The primary is closed withGui.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.
| Method | Returns | Notes |
|---|---|---|
Gui.openWindow(title, w, h) | int | Opens 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() | int | ID 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() | bool | Whether the selected window is still open — not whether any window is |
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.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).
| Constructor | Notes |
|---|---|
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 |
| Method | Returns | Description |
|---|---|---|
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) | array | Returns [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() | int | Get the total number of active nodes in the scene |
Gui.renderScene(bg) | bool | Redraw 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.
| Function | Returns | Description |
|---|---|---|
Gui.loadStylesheet(src) | int | Parses CSS text; returns a stylesheet handle. |
Gui.loadSvg(srcOrPath) | int | Parses SVG markup (or reads an .svg file); the handle is used as src of svg/img nodes. |
Gui.renderTree(root, sheet, w, h[, ctx]) | array | Resolves 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, h1–h6, 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()Gui only for custom drawing (charts, canvases, games).