keal-view / The reference

The reference

Everything public, in one place. The guide explains how the pieces fit; this says what each one is.

All sizes are in logical points. All colours are Ints holding 0xAARRGGBB — build them with rgb, rgba, web or the theme.


Running

runApp(title, w, h, build)open a window and run until it is closed
App(title, w, h)the same, when you want to set more than build
run(app)run one
snapshot(app, w, h, scale, path)draw one frame to a BMP, no window

An App has build, overlay, onKey, onStart and onStop. Every program also understands --snapshot <file> [scale] [w h] and --window-id <file> on its command line without being told to.

State

state(initial)a Cell<T> holding a value
cell.get() / cell.set(v)read it, write it
cell.update({ v -> … })read and write in one go
invalidate()something changed that is not in a cell
revision()how many times anything has been written

Containers

column(kids)stacked downwards
row(kids)stacked across
stack(kids)one on top of another, last nearest the viewer
box(kid)one child, somewhere to hang a background or a padding
card(kids)a surface with a border, a corner and room to breathe
raisedCard(kids)the same, with a shadow
panel(title, kids)a titled surface with a heading strip
scroll(kids)scrolls vertically when the content is taller than the box
tabView(titles, index, onChange, content)a tab strip with its content under it

Space

spacer()takes everything left over — two of them centre what is between
gapV(h) / gapH(w)a fixed gap
divider()a hairline across the container
nothing()no size, no ink, no answer to the pointer

Text

label(s)one line
caption(s)smaller and quieter
heading(s)a section heading
title(s)a page title
paragraph(s)wraps to the width it is given
badge(s)a small pill — a count, a status, a tag
link(s, onTap)a line of text that acts on a click
banner(level, s)something to say: 0 plain, 1 good, 2 warning, 3 failure

Controls

button(s, onTap)with `.kindOf(primary \plain \quiet \danger)`
iconButton(name, onTap)a button that is only an icon
menuButton(name, choices, onPick)an icon button that opens a menu
checkbox(s, on, changed)a box that is ticked or not
radio(s, on, chose)one of several, shown as a dot
toggle(s, on, changed)the same choice as a switch
segmented(choices, index, onChange)a row of choices, one pressed
select(choices, index, onChange)a dropdown, opening a menu of them
slider(value, lo, hi, changed)a value chosen by dragging
stepper(value, lo, hi, step, changed)a number with a minus and a plus
progress(value)a bar showing a fraction, taking no input
field(value, hint, changed)a line of text the user types
secretField(value, hint, changed)the same, showing dots
editor(value, hint, rows, changed)text over several lines, wrapped
tabs(titles, selected, chose)a strip of tabs on its own
icon(name)a shape drawn from strokes

Every control reports what it would become, not what it is: a checkbox hands you true when it is off. Nothing flips itself; the tree is rebuilt from your state.

field and editor

They are two widgets rather than one with a height, because four keys mean different things:

fieldeditor
Returnaccepts — calls .tappable's handlerputs in a newline
Home, Endthe ends of the textthe ends of the row the caret is on
Up, Downnothingthe row above and below, keeping the column
a paste with newlines in itflattened to spaceskept

Everything else is shared, and shared in the source and not just in behaviour: the selection, what a keystroke replaces, what Backspace takes, what a double click takes and what a triple click takes are all written once and work on character offsets. rows is how tall an editor is in lines; past that it scrolls, with the wheel or by the caret leaving the box.

An editor wraps to the width it is given. A word longer than the column is cut rather than left to overflow, and a click on either side of that cut puts the caret where it was clicked — which is not free, and is the one place in the text machinery where an offset alone does not say which row it is on.

.wrappingLines(false) turns that off: the only breaks are the ones the author typed, and the editor scrolls sideways to follow the caret along a long line — by the caret, or by a trackpad, or by a wheel with Shift held. Use it for code. A line of code that folds reads as two lines: an expression breaks in the middle of itself, and anything counting lines beside the text counts the wrong ones. Leave it on for prose, which is what it is for.

Not to be confused with a label's wrapping(), which is the opposite default for a different question — a label cuts itself with an ellipsis unless told to wrap.

A margin

.gutter() gives an editor line numbers down its left side, and .gutter([LineMark(12, theme().danger), …]) marks lines as well — an error, a warning, a breakpoint.

editor(source, "", 30, { s -> src.set(s) })
    .wrappingLines(false)
    .gutter(problems.get().map({ p -> LineMark(p.line, colourFor(p.kind)) }))

The numbers are the lines the author typed: a folded line carries one number and its continuation carries none, because a number in a margin has to mean what a compiler's :12: means. LineMark.line counts from one, as the numbers beside it do, and a mark on a line the text no longer has is dropped rather than drawn — the check that produced it and the buffer it is drawn beside are two different moments.

The margin follows the editor down and not sideways. Numbers that walked off the left edge on the first long line would take the marks with them, and the marks are what you look at while chasing a long line. Its width comes from how many digits the last number needs, so a forty-line file spends two columns on it and a twelve-thousand-line file still fits.

