Retained widgets¶
This page shows you how to build an interface out of real widgets, panels, labels and buttons, from Python. You create the widgets, place and style them, and ask for them to be drawn; d/OS's shared UI engine does the drawing and keeps track of what changed. Your program creates nodes (the engine's word for a widget), sets their properties through UI intents, named requests such as "set this text", and asks for one UI.SYNC per draw. It never repaints by hand and never keeps its own list of what needs redrawing. That is the engine's job, on purpose, in every language.
Thin wrappers, real widgets¶
import dos
# Thin application wrappers; the shared UI engine owns widgets and rendering.
class Widget:
def __init__(self, kind, parent, order):
self.node = dos.intent("UI", "CREATE", kind, parent, order)
dos.intent("UI", "SET_PALETTE", self.node, 4226, 6371, -1, 4327, 6479, 2047, 8452)
def rect(self, x, y, width, height):
dos.intent("UI", "SET_RECT", self.node, x, y, width, height)
def text(self, value):
dos.intent("UI", "SET_STRING", self.node, 1, value)
def pressed(self, value):
dos.intent("UI", "SET_INT", self.node, 16, int(value))
root = Widget(1, 0, 0)
header = Widget(1, root.node, 1)
heading = Widget(2, header.node, 1)
status = Widget(2, root.node, 2)
hint = Widget(2, root.node, 3)
button = Widget(3, root.node, 4)
dos.intent("UI", "SET_PALETTE", root.node, 4226, 4226, -1, -1, 6371, 2047, 8452)
heading.text("d/Python | Retained widgets")
status.text("Button clicks: 0")
hint.text("Click the button, or press Space / Enter.")
button.text("Add a click")
clicks = 0
pointer_down = False
armed = False
def layout(width, height):
root.rect(0, 0, width, height)
header.rect(12, 12, max(1, width - 24), 44)
heading.rect(24, 26, max(1, width - 48), 20)
status.rect(24, 82, max(1, width - 48), 22)
hint.rect(24, 112, max(1, width - 48), 22)
button.rect(24, 148, 144, 32)
def increment():
global clicks
clicks += 1
status.text("Button clicks: " + str(clicks))
dos.service("INVALIDATE")
@dos.on("WINDOW_OPEN")
def open_window():
dos.service("SET_TITLE", "d/Python — Retained widgets")
layout(dos.service("WIDTH"), dos.service("HEIGHT"))
@dos.on("WINDOW_DRAW")
def draw_window():
# The application requests its complete scene; the engine owns damage work.
dos.intent("UI", "SYNC", root.node)
@dos.on("WINDOW_RESIZE")
def resize_window(width, height):
layout(width, height)
dos.service("INVALIDATE")
@dos.on("ON_POINTER")
def on_pointer(x, y, buttons):
global pointer_down, armed
down = buttons % 2 == 1
hit, detail = dos.intent("UI", "HIT_TEST", button.node, x, y)
inside = hit == button.node
if down and not pointer_down:
armed = inside
elif not down and pointer_down:
if armed and inside:
increment()
armed = False
button.pressed(armed and down and inside)
pointer_down = down
dos.service("INVALIDATE")
@dos.on("ON_KEY")
def on_key(key):
if key == 32 or key == 13:
increment()
@dos.on("WINDOW_CLOSE")
def close_window():
dos.intent("UI", "DESTROY", root.node)
This is retained_window.py. Run it, click the button or press Space, and on macOS try resizing the window.
The vocabulary¶
| Intent | What it does |
|---|---|
UI.CREATE kind, parent, order |
creates a node of a kind (1 is a panel, 2 a label, 3 a button in the example) under a parent, and returns its id |
UI.SET_RECT node, x, y, w, h |
places it |
UI.SET_PALETTE node, … |
sets its seven color slots |
UI.SET_STRING node, slot, text |
sets a text property; slot 1 is the label |
UI.SET_INT node, slot, value |
sets an integer property; slot 16 is the pressed state |
UI.HIT_TEST node, x, y |
returns the id of the node under the point and a detail value: a call with two results |
UI.SYNC node |
the one draw request: show the complete subtree |
UI.DESTROY node |
releases the subtree; do it in WINDOW_CLOSE |
Twenty-four of the UI intents have the same test written in both d/BASIC and d/Python. They cover who owns a node, geometry, Unicode text, the caret, hit-testing, selection, cleanup that is safe to repeat, ids that are never reused, and grid and table snapshots that are copied, so later changes to your own arrays do not affect them. The intent coverage page lists them.
The two rules¶
- Ask for the scene, never for the pixels.
WINDOW_DRAWis oneUI.SYNC. Working out what changed and repainting it is the engine's job. - Destroy what you created. Closing the window destroys the widget tree and releases the Python wrappers; the runtime checks that nothing is left behind.
Where this is going¶
d/Python is tied to the retained UI promises as they land, and to the same shared widget library d/BASIC uses, through one published binary interface. The Python names and object wrappers map onto that library's exports through thin layers; they do not reimplement the widgets. Today's example uses the engine's intents directly; the libraries chapter explains how shared widget code in a shared library file, .dbl, becomes callable from Python.