UI toolkits and widgets
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Bryan ebe482a5a2 a key claimed in a form; tabs cross a pipe; a label laid out again
(claim "chord") among a form's keys: a key a host would keep for itself
goes to the parts (toolkit-built-claims?), as the terminal's ctrl+x and
ctrl+z do for an editor's cut and undo. The host prints a tab, a
carriage return and a zero byte as the reader's escapes, not spaces, so
a file's tabs come back from the terminal as tabs. (set label ...) lays
the window out again, a label wanting its text's width.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-01 14:19:54 +01:00
examples what the editor needed of forms and the host 2026-11-01 14:01:02 +01:00
lib a key claimed in a form; tabs cross a pipe; a label laid out again 2026-11-01 14:19:54 +01:00
scripts tabs, a sash, kinds and a title from forms 2026-11-01 13:36:23 +01:00
tools tabs, a sash, kinds and a title from forms 2026-11-01 13:36:23 +01:00
.gitignore toolkit: parts, their layout and a run loop, as a library of their own; the launcher built of them 2026-10-03 13:29:53 +02:00
README tabs, a sash, kinds and a title from forms 2026-11-01 13:36:23 +01:00
VERSION toolkit: parts, their layout and a run loop, as a library of their own; the launcher built of them 2026-10-03 13:29:53 +02:00