A tab moves the pen four spaces and draws nothing; every other C0 control and DEL take no room and draw nothing at all. Four spaces is an advance and not a tab stop: a stop's width depends on the column it starts at, which would make the width of a string depend on where the string begins, and the caret — placed by measuring — would come away from the letters, which are placed by drawing.

Moving the caret from outside

Go to a line, jump to a search result, comment out the line the caret is on, move to the matching bracket: none of those can be said by handing a widget a new string — they are about where somebody is in it. So an application can hold the caret:

val cur = caretAt(0)

editor(src.get(), "", 30, { s -> src.set(s) }).caret(cur)

// anywhere, from any handler
cur.at = offsetOfLine(12)
cur.anchor = cur.at
invalidate()

at and anchor are character offsets, the same ones everything else in the text machinery works on; they are equal when nothing is selected. Read them whenever you like — the widget writes them back every time the user moves the caret. Write them and the caret goes there, clamped to the text rather than indexed past it, because the offsets an application holds come from a compiler or a search and the buffer may be shorter by the time they arrive.

Two-way needs a rule for who wins, and it is this: a difference from what the widget last wrote means the application has spoken, and the widget adopts it. A jump also throws away what the caret remembers between keystrokes — the column a run of Up and Down is aiming for — because an offset somebody named says nothing about a column somebody was aiming at.

And a jump lands with a few lines under it, where a keystroke moves the page as little as it can. They are different questions: a caret that moved by one line is where its reader already was, and scrolling further would drag the page under their eyes; a caret the application placed arrives somewhere nobody was looking. Nothing has to ask for this — an offset that came from outside is a jump, and the widget knows which of the two it received.

cur.screen is where the caret is on the screen, in window points, as the last frame drew it — the point to anchor something to it: a completion list under it, a type beside it. It follows the sideways scroll, and it is written whether or not the widget has the keyboard, because something anchored to a caret still needs to know where it would be.

It is the last frame's, exactly. A keystroke rebuilds the tree inside the event batch and paints after it, so a handler reading screen during the keystroke that moved the caret sees where the caret was before it — one character out. Open what you are anchoring on the frame after the move.

Without a caret, the framework keeps the caret in the state it retains by identity, which is right for every widget that has not asked. Works on a field as it does on an editor.

Putting the keyboard somewhere

⌘F should land in a search box, and a search box cannot be typed into unless somebody can say so. The framework keeps the focus for every widget that has not asked — Tab walks it, a click moves it, Escape gives it back — and an application can hold it instead:

val searching = focusHeld()

field(term.get(), "search", { s -> term.set(s) }).focus(searching)

// in a ⌘F handler
searching.has = true
invalidate()

Read has whenever you like: the widget writes it back when a click or a Tab moves the keyboard elsewhere, so has the user left my box is a question you can answer. Setting it to false gives the keyboard to nothing, which is what Escape does; to move it from one widget to another, ask on the other one — the one losing it is told.

Any widget that can take the keyboard takes a focus: a field, an editor, a button, a checkbox, a radio, a toggle.

Text in more than one colour

.coloured([InkRun(start, end, ink), …]) paints stretches of a field's or an editor's text in colours of their own. [start, end) are offsets into the text; anything no run covers keeps the ordinary ink. Runs must not overlap and must already be in order — nothing sorts them, because whatever made them knew the order and re-deriving it every frame is work done for nobody.

editor(source, "", 30, { s -> src.set(s) })
    .coloured(tokens.get().map({ t -> InkRun(t.from, t.to, colourOf(t.kind)) }))

An InkRun holds two offsets and a colour and nothing else, so a lexer, a spell checker and a diff all use the same field and none of them is named in it. keal-view has no idea what a keyword is and should not acquire one.

Honoured by field and editor and by nothing else: a label cuts itself with an ellipsis and aligns what is left, so offsets into it stop meaning what they said, and secretField ignores runs outright — its dots stand one for one with the characters, so colouring them would draw the shape of the password.

Icon names

check close plus minus chevron-up chevron-down chevron-left chevron-right menu dot search play pause warning folder file trash gear backspace

An unknown name draws a hollow square, so a typo is visible rather than silent.

Drawing your own

custom(draw)a view that paints itself
underlay(draw)the same, taking up no room — a background inside a stack

draw is given a Paint:

p.canvasa Canvas, already clipped to this view
p.areathis view's rectangle, in points
p.theme p.fontswhat everything else is drawn with
p.hot p.pressedwhether the pointer is over it, and over it with a button down. "Over" does not require the view to take clicks — but it is false wherever something that does has the pointer instead, so while a popup is open nothing beneath it is under the pointer
p.text(s, r, align, style, col)one line in a box, cut with an ellipsis if it must be
p.line(s, x, baseline, style, col)one line on a baseline; answers where the pen ended
p.width(s, st) p.lineHeight(st) p.ascent(st)measuring

Canvas: clear fillRect fillRound strokeRound fillCircle line gradient shadow mask plot run push pop clip bounds.

Modifiers

