Skip to content

Windows & events

A window face is not a console loop with pixels added. It has a lifecycle. The host calls your code in bounded turns; your model lives between those turns. One source image may still offer a separate console face for scripting—see Console, Scripts & GUI.

The minimum window

WINDOW SUB OPEN()
  SERVICE SET_TITLE("Hello window")
  SERVICE INVALIDATE()
END SUB

WINDOW SUB DRAW()
  INTENT GFX.BEGIN_FRAME()
  INTENT GFX.FILL_RECT(0, 0, 320, 192, 2081)
  INTENT GFX.DRAW_TEXT(12, 12, "HELLO", 3, -1, 2081)
  INTENT GFX.PRESENT()
END SUB

WINDOW SUB CLOSE()
END SUB

Every window program defines exactly one OPEN, DRAW, and CLOSE handler. They are module-level faces, not methods hidden inside an arbitrary class.

Lifecycle faces

Face Parameters Use it for
WINDOW SUB OPEN() none create model state, set the title, request the first draw
WINDOW SUB DRAW() none emit one complete semantic frame
WINDOW SUB RESIZE(W, H) two INTEGERs recompute layout for the new logical viewport
WINDOW SUB CLOSE() none release application-owned state
WINDOW SUB ACTIVE(State) one INTEGER active/inactive transitions
WINDOW SUB VISIBLE(State) one INTEGER visible/hidden transitions
WINDOW SUB SUSPENDED(State) one INTEGER suspend/resume work
WINDOW SUB MINIMIZED(State) one INTEGER minimized state changes
WINDOW SUB MAXIMIZED(State) one INTEGER maximized state changes

Host parameters are BYVAL INTEGER. Each face may appear once.

State belongs to the application

Use module storage for a small program or put the model and controller into a class:

CLASS CounterCard
  PUBLIC:
    Count AS INTEGER
    Width AS INTEGER
    Height AS INTEGER
    DECLARE SUB Paint ()
END CLASS

DIM SHARED App AS CounterCard

WINDOW SUB OPEN()
  App = NEW CounterCard()
  App.Width = 320
  App.Height = 192
  SERVICE SET_TITLE("Counter")
  SERVICE INVALIDATE()
END SUB

WINDOW SUB DRAW()
  App.Paint
END SUB

WINDOW SUB CLOSE()
  App = NOTHING
END SUB

The host owns the event loop. App owns the session. This is why the same source can live inside a D/OS surface, a browser worker, or a native desktop window.

Keyboard events

ON KEY SUB (K AS INTEGER)
  IF K = 13 THEN App.Count = App.Count + 1
  IF K = 27 THEN App.Count = 0
  SERVICE INVALIDATE()
END SUB

The input is normalized to an integer key value. Common values:

Key Value
Backspace 8
Tab 9
Enter 13
Escape 27
Delete 127
Insert 256
printable ASCII its ASCII value
Arrow Up / Down / Left / Right 257 / 258 / 259 / 260
Home / End 261 / 262
Page Up / Page Down 263 / 264
function key Fn 0x0120 + n

The program decides what a key means. A text field might insert printable ASCII, move its caret on Left/Right, and publish a local edit event. A button might activate on Enter.

Unified UI events

Applications that need committed Unicode text, composition/IME, focus, semantic actions, asynchronous completion, or capability-change events use the unified face:

IMPORT WIDGETS

ON EVENT SUB (E AS UIEVENT)
  ' Inspect the callback-scoped event and update the application model.
END SUB

ON EVENT replaces—not supplements—ON KEY, ON POINTER, and ON MENU; declaring both forms is refused so one input turn cannot arrive twice. OPEN, DRAW, RESIZE, and CLOSE remain the same.

The event kinds are Key, TextCommit, Composition, Pointer, Focus, Refresh, Show, Hide, SemanticAction, AsyncCompletion, CapabilityChanged, and Overflow. A Key event describes physical/logical key state and never inserts text. Only TextCommit inserts committed UTF-8; Composition carries the live IME range and clauses. This split prevents a composed character from becoming a key plus duplicate text.