toolkit: what a program builds its window out of -- parts, laid out,
drawn in the look's colours, given the keys and the pointer by a run loop
-- as a library of its own beside canvas, look and screen.

  lib/toolkit/part.talc   (toolkit part)   what a part is: a rectangle that
                          says what room it wants, draws itself, takes an
                          event, takes the focus; columns and rows of
                          parts; laying out, drawing, finding the part
                          under a point
  lib/toolkit/parts.talc  (toolkit parts)  the parts there are: a label, a
                          field to type into (a cursor, a selection), a
                          list to pick from (it scrolls, a bar inside its
                          edge), a button, a scroll bar drawn as the theme
                          says, a view that scrolls what it holds, a text
                          area of many lines (places, a selection, undo,
                          line numbers, two bars, roles from a colourer),
                          a sash dragged between two parts, tabs over
                          pages (a deck with a row of tabs), a frame
                          around a part that groups it, a table (the
                          list grown columns and a heading row, a
                          cell of which may be a meter), a meter (a
                          bar of how much is in use, with a text)
  lib/toolkit/form.talc   (toolkit form)   a window's content as data: a
                          form naming the parts and how they nest, built
                          in one call, the parts found by name
  lib/toolkit/run.talc    (toolkit run)    a program's windows and its run
                          loop: the screen, the theme and the fonts found
                          from the environment, a window around a part, a
                          popup, events to the parts and the rest to the
                          program, other handles watched, drawing after
                          each frame what changed
  lib/toolkit/colourers.talc  (toolkit colourers)  a text area's colourers by
                          name: talc
  lib/toolkit.talc        (toolkit)        the libraries here under one import
  tools/launcher.talc     the first program built of parts: a field in a
                          translucent window, the programs that match in
                          a list in a popup under it (the screen's super+space)
  tools/preview.talc      the parts looked at without a screen: a column of
                          them drawn into a picture with a theme
  tools/typing.talc       the text area typed at without a screen: a drive
                          of keys and presses played at one, its lines and
                          places printed after each step
  examples/               the picture preview is expected to draw with the
                          built-in theme and with each of ../look's example
                          themes; the drive typing plays and the transcript
                          expected of it
  scripts/build.sh        builds tools/ into bin/, against lib/ here,
                          ../canvas/lib, ../look/lib and the toolchain's
  scripts/regress.sh      draws the parts with bin/preview, plays the drive
                          with bin/typing, and compares

Build:

  scripts/build.sh
  scripts/regress.sh

which need the installed toolchain (compiler on PATH), ../canvas built
(its fonts) and ../look (its example themes and its icons).
../screen/scripts/regress.sh and ../vm pick
the launcher up from bin/ here.

A PART is five things: a rectangle the layout gives it, what room it
would like, how it draws itself there, what it does with a key or a
press, and whether it takes the keyboard. (toolkit part) is that shape
and nothing else; a label needs two of the five. Everything the toolkit
does over parts -- laying them out in columns and rows, drawing after
each frame only the parts that changed, moving the focus on tab,
finding the part under the pointer -- is written once and never knows
which parts exist. The parts there are, in (toolkit parts), are the
ones the launcher and the chooser needed, a button and a scroll bar,
and the text area the editor wanted first:
more arrive with the programs that first want them, and cost nothing
written before if the shape is right. There is no ambition to a
complete set: a program whose control is not here writes it, as ../pad
does, and it is in every way equal to these.

WRITING A PART is one call, (toolkit-part-create kind state wanted
draw! event! focusable?), the same call the parts here are made with:
what room it wants as (width . height), how it draws within its
rectangle, what it does with an event, and whether it takes the focus.
It says (toolkit-part-changed!) when it needs drawing again, and it
paints the whole of its rectangle when drawn, so that it can be drawn
again alone; when it painted less it answers the rectangles it did, and
only those are shown (../terminal's grid answers its rows). It may ask
for a margin around itself and a weight for its share of what a column
leaves, or say that it wants no size at all (toolkit-part-stretch!),
which a column or a row put inside another needs, since it would
otherwise want the sum of its parts' sizes. A column or a row pads
itself with the theme's padding only when it is the window's own: one
inside another pads nothing, so that its parts line up with their
neighbours and the room around everything is the outer one's. The
room between parts is the theme's `gap` metric. That is the whole contract, and (toolkit part) is its one page;
a program's own part, or a library of them from anyone, is made the
same way and is in no way second to these. ../pad is one written that
way, outside this repository, to prove it.

THE EVENTS a part sees, each with the screen's time last, so that two
presses can be told to be one: (key code modifiers text time) when it
has the focus; (press x y button time) when the pointer presses on it,
and from then until the release it HOLDS the pointer -- every (motion
x y time) and the (release x y button time) come to it wherever the
pointer has gone, which is what a drag is; otherwise (motion x y time)
while the pointer is over it, (enter x y time) and (leave time) as it
comes and goes, and (scroll x y axis amount time) from the wheel.
(wake) comes when the part asked for one with (toolkit-wake! app part
milliseconds), between two events, for a blink or a step of an
animation; (popup-done time) comes to a part that owns a popup
(toolkit-popup-owner!) when the person pressed elsewhere; (clipboard
text) comes to the part that asked for the screen's clipboard with
(toolkit-clipboard-ask! app part), text #f when nothing was ever
copied, and (toolkit-clipboard-set! app text) is how a part copies --
one text the screen holds for every program, outliving the one that
set it. The field copies its selection on ctrl+c and pastes on ctrl+v;
a part with no app of its own uses (toolkit-app-current). A part that
needs its window asks (toolkit-window-of app part). Pointer events go
to the innermost part under the pointer and, when it does not use
them, outward to the parts holding it, so a view sees the wheel its
lines leave alone. A part that holds parts of its own says so
(toolkit-part-children!) and lays them out in a function of its own
(toolkit-part-layout!); they are drawn within its rectangle.

KEYS BY NAME. A part asks (toolkit-key? event "escape") rather than
matching a number, and a program reading the screen's own key forms
asks (toolkit-key-code "return"); the names are (canvas keys)', shared
with the screen's bindings file, so there is one list.

A PROGRAM makes its parts, puts them in a window, and runs:

  (define app (must (toolkit-connect)))
  (define field (toolkit-field-create "a program to start" font on-change on-enter))
  (define window (must (toolkit-window-create-translucent app "Run" 520 height field)))
  (toolkit-run! app handler)

toolkit-connect finds the screen, the fonts and the theme in the
environment the screen gave the program (SCREEN, SCREEN_FONTS,
SCREEN_THEME). The run loop gives each key to the focused part and each
press to the part under the pointer, and hands the program whatever the
parts did not use -- escape, up and down for a list it keeps in a
popup, popup-done, close-requested -- through its handler. A part that
changed says so (toolkit-part-changed!) and its window is drawn at the
next frame; a configure from the screen is taken at the size proposed,
the root laid out again and everything drawn.

OTHER SOURCES. The loop is one isolate's, as (canvas window) asks, and
each thing it waits on has a watcher isolate of its own that asks the
loop to read it: the screen's socket, and any handle the program adds
with (toolkit-watch! app handle ready!), whose ready! is then called in
the program's own isolate, between events, to read what came and change
its parts -- ../terminal reads its shell there. An isolate the program
starts itself reaches the loop with (toolkit-tell! app form), which the
handler sees as (told form). So the parts, the windows and the handler
are never touched from two isolates, and a program never writes a loop.

CONTENT AS DATA. A program may say what its window holds as a form --

  (toolkit-build '(column (label "The look") (list looks) (row (button "Close" close))) act!)

-- and get the parts back, the named ones by name (toolkit-named), with
one function told what happened to which: (act! 'pick 'looks part),
(act! 'press 'close part). ../desktop's theme chooser is that one line
and a list filled; a program whose content is not so regular makes its
parts by hand, as the launcher does, and both are the same parts.

A TEXT FROM A FORM. (text name talc) is a text area whose every edit,
typed, undone, redone or put there by the program, is told as (edit
name version from-line from-byte to-line to-byte "text"). A program
keeps its own copy moved by those alone, and saves and searches that,
so the text can be edited where it is shown without the program
drawing it. The colouring is a name, lib/toolkit/colourers.talc's
(toolkit colourers), so it crosses a pipe where a function cannot.
examples/text-drive.talc plays one under bin/forms, which keeps such a
copy and checks it against the part at each step.

TABS, A SASH, KINDS. (tabs name page ...) is a deck with a row of tabs
over a framed page, drawn as a 1995 tab control in a look that raises
its faces and outlined in a flat one; a press shows the page there and
then tells (pick name index), a middle press tells (close name index)
and leaves it to the program, and (tab name page "text" kind) sets a
tab's text and icon. (sash) between two parts of a row or column is
dragged to share the room between them. A list's item may be ("text"
kind), kind folder, talc, executable or file, its icon taken from the
pack the theme names wherever there is a screen. (title "text") is the
window's title. examples/tabs-drive.talc is the editor's shape built
this way, drawn in the plain and the classic look.

THE LOOK CHANGING. When the screen's look changes -- a program asked it
to with toolkit-theme!, the protocol's (theme "path") -- the screen
tells every program, and the run loop reads the theme again, lays every
window out again and draws it whole; the handler then sees (theme path)
for what of the program's depends on the theme's font. So one pick in
the chooser changes every window on the screen at once, frames and
contents. The parts draw out of the theme's elements, so a theme says
how they look without the parts knowing: a button's face flat or
raised, a field's well underlined or sunken, a scroll bar thin (a line
for a thumb, nothing else, what the plain theme has) or with arrows at
its ends, a face for a thumb and a plain or checkered track (what the
classic theme has), and a list's bar sits inside its edge in the same
style.

A PROGRAM OF ITS OWN lives in a repository beside this one and is built
the way ../terminal is: one compiler line naming ../canvas/lib,
../look/lib, ../toolkit/lib and the toolchain's libraries, nothing
installed, nothing here changed. It is started by the screen (its -run,
a binding, or the launcher, from PATH or beside the screen) and finds
the screen, the fonts and the theme in the environment. Its parts are
made with toolkit-part-create like the ones here; what it needs of the
toolkit that is not here arrives here when it is first wanted. Its
regress runs the headless screen with the program in it and compares
pictures, as ../terminal/scripts/regress.sh does. The launcher stays
here as the sample program, the way ../look keeps its preview.

THE LOOK is the theme's, in three layers, all data in ../look's theme
file under (widgets ...): ROLES, colours by what they mean (paper,
page, ink, faint, accent, pick, pick-ink, edge, raised); METRICS, sizes
(bevel, and the theme's padding); ELEMENTS, how a face, a well or a
list's ground is drawn as a style over the two (flat, raised or sunken;
flat, outlined, underlined or sunken), and whether a bevel is one ring
or 1995's two. The parts draw out of (look elements) -- a well, a list
ground, a face, a pick, an edge -- and text in role colours, so the
same field is an orange underline in the plain theme and a sunken grey
well in the classic one, and the toolkit's code says neither. There is
no style of the toolkit's own. A control written anywhere else draws
the same way and takes on any theme; one that wants a colour of its own
asks for it by name with a role to fall back on (theme-widget-color-or),
so a theme that never heard of it still styles it, and a theme that
cares adds a line. A control that wants to look like nothing else draws
with the canvas directly; nothing forces it through the theme.

WITHOUT A SCREEN. Parts are laid out and drawn onto any (canvas)
surface, so tools/preview.talc draws a column of them into a picture
with no screen, which is how a part or a theme's colours are looked at
while being worked on and how this is regressed: one picture per theme,
compared byte for byte, as ../look does for frames.

THE TEXT AREA is the field grown to many lines: the cursor and the
anchor are places, (line . byte), the one edit is a splice (the text
between two places replaced), every splice is kept to undo and typed
runs join, a colourer the program sets answers each line's runs of
roles and the cached answers carry a state from line to line, tabs are
shown to stops of eight columns, and the rows are drawn by key so that
a keystroke draws one row. tools/typing.talc plays a drive of keys and
presses at one with no screen and prints its lines and places, which
is how the editing is regressed; the pictures show how it looks. The
cursor does not blink: a wake would make every picture depend on
time, and a program that wants a blink can ask for one. A program that
keeps several texts in one area takes each one's lines, places, view
and undo as one value (toolkit-text-save) and puts it back
(toolkit-text-load!), as ../editor does for its tabs.

ICONS. An item of a list is a text, or (text . icon) with an icon one
of ../look's -- a (canvas) surface with alpha -- drawn before the
text: every line of that list is as tall as the tallest icon needs,
every text starts past the widest, and a list of texts alone draws as
it always did. A program gets them from the app: (toolkit-app-icons
app) is the pack the theme names ((look icons)), opened from the
directory the screen gave in SCREEN_ICONS, and (toolkit-app-icon app
'folder) one of them, or #f; a program started without an icons
directory gets #f for every name and shows none. When the look changes
to a theme that names another pack the app opens that one, and the
handler's (theme path) is where a program takes its icons again.
tools/preview.talc draws the list with icons when told -icons, which
is how they are regressed in every theme.

A SASH (toolkit-sash-create) sits between two parts of a row or a
column, six pixels across, drawn as the paper and taking the gap's
place on both sides -- the gap is the handle -- and a drag sets the two parts' weights to their sizes in pixels and
lays the window out again (toolkit-relayout!, which a program also
calls after it changed what a part holds). The window's title can be
changed as well (toolkit-window-title!). Tab alone moves the focus;
tab with a modifier is left to the program.

A TABLE (toolkit-table-create columns font on-pick) is the list grown
columns: the columns as (title . align) -- left, right, or fill, which
is left and takes the width the other columns leave -- and the rows set
with toolkit-table-set!, each a list of cells, a cell a text, (text
. icon) as a list's item is, or (meter text permille role), a bar
filled that many thousandths of the cell in the role's colour with the
text over it. A heading row of the titles sits on the
paper above the rows with an edge line under it; each column is as
wide as its title or its widest cell plus the padding, measured when
the rows are set and again when the font changes, and every text is
clipped to its column, so a long name stops at the next column's edge.
The choice, the keys (up, down, page up, page down, return), the wheel,
a press on a row and the bar inside the right edge are the list's; a
press on a heading calls the function toolkit-table-on-heading! set,
with the column's index, which is how a program sorts; a press on a
row is offered first to the function toolkit-table-on-cell! set, with
the row, the column and the pixels from the column's left edge, and
when that answers #t the press is taken and nothing is chosen, which
is how ../processes folds a subtree on its marker. A meter cell's text
is drawn in ink on every row, the chosen one too: the well is the
page colour whatever the row is, and the pick's ink may not read on
it. ../processes is the first program of one, and tools/preview.talc
draws one in every theme's picture.

A METER (toolkit-meter-create font) is that bar as a part of its own:
a well outlined in the edge colour, filled from the left so many
thousandths in a role's colour (the accent for a role the theme
lacks), a text over it; toolkit-meter-set! gives it the text, the fill
and the role, and a press on it calls the function
toolkit-meter-on-press! set, so that a bar can open what it
summarises. It wants no width, so in a row it takes what the labels
leave: ../processes' totals row is two of them.

A WAKE FOR THE PROGRAM: (toolkit-wake! app #f milliseconds) gives the
handler (wake), `w` #f, after that long -- a program whose parts are
all the toolkit's has a clock without a part made for it, as
(toolkit-clipboard-ask! app #f) answers the handler. ../processes reads
the machine on each.

A FRAME (toolkit-frame-create) goes around one part to group it: a
raised bevel in a look that raises its faces, an outline otherwise. A
list, a text area or a terminal inside one is drawn BARE
(toolkit-part-bare!): its ground flat, no bevel or outline of its own,
the frame being its edge, so that a 1995 look gets one bevel where
two would have met.

NOT HERE YET: a view that scrolls sideways (the text area scrolls its
own lines sideways, the scrolled view does not); a resize that keeps
the window's pixels and paints only what it exposed (../terminal did
this on its own once; on the loop a resize draws everything).