Each answers the same view, so they chain. A newline after ) ends a statement in Keal, so keep a chain on one line or break it into statements.

Size and space

.w(v) .h(v) .sized(w, h)a fixed size
.least(w, h)a floor; the content may still ask for more
.grows()a share of the room, starting from nothing (CSS flex: 1)
.growsBy(f)a named share, on the same terms
.fills()what the content needs and a share of what is left
.shares()on a container: children divide the room in exact proportion
.pads(insets) .padAll(v) .padXY(h, v)space inside
.margin(v)space outside
.gaps(v)space between children
.aligned(main, cross)how children sit along and across the axis
.at(x, y)inside a stack: sit here, at your own size

Alignment values: mainStart mainCenter mainEnd mainBetween, and crossStart crossCenter crossEnd crossStretch (stretch is the default).

Look

.bg(col)a background
.outline(col, width)a border
.rounded(r)corners
.raised(blur)a shadow this many points across
.color(col)the text colour
.font(style) .fontSize(v) .bold() .mono()the text
.centered() .trailing()where the text sits in its box
.wrapping()wrap instead of cutting with an ellipsis
.kindOf(v)a button's family: plain primary quiet danger
.withIcon(name)an icon before the label

Behaviour

.off() / .offWhen(c)grey it out and stop it responding
.shownWhen(c)draw it, or do not
.keyed(s)give it a name, so its own state follows it and not its position — required for anything in a list that can be reordered, inserted into or deleted from
.tip(s)a note beside the pointer while it rests here
.pointer(c)the pointer shape: cursorArrow cursorHand cursorText cursorResizeH cursorResizeV
.tappable(f)take a click without being a control
.dragged(grab, move, drop)answer the pointer with a drag; a press that never moved is a click

Over the interface

openMenu(x, y, choices, selected, onPick)a list of choices at a point
openIconMenu(x, y, choices, icons, selected, onPick)the same, with icons
openDialog(title, content)a modal box over a dimmed interface
openSheet(title, content)the same, dismissed by a click outside or Escape
closePopup() popupOpen()put it away, ask whether anything is showing
.tip(s)a tooltip, as a modifier rather than a call

Nothing needs installing: the run loop draws this layer above the application's own overlay, every frame.

Theme

theme()the one in force — everything reads it
useTheme(t)change it
flipTheme()swap dark and light
darkTheme() lightTheme()the two built in

A Theme holds bg surface surfaceHi surfaceDown border borderStrong · textPrimary textSecondary textMuted textOnAccent · accent accentHover accentDown accentSoft · success warning danger shadow · radiusSm radiusMd radiusLg radiusPill · unit hairline · fontSm fontBase fontLg fontTitle · control strip · dark.

And t.gap(n) for n units of the spacing grid, t.body() t.small() t.heading() t.title() for the text styles.

Colour

rgb(r, g, b) rgba(r, g, b, a) gray(v)channels, 0–255
web("#3b82f6")three, six or eight hex digits, with or without the #
redOf greenOf blueOf alphaOfback out again
withAlpha(c, a) fade(c, f)change the opacity
mix(a, b, t) lighten(c, f) darken(c, f)between colours
luma(c) contrasting(c)how bright it reads; black or white on top of it
over(dst, src, cov)composite — the per-pixel operation

An unreadable web string answers opaque magenta, which is easier to see on screen than it is to miss in a log.

Geometry

Rect(x, y, w, h) Pt(x, y) Size(w, h) Insets(l, t, r, b), and all(v) axes(h, v).

right bottom centerX centerY center sizeOf isEmptyreading one
holds(r, x, y)is the point inside? Left and top edges belong to it, right and bottom do not
inset shrink grow offsetmoving one
intersect union overlaps centeredtwo of them
cut(r, edge, amount) / cutRest(…)take a strip off an edge, or keep the rest
clampF clampI lerpF floorI ceilI roundIarithmetic

Docking

Dock(node) dockOf(ids)an arrangement
leaf(ids)a group of tabs
beside(a, b, f) above(a, b, f)a split, a taking fraction f
panelOf(id, title, body)a dockable panel
dock.add(panel) dock.panel(id) dock.holder(id)registering and finding
dock.reveal(id)bring a panel to the front of its group
dock.detach(id)take it out; empty groups and their splits are pruned
dock.dropInto(nid, zone, id)put it back: zoneLeft zoneRight zoneTop zoneBottom zoneCentre
dockView(dock)the arrangement, as views
dockOverlay(dock)the highlight while a panel is being dragged — give it to app.overlay

Events

An Event has kind x y button key mods clicks dx dy text, and e.cmd() (Command on macOS, Control elsewhere), e.shift(), e.alt(), e.has(mod).

Kinds: evNone evClose evResize evMove evDown evUp evScroll evKeyDown evKeyUp evText evFocus evBlur evExpose.

Named keys: keyLeft keyRight keyUp keyDown keyEnter keyTab keyBackspace keyDelete keyEscape keyHome keyEnd keyPageUp keyPageDown keySpace. Printable characters arrive as evText instead.