Ce document est rédigé en anglais, la langue de travail du dépôt, et il est converti tel quel depuis le fichier qui fait foi. Les pages que le site écrit lui-même — l'accueil, la galerie, l'architecture et les guides « je viens de… » — existent intégralement dans les deux langues.
runtime/kv_cocoa.m is verified: keal-view was developed on a Mac.
runtime/kv_win32.c and runtime/kv_x11.c were written against the same
dozen functions, compile on their own platforms in CI, and have never opened
a window. This is the checklist for the first person who does.
Read it top to bottom. The order matters: it separates "the framework is broken" from "the backend is broken" before you have to guess.
Windows — MinGW-w64 in a POSIX-threads flavour, or LLVM clang
targeting mingw32. MSVC cannot compile Keal's runtime: it uses GCC's overflow
builtins. keal doctor says which compiler it found. You also need a bash
— Git Bash or MSYS2 — because tools/build.sh is a /bin/sh script.
Linux — a C compiler and libx11-dev (Debian, Ubuntu) or libX11-devel
(Fedora), plus a TrueType font: fonts-dejavu-core is what CI installs.
Both — Rust, to build the Keal compiler.
git clone https://github.com/geneacta/keal
git clone https://github.com/geneacta/keal-view
cd keal && cargo build --release && cd ../keal-view
tools/build.sh examples/gallery.kealThe compiler moves too. keal-view uses language features that arrived
because keal-view asked for them, so git pull in ../keal and
cargo build --release are part of updating this repository, not a separate
errand. A stale compiler refuses with the C backend cannot compile …,
naming the construct — that message means the compiler is behind, not that
your machine is at fault.
And do not pipe tools/build.sh into anything. A pipeline's exit status is
the last command's, so tools/build.sh … | tail -2 && ./units runs the old
binary after a failed build and tells you nothing about why it behaves
strangely. tools/test.sh prints the compiler it used, for the same reason.
build/units # or build/units.exe
build/gallery --snapshot frame.png 2 900 1900Two commands and nothing else installed: the PNG is written by src/png.keal,
in Keal, so this step needs no Python and no image library on a machine you
are still porting to.
units prints how many checks it ran; the number grows, so compare it against
what it says on a machine that works rather than against a number written
down here.
Neither touches the window, the event queue or the platform's drawing at all. The first asserts several hundred things about colour, geometry, layout, docking, fonts, pictures and input dispatch. The second exercises the rasteriser, the TrueType engine and the layout, and writes the result to a file.
This is the bisection. If frame.png is right and the window is wrong,
the fault is in the backend and nowhere else. If frame.png is wrong, the
fault is above the backend and every platform has it.
Look for, in frame.png:
kvp_font_path found no
font. It tries segoeui.ttf, tahoma.ttf, arial.ttf under
C:\Windows\Fonts; on Linux, DejaVu, Liberation, FreeSans and Noto under
the usual /usr/share/fonts paths. Add yours to that list if it is
somewhere else.byte_order to LSBFirst
and lets Xlib convert. If everything is upside down, that is the line.0xAARRGGBB in a
uint32_t, and all three platforms are supposed to read that without
conversion. Red and blue swapped means they do not, on yours.WM_ERASEBKGND, which the backend does
not handle yet.Tearing during a fast drag is not testable this way, and saying so is
better than guessing. Repeated screen capture tops out around sixty
milliseconds between frames, and a tear lives inside one frame; --trace
counts frames rather than looking at them. A report that says "neither seen
nor ruled out" is worth more here than one that says "looks fine". It would
take a capture card or a camera, and nobody has needed it enough yet.
AreDpiAwarenessContextsEqual(GetWindowDpiAwarenessContext(hwnd), (void*)-4)
answers whether PER_MONITOR_AWARE_V2 took. Beware
GetAwarenessFromDpiAwarenessContext, which answers 2 for both V1 and
V2 — the enumeration has no value for V2, and only the comparison
distinguishes them. Windows asks
GetDpiForWindow after SetProcessDpiAwarenessContext; X11 reads
Xft.dpi from the resource database. Soft, stretched output means the
process was not made DPI-aware and the system is scaling the window for
it.XLookupString gives — and neither path has ever run.build/units — which never opens one — skips it
and says so, even on a machine whose display is answering. Select in a
field of the gallery, Ctrl+C, then Ctrl+V somewhere
else. On X11 this is a real selection owner answering real
SelectionRequest events; try pasting into another application too, and
copying from one. Do it quickly, several times: OpenClipboard on
Windows is a lock the system's clipboard-history service also takes, and a
write followed immediately by a read is where that shows. The backend
retries; a failure here would mean it does not retry enough.clicks should reach 2.DwmSetWindowAttribute 20 then 19, and older builds have neither — a
light bar over a dark window there is correct, not a bug. X11 has no
title bar of its own at all; the window manager draws it, and
_GTK_THEME_VARIANT is a hint it may ignore.Under XWayland the second case cannot be synthesised, and this step is
then half done. XTestFakeMotionEvent is advertised and inert behind the
input portal, and XWarpPointer moves the pointer without the server
emitting the motion that would follow — so a program cannot put the
compositor into the state where a cursor is resting on a surface. Only a
hand on a mouse can. If you cannot get that, report this step as half
done rather than as passing: the half you can measure is the half that
was perfect on Windows while the other one held a core. Take it from accumulated
processor time over several seconds rather than from a one-second sample,
and run the same measurement against a text editor as a control.
Then the two real applications:
build/calculator # digits, +, Enter, Escape from the keyboard too
build/studio # the one that mattersIn the studio: drag a panel by its tab onto another one. A blue region should show where letting go would put it, before you let go — left, right, above, below, or as another tab. Drag the dividers between panels. Close a tab with its ×. Press Ranger to put it all back.
Everything in §1 goes through events this repository made up, which is the right way to test the framework and cannot test the backend — the thing that makes the events. So there is a bench for that, and it needs a person:
tools/build.sh tests/bench.keal && build/benchA text field at the top, an editor below it, and a character count at the bottom. The count is the instrument. Press every arrow, Home, End, Page Up and Down, and forward Delete, and the count must not move. Then Shift with each of them, and the selection must appear without the text changing.
That test takes a minute and found a defect that had been there since the first day. On macOS, Cocoa answers a character for keys that type nothing: the arrows, Home, End, the function row and forward Delete all come back as one code point in Unicode's private-use area, U+F700 upwards. They are above 32 and are not 127, so the guard that drops control characters let every one of them through — and one press of Left added one character to a document, 72 to 73, counted on the screen. Home, End and Shift+Home in sequence replaced a whole line with a single unrenderable box, in a field as readily as in an editor.
Neither of the other two backends can do it. Windows sends no WM_CHAR for a
key that types nothing, and Xutf8LookupString answers no bytes for one. It
was Cocoa's alone, and it survived a thousand assertions and three testers
because nobody had ever pressed an arrow key in a keal-view window on that
platform. Everything there had gone through --snapshot, or through
synthesised events above the backend, and this was the backend inventing one.
The lesson generalises to any backend you write: the question is not whether your text events are right, it is whether you make one where there should be none.
Run on: macOS. Not yet on Windows or on Linux. Reading both backends says
neither can have this defect — but reading is exactly what missed it on macOS
for a week, so until somebody has pressed the keys the answer for those two is
probably, and it is written here as probably rather than in the table as
verified. A Wayland session cannot press its own keys: XTest is advertised
there and inert behind the portal, so this test needs a person and not a
harness.
Both live in this repository, both take a minute, and neither can be a gate — one needs a window and a keyboard, the other needs a browser.
tools/build.sh tests/bench.keal && build/bench # §2⅜ — a key that navigates
tools/site-width.sh # the site, at 500 points
tools/build.sh tests/paintcost.keal && build/paintcost # what a frame costsThe second is not about the framework at all; it is here because it belongs
beside the first. A page whose content is wider than the window scrolls
sideways as a whole, and a screenshot cannot show it: headless Chrome will
not make a window narrower than 500 points, so a capture asked for at 375 is a
500-point page cropped, and it looks the same whether the defect is there or
not. scrollWidth against clientWidth is what tells the truth. It found two
things the first time it ran, one of them a 605-point floor under this very
page.
The third is for the days the language changes underneath. This framework and the language it is written in are developed together, and a change that costs a few per cent on a compiler compiling itself can cost more here: a paint loop is dense in small calls. Run it before and after and compare — one reading says nothing, the machine is usually busy, and six runs took a minute.
It earned its place the first time it ran. A call trace added at every runtime failure site cost the language +2.6 % on a compiler compiling itself and +11 to +35 % here, depending on which of the three columns you read — and the spread is the finding: the more work a call does, the less the frame pays. A measurement taken from outside cannot separate the language's change from your own, so take it before and after one change, and say which when you cannot.
A mutation that does not compile is not a mutation that escaped. This is about the other instrument used constantly here — breaking the code on purpose to see whether an assertion notices. If the build fails and the runner is not told, it runs the previous binary, every check passes, and the report reads "not caught" for a change that never ran. One mutation in this project's history did exactly that and was investigated as a gap in the tests. So:
if ! tools/build.sh tests/units.keal >/dev/null 2>&1; then
echo "DID NOT BUILD — says nothing"; return
fiThe same shape as everything below: an instrument that answers without measuring is worse than none, because it is believed.
**And site-width.sh was wrong three times before it was right**, which is the
part worth carrying to any instrument you write here:
--dump-dom prints a script's
source as well as its result — so the pattern matched the source, found no
number, and reported every page clean. Every page, including one that was
605 wide;file:// iframe never finish loading there — so it measured a
page set in fallback faces, which is narrower than the page a reader gets;Wrong three ways, and each time it answered. An instrument that answers without measuring is worse than no instrument, because it is believed. What settled it was the same control this document asks of everybody else: take the fix out, and see the tool go red on exactly the two pages that then overflow — and at the same number an independent measurement had already given. A tool that has never been seen to fail has not been seen to work.
Both false positives in this project's first two test passes came from the harness rather than the framework, and both were caught the same way: doing the same thing to a program that is known to work.
Arrow keys sent with SendInput and no KEYEVENTF_EXTENDEDKEY are treated as
the numeric keypad's, and Windows then cancels the Shift around them — so
Shift+Arrow selected nothing, in a way that looked exactly like a missing
selection. A burst of synthesised keystrokes dropped characters, which looked
exactly like a full event queue, until the same burst was sent to Notepad and
dropped them there too. An INPUT struct built at 56 bytes instead of 40 made
SendInput refuse in silence, so a click simply did not happen and the window
looked like it was ignoring input. And on X11 under XWayland, XTestFakeMotionEvent
is advertised and does nothing at all, so the first report of "hover does not
work" was about the injector.
And on macOS, System Events' click at {x, y} is accepted by osascript,
returns success, and posts nothing that a window receives — so the first pass
at a double click looked exactly like an editor ignoring the mouse. The
control said otherwise within a minute: the same click did not reach a text
field that had been taking clicks for a week. A pointer built on
CGEventCreateMouseEvent and posted to kCGHIDEventTap works, and then the
double click selected its word and a drag selected across four lines.
Five, from three people, on three platforms. In each case what caught it was the same: an observable that should have moved and did not, checked against a program known to work — or, where no such program would answer synthetic events, against a control window written to do exactly what the framework does.
So: if input seems to be lost or ignored, send the same input to a text editor first. If it is lost there as well, the report is about your injection and not about this framework.
Worth knowing before you build anything of your own, because both were here already and both were used by people who had not written them.
The control (§2½) caught seven false starts across two testers and two
platforms — a wrong-sized INPUT struct, arrow keys without
KEYEVENTF_EXTENDEDKEY, a dropped keystroke burst, an inert
XTestFakeMotionEvent, captures a beat behind the window manager, a layout
believed reset, and a stale window photographed instead of the fresh one. Not
one reached the code.
**--trace found three causes.** A hover defect took four rounds to fix, and
three of those rounds ended with the tester able to say where it had stopped
rather than that it still did not work. wakes=1 frames=0 on a pointer moving
onto a chart is a fact no screenshot gives — a screenshot says only that
nothing changed — and no amount of reading the source would have produced it
either.
The suite has nine hundred and fifty checks and every one of them passes on a
select that leaks a view per rebuild, because a leaked object still gives
the right answer. It has to be looked for with a different instrument, and it
is worth five minutes on any platform:
leaks --atExit -- ./gallery --snapshot g.bmp 2 900 1900 # macOS
valgrind --leak-check=full ./gallery --snapshot g.bmp 2 900 1900 # LinuxThen build the same thing many times and see whether the number moves. A handful of objects held at exit is nothing; the same handful per rebuild is a window that grows while it sits there. Ten lines of Keal in a loop is enough to tell the two apart, and it is what separated them here:
100 builds: 3 600 leaks
1 000 builds: 36 000 leaks
5 000 builds: 180 000 leaksThirty-six objects a rebuild, from select and menuButton each holding a
function that held the view that held the function. A reference count cannot
free a cycle, and a tree that is thrown away and rebuilt whenever anything
changes makes one every frame — about ten megabytes a minute from a window
with a dropdown on it and nobody touching it. What it looked like at exit was
eighteen small objects, which is what a leak checker run once reports and what
anybody would have shrugged at.
The rule that came out of it: a handler must not close over the view it is
attached to. tappableAt hands it the rectangle instead, which is the only
thing such a handler ever wanted from it.
The two measurements that cut through this project's hardest defect both did the same thing, and it is worth copying the shape rather than the numbers.
User time against kernel time, in the same process. An idle window held most of a core; 97 % of it was user time and 0.3 % kernel. That one ratio eliminated the wait, the message pump and every system call in a single line, while the person who wrote the code was looking straight at them.
Two windows of exactly the same size, holding different amounts. A calculator with twenty buttons and a gallery with a scrolling document, both at 336×509: 0.93 % against 20.53 %. Same pixels, twenty-two times the cost — which eliminates the surface, the rasteriser and the platform at once and leaves only the content.
What a ratio buys is that it can contradict a reading. The same tester tried
to price a frame with --snapshot, read 1009 milliseconds flat from 0.18 to
9.9 megapixels, concluded that drawing was cheap, and sent that on — the
instrument had a start-up floor that swallowed everything it claimed to
measure. A second reading would have agreed with the first. The ratio did not:
processor against window size at a known wake rate said 125 milliseconds a
frame, flatly contradicting it, and that number is what decided the fix.
And what a ratio does not buy, which matters more. It only goes quiet when the faulty instrument sits on both sides of the division. An injector that is not delivering anything sits on neither — it is upstream, and it will hand you two perfectly consistent numbers describing an experiment that never happened. Nothing about the shape of a measurement protects you from that. Only §2½ does.
Two things were observed exactly once during the verification of this framework, could not be reproduced deliberately, and are recorded here rather than fixed or forgotten. If you meet one in a clean state, it exists; until then it does not.
Both were reported as unreproduced by the tester who saw them, with the hypotheses they had already eliminated. That is what makes them useful to the next person rather than noise.
What helps, in order: the exact error text if there is one; otherwise what you see, said plainly — "the text is upside down", "the red buttons are blue", "nothing happens on hover". A screenshot if you can attach one anywhere; the description is enough otherwise.
Say which of the two the snapshot in step 1 was. That one fact decides which half of the codebase to look in.
tools/shot.sh photographs a window by its number through macOS's
screencapture. There is no portable equivalent; take a screenshot the
usual way.--window-id answers an HWND on Windows and an X11 Window on Linux.
Both are the system's real handle, and neither is any use to
screencapture.