ChrysaLisp GUI App Skill
A ChrysaLisp GUI app is a long-running task that owns a window, renders
a widget tree, and dispatches events from a mailbox select loop. The
general ChrysaLisp disciplines (see the chrysalisp skill, LLM.md, and
docs/ai_digest/) apply on top of these app-specific patterns.
Canonical File Structure
Copy apps/template/ as the starting point for a new app:
app.lisp: Entry point — imports, mailbox setup, the main event
loop.
widgets.inc: Declarative UI definition — the widget tree, event
enums, and tool tips.
actions.inc: The switchboard — maps event IDs to action functions
and key codes to actions.
ui.inc: The action handler implementations (application logic).
utils.inc, clipboard.inc, undo.inc: Helper, clipboard, and
undo/redo logic.
Reference apps: apps/desktop/docs/ (good but simple),
apps/tools/edit/ (complex, with services and RPC), and
apps/demos/boing/ (timer-driven animation).
app.lisp — The Event Loop
;debug options
(case 2
(0 (import "lib/debug/frames.inc"))
(1 (import "lib/debug/profile.inc"))
(2 (import "lib/debug/debug.inc")))
(import "usr/env.inc")
(import "gui/lisp.inc")
(import "service/clipboard/app.inc")
;our UI widgets
(import "./widgets.inc")
(enums +select 0
(enum main tip timer))
(defq +rate (/ 1000000 1))
;import actions and bindings
(import "./actions.inc")
(defun dispatch-action (&rest action)
(catch (eval action) (progn (prin _) (print) :t)))
(defun main ()
(defq select (task-mboxes +select_size) *running* :t)
(def *window* :tip_mbox (elem-get select +select_tip))
(bind '(x y w h) (apply view-locate (. *window* :pref_size)))
(gui-add-front-rpc (. *window* :change x y w h))
(mail-timeout (elem-get select +select_timer) +rate 0)
(while *running*
(defq *msg* (mail-read (elem-get select (defq idx (mail-select select)))))
(cond
((= idx +select_tip)
;tip event
(if (defq view (. *window* :find_id (getf *msg* +mail_timeout_id)))
(. view :show_tip)))
((= idx +select_timer)
;timer event, re-arm and do periodic work here
(mail-timeout (elem-get select +select_timer) +rate 0))
;must be +select_main
((. *window* :dispatch *msg*))
((. *window* :event *msg*))))
(gui-sub-rpc *window*)
(profile-report "Template"))
Key points:
The +select enum names the mailbox slots. Slot 0 (main) receives
window events; add extra slots as needed (e.g., timer, remote)
and handle them in the cond before dispatch.
The current message is held in the global *msg*, so action
handlers can inspect it, e.g. (getf *msg* +ev_msg_action_source_id).
(. *window* :dispatch *msg*) looks up the event's target widget
and its :connect event in *event_map*, then calls
dispatch-action. The final clause, (. *window* :event *msg*),
handles everything else at the window level.
tip_mbox receives hover-timeout messages from the GUI; look up
the hovered widget with find_id and call :show_tip.
The window is registered with the GUI compositor via
gui-add-front-rpc and unregistered on exit with gui-sub-rpc.
Window placement: view-locate centers a window of its preferred
size on screen; view-fit clamps an explicit position and size to
the screen.
The case 2 debug header selects stack frames, profiling, or
debugger support. (debug-brk "name") and profile-report are
no-ops unless the matching debug import is active.
widgets.inc — The Widget Tree
(enums +event 0
(enum close max min)
(enum undo redo rewind cut copy paste)
(enum button_1 button_2))
(ui-window *window* ()
(ui-title-bar *title* "Template" (0xea19 0xea1b 0xea1a) +event_close)
(ui-tool-bar *main_toolbar* ()
(ui-buttons (0xe9fe 0xe99d 0xe9ff 0xea08 0xe9ca 0xe9c9) +event_undo))
(ui-stack *stack_flow* '("main" "settings") :nil
(ui-grid *main_widget* (:grid_width 2 :color +argb_orange)
(. (ui-button *b1* (:text "1")) :connect +event_button_1)
(. (ui-button *b2* (:text "2")) :connect +event_button_2))
(ui-backdrop *settings_widget* (:min_width 512 :min_height 256
:color +argb_black :ink_color +argb_red :spacing 16
:style :lines))))
(ui-tool-tips *main_toolbar*
'("undo" "redo" "rewind" "cut" "copy" "paste"))
Key points:
The +event enum groups related events. Every interactive widget
gets a unique event symbol via its :connect property; that is the
key used in *event_map*.
The first event passed to ui-buttons is the radio group: it
highlights which button in that toolbar is active.
Named widgets become globals (*b1*); anonymous ones use _.
Containers: ui-flow (with :flow_flags), ui-grid, ui-stack
(tabbed views from a list of strings), and ui-scroll. Flow flags:
+flow_right_fill, +flow_left_fill, +flow_up_fill,
+flow_down_fill, +flow_stack_fill.
Other common widgets: ui-textfield (:hint_text,
:clear_text), ui-files (file tree selector), ui-slider,
ui-label, ui-text, ui-backdrop, ui-canvas, and ui-vdu.
See gui/lisp.inc for the full set.
Use (const ...) for compile-time values and reference other
widgets' properties directly, e.g. (:color (get :color *other*)).
ui-tool-tips attaches a list of hover tips to a toolbar or stack.
Two-Pass GUI Layout & Constraint Primitives
The GUI framework renders views using a strict, non-backtracking two-pass
cycle:
Constraint Pass (:constraint): Traverses top-down to compute the
minimum required dimensions (w, h) of each widget based on content
(e.g., text bounds or child count).
Layout Pass (:layout): Traverses bottom-up to assign final
coordinates and bounds to each widget.
Greedy Sizing & Space Absorption: Flow flags such as +flow_stack_fill
and +flow_down_fill use lastw and lasth properties to absorb
remaining container space.
In custom layout containers, always ensure child bounds respect lastw
and lasth to prevent clipping or improper overflow.
actions.inc — The Switchboard
;module
(env-push)
(import "./ui.inc")
(defq
*event_map* (scatter (Fmap)
+event_close action-close
+event_min action-minimise
+event_max action-maximise
+event_undo action-undo
+event_button_1 action-button-1)
*key_map* (scatter (Fmap)
(ascii-code "1") action-button-1)
*key_map_shift* (scatter (Fmap))
*key_map_control* (scatter (Fmap)
(ascii-code "z") action-undo))
;module
(export-symbols
'(*event_map* *key_map* *key_map_shift* *key_map_control*))
(env-pop)
Key points:
The module pattern — env-push ... export-symbols + env-pop —
keeps the maps local to this file's environment.
Map values are action function names, not calls:
dispatch-action evaluates the list.
Key maps split by modifier state: plain, shift, control. Use
(ascii-code "x") for characters and 0x4000xxxx hex codes for
special keys (arrows, home/end).
ui.inc — The Action Handlers
(defun action-close ()
(setq *running* :nil))
(defun action-button-1 ()
(debug-brk "button1")
(def (. *b1* :dirty) :color +argb_red))
(defun action-minimise ()
(debug-brk "minimize")
(bind '(x y w h) (apply view-fit
(cat (. *window* :get_pos) (. *window* :pref_size))))
(. *window* :change_dirty x y w h))
Key points:
(def (. *widget* :dirty) :prop val) — the :dirty marker tells
the GUI to redraw that widget on the next frame.
Window resizes go through view-fit/view-locate then
:change_dirty.
(debug-brk "name") goes on its own line at column 0 — that is how
the debugger wants to see them.
Advanced Patterns
Single instance per node. Guard the import in app.lisp (see
apps/tools/edit/app.lisp):
(if (= 0 (length (mail-enquire "@Edit,")))
(import "./app_impl.lisp"))
Services and RPC. Declare a service in main with
(mail-declare mbox "Name" "info"), add a remote select slot for
its mailbox, handle remote messages in the loop, and (mail-forget key) on exit (see apps/tools/edit/app_impl.lisp).
Zoom. Scale font sizes with
(n2i (* (n2f size) (n2f (get :zoom *window*)))), set
(def *window* :zoom new_size), and rebuild the affected views
(see apps/desktop/docs/ui.inc action-scale-up).
State persistence. Save app state to a .tre file with
tree-save/tree-load on a file stream (see
apps/tools/edit/state.inc).
Overloading. Import another app's ui.inc and redefun the
actions you need to change (see apps/desktop/docs/ui.inc).
Timer-driven animation. Re-arm (mail-timeout ...) on each
timer tick, use +rate (/ 1000000 fps) for the period, mark
changed regions with :add_dirty and widgets with :dirty (see
apps/demos/boing/app.lisp).
1---2name: chrysalisp-gui-apps3description: Use when writing or modifying ChrysaLisp GUI applications — windows, widgets, toolbars, event loops, and action handlers.4---56# ChrysaLisp GUI App Skill78A ChrysaLisp GUI app is a long-running task that owns a window, renders9a widget tree, and dispatches events from a mailbox select loop. The10general ChrysaLisp disciplines (see the `chrysalisp` skill, `LLM.md`, and11`docs/ai_digest/`) apply on top of these app-specific patterns.1213## Canonical File Structure1415Copy `apps/template/` as the starting point for a new app:1617* `app.lisp`: Entry point — imports, mailbox setup, the main event18 loop.1920* `widgets.inc`: Declarative UI definition — the widget tree, event21 enums, and tool tips.2223* `actions.inc`: The switchboard — maps event IDs to action functions24 and key codes to actions.2526* `ui.inc`: The action handler implementations (application logic).2728* `utils.inc`, `clipboard.inc`, `undo.inc`: Helper, clipboard, and29 undo/redo logic.3031Reference apps: `apps/desktop/docs/` (good but simple),32`apps/tools/edit/` (complex, with services and RPC), and33`apps/demos/boing/` (timer-driven animation).3435## app.lisp — The Event Loop3637 ;debug options38 (case 239 (0 (import "lib/debug/frames.inc"))40 (1 (import "lib/debug/profile.inc"))41 (2 (import "lib/debug/debug.inc")))4243 (import "usr/env.inc")44 (import "gui/lisp.inc")45 (import "service/clipboard/app.inc")4647 ;our UI widgets48 (import "./widgets.inc")4950 (enums +select 051 (enum main tip timer))5253 (defq +rate (/ 1000000 1))5455 ;import actions and bindings56 (import "./actions.inc")5758 (defun dispatch-action (&rest action)59 (catch (eval action) (progn (prin _) (print) :t)))6061 (defun main ()62 (defq select (task-mboxes +select_size) *running* :t)63 (def *window* :tip_mbox (elem-get select +select_tip))64 (bind '(x y w h) (apply view-locate (. *window* :pref_size)))65 (gui-add-front-rpc (. *window* :change x y w h))66 (mail-timeout (elem-get select +select_timer) +rate 0)67 (while *running*68 (defq *msg* (mail-read (elem-get select (defq idx (mail-select select)))))69 (cond70 ((= idx +select_tip)71 ;tip event72 (if (defq view (. *window* :find_id (getf *msg* +mail_timeout_id)))73 (. view :show_tip)))74 ((= idx +select_timer)75 ;timer event, re-arm and do periodic work here76 (mail-timeout (elem-get select +select_timer) +rate 0))77 ;must be +select_main78 ((. *window* :dispatch *msg*))79 ((. *window* :event *msg*))))80 (gui-sub-rpc *window*)81 (profile-report "Template"))8283Key points:8485* The `+select` enum names the mailbox slots. Slot 0 (`main`) receives86 window events; add extra slots as needed (e.g., `timer`, `remote`)87 and handle them in the `cond` before dispatch.8889* The current message is held in the global `*msg*`, so action90 handlers can inspect it, e.g. `(getf *msg* +ev_msg_action_source_id)`.9192* `(. *window* :dispatch *msg*)` looks up the event's target widget93 and its `:connect` event in `*event_map*`, then calls94 `dispatch-action`. The final clause, `(. *window* :event *msg*)`,95 handles everything else at the window level.9697* `tip_mbox` receives hover-timeout messages from the GUI; look up98 the hovered widget with `find_id` and call `:show_tip`.99100* The window is registered with the GUI compositor via101 `gui-add-front-rpc` and unregistered on exit with `gui-sub-rpc`.102103* Window placement: `view-locate` centers a window of its preferred104 size on screen; `view-fit` clamps an explicit position and size to105 the screen.106107* The `case 2` debug header selects stack frames, profiling, or108 debugger support. `(debug-brk "name")` and `profile-report` are109 no-ops unless the matching debug import is active.110111## widgets.inc — The Widget Tree112113 (enums +event 0114 (enum close max min)115 (enum undo redo rewind cut copy paste)116 (enum button_1 button_2))117118 (ui-window *window* ()119 (ui-title-bar *title* "Template" (0xea19 0xea1b 0xea1a) +event_close)120 (ui-tool-bar *main_toolbar* ()121 (ui-buttons (0xe9fe 0xe99d 0xe9ff 0xea08 0xe9ca 0xe9c9) +event_undo))122 (ui-stack *stack_flow* '("main" "settings") :nil123 (ui-grid *main_widget* (:grid_width 2 :color +argb_orange)124 (. (ui-button *b1* (:text "1")) :connect +event_button_1)125 (. (ui-button *b2* (:text "2")) :connect +event_button_2))126 (ui-backdrop *settings_widget* (:min_width 512 :min_height 256127 :color +argb_black :ink_color +argb_red :spacing 16128 :style :lines))))129130 (ui-tool-tips *main_toolbar*131 '("undo" "redo" "rewind" "cut" "copy" "paste"))132133Key points:134135* The `+event` enum groups related events. Every interactive widget136 gets a unique event symbol via its `:connect` property; that is the137 key used in `*event_map*`.138139* The first event passed to `ui-buttons` is the radio group: it140 highlights which button in that toolbar is active.141142* Named widgets become globals (`*b1*`); anonymous ones use `_`.143144* Containers: `ui-flow` (with `:flow_flags`), `ui-grid`, `ui-stack`145 (tabbed views from a list of strings), and `ui-scroll`. Flow flags:146 `+flow_right_fill`, `+flow_left_fill`, `+flow_up_fill`,147 `+flow_down_fill`, `+flow_stack_fill`.148149* Other common widgets: `ui-textfield` (`:hint_text`,150 `:clear_text`), `ui-files` (file tree selector), `ui-slider`,151 `ui-label`, `ui-text`, `ui-backdrop`, `ui-canvas`, and `ui-vdu`.152 See `gui/lisp.inc` for the full set.153154* Use `(const ...)` for compile-time values and reference other155 widgets' properties directly, e.g. `(:color (get :color *other*))`.156157* `ui-tool-tips` attaches a list of hover tips to a toolbar or stack.158159## Two-Pass GUI Layout & Constraint Primitives160161The GUI framework renders views using a strict, non-backtracking two-pass162cycle:1631641. **Constraint Pass (`:constraint`):** Traverses top-down to compute the165 minimum required dimensions (`w`, `h`) of each widget based on content166 (e.g., text bounds or child count).1671682. **Layout Pass (`:layout`):** Traverses bottom-up to assign final169 coordinates and bounds to each widget.170171* **Greedy Sizing & Space Absorption:** Flow flags such as `+flow_stack_fill`172 and `+flow_down_fill` use `lastw` and `lasth` properties to absorb173 remaining container space.174175* In custom layout containers, always ensure child bounds respect `lastw`176 and `lasth` to prevent clipping or improper overflow.177178## actions.inc — The Switchboard179180 ;module181 (env-push)182183 (import "./ui.inc")184185 (defq186 *event_map* (scatter (Fmap)187 +event_close action-close188 +event_min action-minimise189 +event_max action-maximise190 +event_undo action-undo191 +event_button_1 action-button-1)192193 *key_map* (scatter (Fmap)194 (ascii-code "1") action-button-1)195196 *key_map_shift* (scatter (Fmap))197198 *key_map_control* (scatter (Fmap)199 (ascii-code "z") action-undo))200201 ;module202 (export-symbols203 '(*event_map* *key_map* *key_map_shift* *key_map_control*))204 (env-pop)205206Key points:207208* The module pattern — `env-push` ... `export-symbols` + `env-pop` —209 keeps the maps local to this file's environment.210211* Map values are action function *names*, not calls:212 `dispatch-action` evaluates the list.213214* Key maps split by modifier state: plain, shift, control. Use215 `(ascii-code "x")` for characters and `0x4000xxxx` hex codes for216 special keys (arrows, home/end).217218## ui.inc — The Action Handlers219220 (defun action-close ()221 (setq *running* :nil))222223 (defun action-button-1 ()224 (debug-brk "button1")225 (def (. *b1* :dirty) :color +argb_red))226227 (defun action-minimise ()228 (debug-brk "minimize")229 (bind '(x y w h) (apply view-fit230 (cat (. *window* :get_pos) (. *window* :pref_size))))231 (. *window* :change_dirty x y w h))232233Key points:234235* `(def (. *widget* :dirty) :prop val)` — the `:dirty` marker tells236 the GUI to redraw that widget on the next frame.237238* Window resizes go through `view-fit`/`view-locate` then239 `:change_dirty`.240241* `(debug-brk "name")` goes on its own line at column 0 — that is how242 the debugger wants to see them.243244## Advanced Patterns245246* **Single instance per node.** Guard the import in `app.lisp` (see247 `apps/tools/edit/app.lisp`):248249 (if (= 0 (length (mail-enquire "@Edit,")))250 (import "./app_impl.lisp"))251252* **Services and RPC.** Declare a service in `main` with253 `(mail-declare mbox "Name" "info")`, add a `remote` select slot for254 its mailbox, handle remote messages in the loop, and `(mail-forget255 key)` on exit (see `apps/tools/edit/app_impl.lisp`).256257* **Zoom.** Scale font sizes with258 `(n2i (* (n2f size) (n2f (get :zoom *window*))))`, set259 `(def *window* :zoom new_size)`, and rebuild the affected views260 (see `apps/desktop/docs/ui.inc` action-scale-up).261262* **State persistence.** Save app state to a `.tre` file with263 `tree-save`/`tree-load` on a file stream (see264 `apps/tools/edit/state.inc`).265266* **Overloading.** Import another app's `ui.inc` and `redefun` the267 actions you need to change (see `apps/desktop/docs/ui.inc`).268269* **Timer-driven animation.** Re-arm `(mail-timeout ...)` on each270 timer tick, use `+rate (/ 1000000 fps)` for the period, mark271 changed regions with `:add_dirty` and widgets with `:dirty` (see272 `apps/demos/boing/app.lisp`).