UIEVENT and its borrowed payloads are readable only during the callback. Call Snapshot() before returning when the application must retain event data; keeping the borrowed view itself is refused.

Pointer events

ON POINTER SUB (X AS INTEGER, Y AS INTEGER, Buttons AS INTEGER)
  App.HandlePointer X, Y, Buttons
  SERVICE INVALIDATE()
END SUB

X and Y use the surface's logical coordinate system. Buttons is the current normalized button state.

Robust controls reduce a gesture, not merely a coordinate:

  1. on transition from no buttons to pressed, remember which control owns the press;
  2. update visual down/drag state while moving;
  3. on release, activate only when the release satisfies that control's rule; and
  4. clear pressed state on lifecycle cancellation.

This makes a button behave like a button on a mouse, touch bridge, or small-machine pointing device.

Drop events

Drag and drop uses the same face discipline without turning every pointer move into application code:

WINDOW ACCEPTS "DOBJ", "STYL", "TEXT"

ON DROP SUB (D AS DROPITEM)
  IF D.KIND$ = "TEXT" THEN App.InsertText D.TEXT$
END SUB

The compositor negotiates declared representations and shows accept/refuse feedback. ON DROP runs only after a real committed drop. See Drag, Drop & DOBJ for in-app moves, cross-application transactions, native OS bridges, and DOBJ.

Resize means layout, not magnification

WINDOW SUB RESIZE(W AS INTEGER, H AS INTEGER)
  App.Width = W
  App.Height = H
  App.Layout
  SERVICE INVALIDATE()
END SUB

A capable desktop runner gives the program a larger logical canvas when the window grows. The program reflows. A compact head supplies its own viewport. The runner can scale presentation for device pixels, but it does not lie to application layout about the logical size.

Design a compact fallback for very small valid extents:

IF W < 240 OR H < 150 THEN
  App.Compact = -1
ELSE
  App.Compact = 0
END IF

Invalidation is a request

SERVICE INVALIDATE() asks the host to schedule a draw turn. It does not recursively call DRAW, and it does not promise a full-window repaint.

IF App.NeedsPaint() THEN SERVICE INVALIDATE()

Prefer that shape over unconditional invalidation after every ignored key. The widget library's damage ledger makes the decision explicit.

Draw one complete semantic frame

SUB CounterCard.Paint ()
  DIM Text AS STRING
  Text = "COUNT: " + STR$(THIS.Count)

  INTENT GFX.BEGIN_FRAME()
  INTENT GFX.FILL_RECT(0, 0, THIS.Width, THIS.Height, 2081)
  INTENT GFX.SET_CLIP(8, 8, THIS.Width - 16, THIS.Height - 16)
  INTENT GFX.DRAW_TEXT(12, 12, Text, 3, -1, 2081)
  INTENT GFX.RESET_CLIP()
  INTENT GFX.PRESENT()
END SUB

BEGIN_FRAME and PRESENT bound the frame. Clips keep component drawing honest. The host may use a high-resolution real font, a resident device face, a GPU, or a composed head; the source's geometry and ordering stay canonical.

Separate model, interaction, and paint

A maintainable window program has three rhythms:

  • model: the state that would still matter with the screen turned off;
  • interaction: event reducers that change the model and mark damage;
  • paint: a pure-ish projection of current model state into GFX Intents.

Avoid changing application state merely because DRAW was called again. Hosts may expose, coalesce, or repeat draws.

Faces are turns, not threads

Do not block inside an event handler waiting for another event. Start an operation, retain its state, return, and continue on a later face turn or completion. DBC execution itself is sliced to keep a constrained host responsive.

Where widgets fit

Hand-painting one button is easy. Making focus, caret placement, press/release, disabled state, menus, clipping, theme, and damage agree across systems is not. D/Works Widgets provides those reusable objects while leaving lifecycle and application meaning in your code.


Next: Widgets →