This file is the quick reference for AI agents (and humans) building GUIs with widgetkit + core
racket/gui. Read this first. It is intent-first: given what the user wants, it tells you which widget to use and gives a verified snippet, or points you at coreracket/gui.
A curated collection of GUI widgets for Racket. Two rules:
- Fill a gap core
racket/guileaves open. If the toolkit already does it well, it is not here. - Reuse over rewrite. When a mature package already does the job, widgetkit depends on it and re-exports it. New code is written only where no good solution exists.
#lang racket/base, (require widgetkit) plus (require racket/gui/base).
- Does core
racket/guialready do it well? → use core (table below). - Else, is it a widgetkit widget? → use it (intent table below).
- Else, is it a recommended companion? →
raco pkg installit. - Else, hand-roll on
canvas%/panel%, and consider contributing it.
| Need | Use |
|---|---|
| Push button | button% |
| Checkbox | check-box% |
| Radio group | radio-box% |
| Slider / range | slider% |
| Dropdown (pick one) | choice% |
| Combo (type + pick) | combo-field% |
| Single-line text | text-field% |
| Multi-line / rich text | editor-canvas% + text% |
| Static label | message% |
| Determinate progress (known %) | gauge% |
| Visual tab strip | tab-panel% (see footgun) |
| Border/group box | group-panel% |
| Scroll host | canvas% (with scrollbars) |
| Menus | menu-bar%, popup-menu% |
| Open/save file | get-file, put-file |
| Message / yes-no dialog | message-box, message-box/custom |
| Ask for a color | get-color-from-user |
| Tree / outline | mrlib/hierlist (ships with Racket) |
(require racket/gui/base widgetkit)| User intent | Widget | Minimal snippet (verified) |
|---|---|---|
| Placeholder + tooltip on a field | cue-mixin, tooltip-mixin |
(new (cue-mixin "" (tooltip-mixin text-field%)) [parent f] [label "Name:"] [cue "Enter your name"] [tooltip "Full name"]) |
| Placeholder + tooltip (simpler) | labeled-field% |
(new labeled-field% [parent f] [label "Name:"] [cue "Enter your name"] [tooltip "Full name"]) |
| Aligned grid / form layout | table-panel% |
(new table-panel% [parent f] [dimensions '(4 2)]) |
| Date input (dd.mm.yyyy) | date-text-field% |
(new date-text-field% [parent f] [label "Date:"]) |
| Very large list / custom-drawn rows | canvas-list% |
(new canvas-list% [parent f] [items (vector "a" "b")] [item-height 22] [action-callback (λ (c item e) (void))]) |
| Big list of strings, simple pick action | text-list% |
(new text-list% [parent f] [items (vector "a" "b")] [action (λ (item) (void))]) |
| Display an image (fit or fixed scale) | image-view% |
(new image-view% [parent f] [bitmap bmp] [scale 'fit]) or (send iv load-file "x.png") |
| Bottom status bar (+ optional progress) | status-bar% |
(new status-bar% [parent f] [show-progress #t] [initial-message "Ready."]) |
| Modal "Working… / Cancel" dialog | progress-dialog% |
(new progress-dialog% [parent f] [label "..."]); drive from a thread + queue-callback, see examples/progress-dialog-demo.rkt |
| Transient dismissible banner (toast) | notification-banner% |
(new notification-banner% [parent f]); (send nb show-message "Saved." 'success 3000) auto-dismisses; #f ms keeps it up |
| Scrolling log / console output | log-view% |
(new log-view% [parent f] [max-lines 5000]); (send log append-line "...") auto-scrolls |
| Draggable split panes | split-view% |
(new split-view% [parent f] [orientation 'horizontal] [fraction 0.4]); add children to (send sv get-first) / get-second |
| App toolbar (buttons + separators) | toolbar% |
(new toolbar% [parent f]); (send tb add-button "Open" (λ () ...)); (send tb add-separator) |
| Search box (live filter) | search-field% |
(new search-field% [parent f] [callback (λ (q) ...)]) |
| Switchable pages | stack% |
(new stack% [parent f]); (send st add-page); (send st show-page i) — pair with choice% for tabs |
| "Busy", unknown duration | spinner% |
(new spinner% [parent f] [diameter 28]) then (send sp start) / (send sp stop) |
Compact [-] value [+] numeric |
stepper% |
(new stepper% [parent f] [min-value 0] [max-value 20] [initial 5]) |
| Collapsible ("Advanced…") section | disclosure% |
(new disclosure% [parent f] [label "Advanced"] [expanded? #f]) — add children to (send d get-content) |
Each has a standalone demo in examples/ — copy it as a starting point.
Heavier controls are not hard dependencies, to keep widgetkit light:
| Need | Install |
|---|---|
| Interactive OSM map | raco pkg install map-widget |
| Sortable multi-column data grid | raco pkg install qresults-list |
| Spreadsheet editor | raco pkg install spreadsheet-editor |
Embed plot snips |
raco pkg install plot-container |
| Web view (Chromium/native) | raco pkg install racket-webview |
Read these before writing widget code; they are the mistakes an agent will otherwise copy:
button%callback takes 2 arguments,(λ (button event) ...), not 1. The arity is checked at construction, so(new button% [callback (λ (_) ...)])throws immediately.gauge%has nowidthinit field. Usemin-widthto fix its size.bitmap%has nowidth/heightinit field. Create one positionally with(make-object bitmap% w h), not(new bitmap% [width w] [height h]).message%grows to fit its label. For a status bar usemin-width+stretchable-width #tso long text is clipped instead of stretching the window.- A widget only resizes with the window if it is stretchable AND its parent
stretches children.
editor-canvas%,canvas%andpanel%default to stretchable, butpane%andgroup-panel%do NOT stretch their children (they use natural size) — a resizing area placed in one will not grow. Put it in ahorizontal-panel%/vertical-panel%, or set(send w stretchable-width/height #t).log-view%andimage-view%stretch by default. - Custom panel layout (overriding
place-children/container-size): each child-info entry passed to these methods is a 4-element list(min-width min-height stretchable-width stretchable-height)— there are no margin slots.container-sizetakesinfoand returns(values w h);place-childrentakesinfo width heightand returns a list of(list x y w h)in the same child order. To force a relayout after changing layout state, call(send panel change-children (λ (l) l)). Screen coordinates areclient->screen(notclient-to-screen). tab-panel%tabs are visual only. There is no per-instance selection callback, andon-new-tabis not augmentable. To switch pages, drive it from achoice%, or augmenton-new-request. The tabs do not hide/show children for you.cue-mixintakes 2 arguments:(cue-mixin default-cue-string base-class)— it is not a plainclass -> classmixin.tooltip-mixinis(tooltip-mixin base-class)(1 arg). Preferlabeled-field%, which bakes in the correct composition.cue-mixinfires the field callback at construction time (when the cue text is inserted). Any function the callback references must be defined BEFORE the field is constructed — see examples/search-field-demo.rkt.canvas-list%callbacks are 3-argument:(λ (canvas item event) ...)foraction-callback/selection-callback. Prefertext-list%for a list of strings with a one-argument(λ (item) ...)action.on-superwindow-showis not augmentable oncanvas%. Do notdefine/augmentit.date-text-field%starts a one-shot timer scheduled to fire at the next midnight. A script that constructs one will not exit on its own; call(exit 0)in non-GUI scripts. It is harmless inside a real app. This is whyexamples/date-input-demo.rktandexamples/showcase.rktkeep their whole body in(module+ main ...)— see the launch-code rule below.- Modal dialogs block in
show #t. Work that updates a modal dialog (e.g.progress-dialog%) must run in a separate thread and touch the UI viaqueue-callback; running it inline in the callback freezes the dialog. Seeexamples/progress-dialog-demo.rkt. - Class errors surface at load/instantiate, not at compile. "no such
method", "not augmentable", and "unused initialization arguments" are all
runtime errors —
raco makewill not catch them. Always instantiate to test, never just compile. - GUI instantiation needs a display. On headless Linux, run under
xvfb-run -a.
- Compile:
raco make <file> - Instantiate every widget in a hidden frame (catches init-arg and
class-creation errors that compile misses):
(require racket/class racket/gui/base widgetkit) (define f (new frame% [label "t"])) ; never shown (new status-bar% [parent f] [show-progress #t]) ;; ...every widget you use... (exit 0)
- Run the example:
racket examples/<widget>-demo.rkt(needs a display). - Tests:
raco test test/run.rkt(pure logic, runs anywhere) andbash test/run-gui-behavior.sh(real widget behavior; needs a display, wrapped in Xvfb on headless Linux). - Catalog-build simulation:
bash test/run-examples.shrunsraco test examples/, which instantiates every example without running itsmainsubmodule — exactly what the package catalog build (DrDr) does. Launch code therefore lives in(module+ main ...):Top-level launch code (or constructing a;; `racket` runs `main`; `raco test` only instantiates the module (smoke test). (module+ main (send f show #t))
date-text-field%at instantiation — it arms a midnight timer) hangs the process and times out both this script and the catalog build. - Launch all examples:
bash test/smoke-examples.sh
The smoke script (step 5) is the guard that catches runtime errors like a bad callback arity or a missing method — run it before considering GUI code done.
#lang racket/base
(require racket/gui/base widgetkit)
(define f (new frame% [label "my app"] [width 600] [height 400]
[alignment '(left top)]))
;; A form laid out in a grid, with a status bar pinned to the bottom.
(define form (new table-panel% [parent f] [dimensions '(3 2)]))
(new message% [parent form] [label "Name:"])
(new (cue-mixin "" (tooltip-mixin text-field%)) [parent form] [label #f]
[cue "Enter your name"] [tooltip "Full name"])
(new message% [parent form] [label "Quantity:"])
(new stepper% [parent form] [min-value 0] [max-value 99] [initial 1])
(new message% [parent form] [label "Date:"])
(new date-text-field% [parent form] [label #f])
(define bar (new status-bar% [parent f] [show-progress #t] [initial-message "Ready."]))
;; `racket` runs `main`; `raco test` only instantiates the module (smoke test).
(module+ main
(send f show #t))For a fuller, realistic example combining several widgets (table-panel%,
labeled-field%, text-list%, disclosure%, status-bar%), see
examples/mini-task-list.rkt — clone it as the starting point for a real app.
See CONTRIBUTING.md. The bar: it must fill a real gap core racket/gui leaves
open, ship with a verified runnable example, be documented (a row in this
file's intent table + a section in widgetkit.scrbl), and its traps must be
added to the Footguns list above.