This chapter is the visual and interaction specification that every KDOS surface follows: the panel, the Start menu, the settings, the resource monitor, the installer, the lock screen and every popup. It describes the cell grid everything is drawn on, the window frame, the shared controls, colour, the keys and pointer gestures every surface answers, touch, the glyph sets that decide what can be drawn on a text console, pictures, and the frame the compositor draws round other programs’ windows. It is written for people building or reviewing a KDOS surface, and for anyone who wants to know why the desktop looks the way it does.
A surface here means any program, or any front end of a program, that draws its own screen with
the KDOS toolkit; kdos-shell alone provides more than fifty of them. Read
The architecture overview first for where these programs sit in the running
system. If you are about to write a surface, read
Every surface is a grid of character cells and
The checklist first, then
Writing desktop software for the code.
The rules are not a matter of taste. A surface that ignores them looks like a different program placed inside the desktop.
A KDOS surface has no scene graph, no widget tree with its own layout engine and no vector drawing.
It is a two-dimensional buffer of cells (KtuiCell in src/libs/libktui/ktui.h). Each cell
holds one character, a foreground colour slot, a background colour slot and a set of attributes:
bold, reverse, underline (with a style: single, double, curly, dotted or dashed), italic,
strikethrough and overline. Drawing means writing cells. Presenting means comparing the buffer with
what was last shown and sending only the cells that changed.
The same buffer is presented to three kinds of destination, and nothing above that layer knows
which one it is talking to. Each destination is reached through a backend, a KtuiBackend
table of functions:
| Destination | Backend | Where it lives |
|---|---|---|
A terminal, including the Linux text console tty1 |
the built-in terminal backend | libktui (ktui_draw.c) |
| A Wayland window under the compositor | kwl_backend |
libkwl |
An offscreen text dump (--dump, which prints one frame as text; a committed one is a golden) |
the offscreen buffer | libktui (ktui_offscreen_init) |
On tty1 the terminal backend installs the palette into the console with PIO_CMAP and reads the
mouse from /dev/input/event* itself, because the Linux console reports no mouse events of its own.
libkdisp sits one level above the backends and decides which display server a surface talks to;
it names no implementation itself, and the one that ships is kwl_impl in libkwl. That is why
the resource monitor looks the same on tty1, in a window, and in a committed test file.
libktui needs nothing beyond the C library and two of KDOS’s own libraries that need nothing
either, libkbase and libkcolor. The installer compiles all three into itself in the bootstrap
phase (10_bootstrap, step 130_kinstall.sh), with the cross compiler, against nothing but the
C library and the kernel headers; a library that needed a real -l would move the installer out
of that phase. That constraint shapes several rules below: anything that needs pixels, fonts or
a display connection lives in another library.
The consequence to keep in mind is that a control is one row of text tall. Where that is wrong, the answer is a picture drawn into whole cells, not a second renderer. See Pictures are an enhancement layer.
Three things on the screen are not cells:
Terminus (TTF) at 24 points (set in
fs/etc/skel/.config/kdos-comp/rc.xml), which is 32 pixels at 96 dpi and so one cell tall. The
cell grid itself is drawn from the bitmap Terminus face at every size it has a bitmap for, and
from the same TTF conversion, in a cell exactly the asked size, at the sizes it has none (see
libkcell); Pango cannot render bitmap fonts,
which is why the compositor asks for the TTF conversion by its own family name. See
The compositor’s decoration is part of the set.No KDOS surface is drawn by another toolkit. The input-method candidate window shows how far that
goes. An input-method engine normally draws its own candidate list with its own renderer, which on
this desktop would be a rounded, antialiased panel over a text-mode screen. kdos-ime draws it
instead, with the same chrome and colour slots as everything else, by speaking kimpanel, the
input-method framework’s generic D-Bus panel protocol. It is not an input method and knows nothing
about any language: the engine decides the candidates and kdos-ime draws them.
╔═[ Resources ]═════════════════════╗
║ ║
║ body starts at column 1 ║
║ ║
╚═══════════════════════════════════╝
ktui_draw_box draws it; its last argument chooses the double-line set or the single one. The title
is bracketed rather than padded with spaces, because two blanks either side of a title make the rule
look broken where it stops, and the eye reads a missing segment rather than a label. The box adds
the brackets and the padding itself and trims whatever spaces the caller passed, so " Settings "
and "Settings" draw the same. A title is drawn only when the box is more than six columns wide,
and is clipped to eight columns less than the box.
kch_header) fills from column 1 to the second-to-last column. A surface that
draws a header with no box round it therefore shows an empty one-column margin, which is the sign
that a box was forgotten.kdisp_decorated() and, when the compositor is
drawing a frame, skips its own and keeps only the background (sh_frame in kdos-shell). Two
nested frames on screen mean a program drew chrome the compositor had already drawn for it.Chrome is the part of a surface that is not its content. It is drawn by libkchrome and by
nothing else. A second implementation of a control is not updated when the first changes, and the
two diverge.
| Primitive | Draws |
|---|---|
kch_header |
The two-row accent band across the top, with an optional icon, title and subtitle, and a rule under it; returns the first body row |
kch_group |
A heading inside the body, followed by a rule to the right margin |
kch_buttons |
The verb bar: a row of buttons (at most eight), dropped from the right when narrow |
kch_button_at, kch_hover |
Which button the last frame drew under a cell, and where the pointer is, so a button lights under it |
kch_list_wheel, kch_list_clamp |
The scrolling rule for a list |
kch_scrollbar and kch_scrollbar_press / _drag / _release / _grabbed |
A scrollbar that can be dragged; up to four per surface |
kch_tile_* |
A block of cells drawn as pixels |
kch_plot, kch_plot_draw |
An area chart, or a mirrored pair on one axis, drawn as pixels in a tile |
kch_tone, kch_tone_alpha, kch_popup_alpha, kch_slot_rgb |
Shading and body opacity derived from the palette |
kch_px_* |
The plates, rules and rounded ends a backend with pixels records under the cells |
The controls come from libktui, and there is one of each. A surface that draws its own is a
second answer to a question this desktop has already settled.
| Control | What it is |
|---|---|
ktui_button, ktui_check, ktui_radio |
A verb, a flag, one of a set |
ktui_slider, ktui_slider_* |
A number on a track: press, drag, wheel, or an end cap for one step |
ktui_dropdown_* |
A choice, as a list that opens under its row |
ktui_input, ktui_field_* |
A line of text with a caret |
ktui_textarea_* |
Several lines of text with a caret |
ktui_list, ktui_table |
Rows, and rows with columns; a column may sort from its title, align right, or drag wider |
ktui_sel_slots, ktui_sel_row, ktui_sel_dim |
What a selected row looks like, for every surface that has one |
ktui_rows_* |
Which row the pointer is on, for a surface that draws its own rows |
ktui_table_event |
The same for a table: wheel, press, pick, Back, and a sort or a drag in its header |
ktui_tabs_* |
A strip of pages |
ktui_split |
Two panes and the divider between them, dragged or moved with the arrow keys |
ktui_fold_begin, ktui_fold_end |
A section heading that opens and shuts the section under it |
ktui_lay_* |
Not a control: the rows of a page, with one label column for every field on it |
KtuiMenu, ktui_modal_* |
A pane of verbs, and a question |
ktui_progress, ktui_gauge, ktui_sparkline, ktui_heat |
Progress and measurements, in four shapes |
Colours here are named by slot (KT_TEXT, KT_MID, KT_DIM, KT_ACCENT and four more; see
Colour), and the accent is the scheme colour chosen with kdos theme.
A button is a plate with a ░ shadow one column to the right and one row below it, drawn in
KT_DIM. The shadow is what stops a filled rectangle with a word in it from reading as a selected
row. A focused button also carries ► and ◄ at its two ends, so that focus is shown by a shape as
well as a colour; a disabled button has no shadow, because it cannot be pressed.
A slider is drawn as ◄ ████■▒▒▒ ► 55. The track is ▒ in KT_DIM, the run up to the value is
█ in KT_MID, and the thumb is ■ in the accent while the slider is not focused (in KT_TEXT
when it is), so the accent marks the value and nothing else. On a selected row (a KT_DIM fill)
each role steps up one slot, to KT_MID for the track and KT_TEXT for the run, so the track does
not vanish into the fill. On a row filled with KT_ACCENT the thumb is KT_SURFACE, or KT_BG
when focused. The end caps are dropped below 14 columns (KT_SLIDER_MIN_W) and the number below
ten.
A divider is a │ (or a ─ between stacked panes) in KT_DIM on whatever the page put under it,
and in the accent while it has the focus. A fold’s heading is a section heading with a marker in
front of it: ► in the accent while shut, ▼ while open (> and v on the console), the title in
the accent and the section’s rule after it; focused, the row takes the selection fill and the title
KT_TEXT, as a check box does, so the marker is the state and the plate is the focus.
Most controls are a draw / key / hit trio with one frame-level call on top. That is what
lets a surface running its own event loop, such as kdos-display or kdos-audio, use the same
control as one written inside the immediate-mode frame (ktui_frame_begin()), in which a surface
redraws every control on every frame and reads input as it draws, and which kdos-settings,
kdos-prompt and the installer use. A control that existed only inside the frame would have to be
written a second time by every other surface. A control that takes the focus also records what it
is for a screen reader (ktui_announce); see Accessibility.
A backend with pixels can paint beneath the cell grid. While a surface draws, libkchrome records
a display list of plates (kch_px_plate), selected-row bars (kch_px_row, and kch_px_row_anim
for one that slides to a new row), rules, rounded rectangles and display text (kch_px_text); the
Wayland backend replays it under the cells in the pixels they are drawn in: the output’s scale
times the cell, or on a fractional scale the device pixels of a cell drawn at that size, so a plate
edge and a glyph edge cannot drift apart. The rule is
pixels for paint, cells for layout: the fallbacks, the hit maps and every --dump stay in cells,
so a surface is correct without the layer and only better with it.
The plates take their colours from a tone ladder (kch_tone): a rest tone, a hover tone and a
focused tone, each a kcol_mix() of two scheme colours and each laid down at its own alpha, so an
accent change retints them. The ladder exists because the palette’s dark end is compressed: the
surface and background slots measure between 1.00:1 and 1.20:1 against each other in every accent,
so a panel painted in its own background slot is the same colour as the desktop behind it. The
focused plate is solved rather than chosen: libkchrome walks the mix from 68% down and stops at
the first plate that both stands off the bar and carries its label at 7:1, falling back to the best
separating plate where no mix reaches 7:1. A bar’s body is laid at 80% opacity and a popup’s at 95%
(kch_popup_alpha), because a menu is read and a taskbar is glanced at. The plate radius is three
pixels (KCH_PLATE_RADIUS); a larger radius would sit badly beside window frames that are square
on purpose.
kch_px_popup() gives a popup that body in one call, with no bright edge line, because a popup
draws its own ╔═[ Title ]══╗ and a second border a few pixels above it reads as a fault.
kch_px_bare() is the same hand-off with no body at all, for a surface that is only separate cards,
such as a stack of notifications. kch_px_custom() is the hand-off for a surface that draws its
own body; the taskbar uses it for its adaptive opacity and its edge. kch_px_flat() hands over a
page that is one opaque slot, so an ordinary window gains the layer and looks exactly as it did;
kdos-res uses it.
All four go through one cache. The layer is rasterised once per change of what it depends on (the recorded list, the body’s alpha and edge, the palette, the size and the scale) and kept, so a commit that changes only cells, such as a clock tick, a meter or a caret, repaints just those cells: each is laid back on its own rectangle of the cached layer and redrawn, and only those cells are reported to the compositor. A plate that moves, appears or vanishes re-rasterises only its own rectangles, and repaints and reports only the cells it left and reached; a change of the body’s alpha or edge, of the palette, of the size or of the scale repaints the surface. A body at full opacity (the taskbar over a maximized window) also tells the compositor the surface is opaque, so it is copied rather than blended and whatever lies under it is skipped.
Display text is a heading or a figure taller than a row of text, drawn as one op. The rule is
that it is whole rows of the cell tall: text n rows high asks the face for n times the cell
height, so on the 16×32 Terminus cell two rows are the 32-pixel strike doubled, pixel for pixel, and
a size no strike draws exactly comes from the Terminus (TTF) twin (see
The cell’s size). The face is the cell font’s,
never a second typeface. The rectangle’s cells go blank on the slot the backdrop owns, so the text
shows through them; a band of another slot under the text is recorded as a flat rectangle first,
so the text sits on the band it replaces. Its colours are slots like everything else. Where there
is no pixel layer, kch_display_text() draws the same string as cells on the rectangle’s first
row, which is what tty1 and every --dump show. A figure that only repeats what the cells already
say, as kdos-res’s does, is drawn where the layer is live and not at all where it is not.
A button bar drops buttons rather than vanishing. Buttons are ordered most-useful-first and dropped from the right when the surface is too narrow, never half-drawn. A bar that disappeared entirely would leave only a row of key hints (see The hint row), which is what the buttons exist to replace.
Anything that draws over part of a row owns all of it. A button bar shares its row with the status
line, and the single column between two buttons belongs to neither, so a leftover character would
show through the gap. The bar clears its whole span first, and kch_buttons returns the leftmost
column it took so the status text can be clipped to what is left. ktui_sel_row follows the same
rule and clears the full row before it draws its marker.
Five keys mean the same thing on every surface, and the bottom row of the surface says what the other keys do at that moment.
| Key | What it does | Declared by |
|---|---|---|
F1 |
Opens this surface’s help page in kdos-doc |
KtuiKeys.doc, drawn first in the hint row |
F10 |
Opens the surface’s menu bar | KtuiMenu.has_bar |
Shift+F10 |
Opens the menu for the thing under the selection | KtuiKeys.ctx_at |
Alt+letter |
Opens a menu pane by its underlined letter | the & in a pane title |
Esc |
Steps back one level, and closes only from the top level | the Esc ladder, below |
A surface holds one KtuiKeys, calls ktui_keys() first in its event dispatch, and calls
ktui_hint_row() last in its draw. ktui_keys() takes every event, not only keys, because it
routes into the surface’s menu first and a menu also answers the pointer. It returns
KTUI_KEY_PASS for everything it does not own, so the surface’s own dispatch sees the rest
unchanged. Thirty-nine source files outside libktui and its self-test call it.
A key with nothing behind it is neither advertised nor answered. Help pages are text files in
/usr/share/kdos/doc/<name>.txt (in the tree, fs/usr/share/kdos/doc/, which holds nine pages and
an index). A surface with no page leaves doc as NULL, and F1 then passes the key on: a key
that opens an index reading no such document teaches that help is broken. testing/preflight.sh
refuses a keys.doc that names a page which does not ship. The same rule holds for F10 on a
surface with no menu bar, and for Shift+F10 on one with nothing selected.
Esc walks a ladder of declared layers, checked at the moment the key arrives and never cached. A
surface registers each raised state (an open dropdown, a dialog, a search field) once with
ktui_keys_layer(), outermost first and innermost last, up to six of them. Each press of Esc
closes the innermost layer that is up. Each layer is a question (“is this open?”) rather than a
stored flag, so a dialog dismissed by a click leaves nothing behind to swallow the next keystroke.
With no layer up, ktui_keys() returns KTUI_KEY_CLOSE and the surface closes itself; the toolkit
does not own a program’s lifetime. An open menu is always innermost, above the ladder.
ktui_esc_verb() names what Esc does at that moment (a layer’s verb, Cancel while a menu is
open, otherwise Close), so the hint row never reads Esc Close on a screen where Escape goes
back.
The hint row is built during the draw, by whatever has the focus. ktui_hint(key, verb) and
ktui_hint_if(cond, key, verb) add hints to a pool of at most twelve; ktui_hint_row() clears the
pool first, then draws what it held. Clearing first matters because kdos-shell is one binary with
many front ends, and a pool emptied only at flush time would carry one surface’s hints into the
next --dump in the same process. The key is drawn in the accent, the verb in KT_MID, and hints
are separated by a │ in KT_DIM. No row is drawn on a window shorter than eight rows. A fixed
string cannot follow the focus, and a row naming keys the focused control does not answer is worse
than no row, because the value of the line is that every key on it works.
Each hint is drawn whole or not at all: a fragment of one hint against the start of another is worse than an empty half-row. A status message is different. Where a surface’s status row beside its button bar shows either a message or the key hints, the message takes whatever room is left, because it describes what the person has just done.
The row drops its tail: a hint that does not fit ends the row, and every hint after it is lost
with it. Surfaces push Esc last, so on a narrow surface it is the first thing lost. If a surface is
too narrow for everything it answers, drop a hint deliberately rather than moving Esc earlier, or
make the surface wider.
A menu’s sel counts items, never drawn rows. A caller’s show callback can hide rows, and a
selection counted in drawn rows lands on a different item the moment one is hidden. The same
callback answers both the drawing and the hit test, from one walk, because two copies of a
visibility rule eventually disagree and a click then runs the row above the one under the pointer.
An item with no label is a rule: it is drawn and can never hold the selection.
An accelerator is marked, not guessed. & before a letter in a label or pane title marks it;
&& is a literal ampersand. ktui_menu_label underlines the letter, except on a Linux console,
where it brackets it as [F]ile: the console has no underline, and drawing one there puts a colour
on screen that no slot owns.
Colour comes from a slot, never from a literal value. There are eight slots and no more, because the console font has 512 glyphs: the Linux console uses the foreground intensity bit as the ninth glyph bit, so colours 8 to 15 cannot be used as a foreground. Designing for eight means a text console and a true-colour window show the same picture. The same reason is why bold is suppressed on a Linux console.
| Slot | Role | Scheme field |
|---|---|---|
KT_ACCENT |
The accent | primary |
KT_ERR |
Urgent | urgent |
KT_WARN |
Secondary: a caution, not a failure | secondary |
KT_TEXT |
Body text | text |
KT_MID |
Labels, secondary text, rules | pdark |
KT_DIM |
A fill; see below | dim |
KT_SURFACE |
Raised chrome background | variant |
KT_BG |
The background | backdrop |
A scheme has a ninth field, deep, the darkest ground, which no slot takes. It is what a scheme’s
legibility is measured against, and it feeds derived colours such as kcol_muted() below.
The values behind the slots are one table in libkcolor (KCOL_SCHEMES in
src/libs/libkcolor/kcolor.h), expanded at compile time by everything that draws. Nobody keeps a
second copy of the numbers, which is why kdos theme <accent> repaints the whole desktop. The table
holds eight accents: phosphor, amber, ice, bone, norton, borland, perfect and paper.
paper is the one light scheme. The default, bone, is named (KCOL_DEFAULT_ID) rather than taken
from a position, so the table can be reordered without changing what an unconfigured machine starts
in. How to choose one is in Theming.
Every contrast ratio in this section is the WCAG 2 contrast ratio, computed from the eight schemes in that table; a range gives the worst and best accent.
A colour that is not the palette’s to name. A program running in a terminal may ask for a 24-bit
colour, or for one of the 256-colour palette’s cube and grey entries. No palette names those, they
do not follow an accent, and reducing them to eight slots would ruin a photograph. Such a cell
carries the literal colour beside the slot it reduces to (fgc, bgc and ulc in KtuiCell, with
one attribute bit per colour saying which literal is meaningful). Terminal content reaches a frame
through ktui_draw_put. The only other literals are the shadow and the blend described below, which
are mixed from the current palette each time they are drawn. A display that draws in slots alone,
such as a dump or a sixteen-colour terminal, reads the slot and ignores the literal. Chrome never
names a colour of its own, because a piece of chrome holding a fixed literal would stop following
kdos theme. The sixteen ANSI colours stay slots for the same reason: they are colours this palette
names.
Translucency, which is a mix of two colours rather than a colour of its own. A surface that
wants its body seen through sets KDispConfig.opacity, a percentage where 0 means unset and is
treated as 100. Below 100 the display dims the one slot the body is drawn in (KT_BG for a
top-level window, KT_SURFACE for everything else) and uses an alpha-capable buffer, so the
wallpaper and the windows show through while the text and the fills stay opaque. It is the
background slot only, because a translucent character is one nobody can read. The panel and
kdos-term use it.
For a rectangle of cells rather than a whole surface, the toolkit also has a pair of calls:
ktui_draw_bg_take() copies the background colours of the rectangle, and ktui_draw_blend() mixes
what has since been drawn there back towards them. The result is written as each cell’s literal and
the slot is left as drawn, so a display that does not show literals (a --tty run, a test dump,
tty1) shows an opaque rectangle, which is the correct answer where there is nothing to mix with.
Both ends of the mix come from the current palette and are recomputed every frame, so an accent
change moves them like everything else. A picture cell is skipped, because a picture’s pixels are
not a background. No surface in the tree calls the pair; only the self-test exercises it.
KT_DIM is a fill, not a label colourUsed as a foreground, KT_DIM measures only 1.42:1 to 2.12:1 against the surface and background
slots across the eight accents. KT_MID measures 3.40:1 to 6.30:1. Nothing is comfortably readable
below about 3:1.
So a label is KT_MID. Hint rows, empty-state messages, help text and the brackets round a button
all belong there. Searching a new surface for KT_DIM in a foreground position is how this is
checked.
A two-state colour is a different matter. Where KT_DIM is one half of a pair, such as a
scrollbar’s track against its thumb or an unpinned mark against a pinned one, it carries the
difference, and changing it to KT_MID would erase the state rather than make it readable. Those
pairs stay KT_DIM.
For text that must be muted and still readable there is a derived colour, kcol_muted(): the
scheme’s deep field mixed 56% of the way towards its text colour. It measures 3.47:1 to 6.26:1
against the background and surface slots, above 3:1 in every accent, and the self-test asserts that
it sits more than twice as far from deep in luma (perceived brightness) as KT_DIM does. Every
muted text role in the theme generators uses it. KT_DIM keeps fills, borders and selection
backgrounds, where contrast is not the question.
Never use the reverse attribute over a label. Reverse inverts only the cells a character covers, so a two-word name comes out as one lit block per word with a hole between them, and looks correct for every name that happens to contain no space.
Instead, fill the rectangle, swap the foreground and background slots, then draw the text.
kch_header is drawn this way: an accent fill with the title in KT_SURFACE on it.
KT_DIM under KT_TEXT, and the accent is one cell Files FM ■ a row
► System Monitor MO ■ the selection, in a pane without the keyboard
►▓Git▓▓▓▓▓▓▓▓▓▓▓▓GI▓▓■▓ the selection, in the focused pane (▓ is the fill)
ktui_sel_slots() decides this, and no surface works it out for itself; ktui_sel_row() fills the
row and draws the marker. There are three states, because they say three different things:
| State | Drawn as |
|---|---|
| A row | KT_TEXT on the page, no fill |
| The selection, pane not focused | ► in KT_MID, label in KT_TEXT, still no fill |
| The selection, pane focused | the row filled KT_DIM, ► in KT_ACCENT, label in KT_TEXT |
KT_TEXT on KT_DIM measures 8.30:1 in the worst accent and 10.22:1 in the best, so the label
clears 7:1 everywhere, and the ► marker (KT_ACCENT on KT_DIM) clears 3.68:1 everywhere. The
fill is quiet because it is a fill, and the accent is spent on the one cell that says where the
selection is. The page slot is a parameter, KT_BG or KT_SURFACE, because a control that guessed
would paint an opaque band across a translucent window.
Do not fill a row with KT_ACCENT. Writing bg = on ? KT_ACCENT : KT_SURFACE puts the background
colour on the label and drags a lit plate across the screen under the pointer, and it breaks the
rule above: KT_DIM owns selection backgrounds.
A pane without the keyboard keeps its marker and loses its fill. A surface with two panes has two selections; filling both says both are live, and dropping the inactive one loses the place that pane will return to.
KT_ACCENT as a fill has three uses, and none of them is a list row:
kch_header;Each is one thing on screen rather than one per row, and each means this, not here.
grep 'KT_ACCENT :' over the tree lists every conditional use of the accent; most are foregrounds
or state indicators, and one used as a list row’s background is a surface making its own rule. The
process, application, box and device tables in kdos-res break this rule: they fill the selected
row with KT_ACCENT and the hovered row with KT_MID.
The secondary colours cannot be read on the selection fill: KT_MID on KT_DIM measures 2.19:1
to 4.21:1, and kcol_muted() 2.44:1 to 3.38:1. A row with a secondary column (a tag, a two-letter
code, a unit) therefore raises that column to KT_TEXT when the row is selected in a focused pane,
and ktui_sel_dim() answers that in one place. Left muted, the right-hand half of the row would
disappear exactly when someone is looking at it.
The drop shadow uses the same literal-colour mix as ktui_draw_blend(), and the modal, the menu,
the open dropdown list and the installer’s dialogs draw one. ktui_draw_shadow() darkens rather
than erases: every cell in the one-cell strip to the right of and below the rectangle keeps its
character, and both its colours are mixed towards KT_BG, keeping 110/255 of the original, so the
window underneath stays readable. A picture under the strip is left alone. The background slot is
set to KT_BG beside the literal, which is the whole shadow where literals are not shown. A shadow
that wrote blank cells would cut a rectangular bite out of whatever it falls on, which looks like a
rendering fault rather than depth.
After the mix, ktui_draw_shadow() clamps each colour channel so it is never lighter than it
started. KT_BG is not the darker of the two background slots in every
accent: in bone the background is lighter than the surface in red and green, and in ice in green
and blue. Without the clamp those accents would glow along two edges of every window. The clamp
costs nothing where the background is already darker, and it is what keeps a new accent from
getting a luminous shadow.
A character on a filled plate is read by the plate around it. The dark slots of this palette look
like one colour (KT_BG measures 1.00:1 to 1.20:1 against KT_SURFACE), so dark ink on a bright
fill cannot be told apart by colour from the chrome behind the fill, whichever of the two it is
drawn in. What separates them is the fill surrounding the mark.
So a mark drawn in KT_SURFACE on a plate that sits on a KT_SURFACE body must be a character
that stays clear of every edge of its cell; that clearance is the whole boundary between the mark
and the body. A mark that runs close to the bottom of the cell reads as the plate ending early
rather than as a mark on it.
It is a threshold, measured on the console font ter-kdos32n (16 by 32 pixels, so 512 pixels a
cell):
| Character | Lit pixels (of 512) | Rows used | Clear rows below |
|---|---|---|---|
_ |
24 | 27–28 | 3 |
■ |
108 | 10–21 | 10 |
X |
80 | 6–25 | 6 |
↓ |
68 | 6–25 | 6 |
Six clear rows of thirty-two hold; three do not. ■, X and ↓ can carry a mark on a plate, and
_ cannot. The compositor’s own title-bar buttons are not bound by this, because their ground is a
different colour from their mark; see
The compositor’s decoration is part of the set.
The same measurements explain why ink on a bright fill is dark. KT_TEXT drops as low as 1.10:1 on
KT_ACCENT and 2.17:1 on KT_ERR, so a light character on a lit or urgent plate is effectively
invisible. KT_SURFACE clears 3.40:1 on KT_MID, 5.23:1 on KT_ERR and 5.51:1 on KT_ACCENT in
the worst accent of each.
Every surface answers the pointer, and answers it the same way. A surface with rows and no motion handling is a picture: the only way to discover that a row is a control is to click it.
| Gesture | Means |
|---|---|
| Motion | Lights what is under it; on a list that follows hover, selects the row |
| Left press | Activates. On a row that is already selected, opens it |
| Wheel | Steps the selection while the list fits; moves the view when it does not (see the two wheel rules), gliding where the display has a frame clock (Motion). One detent is one step, whether it arrives whole or, from a high-resolution wheel, in fractions |
| Two fingers on a touchpad | The wheel, one step per ten units of travel; a flick coasts on after the finger lifts, slowing to a stop, and the next scroll, click or key stops it |
| Right press | Backs out one level, then closes |
| Scrollbar | Is dragged, and its end caps step one row |
| Column header | Sets the sort; a second press reverses it |
| Click away | Closes a transient surface |
A verb reachable only by a key chord does not exist for someone using a mouse, and the shipped key
card is not the first thing anybody reads. The rule works both ways: every control answers the
arrow keys and Enter as well as a click. That is why the window menu carries every per-window
verb, and why a number in a settings form is a slider rather than a printed value.
The usual way to break the rule is a line such as if (ev.type != KT_EVT_KEY) continue;, which
throws away every pointer event, so the surface cannot be operated with a mouse at all however much
of it looks clickable. A surface that draws its own rows uses ktui_rows_event rather than
inventing its own answer:
ktui_rows_event ignores motion, so hovering over a row does not select it. A list that scrolls
needs the fit-aware wheel described under Drops and the wheel instead.
A surface owns its cells and the display owns the screen. A pointer drawn by the surface would cost a round trip for every motion event and lag behind the hand. The display already holds the device and already knows where it is.
In the cells, the pointer is the reversed cell under it: the pointer every text mode has drawn, and
as fine as a grid of characters goes. Over a Wayland window there is also a real arrow, and the
surface does not draw it: libkwl answers the pointer-enter event with
wp_cursor_shape_device_v1.set_shape, and the compositor draws a cursor at the device’s own pixel
position, so it moves as smoothly as the hand holding it. The enter event carries the only serial a
set-cursor request may use, so a client that does not answer it has no cursor image, and the pointer
disappears over every surface the library draws.
The toolkit defines seven pointer shapes, each saying what a press at that spot would do:
KT_PTR_ARROW (the default, and every unhandled case), KT_PTR_IBEAM, KT_PTR_SIZE_NS,
KT_PTR_SIZE_WE, KT_PTR_SIZE_NWSE, KT_PTR_SIZE_NESW and KT_PTR_MOVE.
The list is what this desktop can mean, not what any protocol carries. It is neither the cursor shape protocol’s list nor X11’s, for the same reason the raw event codes are not libinput’s: a number that happens to equal an upstream one is a coupling neither side can see. Whatever forwards a shape maps it in a switch.
A backend holds no window state, so it cannot choose a shape; it cannot tell a border from a
box-drawing character. Whatever owns the pointer names the shape with ktui_draw_cursor_shape(),
only when it changes, because a pointer crossing a window spends hundreds of frames over the same
thing, and KtuiBackend.pointer carries it to the backend.
The shape is a hint and never a promise. No surface sets a shape, and no backend fills
KtuiBackend.pointer: a terminal, a dump, tty1 and a Wayland window all reverse the cell whatever
the shape says, and on Wayland the compositor shows the arrow that libkwl requests. So a control
must say what it does in its own cells; the shape is a second telling for those who can see it. A
backend that meets a shape it does not recognise draws the arrow rather than failing, so a newer
session with more shapes still has a pointer on an older display.
Nothing shows a busy pointer. libkdisp’s cursor list (enum kdisp_cursor) includes default,
text, hand and progress shapes, and no surface sets any of them: nothing here tracks a window as not
answering in a way a pointer could report.
ktui_draw_cursor(x, y) names the cell the pointer is on; ktui_draw_flush() decides how it is
drawn. The contract:
KtuiBackend.pointer. Returning 1 is a
promise that the cells reach the screen exactly as the surface composed them, with the backend
drawing its own arrow; returning 0 declines for that frame. A backend that leaves the entry
NULL, as every backend in the tree does, gets the reversed cell. The arrow cannot be drawn in
libktui itself, because that library has no pixel or font dependency and an arrow needs a
pixel buffer.x
means no pointer, and it is the only thing that tells a backend to remove the last arrow; a
backend told nothing would leave one where the hand last was.KT_A_GUEST gets no pointer. The bit marks a cell drawn from an embedded
client’s own pixels, where the client’s compositor has already drawn a cursor, and a second one a
cell away is the one nobody is aiming with. The rule is applied before the hook, so every backend
is told the same thing. The flush honours the bit, but nothing in this tree sets it.Where the reversed cell is what gets drawn:
kcell_paint fills those cells
with the foreground colour instead.KT_A_REVERSE is also how selected text is drawn, and a panel icon on a
hovered row would come out in negative.Nothing is drawn before the first motion, since the pointer starts on no cell, so a machine with no pointing device does not show a pointer in its corner all session.
Nothing this desktop routes on is finer than a cell: a pointer, a wheel step and a coast all land on
whole rows and columns. Two things are finer, and neither reaches a surface’s cells. A gliding list
is presented a fraction of a row off for the frames of its slide, in libkwl’s pixels only. And
the raw input stream, KtuiBackend.poll_raw, reports every key as a switch, every motion in the
backend’s own pixels and every scroll with its real distance, its value120 and whether the device
reversed it, beside the cell-level queue, for a consumer that is not cells; libkwl fills it, and
nothing in the tree reads it.
A touchscreen follows the same contract, because one recogniser turns fingers into the pointer
events above. ktui_gesture_feed in libktui is fed by wl_touch in libkwl; a recogniser
in each backend would duplicate the code and let the two backends interpret the same fingers
differently.
| Gesture | Reported as | Also arrives as |
|---|---|---|
| Tap | KT_GEST_TAP |
a left press and release |
| Long press | KT_GEST_LONG |
nothing; a surface wanting a context menu reads the gesture |
| Drag | KT_GEST_DRAG |
motion with the button held |
| Two-finger scroll | KT_GEST_SCROLL |
one wheel step per row the fingers’ centre crosses |
| Pinch | KT_GEST_PINCH |
nothing |
| Edge swipe | KT_GEST_SWIPE_EDGE |
motion with the button held, and it says which edge it came from |
A tap is a touch shorter than 250 ms (KT_TAP_MS) that never left its cell; a long press is one
held past 500 ms (KT_LONG_MS) without leaving it. An edge swipe is a drag that began in the
outermost row or column. So a surface written without touch in mind already works under a finger,
and one that wants more reads ev.gesture on a KT_EVT_TOUCH event.
The recogniser follows four rules:
libkwm’s kwm_drag_threshold().dy keeps the per-finger movement, for a surface that
forwards the gesture.A long press has no event to arrive on, because the finger is down and nothing moves, so it is
checked with ktui_gesture_tick from the backend’s idle wait and reported once, using the same
CLOCK_MONOTONIC milliseconds the recogniser was fed. A deadline compared against a different clock
never expires, and nothing reports that.
A long press is Shift+F10 with a finger, and the contract answers it: ktui_keys() opens the
surface’s context menu on KT_GEST_LONG, so a surface that declared one supports touch without a
touch path of its own. It opens at the finger, not where ctx_at says the keyboard focus is,
because someone holding a row expects the menu on that row; ctx_at is still asked, so a surface
that refuses a key refuses a finger too. A tap opens nothing: a tap is a click, every control
already handles one, and a menu on every tap would put a pane under every finger.
A drag-and-drop drop is a position and a payload, and an event has room for only one of them.
KT_EVT_DROP carries the position; ktui_drop_take returns the payload once, so two surfaces in
one process cannot both act on one drop. A text/uri-list payload arrives exactly as sent, URIs
separated by CRLF with comment lines included, because what a URI means differs from surface to
surface.
Drop and wheel handling depends on four facts about the event stream:
libkwl drops motion when the cell is unchanged.KT_MB_WHEEL_UP or KT_MB_WHEEL_DOWN, and no release follows it. Dispatch on the button, not on
the press alone, or a scroll will raise, focus, move or close whatever is under the pointer. A
scroll reaches the thing under the pointer and does nothing else to it: it does not raise it, take
the keyboard or start a move or resize.The fit-aware wheel is kch_list_wheel() in libkchrome: a list that fits gets a selection step,
and a list that scrolls gets its view moved by three rows (SH_WHEEL_ROWS) with the selection left
where it was. kch_list_clamp() keeps the view inside the list afterwards. ktui_rows_event does
not follow that rule: it steps the selection on every detent, fits or not, and leaves the view to
follow the selection. Twelve call sites in eleven source files use the fit-aware rule; the five
kdos-shell surfaces that draw their rows through ktui_rows_event (the recorder, trash, command
palette, theme and find surfaces) step the selection on every detent instead.
The flag that says “pull the selection into view” (follow) is set by everything that moves the
selection and by nothing that scrolls the page, or the next frame would undo the scroll. A surface
that assigns a new view from a scrollbar drag clears its own flag for the same reason.
After the clamp, a list that scrolls declares its rows and its first item with kch_list_view(),
which is what lets the backend present a change of view as a glide (Motion). It declares
the rows alone: a header scrolls with nothing, and a scrollbar’s thumb is drawn at the new view at
once.
A list that scrolls shows a scrollbar. It is one column wide: a ▲ cap, a ▒ track in KT_DIM
with a █ thumb in KT_MID, and a ▼ cap, all characters every glyph tier has, so it looks the
same on a console. A cap at the end of its travel is drawn in KT_DIM. Nothing is drawn when
everything fits, because a full-height thumb says nothing a missing bar does not. Where rows would
reach that column, the rows give up a cell rather than the bar drawing over them: a selected row is
a filled band, and a bar on top of it would put a notch in it. kch_scrollbar is called every
frame, including frames where the list fits, so a bar that has stopped being drawn stops being
grabbable.
A hit map records where each clickable thing was drawn, so a click can be matched to it. It is recorded from what was drawn and never worked out a second time from the geometry the drawing computed.
Both copies agree until the window is resized, a sidebar collapses or a border is added, and then a click lands on the row above the one under the pointer. In a network dialog, that means joining the wrong network.
Two rules keep the hit map and the drawing in step:
What a surface can draw depends on the terminal or display. libktui picks from its capabilities
(ktui_caps):
| Tier | When | Box drawing and symbols | Ramp (bars, charts) | Ramp levels |
|---|---|---|---|---|
| rich | UTF-8, not a Linux console | glyph_utf8 |
eighth blocks ▏…█, ▁…█ |
8 |
| vt | UTF-8 on a Linux console (KT_CAP_LINUXVT) |
glyph_utf8 |
░ ▒ █ |
3 |
| ascii | no UTF-8 | glyph_ascii |
. : # |
3 |
The box-drawing and symbol table (glyph_utf8 in src/libs/libktui/ktui_draw.c) is the same for
the rich and vt tiers, so it must stay inside what the console font can draw. Only the ramps differ
(ktui_chart.c). With no UTF-8, every character above ASCII is drawn as ?.
The vt tier exists because the console font, ter-kdos32n, has 512 glyphs. A character the font
lacks is drawn as a blank on tty1, so an eighth-block bar there is not merely coarse, it is
invisible. Three levels is the true resolution of that font.
What the console font has: ░ ▒ █, the single box-drawing set, the double rules ═ ║, the double
corners ╔ ╗ ╚ ╝ and ╬, and · • ■ … ° ↑ ↓ ◀ ▶.
What it does not have: eighth blocks, half blocks (▀ ▄), ▓, braille, and the double tees
╠ ╣ ╦ ╩ and the mixed joins ╡ ╞. The double set is only partly there: the rules and the
corners are, the tees are not.
The terminus-font recipe (ports/core/fonts/terminus-font/build.sh) builds the font from Terminus’s
xos4-2 character set and swaps six spacing diacritics for the double box-drawing characters
═ ║ ╔ ╗ ╚ ╝, which the KDOS block logo needs. The font answers to 627 codepoints in all, because
its duplicate tables map extra codepoints onto existing shapes. ▲ ▼ are drawn as ↑ ↓, and
◄ ► and ← → as ◀ ▶, so the control glyphs ◄ ► and the direction glyphs ◀ ▶ in the table
draw the same two shapes on tty1. A glyph the font answers to is not necessarily a distinct
shape, so two table entries that must look different on tty1 need two different glyphs, not two
codepoints mapped to one.
Anything that can reach tty1 stays inside the vt tier, and glyph_utf8 is entirely inside it.
The toolkit uses that table whenever the backend reports UTF-8, which the Linux console does, so an
entry the font lacks would be a blank on tty1, with nothing reporting it. A slider built on ▓
would draw its filled part as nothing at all.
testing/preflight.sh reads the built ter-kdos32n (from build/fs) and refuses any glyph_utf8
entry the font cannot draw, because a written list of what the font carries is the thing that goes
stale. Two consequences are visible in the controls: the slider separates its track from its run by
colour slot rather than by a third shade, and a button’s shadow is ░ rather than a half block.
A frame’s title is bracketed [ like this ], and the brackets are ASCII for a second reason beyond
the font. The test dumps are drawn at the ascii tier (+=[ backup ]=====+ is what the top of a
committed frame looks like), and every join in that table collapses to +, so a title bracketed
with ╡ ╞ or ┤ ├ would read there as two corners in the middle of the top rule. [ and ] are
the same at all three tiers, exist in every font, and are the bracket the DOS file managers put a
title in.
A wide character is measured, not assumed. The toolkit computes display width with its own table
(ktui_wcwidth, not the C library’s) and reserves a continuation cell, so double-width text does
not break the row layout. The console font has no wide characters, so on tty1 a wide character is
written as a single ? in one cell. Chrome that must read on both stays inside the small set.
On a Wayland output the tier is rich at every scale, and a scale changes the pixels of a cell, never
the cell. A whole-number scale draws each glyph of the 16×32 cell that many times over with
nearest-neighbour filtering, so the letters are the tty1 letters with every pixel doubled. A
fractional scale draws the same face at the device size instead of a doubled grid the compositor
shrinks: at 1.25, 1.5 and 1.75 the chrome is Terminus (TTF) in a cell of exactly 20×40, 24×48 and
28×56. A surface keeps the same number of cells in the same logical room either way. The box-drawing
and block characters are synthesised from their codepoints at whatever size the cell is, so frames
join at every scale. See
The fractional scale.
A sprite is a picture registered in a numbered slot of the toolkit’s sprite table (up to 4096
slots) and drawn into whole cells. The icon atlas is the theme’s icon set, one file
(/usr/share/kdos/icons/atlas.kia, read by libkicon).
Icons and pixel tiles are drawn into whole cells, with the picture’s slot and position within it encoded in the cell’s character. The ordinary row comparison therefore already tracks what changed, and a text backend draws the sprite’s fallback character instead.
Every surface must draw correctly when the picture is unavailable. The icon lookup answers “none” on
a terminal, when icons are turned off (icons = no in ~/.config/kdos/comp.conf), when there is no
icon atlas, and when the sprite table is full; each of those is a normal state, not an error. The
test harness (testing/fixtures/shell/dumpmain.c) stubs the lookup to exactly that, so a committed
reference frame is the character grid, and a layout that only lines up once the pictures load is a
broken layout.
A pixel tile (kch_tile_*) is a block of cells a surface paints as pixels through a canvas, such
as the panel’s Start button and its meters. It can be any size up to the grid: past the 16×16 cells
one slot covers, it is a grid of slots over views of the one canvas, like a picture in a terminal
below, and kch_tile_draw() writes all of its cells. Two rules apply:
kdos theme drops every tile (kch_tile_reset), because each was rasterised in the palette being
replaced.
Chrome is hard-edged: plates, rules, rounded ends, gridlines and the compositor’s title-bar buttons
are whole pixels, because a smoothed edge beside a cell grid is what makes chrome look as if it came
from another toolkit. A data mark is the one exception. The trace of a chart and the area under
it (kcell_canvas_series), and a segment drawn with kcell_canvas_line, are antialiased, because
a measurement’s slope is information and a staircase at a fixed pixel pitch misstates it.
Every chart on the desktop is drawn by one renderer, kch_plot(), in one style: a faint plate in
KT_DIM, gridlines in KT_MID every ten seconds keyed to the sample number so they move with the
data, a base line, the area at a third of the series’ weight and the trace at full weight over it.
A trace lying on its base is drawn at the area’s weight, so an idle chart is a quiet line rather
than a hard rule. A pair of series (receive and send, read and write) shares one axis and is
mirrored about a midline, the first in KT_ACCENT above and the second in KT_WARN below. Every
mark takes a slot and an alpha, never a colour. The chart is a tile, so tty1 and --dump draw the
surface’s cell chart instead, and a reference frame asserts that.
A new picture in the same slot changes no cell, which is what ktui_draw_dirty() is for. An
animation’s next frame writes cells identical to the last, so the flush finds nothing to send and
the screen would keep the first frame indefinitely. Marking the rectangle the slot covers costs that
rectangle; the alternative, repainting the whole screen for every arriving tile, costs every
character on the desktop and a full upload dozens of times a second.
A backend that keeps its own copy of the previous frame must implement dirty and mark every copy
it keeps. libkwl keeps three: the cells the compositor is showing (which the damage rectangles
are cut from) and one per shared-memory buffer (which each paint compares against). Mark only the
buffer copies and the pixels land in a buffer the compositor is never told to re-read; mark only the
screen copy and the damage names rows nothing repainted. Every one of these failures is silent and
looks like an animation that stopped. A backend without dirty compares against the prev frame
it is handed and needs nothing more.
A backend that caches uploaded pictures compares the sprite’s generation counter (gen), not its
pointer: an animation re-registers the same key, and the allocator often hands the freed picture’s
memory straight back, so a pointer comparison would take the first frame and no other.
A picture shown in a terminal uses the same sprite mechanism. A slot covers at most 16 by 16 cells, so a larger picture becomes a grid of sprites sharing a key prefix, handled all-or-nothing: it is evicted and re-registered as a unit rather than leaving three quarters of a photograph on screen. Each backend does what it can:
| Backend | What a picture is |
|---|---|
| Wayland | Real pixels, scaled to the cells it occupies |
| A text terminal, a dump, or a display built without a pixel library | The fallback shade, in every cell of the picture |
A picture that shows as nothing is worse than one that shows as a mark, because blank cells look like output that never arrived. That is why the tiled path carries a fallback character rather than a space.
A tile that is a control carries the control’s state in its cells’ background slot. The backend
fills a cell’s background before drawing the picture over it, so that slot is the button’s body
wherever there is no pixel layer to record a plate into. KT_SURFACE is right only where there is
one, since the recorded plate shows through it; on a plain character grid it would leave the button
with no fill, no edges, and no visible hover or open-menu state. So the slot follows the same ladder
the pixel plate draws: a quiet fill at rest, the accent under the pointer, the warning colour while
the control’s own menu is open, with the ink following the fill. The Start button is the example.
An icon sitting inside a wider field shows its state through the field instead.
A sprite beside text is two cells wide and one tall. A cell is twice as tall as it is wide (16 by 32 pixels at the default font), so two cells across one row is a square on the same line as the text. A two-by-two block next to a single row of text centres the picture across the boundary between rows and makes the whole surface look misaligned.
The window frame is the one piece of chrome a KDOS program does not draw for itself. kdos theme
generates it for the current accent into ~/.config/kdos-comp/themerc-override, the labwc theme
file the compositor reads last, and the compositor re-reads it on the same SIGHUP that retints
everything else. It is designed to match the cell grid:
<cornerRadius>0</cornerRadius> in rc.xml). A rounded corner is the one
thing a cell grid cannot express, so it is the one thing that gives a server-drawn frame away.border.width: 2), because a hairline disappears beside a
32-pixel cell. An inactive frame is drawn from the same scheme with the accent removed: its
border is the dim colour, its title bar the deep ground, and its rule and label are deep
mixed 25% and 45% towards text. No second hue appears, so focus does not read as two kinds of
window.════ Title ════[_][=][X]. This is the flat kdos title texture of
kdos-comp, a fork of labwc (see kdos-comp); its colorTo is the
rule’s colour.src/desktop/kdos-comp/src/theme.c) are
the compositor’s own: a bar, a framed box, a cross and a stack of rules. A button image is drawn
in the accent (window.active.button.unpressed.image.color) on a title bar that is a different
colour, so the minimise bar works there with the one clear row it keeps beneath it. On the cell
grid the same bar would fail the six-row threshold in
A mark on a fill is a shape, which is why the two
sets of marks are allowed to differ.window.active.button.close.unpressed.image.color),
and the others are the accent. It is the one window control that cannot be undone, and on an
eight-colour palette that difference is the whole signal.window.button.hover.bg.color carries alpha 66). The
compositor has no separate hover images; it lays that colour over the plain button image, and an
opaque colour would paint the symbol out, leaving every button blank under the pointer at the
moment it most needs to say what it is.menu.width.max: 900, because at
32-pixel text the compositor’s default cap of 200 pixels holds about eleven characters.accent = in
~/.config/kdos/boxes/<name>.conf) carries a square of that colour at the left of its title
area, the height of the title bar, so you can see which box it came from. A box with the
session’s own accent draws no chip, so a default installation shows none.Style overrides written by kdos theme style are kept in ~/.config/kdos/style-themerc and
appended after the generated block each time it is written, so they survive an accent change.
Nothing on the cell grid moves. A cell is drawn or it is not, and that is what tty1 and --dump
show, so no surface’s cells may depend on a clock to reach their final state. Motion lives in three
places, all of them pixels over, under or in place of cells that are already final.
The compositor re-blends pictures a surface has already finished. A surface on the top or overlay
layer fades in when it maps and out when it unmaps, from a snapshot of its last frame; peek fades
the windows; with window_motion on, a window fades and rises into place as it opens, fades and
sinks away as it closes or is minimised, and slides sideways with a workspace switch. It changes
position and opacity, never size. A surface needs no code to get this and cannot tell it is
happening.
A surface animates its own pixel layer with a KtuiAnim from libktui: a start time, a duration
and a curve, read by the draw that uses it. The Wayland backend keeps the frame clock. While any
animation in the process is running, its event wait returns a KT_EVT_TICK once per display frame,
so a loop that draws on every return animates at the display’s rate; when the last one ends, one
more frame draws the end value and the loop goes back to its own timeout. An idle surface gets no
tick, no extra commit and no shorter wait from any of this. Two surface animations are shipped:
the panel’s launch pulse, and the selection plate in the Start menu, the cascading and window
menus and the tray menu, which slides to the row the selection moves to (kch_px_row_anim). Only
the plate slides: the row’s cells and its hit map are the new row from the first frame, and a list
that scrolls under the same selection carries the plate with it at once rather than letting it
trail the text.
The Wayland backend presents a scrolled list gliding. A list declares where its rows are and which
item is in the first of them (kch_list_view, beside kch_list_clamp), and when that item changes
the list’s pixels slide from the picture on the screen to the new one, eased out, with the frames
in between committed by libkwl itself. The list’s cells have already moved by whole rows: the
pointer hits the rows the cells say, a key acts on the new view at once, and the last frame of the
slide is the cells’ own picture. A second notch while one is under way continues the slide from
where it is. A page’s worth or more is a jump, and so is a new list in the same place (a filter
typed, a group opened), which the backend tells from the cells. The lists that declare themselves
are the Start menu’s three columns, the cascading and window menus, the tray menu, the status and
notification centres, the clipboard picker, the file chooser, Open With, the teams list, the
recorder’s files, and kdos-res’s Applications, Processes and Boxes tables.
A touchpad flick coasts: a finger that leaves the pad while still moving keeps the list moving, slowing to a stop over about a second. That is input, not animation: the backend keeps producing the wheel ticks the finger would have made, so every surface coasts without knowing it, cells included, and the next scroll, click or key stops it, as does the pointer leaving the surface. A finger resting on the pad without moving does not: the backend binds no gesture protocol that would say so. See Input the backend cleans.
| What moves | Duration | Curve | Settles on |
|---|---|---|---|
| A top- or overlay-layer surface opening / closing | 120 ms / 90 ms | ease-out | shown / gone |
| Peek (Show Desktop hover) | 150 ms each way | ease-out | faded / restored |
A window opening, restored / closing, minimised (window_motion) |
150 ms / 120 ms | ease-out | in place / gone |
| The panel’s launch pulse: the accent over a pinned button, twice | 1100 ms | ease-in-out, there and back | the button at rest |
| A menu’s selection plate moving to another row | 90 ms | ease-out | the new row |
| A list scrolled by the wheel, a touchpad or a key | 100 ms from the last step | ease-out | the new first row |
Choose the curve by what the motion says. KT_EASE_OUT (cubic) is an arrival: fast, then
settling. KT_EASE_IN_OUT is a pulse or a swap, slow at both ends. KT_EASE_LINEAR is for a
progress, never for a movement. Keep an animation under a quarter of a second unless it is an
acknowledgement that has to be seen, as the launch pulse is, and never make anything wait for one:
keys and the pointer act on the end state at once.
The end value is the picture wherever nothing moves. On tty1, under --dump, through a
capture backend and with motion switched off, a KtuiAnim is worth its end value from the moment it
starts, so a golden never holds a frame from the middle of one. A surface must not put anything in
a motion that its end state does not also say.
Reduce motion is one switch. motion = no in ~/.config/kdos/comp.conf turns off both halves:
the compositor reads it at each fade, and every Wayland surface reads it at each animation’s start,
so a change reaches a running panel without a restart. With it off, a compositor fade is a single
frame and a surface’s animation is its end value. Where the motion is the message, the state is
held still instead of dropped: the launch pulse becomes a steady accent over the button for the same
1.1 seconds, so the click is still acknowledged and nothing moves. A selection plate lands on its
new row in the frame the selection moves, a scrolled list jumps to its new rows, and a touchpad
flick stops when the finger lifts. What the compositor fades and what each fade costs are
in kdos-comp.
A new surface is not finished until every line is answered.
ktui_draw_box around it, with the title on the top edge.kch_header for the band, kch_group for headings, kch_buttons for verbs.KT_MID for labels; fills for emphasis, never the reverse attribute; no
KT_ACCENT fill on a list row.--dump at the sizes the surface is used at (80x24 and 132x43 for a window, its own size for a
popup), with a reference frame (a golden, under testing/goldens/) committed for each.tty1.KtuiKeys, with ktui_keys() first in the event dispatch and ktui_hint_row() last in the
draw, on every path including the --dump one, because that call is what clears the hint pool.ktui_keys_layer() rather than handled in an Esc branch.fs/usr/share/kdos/doc/ if keys.doc names one, and doc left NULL if not.If grep -c KT_EVT_MOUSE on a new source file returns zero, line 4 has not been answered.
libktui, libkchrome, libkcolor and the restkdos themePart III — Architecture, chapter 18. Previous: 17. The security model · Contents · Next: 19. The window model