TODO
What is deferred, known-broken, or specified and unbuilt. What is built is in status.md.
These are tracked in the decision log and need answers before the milestones they block can be scheduled honestly. Every entry says what the gap is, why it is one, and — where it is known — what it would take, with the decision record that argued it. An entry leaves the top half when it is answered and moves to Answered rather than being deleted, because each of those records a trap somebody hit and the reasoning that got out of it.
Where the documents disagree with each other — as opposed to with the code —
is listed in docs/ARCHITECTURE.md §17.1. docs/design-system.md and
docs/core-widgets.md are the authority; the architecture document is a summary
of them and records where it knowingly departs. Five of the seven were taken on
2026-09-17 and each went the way the section says it should: the platform
primary modifier is a modifier you can name
(ADR-0378), a disabled
container reaches the cascade
(ADR-0379), text style=
is built (ADR-0381), the
tooltip row’s radius and rank are what ships
(ADR-0380), and “one module or two”
had been settled in core-widgets.md itself a month before anyone noticed. What
is left is where the emoji font lives and who owes its attribution, whether
goldberry-charts is an artifact, what “zero new natives” costs (see Content
modules), and — recorded in §17.1 and never counted here —
dual y-axes, the word checkable doing two jobs, and masonry’s missing row. Pixel-precise wheel deltas left this list and
have now left the list below it too:
ADR-0115 settled it as a
difference rather than an agreement — what §2.4 wanted from “pixel-precise” is
scrolling that does not quantize, and a fractional line delivers that without
the mechanism the sentence named — while the entry itself sat on under
Input, focus and the pointer for two milestones. That section is gone: its
other entry was a cost nobody had measured, and measuring it was the answer.
Overlays, popups and windows
- A popup may not give the platform’s keyboard focus back, and the widget
layer has nothing to do with it — the second half of this entry was wrong
and has been measured. A popup gets its own tree and its own router, and
nothing in the open or close path touches the owner’s: a probe through the real
launcher, with a widget logging every focus change, saw a menu open and close
over a focused control without that control losing focus once. So there is
nothing to remember and nothing to restore at the router level. What is left is
the platform’s own window focus — SDL gives a
POPUP_MENUwindow focus on some drivers and not on others — which the headless backend cannot show and which no test here can currently reach. — ADR-0180, ADR-0104 - A popup’s contents inherit nothing from the widget that opened them, and the
answer this entry proposed is wrong for the widget it named. They are the root
of a second element tree, so no
color, nofont-sizeand no descendant selector reaches in. Right for a menu, whose items are a list rather than part of a button’s subtree. This used to add “a limitation fortooltip, which wants the styling of the thing it describes, and the answer there is to pass the anchor’s resolved style in” — andtooltippins its typography for a stated reason, §1.4’scaptionrank being the one departure in its row that somebody had thought about (ADR-0263). Inheriting the anchor’sfont-sizewould draw a tooltip on adisplay-ranked heading at 28px. So the subject stands and the example does not: what is left is apopoveror amenuwhose application wanted a descendant selector to reach in, which nothing has asked for. — ADR-0263, ADR-0103 - 60 ms is how long a focus-lost is disbelieved for, and it can now be told
otherwise. Long enough to cover the focus-lost/focus-gained pair that opening
a popup produces, short enough that nobody sees a menu over another application.
It is still one default covering every driver and it cannot be derived — the
gap between the two events is the compositor’s own scheduling and nothing
reports what it will be — so what changed is that
-Dgoldberry.popup.settle=250overrides it without a rebuild, clamped to 1–2000 ms because a delay of zero acts on the first of the pair every driver sends and is the exact bug the delay exists for. A driver that needs the flag will still look like a menu that closes as it opens until somebody sets it. — ADR-0144
The catalog: specified and unbuilt
text-input, and what §4 still owes
-
A field’s scroll offset uses the previous frame’s width. ADR-0116 already decided that is what a viewport does, and it is wrong for one frame after a resize — invisible, because a resize is followed immediately by another frame. Worth writing down because it is the second widget to need the measurement
rendercannot have, and a third would be an argument for handing the width torenderrather than toMeasured. — ADR-0167 -
Nothing re-places the caret when the font changes under it. A restyle that changes
font-sizereshapes the paragraph and the caret follows, because both are computed in the samerender. A density change does the same. Neither is broken; what is untested is a font-family fallback swapping mid-edit, which no test can currently provoke. — ADR-0167 -
isModalhas one consumer, which is one fewer than a mechanism should have.sheetis the plausible second, and it is not built. Awizardis not: §6 makes its content a focus-scope, and a wizard written inline that trapped the keyboard and the pointer would lock the window around it. It is tested in:coreagainst bare widgets rather than throughdialog, so the second one finds a mechanism rather than a dialog-shaped hole. — ADR-0176, ADR-0356Read again on 2026-09-30, and it stands.
DialogPanelis still the only widget that answersisModal. What is new is a reader:web-viewasksHost.isModaland parks its page while a modal is up (ADR-0444), which is a consumer of the answer rather than a second thing that traps. (An “on hold” note for the accessibility bridge sat here by mistake; this entry never waited on it.) -
There is no third text rank, and one was invented and taken back out. A tour’s step counter wanted something quieter than
--gb-text-muted;--gb-text-subtlewas added, resolved tonord3, and produced a counter nobody could read onnord1— §1.2’s contrast floor applies to metadata as much as to prose, and the Nord palette has nothing between muted and the border colour. The size carries the demotion instead. A real third rank would need a colour the palette does not contain. — ADR-0121 -
A scrollbar’s thumb stops being proportional on a very long document. It is floored at 24px, so past about four screens the thumb no longer says how much is visible — only that there is a lot. The trade every scrollbar makes, named here because it is a place the widget knowingly stops telling the truth. — ADR-0117
-
Measuredhas several consumers whose reason is a sibling’s geometry, which is new: a toast stack banks how tall each toast came out so that it can move the survivors by the height of the hole when one goes (ADR-0178). Every other consumer reads its own box. It obeys the third rule by construction for the same reason the scrollbar does — a reflow is atransform, so the box it moves is laid out where it always was. -
A virtual list can have its focused row scrolled out of existence. Wheel far from the focus ring and the focused row leaves the window, is unmounted, and the router drops it — which is ADR-0180’s rule doing exactly what it should to an element that has left the tree. Arrow keys are unaffected, because the ring asks the viewport to follow it; only pointer-scrolling away and then pressing one loses the place. Every recycling list has this unless it pins the focused index, and pinning it would keep a row nobody is looking at built for ever. — ADR-0213
-
A
tablefocuses rows and not cells. Right for §10’s grid semantics and wrong for a spreadsheet, which is a different widget rather than an option on this one. Horizontal virtualization is absent for the same reason: it is a different arithmetic, and it is worth it past about fifty columns, which is past where a table is the right thing to be looking at. — ADR-0214 -
A segmented control fills its parent when nothing gives it a width, which is new and is a real loss of convenience: in a toolbar beside other widgets it takes the whole row until an author writes
width. It buys the travelling indicator, and there is no third option under flexbox — content-sized cells cannot be travelled between, and a zero basis collapses the bar entirely.
The shell: the tray, and what it cannot say
§9’s tray-icon ships (ADR-0191).
What follows is what it does not do, and in three cases what no platform lets it
do — recorded here rather than left to be rediscovered by an author whose
description had no effect.
- §9’s “activate event” is not built, and SDL has no callback for it. The sentence asks for icon, tooltip, menu and an activate event; SDL3’s tray API registers a callback per entry and none for the icon itself. So a click on the icon opens the menu and nothing else can be attached to it. Doing this properly means going around SDL to three platform APIs, which is the move ADR-0056 declined when the wheel wanted it; the workable answer is a first row that means “open the window”, which is what most tray applications ship anyway.
- A checkbox’s tick can end up disagreeing with the application. The shell
toggles it before the handler runs and an
Item’s command takes no argument, so a handler that declines leaves the platform showing a tick nobody believes in.SDL_SetTrayEntryCheckedis deliberately unbound — correcting one row would be the only mutation in an otherwise rebuilt-from-a-description menu — so the way to say no is to close the tray and show it again. - The menu cannot change while the icon is up.
BackendTraysets the icon and the tooltip and nothing else: its rows are platform objects the shell may have open, and replacing one would mean re-inserting entries underneath a user. A declarative caller closes and reopens, which is correct and is also a flicker in the notification area on some shells. - An accelerator on a tray row is dropped, with a warning. A shortcut is bound
to a window and a tray has none. The same
Itemin amenubarstill registers one, which makes this the first place in the catalog where one value means two different things depending on who draws it. - The deprecation warning on Linux is SDL’s to fix. Loading a tray prints
libayatana-appindicator is deprecated. Please use libayatana-appindicator-glib, from the distribution’s library as SDL opens it. SDL’sappindicator_nameslist holdslibayatana-appindicator3.so.1andlibappindicator3.so.1and not the successor, so the only ways out are a patched SDL or a newer pinned one — neither worth doing for a line on stderr that no user of an application ever sees. - Windows and macOS are unverified. The Linux path ran for real — in
SdlTrayTestagainst this machine’s session and in the showcase — and the other two are SDL’s code, compiled and never looked at. A tray is the one widget CI cannot cover: there is no notification area on a runner and no golden image of a GTK popup. - A tray callback arriving off the UI thread is logged, not handled. Every
platform dispatches it from inside the pump the UI thread is already in, so the
warning in
SdlTray.invokeis the only evidence there would be if one ever does not. Doing better means posting to the loop, which is a second delivery path for one hypothetical.
canvas, and what a document cannot say
- Markup cannot name a painter. A
canvasnode inflates to a styled, sized surface that draws nothing; the drawing is Java.iconsolved the same problem with a registry the application owns (ADR-0043) andactionwith another, so the shape is known — what is not known is whether a painter is a value a registry holds or a method on a model, which is the same question@Actionanswered for commands and would have to answer again here. Nothing has needed it: every consumer so far is written in Java — the chart widgets,web-view’s placeholder and the showcase’s tile floor. - A canvas has no intrinsic size, so one in a
rowwith nothing else to size it is zero wide and silently invisible. A measure function that guessed would be a number the toolkit invented and the application drew into; a diagnostic when a canvas is laid out to nothing would be noise in the collapsed-split-pane case, which is legitimate. Left as a documented sharp edge. - A painter is called on every paint of its box, not only when it says
something changed. That is what immediate-mode means and it is right for a
chart whose data changed; it is wasteful for a canvas whose drawing is static
and expensive. The seam for fixing it exists — a canvas that wants caching is a
repaint boundary with a
Layer(ADR-0071) — and nothing has measured a case that needs it.
Images, and what the primitive is not
Image.decodeis still synchronous. A large JPEG is tens of milliseconds, and acanvaspainter that decodes pays it on the UI thread. Theimagewidget does not: it decodes on a virtual thread throughImageLoader(ADR-0358), and that is the seam a painter should use too. Nothing has measured a painter that needs it.
Editing text
- No bidi caret.
Paragraph.isBidiApproximatealready says the shaping does not promise visual order for mixed-direction text, and a caret in it needs a walk the toolkit does not have. Latin, Cyrillic and CJK are exact; Arabic and Hebrew are approximate in the same way the paragraph is. - An
Editordoes not scroll. It draws where it is told and does not know it has been clipped. A canvas with a long document moves its own transform, which it is already doing for everything else on it; a widget wanting this istext-area, which has a viewport.
The clipboard
- Nothing watches it. There is no “the clipboard changed” notification, so a paste button cannot grey itself out until its menu opens and asks (ADR-0286). X11 and Wayland both deliver ownership changes and Windows has a viewer chain; what is missing is a consumer worth the plumbing.
Content modules
docs/content-widgets.md’s table has thirteen rows, and four of them are
artifacts now: :html, whole, with no engine under either half; :emoji;
:gpu, built in part; and :media, in progress. goldberry-charts merged into
:widgets and goldberry-web became a widget rather than a module. None of the
other seven is scheduled while M3 still owes client-side decorations and the rest
of §4. The shape they share is
ADR-0190 and the summary
is docs/ARCHITECTURE.md §11.1. What follows is what each is actually waiting
on, which in four cases is the same thing.
-
Both halves of
goldberry-htmlare built, and neither has an engine under it.:htmlshipsMarkdown.parse,MarkdownHtml,markdown-view,Html.parseandhtml-view(ADR-0294, ADR-0295, ADR-0298), and what they do not do is a short list that mostly has one cause — there is no inline layout engine under either, because that is what litehtml would be:-
Neither can lay out a line of mixed faces as one shaped run, so there is no justification and no hyphenation. This is the item that is litehtml’s, and it is now the whole of what an engine would buy: links, images, task boxes (ADR-0300) and text selection (ADR-0301) all used to be on this list and none of them needed one.
-
Dragging a selection past the edge of a viewport does not scroll on. Both views select, copy and highlight (ADR-0301); what a drag to the bottom of a pane does is stop selecting rather than carry the viewport with it. The same want a
text-areahas, and neither has it — an auto-scroll is a timer plus a clamp, and the interesting part is deciding what it does on a touchpad’s fractional deltas.Closed by ADR-0500: a drag held past the edge carries the viewport on at a speed set by how far past it the pointer is, through
EdgeScroll, whichtext-areashares. There were two faults rather than one — the selection also froze at the edge, because every word is clipped to the viewport and a pointer below it was over none. The touchpad answer is that the speed comes from the pointer’s distance and never from the wheel; a wheel mid-drag scrolls as usual, and the fractional steps are applied unrounded. -
Emphasis is a faux oblique —
transform: skewX(-10deg)— because §6.1 ships two upright faces. A third face is an asset decision rather than a code one, and:assetsis where it would be made. -
A hard break inside a paragraph does nothing.A wrapping row has no widget meaning “start a new line here”, and aspacerwithflex-grow— the obvious trick — makes the line before it look justified. Closed by ADR-0426, below. -
A table’s cells have no rules between them, and a fence does not scroll sideways. Both are the CSS subset:
borderis uniform, so there is noborder-left, and horizontalscrollis not in §10 either. The rules are closed by ADR-0505: a border has four sides now, a row draws the rule above it and a cell the rule before it. Two things the entry could not have said: a border has never taken layout room here — it is drawn over the padding, and every bordered widget counts on that — and the first render’s vertical rules stepped at every row, because each row sized its columns by its own content, so the cells areflex-basis: 0now. The quotation bar became theborder-leftit always meant, with no pixel moved. A fence still does not scroll sideways. -
src="…"is not a thing on either view. Reading a file from markup means deciding what a relative path is relative to and what a missing one does — three answersIconsand the stylesheets each needed a resolver for. An application reads the file and passes the text. -
<style>andstyle=are kept in the HTML model and applied by nothing, and neither is a<script>run. The cascade anhtml-viewis under is the application’s stylesheets, which is what makes a page follow the theme; an author’s own colours would fight it (ADR-0298). -
A keystroke re-parses and rebuilds the whole preview (ADR-0296). Right for a note in a pane, and the widget count is what bites first on anything longer — ADR-0295 put it at roughly one per word. An incremental parse is md4c’s to offer and it does not; a rebuild bounded by what the viewport shows is the
listvirtualization argument applied to a document, and nothing needs it yet.
Closed — ADR-0426. A hard break is now its own
Words.Piece, and a paragraph with one becomes a column of line rows; with none it builds exactly the single row it built before, which is why no:htmlgolden moved. The CSS split is the decision:.md-proseand.html-proseare declaration-less paragraph hooks now, the row geometry moved to.md-line/.html-line, and the column carries the same0.25emgap so a typed break and a width break sit at the same leading. A break inside a link deliberately does not split — onebutton.linkis one Tab stop and one hover — so it becomes a space in the label. Three things the entry could not have said: a trailing hard break is unwritable in Markdown (md4c strips the two spaces), two in a row come from a lone backslash and do survive as an empty line, and a table cell provably cannot hold one.gallery-markdownmoved, because the showcase’s own sample says “Two spaces at the end of a line / are a hard break” and now demonstrates it. -
-
A code editor is
goldberry-code, and that module does not exist. The Markdown screen’s editor is atext-areain the code face: a caret, a selection, undo, the clipboard and an input method. It has line numbers now (gutter=#true, ADR-0331) and a seam an application can write shortcuts against (onEdit/edit, ADR-0332), soCtrl+B, list continuation andTab-indent are an application’s to write rather than impossible. What is still missing here is highlighting — Tree-sitter is what that waits on, and the fence’s language already reaches the model for it to read (CodeBlock.language()). -
The export list has no paint surface wide enough for a native
document_container.goldberry-htmlputs litehtml’s C++ container inside its own native library because FFM cannot implement a virtual class, and that container draws throughlibgoldberry’s exported C symbols. There are twentybl_context_*entries and they are the ones the toolkit’s own painter needs: no gradient, no rounded geometry, and nobl_context_save— the symbol file says why in its own comment, that there is only ever one clip depth here.content-widgets.md§1.5 promises linear and radial gradients andborder-radius, and CSS state nests. So the first commit of an engine-backedgoldberry-htmlis a widening of the toolkit’s own native surface, reviewable on its own, and it is shared work:goldberry-vectorandgoldberry-terminalwant the same surface. Nothing on screen is waiting on this any more (ADR-0298):html-viewrenders through the widget tree, and what an engine would add is the inline layout and the text selection above. When it lands it is a second renderer over the same model rather than a replacement for one. Statically linking a second Blend2D into the module is the way out that does not work — two runtimes in one process, and aBLContexthanded across them is undefined behaviour. — ADR-0190, ADR-0007Narrower than it reads — found by the 2026-09-30 sweep. Two of the three gaps were closed for the toolkit’s own reasons and the entry was not told: gradients are exported (
bl_gradient_*andbl_context_set_fill_style, ADR-0207), and so arebl_context_saveandrestore, whose comment in the symbol file now says a second clip depth is needed (ADR-0193). There are 25bl_context_*entries rather than twenty. Rounded geometry is cubic paths plusbl_path_elliptic_arc_to, with no round-rectangle primitive. Whether what is exported now is enough fordocument_containerhas not been checked against its virtuals, and that check is the first commit of an engine-backed module. Text selection is not something an engine would add any more: ADR-0301 built it. -
No SDL audio or camera symbol is exported, so
goldberry-camera,goldberry-micand the coreSoundAPI thatcontent-widgets.md§8 hands to SDL audio for UI effect sounds all begin at the same file. This is no longer a guess about what that costs:tray-iconbegan there too and paid eleven symbols, two binding classes and five probe constants for it (ADR-0191). The modules’ “zero new natives” claim is true of the binary and not of the surface.Half of it moved, for
:media— corrected 2026-09-30. Audio output is exported now: nineSDL_*audio-stream symbols, which:media’sAudioSinkwrites through (ADR-0462), with SDL’s ALSA and PulseAudio drivers required on Linux (ADR-0488). Of the 149SDL_*entries on the list, 56 areSDL_GPUand 9 are audio. Still missing: every camera and recording symbol, and the coreSoundAPI, sogoldberry-cameraandgoldberry-micbegin at the same file as before. -
The backend SPI has no PTY.
goldberry-terminalneedsOptional<Pty> openPty(cmd, env, size)—forkpty/openptyon Linux and macOS, ConPTY on Windows — which is the same optional-capability shape asgpuSurface()and is the real platform work in that module. libvterm itself is a state machine and a cell grid, which is the part the text stack is already good at. -
goldberry-pdfis the only module that vendors a prebuilt. PDFium’s own build wants gn/depot_tools, so:natives-pdfconsumes pinned, checksum-verified community binaries — which is a different supply-chain posture from every other native in the toolkit, where the superbuild compiles from a pinned commit (ADR-0030). Worth an ADR of its own before the first jar. -
goldberry-codehas a consumer before it has a widget, and the consumer now exists. md4c’s code fences want a highlighter;markdown-viewrenders a fence as plain monospace lines with the language shown above them, which is the “renders fences plain” branch — and the language is already in the model (CodeBlock.language()), so the seam is a real one rather than a plan. Sogoldberry-htmleither depends ongoldberry-codeoptionally or keeps rendering them plain. The optional-dependency mechanic — a module that improves when another is on the module path — exists now, as JPMS services::coreuses anEmojiFontthat:emojiprovides (ADR-0384) and aCompositorthat:gpuprovides (ADR-0479), andvideo-viewdraws through a GPU layer when:gpuis present (ADR-0484). Sogoldberry-html→goldberry-codeandgoldberry-vector→image/svg+xmlare two more services of a known shape, and what is missing is the module. -
goldberry-plot’s colormaps are data with a provenance. viridis-class tables are public domain, which is a claim the licence tooling has never had to check for something that is neither a font nor a library../gradlew checkLicensesknows about artifacts.
Style, colour and motion
-
Nothing in the catalog wears an elevation yet.Five surfaces do (ADR-0312):cardat §1.5’s level 1 and lifting to level 2 oncard.interactive:hover,dialog,tour-cardandtoastat level 2, andaffix:affixedat level 1 with thetransition: box-shadow§1.7’s motion table has asked for since before there was a shadow. The edges all stay, for ADR-0166’s reason: a shadow cast onto another card falls on that card’s own colour and says almost nothing, where the rim says it exactly.popover,menuandtooltipare the ones still without, and no longer because the subset lacks the property. They are drawn in popup windows created at the panel’s own measured size (ADR-0104), so a shadow — which is drawn outside the box that casts it — would fall entirely outside the window and be clipped: a run of fills that draws nothing. What it needs is a popup sized to the panel plus the shadow’s reach with the extra transparent, which wants the same transparent-popup compositor support the rounded corners are waiting on.--gb-elevation-3is unused and stays so: it is the level for a thing the pointer is dragging, and nothing here is dragged. — ADR-0312, ADR-0166 -
A popup’s transparent corners need a compositor, and are unverified on Windows and macOS. Without one the flag is ignored and the corners are whatever the platform leaves there. The fallback that always works — filling the frame with the panel’s own colour, for square corners — is kept in reserve. — ADR-0111
Rendering and performance
-
Opening a long note still shapes all of it, on the frame that opens it. A keystroke into a 500 kB
text-areacosts what a keystroke into a 2 kB one costs now, because the text is shaped one hard line at a time and only the rows on screen are drawn. The first frame is not: 499 079 characters, 78 to 95 ms across runs on this machine, and again whenever the cascade resolves a different face. It is irreducible in the shape the control has — how far the content scrolls is a fact about every line, and nothing knows a line’s height without shaping it — so closing it means shaping the lines below the fold off the frame and filling in the scroll range as they arrive, which is a different control and a different promise about what the scrollbar means. Nobody has reported it: the downstream editor’s complaint was the keystroke, andTextAreaFrameBenchmarkis where the number would have to come from first (ADR-0045). — ADR-0388 -
The scale-invariance thresholds are calibrated on one CPU. The worst honest disagreement measured over the corpus is 0.332% of pixels against a 1.2% limit, and Blend2D JITs its antialiasing for the CPU it finds (ADR-0030) — so the margin on AVX-512, on Apple Silicon and under MSVC is answered by the next CI run rather than by argument, exactly as the goldens’ own tolerance is.
-Dgoldberry.golden.scales.report=trueprints what every check measured, which is how a runner pressing against the limit would say so in numbers. — ADR-0162Partly answered by CI, as it said it would be. The golden suites run on
windows-x64under MSVC and onmacos-aarch64’s NEON path (windows.yml,macos.yml), and both are green. What is still unmeasured is AVX-512, and the thresholds themselves were still set on one machine. -
The rounded corners and the transforms have not been rasterized on AVX-512. Blend2D JITs its pipelines per CPU. The goldens now run on Apple Silicon’s NEON path and under MSVC in CI, and they pass there; the four cubics and the eleventh golden’s rotations and skews on AVX-512 are still answered by a future run rather than by argument — which is what the golden images’ per-channel and area tolerance is for. The transform half also rests on
BLMatrix2Dbeing six consecutive doubles in the ordermatrix(a, b, c, d, e, f)writes them, which the layout probe now checks against the compiled library on every target because the operand crosses asvoid*and a reordered union would produce a skewed frame andBL_SUCCESS. — ADR-0064, ADR-0068, ADR-0050 -
The units between the two text libraries are a convention, not a checked fact. HarfBuzz reports positions in whatever scale its font was set to; Blend2D multiplies them by
size / units-per-em. Both are right, and applying a size on both sides applies it twice — 128× for Inter at 16 points — which draws text off the edge of the window and returnsBL_SUCCESS. The layout table cannot catch this: it is an agreement between two libraries, not a fact about either. What holds it isFontowning both objects and never scaling the shaper, plus a test that compares the inked span against the measured width. Anything that builds aShapedFontand aBlendFontby hand can still get it wrong. — ADR-0034 -
A line boundary keeps a kern it should drop. Each line is a slice of the whole paragraph’s single shaping, so the kern between the last character of one line and the first of the next is included where a per-line shaping would drop it. A fraction of a pixel at the end of a line, in exchange for wrapping that costs no shaping at all. Re-shaping only the final lines, and only for painting, is the fix if it ever shows. — ADR-0036
-
Element.updateinvalidates a subtree only when the cascade could see the change. ADR-0149 narrowed the state path and ADR-0315 narrowed this one: a rebuilt widget throws away what is below it whenmatchesDiffersays its identity to the cascade moved — its type, its classes or its id — and invalidates its own style alone when it did not. What is left is the case where the identity did move, which still costs the subtree and which nothing has measured as a problem. — ADR-0149, ADR-0315 -
A HUD costs about three shaped paragraphs a frame, and reports the cost as its own. Its readings are strings that change every frame, so no cache keyed on the string can hold them. The caption says so rather than hiding it, and the ways out are all worse than the disclosure: refreshing the text at 10 Hz would need per-frame state a widget cannot have, and excluding the overlay subtree from the timings would report a frame the window did not paint. — ADR-0152
-
libgoldberry-webviewis opened at start-up, and WebKitGTK with it. The backend asks it whetherCapability.WEB_VIEWholds, and opening it maps WebKitGTK and GTK 3: 26 ms of a 520 ms native start, before any page is asked for.docs/content-widgets.md§11 calls the library “opened on demand”; the capability question is the demand. Answering it without the library — a build fact, like ADR-0422’s — or on first use would take it off every start. — ADR-0506, ADR-0441
Platform, compositor and CI
-
The 60 fps claim is measured on one machine, and closing M1 is a CI job. §16’s M1 asks for a styled wrapped paragraph resized at 60 fps on Linux, macOS and Windows. The budget half is met with 3.9× of headroom (ADR-0047); the breadth half is one VirtualBox VM. Scheduled at M5 — see status.md for the shape of it. The three things that were missing are all built (ADR-0342):
Window.resizeand--resize=WxHwalk a window’s size from outside,FrameSummaryprints what a run cost at exit, andshowcase.ymlpaints 300 frames while resizing on each runner. It asserts no budget, on any platform (ADR-0452): Xvfb reports no refresh rate, so “late” on a runner counts missed ticks of a software timer. The first run was made by hand and all three legs report what they cost —linux-x64302 frames and 75 late,macos-aarch64300 and 200 — which are the first numbers any leg has produced, and two samples locate a ceiling no better than none. What is missing now is a budget somebody measured, on a display somebody chose, and there is still no tag. The caveat travels with the numbers: GitHub’s runners are GPU-less VMs, so what this can prove is that three platforms’ drivers hold the budget, not that hardware does. — ADR-0045, ADR-0147 -
A like-for-like Wayland frame measurement is still owed. ADR-0037’s numbers — paint 5.10 ms, present 1.92 ms, 7.86 ms median — were taken on X11, after the Wayland run crashed the compositor, and they are compared against ADR-0031’s Wayland ones. Nothing in that work made present faster; the driver changed. The two rows should not be read against each other until the same frame has been measured twice on the same session type. — ADR-0037, ADR-0031
-
The Wayland preference is evidence from one compositor. SDL chooses X11 on a Wayland session unless the compositor advertises
wp_fifo_manager_v1, which GNOME’s Mutter does not; Goldberry asks forwayland,x11instead, because XWayland resizes visibly worse. Confirmed on GNOME only — KDE, Sway and the rest are untried, and the driver is logged at start-up so a report can say which one it got. — ADR-0027The preference itself has since been reversed (ADR-0086): on a Wayland session Goldberry asks for
x11,wayland, unconditionally, because under XWayland the window manager decorates the window. So the one-compositor evidence is now evidence for a path taken only with-Dgoldberry.backend.videoDriver=waylandor where there is no XWayland. -
The macOS window opens, and the CI leg still would not have caught it.
gradlew runfailed with “No available video device”, which points at the superbuild and was not the superbuild: macOS drives AppKit from the process’s first thread and the java launcher does not putmainthere. The showcase passes-XstartOnFirstThreadon macOS andSdl3Backendappends the explanation after SDL says no — as a diagnosis rather than a precondition, since a JVM embedded on the real main thread would lack the launcher’s environment variable and would work anyway. The hole that hid it is half closed:macos.ymlstill links the library and runs the tests without ever opening a window, butshowcase.ymlruns the packaged image onmacos-14and asserts it painted three frames — so a repeat of this failure would now turn a tick red. What that leg cannot catch is anything aboutgradlew run, which is the path this bug was found on and the one an application author uses. — ADR-0039 -
The compositor still dies, and shutting down cleanly did not stop it — the core dump says whose bug it is. The entry below concluded that exiting with a live Wayland surface was the trigger and that
Goldberry.shutdown()was the fix. The showcase has calledshutdown()ever since, and GNOME Shell crashed twice more on 2026-08-17./var/crashhad the core, and it names the frame:text wl_event_loop_dispatch libwayland-server → wl_client_destroy libwayland-server → <destroy listener> libmutter-14 → g_signal_handler_disconnect libgobject → g_type_check_instance ← SIGSEGVMutter, tearing down a departing client, disconnects a signal handler on a GObject thatg_type_check_instancerejects — an instance already finalized. That is unambiguously a compositor bug:wl_client_destroyruns whenever any client goes away, for any reason, and surviving it is the one thing a compositor cannot be excused from. Our own process exits 0 with no JVM crash log, having destroyed its window and calledSDL_Quitfirst. The nearest exported symbol below the faulting frame ismeta_xwayland_signal, 2.2 KB back, so the crashing function is a static one in Mutter’s Xwayland area — suggestive, not conclusive, and not enough to file upstream on its own. What is left for this repository is not a fix but a defence: nothing should be able to open a real surface by accident. See the entry below on the two unreliable ways to ask for a headless run. Reproducing this deliberately costs the developer their session, so it is not something to iterate on casually. gnome-shell 46.0-0ubuntu6~24.04.14, Ubuntu 24.04, under VirtualBox/vmwgfx. -
The toolkit never shut SDL down, and a compositor died of it.
Sdl3Backend.close()destroys every window and callsSDL_Quit; nothing called it.Goldberry.run()returning does not shut the runtime down — its contract says so — andGoldberry.stop()ends the loop with the window still open, so the showcase exited with a live Wayland surface and let the socket close. GNOME 46’s Mutter then crashed unwinding the connection, inwl_client_destroy→ its destroy listener →g_signal_handler_disconnect, on a GObject already freed. That is a compositor bug — every killed process disconnects abruptly and a compositor has to survive it — but disconnecting properly is right regardless, and the showcase now callsGoldberry.shutdown(). Open: whetherrun()should shut down on return, which would change a documented contract. Seen once, on GNOME 46.0 under VirtualBox/vmwgfx, after SDL3 moved fromrelease-3.2.0torelease-3.4.14in the same session. — ADR-0022Narrowed since:
Goldberry.launch(), the documented front door (ADR-0093), owns the runtime and callsGoldberry.shutdown()itself, so the showcase no longer has to. The open question is now only aboutrun(), whose contract still saysshutdown()is rarely needed, and so only about applications that assemble the loop by hand. -
No CI leg exercises Wayland.
showcase.ymlruns underxvfb-run, which is X11, where the window manager decorates the window and libdecor is never reached — which is why two consecutive decoration bugs shipped without a single red tick. A Wayland leg needs a headless compositor in CI (weston --backend=headlessorsway --headless), which is a job nobody has written yet. — ADR-0084 -
Native decorations on Wayland need a launcher that embeds the VM. The GTK plugin is the only thing that draws decorations matching the desktop, and its one requirement is
getpid() == gettid(). The stockjavalauncher runsmainon a thread it creates and so fails it; a launcher whose ownmaincallsJNI_CreateJavaVMand then the Javamainruns Java on the primordial thread, and the plugin loads there — demonstrated with a throwaway C launcher against the real showcase.jpackagedoes not help; it goes through the sameContinueInNewThread. Shipping one is a distribution change (a native binary per platform, VM argument handling, and a story for./gradlew runandjava -jar), so it is recorded as the answer and not yet taken. Two things bound how much to invest in it: upstream is building an out-of-process GTK plugin (libdecor MR 176) that dissolves the thread restriction entirely when it ships, and the ecosystem’s own answer on GNOME/Wayland is that every non-GTK toolkit — Qt, Firefox, Chromium — draws its own decorations in-process, which is theSdlWindowFlag.BORDERLESSdesign Goldberry has reserved but not built. — ADR-0084 -
A window on GNOME/Wayland needs two packages from two different phases.
libdecor-0-devat build time, or SDL compiles no libdecor support at all (ADR-0083), andlibdecor-0-plugin-1-cairoat run time, because the GTK plugin that libdecor pulls in by default refuses to start off the process’s initial thread and a JVM is never on it (ADR-0084). Installing either alone leaves the window bare. Whether Goldberry should carry its own decorations instead —SdlWindowFlag.BORDERLESSalready describes the design — is the standing question behind both records. Since ADR-0086 this bites only where Wayland is forced or there is no XWayland. -
CI is green, and the fixes that made it so were written blind. Nine causes on Windows and macOS were diagnosed from runner logs and fixed on a Linux machine; all passed at
fd36169aandd478ecfe. What that leaves: no machine here can run a Windows or macOS test before a push, so a platform-specific regression is caught by the Snapshot rather than locally. The annotations make that cheap to read, not free. — ADR-0338And it has happened since, twice. Windows went red and was fixed blind again (ADR-0450, ADR-0454). Then Snapshot runs 32 and 33 were red on every OS in
:natives:gpuTest, whose teardowns called SDL after a missing library had skipped their setup. That fix (ADR-0495) passes here and has not been through a CI run:masterhas not been pushed since. -
Two workflows are written and have not passed.
media.ymlbuilds FFmpeg and runs:media:checkwith FFmpeg and the platform decoders required onmacos-aarch64andlinux-x64, with GStreamer’s plugins on the Linux runner (docs/media-plan.md, phases 1 and 5). The GPU lane inlinux.ymlruns lavapipe under theoffscreendriver on both Linux targets with a device required, and asks the macOS runners without requiring them (docs/gpu-plan.md, 2026-09-24). Which runners can host a GPU device at all is still the open question ingpu-plan.md’s measurements table. Both are answered by a push. — ADR-0495, ADR-0480The GPU lane had run, twice, and is repaired — ADR-0503. “Never run” was this list’s mistake: Snapshot runs 32 and 33 both stopped at the lane’s own ICD check, because Mesa names the file
lvp_icd.jsonnow. Run here on lavapipe, the lane then found a real bug — a test destroying a GPU device afterSDL_Quit, theVULKAN_DestroyDevicesegfault that had been put down to this machine’s NVIDIA driver — and a golden blessed on Metal that no other driver draws to the pixel. All three are fixed, and the lane passes here as CI runs it.media.ymlhas still never run. -
Media on Windows and on
linux-aarch64is written and untested. The superbuild and the loader cover all four targets, and linux-x64 and macos-aarch64 are the only two that have run. D3D11VA is on by default on Windows and has no runner to prove it; VAAPI is off by default on Linux, on purpose, because it makeslibavutillinklibva. — ADR-0486,docs/media-plan.md
The native build and its bindings
- The layout registry’s constant half is where the value is. When this was
written it was seven struct layouts and 61 constant rows, 48 of them Yoga
enumerators; it is 66 struct layouts now, 27 of them
SDL_GPU, with the constants generated from the binding enums. The struct half has a known limit —YGSizeis identical on all six targets, so its row proves nothing the round trip in ADR-0017 does not — but the constant half is where the value is:YGAlignCenteris 2 andYGJustifyCenteris 1, and a Java constant that drifts from either produces a layout that is wrong on every platform at once and never an error. — ADR-0010, ADR-0029 VideoPlaybackTesthas two races, and one of them fails alone.statistics(“expected 4 shown, got 5”) was believed to fail only when:media:testand:media:testWithoutGpuran side by side; on 2026-10-01 it failed once in three runs on its own.playsToTheEndfailed in a fullcheckwith[BUFFERING, PLAYING, ENDED]where it expectsOPENINGfirst — a status listener attached after the first transition. Both are the test’s clock and the video thread meeting in an order the assertion does not allow, not a player defect anybody has seen; and both make a redcheckmean less than it should. — ADR-0463
Build, artifacts and release
-
A build with no network cannot produce a usable
goldberry-core. The bundled fonts and icons are fetched from upstream releases and cached, so this bites once per checkout rather than once per build — but a jar assembled without the asset step contains a toolkit that cannot render text. The build already needed network for the native superbuild, so no new constraint; it is written down because the failure is far from its cause. — ADR-0033 -
A release has never run against Central.
The publishing chain has never run against Central.Central’s side is done — the namespace, snapshots enabled for it, the token, the key and the secrets — and snapshots have gone out since run 17 on 2026-09-19. What has never run isrelease.yml→publish.yml→ a Central Portal deployment, which waits on the first tag.docs/releasing.mdis the list. — ADR-0334 -
The macOS and Windows native showcases are built from unreviewed traces. The checked-in reachability metadata was traced on linux-x64 and is reviewed as source; CI traces the other two headlessly before building, for 120 frames, and uses what it saw. A screen that run never reaches can lack a registration and fail when opened. Diffing the first CI traces against the checked-in file says whether per-platform traces are needed at all; if they are, they belong in the repository beside the Linux one. The
macos-14runner’s 3 cores are the likeliest place for the build to be slow (2.27 GiB peak and 1 min 23 s on 8 Linux threads). — ADR-0337 -
A stale Linux trace is found by a native build, not before it. The foreign calls no longer depend on the trace at all — every holder and every upcall owner is registered from the bindings, and
ForeignSurfaceTestholds the owner list to the sources that callupcallStub(ADR-0339). What the trace still carries is reflection and resources, and a screen the run never opened can still lack a reflective registration; with the showcase built only on a tag or by hand (ADR-0340), that is found later than it was, on the release build. — ADR-0339 -
An application still adds its platform’s natives jar by hand. The
goldberryumbrella cannot pickgoldberry-natives:<v>:linux-x64for the consumer’s platform — a POM has no way to — so the BOM lines up its version and the classifier is the application’s. A Gradle plugin, or module-metadata variants keyed on OS and architecture, would close it. — ADR-0336Narrowed — ADR-0438. Half the proposed fix does not work, and it was measured rather than argued. Module-metadata variants keyed on OS and architecture cannot close this: a variant is chosen by matching the consumer’s attributes, and a plain JVM consumer declares no operating system — so it resolves the unattributed jar silently, exactly as today but with more machinery behind it. A consumer that does declare one then fails with an ambiguity, because a variant that is missing an attribute is compatible with every value of it, so the ordinary
runtimeElementsties with the platform-specific one. The three ways out of that tie are all the consumer’s: attributing the shared bindings jar (which is not platform-specific), deleting the unattributed variant (which breaks every consumer that works today), or a disambiguation rule — and those are registered on the consumer’s schema, where a producer cannot put one. So a Gradle plugin is the whole of the answer, which is what JavaFX, LWJGL and sqlite-jdbc each ship. It is not built: it is a new published artifact with its own release surface, on a release path that has never run.:mediahas the same problem since ADR-0495: an application picksgoldberry-media’sffmpeg-<target>classifier by hand too. What did change is the documented snippet — all four classifiers, becauseNativeLibrarypicks at run time and the one-platform form is the one that fails quietly for somebody building on macOS for Linux. -
The release job has never uploaded to a GitHub Release. ADR-0340 attaches the three native images to the tag’s draft release with
gh release; the firstv*tag is its first run, and a manual dispatch exercises everything but that step. — ADR-0340 -
A release refuses to publish
goldberry-mediawith two of its four FFmpeg builds missing.media.ymlbuildsmacos-aarch64andlinux-x64, so a snapshot carries those twoffmpeg-<target>classifiers, and the release path requireswindows-x64andlinux-aarch64as well. Both are written in the superbuild and neither has been built. — ADR-0495 -
A native image resolves a host name before
main. About 50 ms of its 70 ms before the toolkit’s first line is a lookup throughlibnss_mdns4_minimal(/etc/nsswitch.conf,/etc/hosts, then the wait). The JVM does no such thing — its onensswitch.confread is for the user’s name — and logback resolvesHOSTNAMElazily and this configuration never asks, so it is not logback’sContextBase. The image is stripped, so the stack did not say whose it is; a build with symbols, or anInetAddressResolverProviderthat prints its caller (which found nothing on the JVM), would. — ADR-0506 -
The native image cannot install the GLib log handler. It logs “no handle to bind a GLib callback to”, and GLib’s messages go to stderr there — what ADR-0443 routes into the logger everywhere else. An upcall the image’s foreign-call registrations do not cover, which ADR-0339’s rule says cannot happen: every upcall owner is registered from the bindings. — ADR-0506, ADR-0339
On hold: the accessibility bridge
On hold — ADR-0440. Every entry in this section waits on the AccessKit bridge, which is not being built and which no milestone owns. Each keeps its prose and its reasoning, which is still the right reasoning; where one says “M5”, read “no milestone”. What has changed is that the thing at the end of them is not coming on a schedule. The way back is a consumer asking, not a date.
- A toast is not announced, and the only thing still missing is the bridge. The widget half is finished: a toast answers [Live#POLITE] and [Role#STATUS] and names itself with its own text, which is the claim §7’s “live region” is and the claim a role and a name cannot make. It matters here and nowhere else in the catalog because every other widget is announced when something happens to it — the focus lands, the pointer arrives — and a toast has no such event: nobody focuses it, nobody has to click it, and it is gone in five seconds. So a reader that speaks only what is reached would still have said nothing about it on the day the bridge landed. What is left is M5’s AccessKit bridge and no decision from the catalog. — ADR-0225, ADR-0177
Rolehas no link and no list.linkanswersBUTTON, andsteps,timelineandbreadcrumbsanswerGROUPoverROWs, each with the reason written on it: a role nothing consumes is a value written for a bridge that does not exist. The AccessKit bridge is where the words arrive. — ADR-0346- Four widgets announce what they are and cannot say what they hold.
Each has a specification sentence with two halves and only the first is built.
code-inputis “a single textbox with the whole code as its value” —Role.TEXT_FIELD, one Tab stop, boxes with no role at all.calendaris “grid with each cell’s full date as its name” —Role.GRID, cells that are parts.date-pickeris “combobox owning a grid, with the formatted date as its value text” —Role.COMBO_BOX.color-pickeris “combobox with the hex as its value text”, and it gets half of that one: its closed swatch is aRole.BUTTONwhose accessible name is the hex, which is as close as a name can come to a value. The second half of all four needs the same thing and there is nowhere to put it:Semanticsis a role, a name and a liveness, with no value channel and no per-cell channel for any widget. So this is the AccessKit bridge’s entry rather than any of theirs, and the four are named because they are the controls whose specifications spent a sentence on what they would say. M5. — ADR-0276, ADR-0274, ADR-0273 - A trail is not a landmark, and a crumb is not a link. §6 gives
breadcrumbs“navigation landmark containing links, current page marked”, andRolehas neither a landmark norLINK: the row answersRole.GROUP— “a boundary with content in it and no better word” — and the crumbs answerRole.BUTTON, which is true of what pressing one does and silent about what it is. The third of the three, “current page marked”, is built, through:checkedand the accessible name. Filed rather than guessed at, for the reason the two entries below are: a role nothing can consume is a constant written for a bridge that does not exist, and addingLINKand a landmark now would make this gap look closed. M5. — ADR-0306 - A slider with two axes has no role, here or in ARIA.
color-picker’s plane answersRole.SLIDER, which is true as far as it goes — a control whose value you move continuously — and says nothing about the second axis.GROUPis “a boundary with content in it” and a plane has none;GRIDpromises cells addressed by row and column, which is the one thing a continuous plane is not. Filed rather than guessed at: the answer is probably a role and a second value channel, and the shape of that depends on the AccessKit bridge nothing has built. M5. — ADR-0276
Answered
Kept rather than deleted: each is a trap somebody hit, and the reasoning that got out of it is usually worth more than the fact that it is fixed.
-
The LGPL corresponding-source offer for FFmpeg is not decided.The licence texts,NOTICEandffmpeg-NOTICE.txtwith the tag and configure line ship with the natives jar, and relinking is-Dgoldberry.media.libdir. What LGPL-2.1 §6 also asks of a binary distributor — the source itself, or a written offer of it — is not settled, and it gates FFmpeg’s first appearance on Central in a release. — ADR-0495, ADR-0490Closed — ADR-0508. The binaries are FFmpeg itself in object form, so the clause is LGPL-2.1 §4 and not §6, as this entry had it: the complete corresponding source goes with them, or is offered from the same place.
goldberry-mediapublishes it as itsffmpeg-sourcesclassifier, one jar per version, beside everyffmpeg-<target>— snapshots included, because a snapshot on Central is a distribution too. It holds FFmpegn8.1.3and dav1d1.5.4fromgit archive, checked against commits now pinned beside the tags (the superbuild checks its clones against the same ones), the superbuild as the recipe, every target’s notice, the licence texts, and a README on rebuilding offline and relinking; 24 MB, byte-identical from a fresh clone.goldberry.publishrefuses anyffmpeg-<target>without it before a single module uploads. -
“Starts in milliseconds” is still unproven.The timeline exists and the first numbers are in ADR-0028 —SDL_Init(VIDEO)is ~99ms and dominates, while mappinglibgoldberryis under 2ms — but they were measured undergradle run, which adds a launcher and its own JVM. The headline claim needs the example launched directly. — ADR-0028Closed — ADR-0506. Launched directly, timed from
execby an outside clock: the native image opens its window in about 120 ms and presents its first frame in about 520 ms (365 ms with the GPU off); the JVM takes about 2 s, 1.3 s with a JDK 25 AOT cache. The entry’s premise was the timeline, and the timeline was wrong: its zero wasProcessHandle’s start instant, which on Linux is built from a boot time in whole seconds and was 218 ms late on this boot, so ADR-0028’s 533.8 ms “runtime starting” was never what it said.ProcessAgereads the kernel’s clock at both ends now and agrees with the outside clock to 10 ms. SDL’s video subsystem, the 99 ms this entry named, is 14 ms. -
No primary selection.X11’s middle-click buffer has its own SDL calls (SDL_GetPrimarySelectionText) and is unbound: it is one platform’s idea, and the widgets that would fill it — a text field on X11 — would have to know they are on X11.Closed — ADR-0504. The three SDL calls are bound (ABI 17) and offered as an optional
PrimarySelectiononBackendandHost: the sdl3 backend offers one only when SDL’s driver isx11orwayland, and the headless backend an in-memory one a test can turn off.text-input,text-area,Editorand the content views publish a finished selection — a pointer selection on release, a keyboard one when the key lands, and not the select-all a Tab arrives with — and a middle click in a field moves the caret there and pastes, as one undoable edit. Apasswordnever publishes. The premise did not hold: no widget knows it is on X11, because a field only asks its host whether a primary selection exists. The trap was the one the entry did not name — SDL answers these calls on every driver, from a private in-process buffer off X11 and Wayland, so the decision is the driver’s name in the backend and not whether the calls work. -
What a widget writes there is unthemeable and unoverridable — right for a number nobody else can compute, wrong for anything else. It has one rule (“only what a stylesheet could not have written”), and the count it was written for has outgrown the sentence that watched it:Styled.restyleis an escape hatch with nine overrides now, and the honest risk is what goes into it.ColorSwatch,SegmentedDivider,SegmentedIndicator,TabIndicator,Tab,ScrollContent,ScrollThumb,ScrollViewportandAffixContent(counted 2026-09-30). The signal this entry set — a caller that is not a count — has not been read against those nine.Closed — ADR-0499. Read against the rule, seven of the nine write a number no stylesheet could have: from a count, the application’s data, a measurement, input, or a sum §8 has no
calc()for. Two wrote something a stylesheet could have.SegmentedDivider’sopacity: 0is thebeside-selectionclass and a rule incontrols.cssnow, andScrollContent’sflex-shrink: 0was a pin against the stylesheet and is set inrender. No picture changed.RestyleSweepTestholds every override to a list with its reason, so a tenth fails until somebody writes down why a stylesheet could not have written it. -
Nothing paints a tray icon for you, and nothing swaps it on a theme switch.The mechanism is there —TrayIcon.icon(pixels)andBackendTray.icon— and §9’s “theme-aware light/dark variants” is an application’s twoPixelBuffers and aWindow.onSystemThemeChangedhandler it has to write, which rebuilds the tray with the other one. The toolkit ships no default mark of its own, so a tray with no icon is whatever the desktop draws for an application that supplied none.Closed — ADR-0501.
TrayIcon.icons(forLightShell, forDarkShell)carries §9’s two variants, each named for the panel it sits on rather than for its ink, andTrays.showswaps the icon in place throughBackendTray.iconon every theme change, leaving the menu alone;forLightShellwhere the desktop says nothing, as CSS reads no preference. Reality differed in two places. The swap could not be built without a leak:Host.onSystemThemeChangedreturned nothing, and a tray is closed and shown again whenever its menu changes, so it returns aSubscriptionnow, which the tray’s handle closes. And the setting SDL reports is the desktop’s application theme, not the panel’s shade — GNOME’s top bar is dark either way — so a pair follows the best signal there is rather than the truth. The other half stands on purpose: no default mark ships, because a tray icon names the application and Goldberry’s on every one that forgot would misname them all. -
, re-running the whole cascade at every ancestor, so it is O(depth × rules) where it could be O(rules). The style cache amortises it almost to nothing — each level is cached against its parent map’s identity (ADR-0152), so an ancestor’s cascade reruns only on a miss — but a first frame and every invalidated subtree still pay it. Worth doing when a deep tree makes a first frame visible. — ADR-0070customPropertiesForstill walks to the rootClosed — ADR-0502, and the entry had the cause wrong. Measured with
DeepTreeStyleBenchmarkat depths 51, 101 and 201 against the catalog’s sheets: the walk never re-ran an ancestor’s cascade, because ADR-0152’s cache and the renderer’s top-down order make every ancestor a hit, and it cost 0.5–1.5% of a first frame. What cost was the line after the cache check — each node copying the root’s ~180 inherited custom properties into a fresh map and comparing it back, to find that it declared none: 48–63% of a first frame’s style resolution. A node now copies only when one of its own--*winners differs from what it inherits. First-frame resolution is 1337 → 505 µs at depth 51 and 7796 → 4098 µs at 201, with identical results, whichCustomPropertiesCacheTestchecks against an uncached walk. The term that still grows with depth is descendant-combinator matching, recorded in the ADR and not scheduled. -
A§5 asks it to scroll a target into view, andtourcannot find the viewport its target is in — read against the code, and it stands.Stoptakes aScrollControllerthe application supplies. Discovering it means walking from an element to its nearest scrolling ancestor.BuildContext.findAncestorStatelooks like the answer and is not: it walks up from the element being built, and what a tour needs is a walk up from the target it names — a different question, and one the tree offers no way to ask. ADR-0120 avoided the same wall by turning the question around; here there is nothing to turn around, because the tour is not the thing being revealed. — ADR-0268, ADR-0121Closed — ADR-0439. Both premises are true and the conclusion is false, which is why re-reading it twice did not catch it.
ElementimplementsBuildContext, sofindAncestorStatewalks up from whatever element it is called on rather than from the one being built; andHost.anchor(id)already returns a region whoseowner()is that element — the tour was calling it on every build for the rectangle and discarding the owner.ScrollScope.enclosing(target)is the walk. ADR-0120 had written down thatfindAncestorState“stays, because it is how an application-levelscrollIntoViewfrom inside a scroll view reaches the viewport”, which is this call, kept for it, three hundred decisions earlier.Stop.within(controller)survives for the application that means an outer viewport, since the walk finds the innermost. -
It is now (ADR-0311), and this entry closed the case on the wrong evidence. It was right thatmarginis not in §8’s subset, whichtab-newfound afterborder-bottomandcurrentColor.tab-newstopped wanting one — what that widget reached for was a way to sit somewhere other than the top of its row, whichalign-selfanswers (ADR-0244) — and wrong to conclude from it that the property had no consumer, becausealign-selfis the cross axis. On the main axis a box that wants to centre itself, or to sit at the far end of a row its container is not arranging for it, had no spelling at all:justify-contentis the container’s decision about every child at once, and aflex-grow: 1spacer is a box in the tree that draws nothing.margin: 0 autoandmargin-left: autoare what those are, and Yoga’s binding has had theautocall since ADR-0029.The entry’s other half stands and is worth keeping: three properties a widget reached for and did not find, all silently ignored, and the subset is right to be small.
and nothing warns when a declaration is dropped.Something does now, for the toolkit’s own sheets:border-bottomwas written a fourth time, intable-head, and drew nothing (ADR-0215).SupportedPropertyTestresolves every rule the catalog and the showcase ship through the real cascade and fails on anything reported as unsupported — so a dead declaration is one failure with the property in it rather than a debug line among thousands. And on anything reported as a bad value, since ADR-0216:border-radius: 7px 7px 0 0andbackground: nonewere two more rules doing nothing, with the property spelled right and the value refused. An application’s stylesheet is still on its own, deliberately: namingbackdrop-filterbefore it exists must not stop a window opening. (That sentence saidbox-shadowuntil ADR-0310 built it;backdrop-filterandletter-spacingare what is left of §8’s unimplemented list.) — ADR-0216, ADR-0215, ADR-0109 -
The catalog’s specified surface roughly tripled, and most of it is built now.docs/core-widgets.mdgained twenty-one widgets and four options in one pass —link,affix,segmented,date-picker,time-picker,color-picker,code-input, autocomplete on bothtext-inputandselect, tree-select,collapse,carousel,statistic,skeleton,breadcrumbs,steps,wizard,message,tour,tree,calendar,timeline, andbutton’soutlined/square/circle/floatoptions — each with adesign-system.md§3 metrics row and, where it moves, a §3.1 row. §5 requires a spec and a metrics row and gallery coverage before code, in that order: they had passed two gates of three, and the third is what “built” means.This entry said “none of it is built”, then “one of them is built now”, then “four widgets and four options are left” — and now none are. The last four went in on 2026-09-17:
link(ADR-0346),stepsandwizard(ADR-0344),timeline(ADR-0345), andbutton’soutlined/square/circle/float(ADR-0347). What each left behind is its own entry under The catalog below.Everything else on it went in:
segmentedfirst, thenaffix, the three pickers,code-input, autocomplete on both controls, tree-select,collapse,carousel,statistic,skeleton,message,tour,tree,calendar, andbreadcrumbslast.The way
segmentedwent is still the argument for writing them down first, read from the other end: two of its five specified metrics and both of its specified transitions turned out to be undrawable in §8’s subset, and that was found by implementing it rather than by writing it. The point of writing them down first is that the arguments are cheap then and expensive later —messageagainsttoast,segmentedagainstradio-group,code-inputagainst a styledtext-inputare all decisions that would otherwise have been made by whoever happened to need one, and none of them was. -
, which changes what M5 owes. ARCHITECTURE §17 defers “tables/trees”;treemoved from deferred to specified, andtablehas since followed ittablestill is, because it waits on virtualization, buttreereuseslist’s model and item-factory and does not — andselect tree=#trueneeds it, so the two arrived together.Answered — both are built.
tablestopped waiting on virtualization when virtualization arrived: it is a list with columns (ADR-0214), anddocs/ARCHITECTURE.md§17 lists both as built. Nothing is left of M5’s debt here. -
A toggle’s thumb does not follow the pointer during the drag — and the design system says it should not.Left open as a defect after ADR-0075 and closed by reading rather than by building: §1.7’s first principle names the controls that track 1:1 — “drags (slider, knob, fader, splitter, scroll) track the pointer 1:1” — andtoggleis not among them, while §3.1’stogglerow asks for the opposite, “thumbtranslatebase”. A switch here is a control with two positions that animates between them, and tracking the finger would be a third behaviour neither document asks for. It would also cost the mechanism the entry named: transient per-element state for a value that is neither the model’s nor the stylesheet’s, which nothing else in the catalog wants. Reopened only if the design system changes its mind, in writing. — ADR-0075,docs/design-system.md§1.7, §3.1 -
The toggle does not shrink with a compact density, and that is answered rather than open (docs/widgets-finishing.md, ADR-0356): §3’s row carries no compact value fortogglewhere the rows that shrink carry one, so the pill staying 36×20 inside a 28-tall row is the specification rather than a gap. Kept here because the screenshots are what would say whether §1.3 meant it. Read off §3 rather than decided: the rows with a compact value carry it in parentheses and thetogglerow does not, so the pill stays 36×20 while the row around it takes--gb-toggle-height. Whether a 28-tall row holding a 20-tall pill is what §1.3 intends is a question for whoever writes the compact screenshots. — ADR-0075, ADR-0074 -
No file lists.text/uri-listis bytes like anything else and works today, but nothing turns those bytes into paths. Drag-and-drop is a different platform mechanism and is built now:Window.onFileDropdelivers oneFileDropper gesture, with the paths and the point they landed on (ADR-0330). What is still unbound there isSDL_EVENT_DROP_TEXT— the same shape, and nothing has asked for it.SDL_EVENT_DROP_TEXTclosed — ADR-0408. “The same shape” turns out to be literal rather than loose: SDL tokenises dropped text on\r\nand raises one event per line, then one sharedDROP_COMPLETEfor both kinds — soTextDropcarries a list of lines, and a test exists specifically to stop the shared completion turning a file drop into a text drop. What had blocked it was diagnosed here and is worth keeping: the blocker is a missing constant, not a missing symbol, so adding the enum value fails the layout probe rather than the link, and the bill is a shim row and an ABI bump on four platforms. ADR-0422 was bumping the ABI anyway, so the bill was already paid.The other half closed too — ADR-0406. “Nothing turns those bytes into paths” stopped being true on 2026-09-19:
UriListreadstext/uri-listinto names, and the entry was never told. Found by the 2026-09-30 sweep. -
LGPL relinkability means libVLC stays a separate shared object with its plugin tree beside it, and every packaging rule ingoldberry-mediabreaks the one-library assumption.:natives— one static library, hidden visibility, one export list — assumes the opposite. It also needs a codec/patent note written before it gets code, whichcontent-widgets.md§8 says and this list repeats because it is a gate rather than a caveat.Answered — libVLC is gone, and both halves went another way.
:mediadrives FFmpeg from Java (ADR-0460) and binds its own libraries outside:natives(ADR-0461), so the one-library rule is untouched and there is no plugin tree. FFmpeg ships as shared objects under sonames of its own (ADR-0490) inffmpeg-<target>classifier jars, and-Dgoldberry.media.libdiris the relinking path (ADR-0495). The codec note was written before the code: royalty-free codecs only, with theDecoderSPI for the patented ones (docs/goldberry-media.md). What is still open about shipping it is under Build, artifacts and release. -
Text selection in, and it is the same character-quad work as text-editing depth (html-viewis deferredARCHITECTURE.md§17) and aspdf-view’s selection. Three widgets waiting on one mechanism is an argument for building it once, in core, rather than in whichever module lands first.Answered — ADR-0301. Both views select, copy and highlight, through geometry the frame already had rather than character quads. The
goldberry-htmlentry above has said so since 2026-09-13; this one was never struck.pdf-viewis still unbuilt. -
Built, and not as a module (ADR-0441). Every word of the entry was true about Servo and none of it was about the question: libservo is Rust-only against a deliberately unstable API, so the module would indeed own agoldberry-webis parked, not deferred.cdylibshim and its breakage — and nobody asked whether a page needed an engine of this project’s at all.webview/webviewis MIT, is one header, and brings no engine: it drives the WebKitGTK, WebView2 or WKWebView the desktop already has, so neither condition that quarantines a content module applies. This is the fifth entry in this run of work that was wrong about itself, and it is the most expensive kind for the second time: “parked” reads like an answer and stopped anybody re-reading it for two milestones.What it is not is a widget.
webview/webviewcannot render offscreen, so a page is always a platform window; and a Wayland session allows neither reparenting a foreign surface nor placing a window where a widget is, so aweb-viewin a layout would be a box on X11, Windows and macOS and a loose window on the default Linux desktop. It ships as §9’s secondwidget.shellmember instead — a value and the call that opens it,tray-icon’s shape. CEF-OSR stays the documented escape hatch, and is still the only engine that would have made a box possible.That paragraph was overtaken the next day (ADR-0442).
web-viewis a widget wherever the window system allows a child window: on X11 the page is reparented into the toolkit’s window and is never the window manager’s (ADR-0446), and the window stays on the GPU (ADR-0491). On macOS it is a subview (ADR-0458). On WindowsSetParentis written and unverified. On Wayland it opens nothing and paints why, rather than a loose window.WebViews.openstays as the separate-window form. -
Nothing in the catalog uses a margin yet.It does (ADR-0312):dialog-actionswrites the top margin §2 asked for instead of thepadding-topthat stood in for it, andtour-card’s footer lost theSpacerthat pushed Skip away from Back and Next —margin-right: auto, pixel for pixel the same picture. The showcase’s notice bar likewise.spaceris not deprecated: it is a §1 widget an application writes in markup, and a document has no stylesheet of its own to put a margin in, so the showcase’s status bar keeps one on purpose with the notice bar beside it as the comparison. -
A dropped declaration is reported once, andThe Panels screen filled the console while it scrolled:align-items: startis why.startis CSS’s alias forflex-startand Yoga has only the second, so the declaration was dropped — correctly — and reported per element per style resolution, which on a moving screen is sixty times a second. The typo is fixed and the report is now deduplicated by property and value, because a stylesheet is static and a value that is not one cannot become one on the next frame. What changed since:startandendare taken now, because they are not aliases but CSS — Box Alignment Level 3 defines them and Yoga has only theflex-pair, so the toolkit had been dropping a declaration the specification allows (ADR-0247).leftandrightare still refused, and for a reason rather than an omission: they are not the same asstart/endunder RTL. -
A segment’s focus ring lands exactly on the bar’s edge.§2.2’s ring is 2px at a 2px offset and the bar’s inset is 2, so the two coincide — legible insegmented-focus.png, and an accident of two numbers derived separately rather than a thing anyone chose. If either moves, look at the image.Answered — ADR-0217. One of them moved: the bar’s inset is 1px now, and the ring sits off its edge. Answered on 2026-08-30 and found still open by the 2026-09-30 sweep.
-
A generated registry can fail at class-init time now, and only for private members.AVarHandlelookup that cannot find its field throwsExceptionInInitializerErrorwhere a direct field reference would have thrownNoSuchFieldErrorat link time — the same class of failure with a different exception, and both are impossible within one compilation, which is how a registry and its model are always built. Recorded because it is the one thing ADR-0098 moved later rather than earlier.Answered — ADR-0125. There is no generated registry any more: the weaver rewrites the model’s bytecode, and
weaver/src/mainholds noVarHandle. ADR-0125 superseded both ADR-0096 and ADR-0098, and this entry outlived them. -
How damage is computed, and the bug a resize found in it.Each render object remembers where it was, and a node that changed damages the union of where it was and where it is — both, because damaging only the new position leaves the old drawing on screen. It reads the node’s own changed flag rather than its subtree’s, or a parent whose child moved would report the whole window. A resize broke it in the field: a remembered rectangle belongs to the previous frame, so the union fits neither when a window is dragged a pixel narrower, and the backend refused the frame mid-drag. Damage is now clamped on the way out rather than only where each rectangle is computed — and the regression test resizes by one pixel, because that is what a drag produces and a test that jumped by fifty would have passed against a fix that only handled large changes. Every damage test had used a single frame size, which is the natural thing to write and the one case that cannot fail. — ADR-0071, ADR-0072 -
Both ways to run the showcase headlessly were broken, and one of them cost a desktop session.goldberry.backend.videoDriverexisted and was not in:example’s forwarded-property list, so-Dgoldberry.backend.videoDriver=dummyreached the Gradle daemon and stopped there — the exact failure the comment beside that list already described forgoldberry.log.level. The obvious fallback,SDL_VIDEODRIVER=dummyin the environment, does not work either: aJavaExecfork inherits the daemon’s environment rather than the onegradlewwas invoked with, so it applies or does not depending on how the daemon happened to be started — which reads as flaky rather than as broken. A run intended to be headless therefore opened a real Wayland surface and took GNOME Shell down with it. The property is now forwarded, and./gradlew run -Pgoldberry.backend.videoDriver=dummyis the checked way to drive the showcase without a compositor. -
What does the release container actually compile into its Wayland driver?Two dependencies decide it andlinux.ymlinstalls neither.eglis one of the five specs in SDL’s singleCheckWaylandpkg_check_modules— lose any one and the entire Wayland driver is dropped silently, and the container has nomesa-libEGL-devel.libdecor-0decides whether a Wayland window that does get built has a titlebar and a resize edge. The manylinux leg runs CMake directly with no JDK, socheckToolchainnever gets to ask either question, and the drift guard deliberately held that workflow only to the packages SDL refuses to configure without. Answering it means readingSDL_VIDEO_DRIVER_WAYLANDandHAVE_LIBDECOR_Hout of a container build’sSDL_build_config.h— not another look at the table. — ADR-0082, ADR-0083Half of this moved while G32 was being closed (ADR-0325), and what is left is now a packaging problem rather than an unknown. Measured in
quay.io/pypa/manylinux_2_28_x86_64:dbus-devel,systemd-devel,ibus-develandmesa-libEGL-develall install and all provide their.pcfiles;libdecor-develandxkeyboard-configare in no repository the container has, so those two cannot be fixed by adding a line to the workflow. The superbuild does now read SDL’s generatedSDL_build_config.hand cross-checks it against its own probe — forHAVE_DBUS_DBUS_H,HAVE_IBUS_IBUS_HandHAVE_LIBUDEV_H, which is the same machinery this question asks for pointed at three other defines — and the drift guard now also holdslinux.ymlto every package a capability depends on. Extending both toSDL_VIDEO_DRIVER_WAYLANDandHAVE_LIBDECOR_His the remaining work, and the honest form of it is probably aCapability.WINDOW_DECORATIONS, since the answer for the container may be “it cannot” rather than “install this”.Closed — ADR-0422. It guessed the honest form right:
Capability.WINDOW_DECORATIONSandCapability.WAYLAND, warned about rather than required, becauselibdecor-develis unavailable in the release container and aREQUIREDprobe there would produce no library at all. What it did not anticipate is that extending both was the wrong move. The existing pattern is a pkg-config prediction confirmed against SDL’s generated header, and SDL decides the whole Wayland driver with one check over five specs plus a scanner binary — so a prediction narrower than that reports “present” where SDL reports “absent”, and the cross-check then fails a build that was fine. These two are therefore read out ofSDL_build_config.hand never predicted, which is strictly better where it is available and is why a header SDL did not generate now reports both bits absent rather than carrying on.WINDOW_DECORATIONSis also deliberately narrower than it sounds: it is libdecor at build time and says nothing about ADR-0084’s plugin, because a bit that was set on the exact machine where the bug is would be the worst possible value. -
The export machinery has now caught the same class of bug three times.--exclude-libs,ALLforced static-archive symbols local, soSDL_Initlinked in without being exported; removing the flag fixed it, because a version script cannot promote a symbol already marked hidden. Blend2D then hit the identical wall from the other side: a static build definesBL_STATIC, which makesBL_APIexpand to nothing, so the superbuild’s globalhiddenvisibility applied to every Blend2D function. All 13 linked in and arrived local —nm -Dshowed none of them whilenmshowed them all ast. HarfBuzz then did it a third time and more bluntly:HB_EXTERNis defined as bareextern, with no visibility attribute at all, so all 24 of its symbols went local too. Fixed by giving both targets default visibility; the version script’slocal: *still gates the output. The fix is a loop rather than two blocks, because the next static upstream will probably need it as well. The equivalent question on the MSVC.defand Mach-O-exported_symbols_listbranches is still answered by the next CI run rather than by argument — and the Mach-O branch has the same dependency on visibility that this fix addresses. — ADR-0018, ADR-0031Answered by CI, as it said it would be. The Mach-O branch linked and passed on
macos-14(ADR-0338), and Windows is green on both generators, with the force-link list moved into a file like every other platform’s (ADR-0454). -
Only aFollowing is a property of having been opened by id, which ispopoverfollows a scrolling anchor; amenuand aselecthold the rectangle they opened against.Popover’s documented shape and the one the entry that asked for this named.MenusandSelectStateresolve the anchor to a rectangle themselves, because they want a minimum width and aFitas well and noHost.popupoverload takes an id and those two. It is one overload’s worth of work and nothing has asked for it: a dropdown is dismissed by a press elsewhere, and the wheel over an open one scrolls its own list. — ADR-0270, ADR-0145Closed — ADR-0432. The overload exists —
Host.popup(content, anchorId, placement, minimumWidth, fit)— andMenusopens by name through it. The entry is wrong aboutSelectState: aselecthas no id to be anchored by,SelectFieldisLocatedand takes its rectangle from the frame, and ADR-0119 explicitly rejected generating one because two unnamed selects in a window would then depend on that generation being unique. So onlyMenuswas a customer andselectstill does not follow. The entry’s own “nothing has asked for it” stands as the value of this record on its own; what it is really for is being the prerequisite of the entry below. -
A popup whose anchor scrolls out of sight follows it out of sight.Now that apopovertravels with its anchor, an anchor scrolled past the top of its viewport takes the popup with it, and the placement clamps it to the work area rather than dismissing it — so a menu can end up pointing at a widget that is no longer drawn. The region carries the clip that would answer “is it still visible”, so the mechanism is there; what is missing is a decision about what should happen — close it, hide it, or pin it to the viewport’s edge — and nothing has asked for one yet. — ADR-0270, ADR-0114Closed — ADR-0433. The decision is close, and the other two were rejected for reasons worth keeping. Pin is the only one that makes the toolkit lie: a menu parked at the viewport’s edge points at whatever row scrolled up to meet it and the user cannot tell. Hide leaves a popup holding the keyboard, so
Downmoves a selection nobody sees andEnterruns a command nobody chose. Close costs the in-progress interaction and nothing else, which is the one failure a user can see and undo. The threshold is intersection rather than containment, and closing takes the popups opened after it, since a submenu anchors to a rectangle inside its parent. The region’s clip answers only half the question, which the entry assumed was the whole of it: a row scrolled fully away is still reported — ADR-0114’s empty-clip stop is about a subtree’s own clip — and a box with no clipping ancestor sits under an infinite clip, so the predicate is the clip and the window’s rectangle. One shipped behaviour changed: ADR-0270’sfollowsAScrollingAnchorasserted a menu travelling with an anchor that had left the window entirely, which is the picture this record calls wrong. -
ATwo columns at 1200px are two columns at 720px — half as wide and twice as tall — because the count is a constructor argument and no selector can count columns. This used to say that “as many columns as fit at a minimum width” is “a layout pass that reads its own width, which is the loop ADR-0196 built the last-frame read to avoid”, and that reads the record backwards: ADR-0196 is the last-frame read,masonry’s column count is a number and not a breakpoint, and what stops it is the spec gate rather than the mechanism.masonryalready banks every card’s height throughMeasured, and reading its own width is the same door one step over.Measured’s third rule holds for it too, with one caveat worth stating — a column count changes the masonry’s height and not its width, so the number is stable under the thing it causes for a masonry whose width comes from its parent, which is every one in the showcase and not every one imaginable. What actually blocks it is thatmasonryhas no row indocs/core-widgets.mdat all — it is named once, as what the showcase’s screens are made of — so §5’s spec-then-metrics-then- gallery gate has nothing to have passed. — ADR-0222, ADR-0196Closed — ADR-0436.
min-column-widthis built, exclusive withcolumns, defaulting to 320, with the wall’s own width read throughMeasuredfromMasonryBox— and the resolvedgapread with it, becausencolumns neednminimums andn−1gaps and counting without them over-counts at every boundary. It settles in 3 passes worst case, which matters exactly:Offscreenmeasures twice and paints the third, so a responsive wall is photographed settled with nothing to spare. The spec row written for this task was wrong about shrink-to-fit and has been corrected:masonry-columnisflex-basis: 0, so a wall with no definite width measures zero whatever its count is and the count is independent of itself by construction — degenerate and stable, and identical with a fixedcolumns, so it is a pre-existing defect rather than anything this option introduces. The showcase adopts it on the Basic screen only, at 560 rather than 320, because at 320 a 1168-point wall becomes three columns a third narrower than its cards were built for. -
The gallery goldens cannot see typography at all, and the 150% half is now waiting on a decision rather than on a mechanism.GalleryGoldenTestbuilds its renderer with the single-font constructor — which ignoresfont-family,font-sizeandfont-weightby design, so that a golden image is not a test of whichever Inter is on the machine — so every screenshot draws prose, headings and button labels at one size. A screen with no typographic hierarchy looks exactly like a screen with one, which is how a screen title and the paragraph under it stayed the same 13px with nothing catching it.ShowcaseTypographyTestasserts sizes through the cascade instead. The clipping half read as a gap in the tests and was a gap in the toolkit: nothing enforced §1.4’s 150% because nothing implemented it, so there was nothing for an image to be of.renderer.textScaleexists now (ADR-0267) — it scales the text and deliberately not the boxes, which is the condition §1.4 asks components to survive. What is left is what to assert: sincetext-overflow: ellipsisshipped, some cutting is correct, so “no text is clipped” is no longer the sentence, and a golden of eleven screens at 150% would pin every one of those decisions at once in a picture before anybody had taken them. — ADR-0267, ADR-0118Closed — ADR-0435. The assertion is a rule, not a picture, and it is differential between 100% and 150%: no line is cut without something asking for the cut, and no box overruns its container. Three candidates were rejected, one of them by measurement — “every ellipsis at 150% was reachable at 100%” is backwards and false on the corpus, since HTML gains one correct marked cut and Markdown two. The entry’s premise is also wrong in a way that would have made the picture worthless:
renderer.textScaleexists but does not reach the gallery, becauseWidgetRenderer’s one-font constructor discards the style the scale is applied to — a 150% golden taken the way the gallery’s are taken would have photographed the 100% tree and passed for ever. The audit opens a book instead, making it the first check here to lay the gallery out with realfont-family,font-sizeandfont-weight.OverflowWatchanswers half: its noise is a fact, its silence is not, because the walk is gated on the root node’shadOverflow. The gallery does not survive 150% today — the overruns are carried as a named ratchet, and the five the narrow Basic screen had are already gone, removed bymasonry’s responsive columns rather than by anything aimed at them. -
A widget that sizes itself from last frame’s measurement lags its own content, and one that does so in a way that changes the measurement never settles. Nothing enforces the rule that keeps it safe — read geometry to interpret an input or to draw something that cannot affect layout, never to decide a size — and the scroll view obeys it by construction rather than by check. — ADR-0117Measuredis a door every widget can now open and almost none should.Closed — ADR-0420.
Settled, a harness that drives the real frame loop to a fixed point and fails on oscillation, now holds six consumers to the rule. A runtime check was considered and refused, because it cannot tell an oscillation from a resize drag. There are eleven consumers now, not one, and the interesting number is how many actually feed back: masonry, scroll and split-pane settle in 2 passes; table, text-area and text-input in 1, meaning they show no layout feedback at all — so the harness also asserts that a consumer was reached, which is what caughttoastplacing no box in a windowless harness and passing vacuously.toast,tour,imageandIconSheetstay uncovered, each with its reason written down. -
A row’s focus name still collides between two unnamed lists.host.focustakes a name global to the window, andlistscopes its rows by the list’s ownid— which settles it wherever an application named one, and leaves the case of two lists, both unnamed, holding an item with the same identity.treehas the unscoped version of the same thing. What would close it properly is a focus name that is relative to a subtree, which the router has no notion of. — ADR-0212Closed — ADR-0437.
PointerRouter.focusByIdresolves inside the enclosingfocus-scopechain before falling back to the window. The entry says the router “has no notion” of a subtree and it does —enclosingScope, used for traversal and never for resolving a name — so the fix is about eight lines and no published API moved.focus-scopeis the right naming boundary for a reason that is not a coincidence: the elements a composite manufactures names for are exactly the ones its arrow keys rove over, because a row gets a name so thatEndcan reach it. The entry also understatestree, which is not merely “the unscoped version” — a named tree collided too, where a named list was already settled by its id prefix. One residue is written down rather than fixed: a virtualized unnamed list’s not-yet-built row is not in its own scope to be found, so the cross-frame retry can still reach another list’s row for one frame. -
It now has two callers, which is what movedSelectListis in the wrong package.Optioninto a package of its own; it stayed put because the CSS type it carries isselect-list, so moving it renames a type in every stylesheet and every golden rather than editing one file. Autocomplete itself reaches markup throughsuggestions=andoptions=(ADR-0367). — ADR-0182Closed — ADR-0417. The reason this entry sat is false.
cssType()returns the string literal"select-list"; nothing derives a CSS type from a class’s package or simple name, so the move renamed nothing in any stylesheet, golden or test — everyselect-listincontrols.css, theselect-list-darkgolden name andSelectTest’s assertion are byte-for-byte unchanged. Eight Java files moved and that was all. The other half of ADR-0182’s note was true and was the real defect: the class documented itself as “not constructible” while sitting public in an exported package, so the new package is not exported. -
A slider maps the pointer over the track’s full width, so at the extremes the thumb’s centre is up to 8px from the finger. Mapping over the travel needs the thumb’s width, which is the stylesheet’s and not the widget’s. The mapping is monotonic and reaches both ends exactly. The door this entry named is open and a different one is shut: “a widget being told a resolved metric” isPaints.Context.lengthand has been since ADR-0251 — but it is arender-time read, and the pointer arrives atonPointerwhere there is no context to ask.scrollsolved exactly that by banking the number into itsState; aSlideris arecordwith nowhere to bank one, so closing this means makingsliderstateful. That is still a bigger change than 8px, and it is now a different sentence. The tick marks do not have this problem: their inset is half a thumb, written in the stylesheet beside the thumb’s own width, so a mark and the thumb agree exactly while the finger is the thing that is up to 8px out. — ADR-0080, ADR-0079Closed — ADR-0430.
slideris stateful onscroll‘s arrangement —Slider(record) buildsSliderControl(the CSS type), andSliderStatebanks--gb-slider-thumb-sizeread atrenderforonPointerto use. The mapping is over the travel. The entry was right about everything including the tick marks, whichSliderGeometryTesthad been asserting all along and which pass untouched. One cost it could not have known: there is nocalc(), so the thumb’sborder-radiusandslider-ticks’ inset stay hand-maintained halves of the new token, held together by a test rather than by arithmetic. -
An indeterminate bar turns where it should run off the edge, and that is now a choice rather than a limit.progress’s indeterminate sweep travels there-and-back within its track because the off-the-edges drawing — the more common one — needs the bar clipped at the track’s edges. This entry said nothing clipped;overflow: hiddenhas shipped since ADR-0114, so the drawing is available. What is left is a design decision about a shipped animation rather than a missing mechanism — and it is the only thing left on ADR-0235’s list, now that the label half has been built (ADR-0255). — ADR-0235Closed — ADR-0418. The sweep crosses and leaves — −100% to 333% of the bar’s own width — with
overflow: hiddenwritten incontrols.cssrather than forced in Java, so the clip stays the stylesheet’s. Two goldens moved and were re-blessed;progress-determinate,progress-light,progress-reducedand both spinners did not, which is the evidence the clip costs the other drawings nothing. One new cost, named rather than hidden:Clipis a rectangle and not CSS’s rounded clip, so the track’s 2px cap squares off momentarily — about 0.86 px² per corner. -
NoScaling happens at the blit, which is where the destination size is known. A resampled copy — for a thumbnail written to disk — is a different operation and would need a filter argument thatImage.scaled(...).bl_image_scalehas and nothing has asked for.Closed — ADR-0428.
bl_image_scaleis bound asBlendScaledImage, mirroringBlendDecodedImage. The filter argument the entry names is real and mandatory in C, and the answer is an enum with a default: no single filter is right both for shrinking a photograph and for doubling a 16×16 icon, and the wrong choice is silent in both directions, soImage.scaled(w, h)is Lanczos andImage.scaled(w, h, Resampling)is the other four.BL_IMAGE_SCALE_FILTER_NONEis deliberately unbound — it is the absence of a filter rather than one of them. -
The frame sequence exists twice.Launcher.paintandOffscreenrun the same steps in the same order, and only one of them is the hot path with damage, frame statistics, the HUD and the models’ refresh woven through it (ADR-0284). Extracting the common core is the right refactor and was not taken during a feature: what holds them together meanwhile is that every golden image goes throughOffscreen, so a divergence moves a picture.Closed — ADR-0423.
FrameSequencein a new non-exportedframepackage holds the element tree, the render tree and the router, and both callers use it — every golden unmoved, which is the safety net this entry itself named. The entry was wrong that it is one sequence. There are three orders, not two: a window lays out, paints, then captures; a measuring pass lays out and captures without painting; the drawing pass lays out and paints without capturing. A single “run a frame” method would have needed two booleans about windows. So five of the six steps are shared anddrawis deliberately left out — it differs by design (ADR-0072) and has no ordering constraint to protect. -
No animation strip.One call, one picture. A caller wanting frame 3 of a transition wants to drive the clock between paints, which is an object with a lifetime rather than a builder that renders once.Closed — ADR-0424.
Offscreen.strip(Widget)returns aFilmstrip: a closeable object that mounts the tree once and answersadvance(millis)andframe()repeatedly. The hard part was what stays alive between frames, which the record states rather than leaves to be discovered — every frame gets its own buffer, and the pictures survive closing the strip. -
No reuse and no cache.Each render builds a fresh element tree and unmounts it, so rendering the same document twice does the work twice. A font book can be handed in and kept; nothing else can.Closed — ADR-0425.
Studiokeeps a renderer over a font book and hands out wiredOffscreenbuilders. The entry named the wrong thing as the cost. “A font book can be handed in and kept” is true, and the book was not the expensive part: the cascade index and the shaping cache were, and both live on the renderer, which had no way in. A result cache is still refused, and for a reason worth keeping — aWidgethas anequals, which is exactly what makes it tempting and wrong, because a card closing over a mutable model is equal to a stale one. -
Nothing renders off the UI thread, and nothing says it must not. A render touches no window and no backend, so a server thread is probably fine — “probably” is why it is written here rather than in the javadoc. What would have to be checked first is the shaping cache and Blend2D’s own worker pool.Closed — ADR-0425. Folded into the entry above, because they are one decision: the reusable unit is the renderer over a font book, and that is precisely the object that must not be shared across threads. Both suspects the entry named were clean. The shaping cache is per-renderer and already fail-fast; Blend2D’s pool is process-wide but already degrades to synchronous with identical pixels. The real hazard was
Fonts, which had documented confinement since ADR-0044 and enforced nothing — and the unsafe arrangement was the oneOffscreen’s own javadoc recommended, “hand over oneFontsand keep it”. That advice is gone and the assert is there;rendersConcurrentlydrives eight threads to a pixel-identical result. So the javadoc says it rather than saying “probably”. -
No word-wrap-awareThe page is ten lines, hard-coded, because an editor drawn on a canvas has no viewport to measure. A caller that knows its own height moves the caret itself.PageUp/PageDown.Closed — ADR-0410.
Editor.viewportHeight(double)makes a pagemax(1, floor(height / lineHeight)), counted in visual lines, with ten kept as the fallback and defended against the four alternatives. A page is the screenful rather than the screenful-less-one, because the overlap belongs to a scroll and this editor’s caller owns the scroll. The entry’s “a caller that knows its own height moves the caret itself” turns out to be more expensive than it sounds — it means re-implementingdesiredXcolumn-keeping and intercepting the key beforeonKey, which is the argument for the setter. -
The headless clipboard is eager.It keeps the bytes rather than serialising on demand, so nothing in a test exercises the laziness the platform imposes; the upcall path is covered in:nativesagainst the real SDL instead.Closed — ADR-0407. It holds a supplier per type now, so a test can assert that nothing was serialised until something asked. Every
readcalls the supplier again rather than caching, which is the platform’s actual contract: a double that produced bytes atwritetime rewards an application for serialising per copy instead of per paste, and the real clipboard then silently forgives it. -
A refusal is not modelled anywhere.Every write returns a boolean and the in-memory clipboard always returns true, so the branch an application writes for “the compositor declined” is only ever taken on a real desktop.Closed — ADR-0407.
refuseWrites(boolean)makes thefalsebranch reachable from a test, with the default behaviour unchanged — a test seam rather than a new policy. Worth noting thatClipboardDataTest’s own javadoc asserted this gap (“what it cannot model is the platform’s laziness or a refusal”) and was wrong from the commit that closed it; ADR-0286’s identical sentence is left as written, because it was true when it was written. -
Atext-inputholding a long value shows its end, not its beginning.TextEdit.ofputs the caret at the end and the field keeps the caret in view from its first layout, which is whattext-areadid until ADR-0297. The fix is the same flag and the same argument; it is not done here because a field is not a document and changing two controls on one screen’s evidence is how a fix becomes a regression somewhere nobody looked.Closed — ADR-0412. An untouched field shows the head; the caret stays at the end; a press, key, edit, composition or focus makes it chase again. No markup attribute — the default is the decision, and ADR-0326’s own argument applies, that a call site which must say where the caret goes can forget to. The entry said “the same flag and the same argument” and only the flag was the same: ADR-0326 had already fixed the read-only half and left a test asserting the editable tail on purpose, so this had to argue against a written sentence rather than against silence. It also turned up a real bug the entry could not have predicted — the flag was set after
apply()’s equality check, soEndon an untouched field did nothing at all. -
An icon larger than its slot overflows it.AnIconis a path built at a size and cannot be rescaled at paint time (ADR-0043), so a 20px glyph in a menu’s 16px leading column is 20px — centred now rather than parked in the corner, which is the difference between “large” and “misaligned”, but still larger than the column. An application that wants them to fit builds them at 16, and nothing says so at the door. — ADR-0143Closed — ADR-0419.
Icons.SLOTandIcons.bind(String)name the size at the door, andItemLeadreports an overhang at debug, deduplicated by name, size and column — ADR-0394’s rule, since a warning firing on every legitimately larger icon says nothing. The entry was misleadingly general:item-leadis the only slot in the catalog an icon can overflow, because every other widget usesBox.icon, which sizes the box to the glyph. It also corrected a comment that had the override backwards —.style(style)is applied last, so the CSS width wins, notBox.icon. -
An outer shadow is painted under the box, not cut out of it.CSS knocks the border box out of abox-shadowso a translucent background does not have its own shadow showing through from underneath. The toolkit paints the whole shape and relies on the box being drawn on top — and cannot do better today, because cutting the hole needs a path clip or a fill rule and the Blend2D binding exports neither. The obvious trick is worse than the problem: a reversed sub-path under the default non-zero winding fills the parts of itself the outer shape does not cover, so the inner half of a blur would paint a dark ring where it was supposed to erase one. It is invisible under an opaque background, which is every shadowed surface the design system has, and shows under a box mid-opacitytransition, which fades its shadow by the same factor and so darkens itself slightly. What it would take:BLContextSetFillRuleor a path-clip call on the export list, and then one reversed sub-path per band.ShadowPaintTest.throughATranslucentBoxpins the current behaviour, so the day that lands there is a test that says the deviation is gone. — ADR-0310Closed — ADR-0427. Each band is now filled together with the border box under
BL_FILL_RULE_EVEN_ODD, so a point inside both is crossed twice and left empty — one extra sub-path per band, no extra fill. The entry offered a choice that does not exist: Blend2D clips to a rectangle and nothing else, and bothbl_context_clip_to_rect_i/_dwere already exported, so there is no path clip to prefer and the fill rule was the only option rather than the cheaper one. Its reversed-sub-path warning was right and is why the rule matters. “Invisible under an opaque background” turned out to be true of interior pixels only: twelve goldens moved, every one of them on the anti-aliased arc of a rounded corner where the box covers a fraction of a pixel and the shadow beneath that fraction is now cut away — the same seam a browser has. The fix also retired machinery the entry did not mention: ADR-0310’soccludedband flag has one answer once the hole is cut, so culling moved toShadowGeometry.coveredAt.ShadowPaintTest.throughATranslucentBoxasserted the deviation and now asserts its absence. -
One non-text pair is below §1.2’s 3:1, and no colour can lift it.This entry said sixteen, and filed them as one thing waiting for one decision. Measured against the arithmetic rather than against the sentence they were three, and fifteen are fixed (ADR-0258). The twelve control boundaries were a gap in the palette nobody had put anything in: Nord stops between--nord3and--nord4, which measure 1.17:1 and 6.39:1 against--gb-surface-2, so a palette edge is either invisible or a white ring around a dark control —--gb-checkbox-borderis the midpoint, at 3.17:1 and 3.22:1. The three marks were--gb-accenton--gb-border, one pair wearing three names, missing by 0.02; the light accent slid to#5c7ea8and every other pair it appears in moved the same way, so there was nothing to trade against. What is left is the light theme’s slider thumb, and it is not a ramp question: the track sits between a white thumb and a dark accent fill, and clearing 3:1 against both needs its relative luminance at once ≤ 0.300 and ≥ 0.688. No solid colour is both. What has to change is what a light-theme thumb is — a border round it, or a fill that is not white — which is a sentencedocs/design-system.md§3 does not contain and is the one genuine decision in the original sixteen. Twenty-two goldens moved, which is why this had waited. — ADR-0258, ADR-0240, ADR-0239, ADR-0088Closed — ADR-0429. The entry called this “the one genuine decision” and framed it as a choice between a border and a fill that is not white. It is not a choice: the groove needs a thumb at luminance ≤ 0.209 to clear 3:1, and the light accent fill sits at 0.200, so every fill dark enough to be seen against the bare groove vanishes into the half of the track that is filled. Clearing both at once needs ≤ 0.083 —
#525252or darker — which is the “hole punched through the control” the theme file already rejects twice for the switch. So the fill answers the accent and a 1px--gb-slider-thumb-borderanswers the groove, and the dark theme sets ittransparentbecause nord6 on nord3 is already 6.40:1. The measurement found a worse pair than the one it went looking for: the light theme’s pressed thumb wasvar(--nord4), the groove’s own colour, at 1.00:1 for as long as the control has existed — the sweep had only ever looked at resting fills. It sweeps all three thumb states now, one border token covers all three, andMARKS_BELOW_FLOORis empty. “Twenty-two goldens moved” was the cost of sliding a ramp; an edge touches only the thumb, and two moved. -
Its fill isbutton.ghosthas no contrast ratio, and is therefore not checked.transparentand its hover is a#ffffff14wash, so what a user reads depends on the surface underneath — there is no single pair to measure. It is left out ofContrastTestrather than measured against black, which is what ignoring alpha would silently do and would score it as passing. The same is true of--gb-selection. A backdrop-aware check would need the painted frame rather than the cascade, which is a different kind of test. — ADR-0087Closed — ADR-0431.
BackdropContrastTestrenders real trees with the real rasterizer and reads the pixels back — five surfaces × four probes × two themes, forty pairs. It deliberately does not reimplementsrc-over: a hand-rolled compositor is a second opinion and the first one is what ships. Nothing failed, worst at 4.79:1 (button.ghost:activeon the dark theme’s--gb-surface-2), so the entry’s four exclusions were correct to make and correct to leave — what changed is that an unmeasured pass is now a measured one. Worth recording that the check’s own first draft reported three failures that were its fault, sampling three pixels into an unpadded row and reading ink over fill; the flat-region guard that caught it is why the rest is believable. -
What ADR-0242 left:remis the configured root size, not the root element’s.emresolves against the element’s own computed size now, andremstill readsCssLength.Context.rootFontSize(). CSS says the root element’s computedfont-size, so the two agree unless a root declares one — and recovering that insideComputedStyle.ofis not possible, because a node is handed its parent’s style and not the root’s. It needs a third thing threaded down, or a field on the renderer that is only correct after the root has resolved. Nothing in the catalog styles a root’sfont-size, so this is exact today. — ADR-0242Closed — ADR-0416. Both halves, split: the root’s own
remfalls out of the two-pass structure ADR-0242 already built forem, and descendants get it throughWidgetRenderer. The entry called the renderer-field shape “correct only after the root has resolved” as a drawback, and it is the specification — CSS saysremon the root’s ownfont-sizerefers to the initial value. And the claim that this was “not possible insideComputedStyle.of” was half wrong: that half was already in reach. -
A bare, which is ADR-0066’s deliberatetextwith no ancestor settingcolorrenders blackINITIALand a trap all the same: the showcase’s new gain label was unreadable on the dark theme. A control gets away with saying nothing becausecontrols.csssetscoloroncheckbox,radio,toggleandsliderthemselves; a primitive does not. The showcase now setscolor: var(--gb-text)on its root, which is what an application should do — but nothing warns one that has not. — ADR-0066Closed — ADR-0415.
StyleLint.uncolouredRoot(root)answers it as a lint asked for rather than a warning logged, which is ADR-0257’s shape and avoids ADR-0394’s trap of firing sixty times a second on a correct application. The entry implies the check can read the sheets, and it cannot: the showcase — the one application that does this right — writes#rootrather than:root, so a synthetic:rootprobe would have reported the reference application as the defect. It takes the root element instead. -
StyleElementdocuments three nullable members inside a@NullMarkedpackage and annotates none of them.type(),id()andparent()each say “or null” in their own javadoc and each is declared as a plainStringorStyleElement, in acsspackage that is marked — so NullAway reads all three as non-null. Nothing had noticed because every implementation lived in an unmarked package;StyleLint’s probe is the first written in a marked one, and it cannot say what the interface says. The lint’s package is unmarked as a result, which is the wrong end to fix it from. Closing it properly means annotating the interface, which moves every implementation and every caller — and would probably find real nullness bugs on the way, which is the argument for doing it rather than against. — ADR-0257Closed — ADR-0413. All three are
@Nullablenow,Selector.Compoundwith them, andcss.lintis marked rather than unmarked — which was the entry’s point, that the lint’s package was the wrong end to fix it from. The entry predicted “real nullness bugs on the way” and there were none. Six NullAway diagnostics, every one already handled at the call site, several with comments explaining why. The finding is better than the prediction: a@NullMarkedpackage had been enforcing a contract nobody believed for long enough that the first code physically unable to lie about it was made to leave the room instead. -
A widget’s CSS classes share a namespace with the design system’s.Ahudreading nameddisplaypicked up §1.4’s.displaytype rank and rendered at 28px. Renamed, and nothing prevents the next one: there is no prefix convention, no check, and the two sets of names are written in different files by different people. — ADR-0153Closed — ADR-0414. The convention is not a prefix, which is what the entry guessed: both namespaces are published vocabulary, and a widget’s class is already namespaced by its type, so the seven §1.4 rank names are reserved instead and
ClassNamespaceTestholds the line. There were two more collisions already in the tree, which is the entry’s “nothing prevents the next one” having already happened twice.TreeRowmintedheading, so every branch label in every tree drew at 15px/600 in a fixed-height row — three goldens moved when it was fixed.Skeleton.Shapemintedtitle, harmless in pixels today and waiting for the firstem. -
TheEditorstill shapes its whole text.canvasediting seam from ADR-0285 holds oneParagraphover everything it is given, which is whattext-areadid until ADR-0388.TextDocumentis exported and is the obvious second caller. Nothing has measured anEditorover a document, so nothing has earned the change. — ADR-0388, ADR-0285Closed — ADR-0411.
Editorholds aTextDocumentnow, and the numbers the entry said nobody had taken are taken: a keystroke into 500 kB goes 111.5 ms → 1.4 ms, and the characters shaped go 500 097 → 132 against 129 for a 2 kB document. Opening is still proportional (130 ms for 500 kB) and the record says so. Only the shaping half of ADR-0388 ports: its “drawn a screenful at a time” half does not, because the rows in view are the caller’s scroll and transform rather than the editor’s.gallery-canvasis byte-identical, which is the evidence the change is pixel-neutral. -
What is still asserted only at 1x, now that the goldens are not.Every image in the golden corpus — about 245 of them now — is drawn again at 2x and 1.5x and checked for being the same picture, andClipTest,TransformPaintTestandIconPaintTestdo the same without a golden behind them — so the whole widget catalog, text included, is now covered against the logical-against-physical family ADR-0157 found. Four classes of direct pixel assertion are not, and two of them are deliberate:BoxPainterTestandTextPaintTesteach already carry their own scale cases and would gain little;DamageTestis excluded on purpose, because a damage rectangle is in physical pixels by design and legitimately differs between scales, so an invariance check there would assert something false; andThreadedPaintTestis about two worker counts agreeing, which is orthogonal. What no test at any scale covers is a fractional scale other than 1.5 — 1.25 and 1.75 are ordinary Windows settings and neither is exercised. — ADR-0162, ADR-0157Closed — ADR-0434. 1.25 is in every
check’s sweep and 1.75 goes nowhere, which is the opposite of what the entry asked for and is arithmetic rather than thrift: a multiplier exercises Yoga’s rounding, so what matters is the offsetsk·m mod 1visits — 2 gives{0}, 1.5 gives{0, ½}, 1.25 gives{0, ¼, ½, ¾}, and 1.75 gives the same four quarters, re-asking a question 1.25 has answered while its magnitude is bracketed by 1.5 and 2. The cost is about 4 s ofcheckper multiplier repo-wide. Two things the entry could not know:gallery-canvasmisses at 1.25 (1.229% against a 1.200% budget) because a hard-edged QR module grid resampled at 5/4 comes back inverted rather than blurred, so that one image is excluded from the sweep by name rather than the budget being loosened on 245 images that meet it with tenfold room; and on that same screen a natural-size image tile differs wholesale between scales where the stretched and cropped tiles beside it differ only at their outlines, which has the shape of ADR-0157’s bug and is now recorded but unswept. -
Present costs 6.6 ms with no compositor to wait for.The question ADR-0045 opened while closing another. ADR-0031 measured present at ~10 ms and concluded “most of it is waiting on the compositor rather than copying”. Under SDL’sdummyvideo driver — no compositor, no display, no surface to hand anyone — present still measures 6.6 ms, essentially the same as under Wayland. Whatever that time is, the explanation on record is wrong, and present is the largest single term in a frame. — ADR-0031, ADR-0045Closed — ADR-0409. The entry was right that the explanation on record was wrong and wrong about everything else, its own number included. Measured over 300 frames under the driver it names, present’s median is 0.127 ms and its p95 0.319 ms — off by a factor of about fifty — and it is 0.8% of a frame rather than the largest term in one. The largest term is
end, at 10.2 ms: the join that waits for Blend2D’s workers, withdraw’s 5.9 ms of queueing in front of it, so a frame underdummyis rasterization and almost nothing else. Two plausible misattributions were ruled out on the way and both were already right — the Blend2D join is timed before the present boundary, and the frame pacer’s sleep caps the event wait rather than the painted frame. What survives is the qualifier: underdummythe frame is rasterized straight into SDL’s surface and present copies nothing, so 0.127 ms is the floor rather than the universal figure. ADR-0031’s Wayland number is left standing because it cannot be re-measured here — opening a real surface on this machine takes GNOME Shell down — and the like-for-like Wayland measurement below stays open. -
Nothing bumpsForgetting leaves master publishinggoldberryVersionafter a release.2026.1-SNAPSHOTafter2026.1is out, which Maven orders below the release. A step inrelease.ymlthat opens the bump as a pull request would close it. — ADR-0333Closed — ADR-0421. That is exactly what landed, and the entry named the right shape: a
bumpjob thatneeds: publish, checks out the default branch and opensbump/<next>. Two things it did not say. The arithmetic is the part with a decision in it, and it is a tested value inbuild-logicrather than ased—VersionBumpmoves a patch line to its next patch, because arelease/2026.1branch bumped to2026.2would claim a feature release from a maintenance branch, and asedthat incremented the last number would get that right and get2026.3in January wrong. And the guard needed no rule of its own: comparing what the default branch declares against the tag rules out both a patch tag and a re-run whose bump already merged.CalendarVersion.nextReleasehad been written by ADR-0333 and called by nothing until now. -
PNG is the only format written.WebP is written too, 2026-09-17, and losslessly by default: VP8 is worst at the flat colour and hard edges a user interface is made of, soencodeWebp()is the lossless path andencodeWebp(quality)is for a photograph. JPEG is still not written and is what is left of this entry: Blend2D ships a JPEG decoder and no encoder, so it means a third codec library or a written one — neither worth it while a lossless WebP is a third of a PNG. — ADR-0385 -
Reduced motion is obeyed but not detected.Detected, 2026-09-17, and obeyed by a renderer the launcher builds: the settings portal on Linux,SystemParametersInfoWon Windows andNSWorkspaceon macOS, each a read-only FFM query against a library the process already has, with no native build behind any of them. “The desktop does not say” is still an answer and still is not an instruction. Asked once per process; a change mid-session waits for a restart. — ADR-0383 -
Nothing detects the density the user wants.Answered: there is nothing to detect, 2026-09-17. Reduced motion was detectable because three desktops expose it; density is not, because none of them has such a setting — §1.3’s compact mode is an application’s own decision about its screens, which is what this entry suspected and what asking the three platforms confirmed. — ADR-0383 -
Layout verification has not yet passed in CI.It has, since the first all-green snapshot. The verify legs on all three runners ran the layout probe against the downloaded artifact atd478ecfe, which is the runbook/src/status.mdrecords as twelve green jobs. — ADR-0016 -
AsmJit’s W^X handling on Apple Silicon is now reachable.Reached, and green. The showcase painted frames onmacos-14on all three legs and the macOS goldens ran, which is the JIT it was a question about doing its work. What is still untried is AVX-512, which is a different machine and is on the scale-invariance entry rather than this one. — ADR-0338 -
It has, 2026-09-17, and it is the same class the stylesheet already had a rule for:texthas nostyle="body"attribute.style=names a closed vocabulary and is checked,class=is the open one and is not. — ADR-0381 -
One frame only.Every frame of a GIF, 2026-09-17, composited under the file’s own disposal rules, with its delays and its loop count;image.anim.Animationsays which one is showing and holds no clock. An animated WebP followed a few hours later, oncewebpdemuxwas linked — libwebp composites its own canvases, so the disposal model is upstream’s there. — ADR-0382, ADR-0385 -
AIt travels, 2026-09-17, by being displaced onto the header it is leaving and then let go of — a difference between two painted rectangles rather than a position, so a strip painted with no router behind it draws exactly what it drew before. — ADR-0377tabsindicator still cannot travel, thoughsegmented’s does. -
Two key maps.One, 2026-09-17, and there were three by the time it was read again.text.edit.keysis the table; each editor keeps its own text and answers a sealedEditCommand, so a key added tomorrow fails to compile in the three places that have to answer it. Converging the editors is still the rewrite it always was. — ADR-0376 -
Nothing has a minimum size, so overflow is silent.It says so once, 2026-09-17. The layout pass asks the root whether its line overran — one foreign call on a frame where everything fits — and names what is off the edge when it did. Ascrollviewport and an absolute child are not overruns. — ADR-0375 -
It is resolved, 2026-09-17, defaulting toalign-contentis still absent.stretch, which is what every box in the catalog already did. It also givesSPACE_BETWEENand its two neighbours a property that means them: they were constants the enum advertised and no declaration could reach. — ADR-0374 -
It resolves, 2026-09-17, andflex-basisis one of two layout properties §8 names and nothing resolves.masonryis the consumer that wanted it:flex-basis: 0withflex-grow: 1is 1/n of a row after its gaps, where the inlinewidth: 100/n %it replaces was 1/n before them. The collapse that took it out in the first place is still real and is now the author’s to avoid. — ADR-0373 -
Built on 2026-08-23, and the entry outlived it:statistic’s sparkline waits oncanvas.canvasis in the catalog and the sparkline is the last child of the column, exactly as this said it would be. -
No image cache forAnswered rather than built, 2026-09-17.Image.decodeitself.ImageLoader.shared()is public, bounded and off-thread, and acanvaspainter that decodes by hand can use it — which is what the entry itself named as the seam. A second cache insideImage.decodewould be one the caller cannot see, cannot bound and cannot clear. — ADR-0358 -
Nothing reorders tabs.A strip withonReorderdoes, 2026-09-17. The dragged tab follows the pointer 1:1, which needs no interpolation, and the drop asks the application for the new index; the others still jump into place, which is ADR-0097’s geometry and nothing asked for more. — ADR-0372 -
AnOn one per axis, 2026-09-17:affixpins on one axis.edge="top left". There was no rule to write about which wins, because each axis is its own subtraction. — ADR-0371 -
A reveal moves both axes at once.By default, still, 2026-09-17, and a caller that means one axis says so withreveal(self, clip, axes). — ADR-0370 -
The circular drag is not built, and §3 offers it.It is, opt-in, 2026-09-17, with no accumulated angle: a jump across the gap is recognised from the knob’s current value, and held at the nearer end. — ADR-0369 -
AIt has the tree’s own, 2026-09-17. The design question was answered by ADR-0209 (visible rows only). The defect was elsewhere: a tree moves its typeahead withselect tree=#truehas no typeahead.host.focus(id), and a popup’s host asked only the window. Focus by name now tries the open popups, topmost first. — ADR-0368 -
AA document places one, 2026-09-17, andlistis Java, likecanvasand like autocomplete.tableandtreethe same way:bind=names the widget the model built, since its factory is code. Autocomplete names the bound list its answer lands in, withsuggestions=oroptions=. — ADR-0367 -
A tab’s content is rebuilt when it is selected again.Only by default, 2026-09-17.keep-alivekeeps every shown tab mounted and hidden while another is selected, through a newStyled.isHidden(): kept, not rendered, not focusable. §5’s lazy default is unchanged. — ADR-0366 -
A tab strip scrolls, and has no chevrons at either end.It has them, 2026-09-17, while it overflows: aScrollControllercan now say where its viewport is and when that changes, which was the missing question. — ADR-0365 -
The “always show scroll bars” gutter is not built, and nothing switches it.Both, 2026-09-17, in density’s shape rather than a settings mechanism:Scrollbars.ALWAYSis a token stylesheet an application passes toControls.stylesheets, and a viewport that finds a gutter pads its content by it and stops fading its bars. — ADR-0364 -
A revealed row lands rather than glides.It glides, 2026-09-17, over the overlay duration. The offset goes to the target at once and the viewport draws the way there on the frame clock, so direct input never waits; a reveal asked mid-glide measures where its row will be. — ADR-0363 -
AIt hastext-areahas no visible scrollbar.scroll’s, 2026-09-17: neither ascrollaround the text nor a second bar.ScrollBaris three numbers and two callbacks, and a text area knows all three. — ADR-0362 -
AIt has, 2026-09-17, by asking: a resizable column’s header carries a grip, and a drag asks the application for a width in pixels anchored at the width the header last came out as. Not atablehas no column resizing.split-panebetween the headers, which divides one box between two panes. — ADR-0361 -
A pinnedIt is, 2026-09-17, without knowing its sibling: an affix stays inside the box it is in, which is CSS’s rule foraffixis not pushed out by the next one.sticky, so a section’s header leaves with its section. The same change gavetablea sticky header, which this entry was blocking. — ADR-0360 -
AAs wide as its widest option, 2026-09-17. The value cell’s preferred width is the widest of the option labels and the placeholder, shaped inselect’s field is as wide as its current value.render; a stylesheet’s width still wins. — ADR-0359 -
There is noThere is, 2026-09-17:imgwidget.image, with theobject-fitmodes, a natural size that takes part in layout, loading and error states and a decode that does not run on the UI thread, which is the list this entry said a catalogue entry would need. SVG is still not decoded. — ADR-0358 -
A step’s connector fills by colour, not byIt grows, 2026-09-17. The reason given was that §8’s subset has noscaleX.transform-origin, and it has had one since ADR-0068. The sentence was copied from an older note and never checked againstTransformTest. The connector is now a track with a fill scaled about its start edge. — ADR-0356 -
AIt can, 2026-09-17, from abadgecannot be a timeline’s marker.markerchild thatentrylifts onto the axis. The slot is named rather than inferred, so a badge written as content stays content. The rail keeps its width and a wide marker overhangs it. — ADR-0356 -
A floating button does not scale in.It does, 2026-09-17, from an@starting-style: the entering state the overlay layer had no way to give it, and CSS’s own answer to “what does an element transition from on its first frame”. And it scales out: aFloatSlotbound to a switch putsleavingon the button, the stylesheet plays the exit onfast, and a host timer removes the overlay after it. A general closing phase for overlays is still not built, and nothing else has asked for one. — ADR-0352, ADR-0355 -
A field’s room isEach edge comes off once, 2026-09-17, inwidth - 2 × left padding.text-inputas intext-area. — ADR-0355 -
§8’s subset has noIt has one, 2026-09-17. ADR-0081’s argument was about loops that must stay in phase, and those stay clock functions. What it did not cover is authored motion (sequences, staggers, fills) that otherwise needs a widget of its own. §1.7’s rule 4 still binds the toolkit’s sheets, and@keyframesand is not going to grow one.ToolkitLoopsTestchecks it. — ADR-0353 -
The published javadoc is built with doclint off.On, lessmissing, and clean across every published module. The 120 errors were one idiom —@paramlines on a…Callsholder class rather than on itscallmethod, 425 of them, moved by a script — and twenty[Foo]links to a type in another package or module, three of which named things that no longer existed. Themissinggroup stays off because this codebase documents in prose. — ADR-0343 -
No licence text is vendored yet.All seven are, 2026-09-17. The superbuild’s cached checkouts undernatives/.deps/<target>/<name>-srcare the pinned revisions — theirHEADs were compared againstlibs.versions.tomlbefore copying — so the verbatim files came from there rather than from a download that might have been a different tag.checkLicenses -Pgoldberry.releaseCheck=truepasses with eleven components. What stays true: a bumped pin means re-copying that one file, because the copyright lines are the upstream’s and not the licence’s. — ADR-0015 -
IME preedit is not drawn, and committed text already works.IME preedit is missing entirely.Both were closed bydocs/gaps.mdG15 and G16 and the entries had not been struck.SDL_EVENT_TEXT_EDITINGandSDL_SetTextInputAreaare bound,text.edit.Editordraws the composition where it will land, andtext-inputandtext-areatake one inline; apassworddeliberately does not, because a candidate window is an unmasked window. — ADR-0289, ADR-0292 -
The native image’s foreign registrations are generated but the fix is untested on an image.Tested by hand on all three platforms, 2026-09-17. A manual Showcase run built the image on Linux, macOS and Windows, and the html, canvas and Markdown screens open on each. What the hand test found on the way was not a foreign call but a resource: the canvas screen’s five sample images had never been declared, which ADR-0160’s rule already covered andDeclaredResourcesTestnow enforces. What is still open from this entry: CI runs the image for three frames and opens no screen, so a--screen=<name>launcher argument would let it do what the hand test did. — ADR-0339 -
AThe second seam turned out to be a composition, andValidatoris over aString, anddate-pickerwill want otherwise.fieldneeded no change at all.Validator.parsing(parse, message, rule)is a rule over the parsed value expressed as a rule over the text it was parsed from, soFieldstill holds aValidator<String>,FieldStatestill reads its control’s binding as text, and neither of them knows a date was involved — which is the evidence that this was never a type parameter. It also keeps this entry’s own premise intact rather than contradicting it: what the user typed is text until something parses it, and a validator is exactly the thing that decides whether it can be.parsemay throw or answer null and both mean the same thing, becausejava.timethrows where a hand-written parser returns null and a seam that took only one would make the other an application writing a try/catch to satisfy a method. An empty value passes without the parser running, formatching’s stated reason. — ADR-0274, ADR-0169 -
An absolutely positioned child is placed against the border box, and the clip is the padding box.Fixed where ADR-0265 said it was, and the golden tail was somewhere else.ContainingBlockshifts an absolute child’s inset by its containing block’s padding on the way to the Yoga node, per edge and only on the edges the box named — Yoga’s no-inset path already lands on the padding and is already right. On the style rather than on the computed rectangle, because withleftandrightboth given Yoga derives the child’s width from them: a correction applied after the layout pass could have moved the child and could not have resized it. Percentages are declined on both sides of the sum and say so, since a percentage inset resolves against a size that does not exist yet.text-input’s andtext-area’s compensations came out in the same commit, as ADR-0265 insisted they must. What the record could not predict is where the images moved: notsegmented,tourorscroll, whose parents genuinely have no padding, buttabs— an underline pinned across a header withpadding: 0 12pxcame out 24 points short of its own label — andtoast, where an overlay pinned to a corner started counting from the application’s content box instead of from the window. Both mean the border box and now say so, throughacrossBorderBox, in terms of the padding their own style resolved rather than a number repeated in a stylesheet. — ADR-0272, ADR-0265, ADR-0167 -
A window that moves does not re-clamp its popups, and a scrolling anchor does not drag one.Both halves are built, and the second one was a question nobody was asking rather than a report nobody was making. The move half is what the entry described:BackendEvent.Moved, an SDL translation ofWINDOW_MOVEDdeduplicated per position, and a fabricated-event test under thedummydriver. It re-places immediately rather than after the next paint, which is the opposite of the resize case and for the stated reason — a move produces no new capture and invalidates none, so the current one is the right one, and no repaint follows a move at all because nothing inside the window changed. The scroll half was proposed asLocatedon the anchor, and the anchor turns out to have nothing to report:anchor(id)answers from a hit-test capture taken every frame, so the position already had a fresh answer and what was missing was the question.replacePopupsnow runs at the end of any frame with a popup anchored by id, which is also the guard — a popup opened against a rectangle a caller computed has nothing to re-resolve. Underneath both was a third thing, and it had been wrong since before anything scrolled: a popup anchored toRegion.bounds(), the layout rectangle, which for a button inside ascrollis hundreds of pixels from where the button is drawn. It readspainted()now, in all three places, and the two rectangles are identical for every box nothing transformed — which is why it took a scrolling anchor to show it. — ADR-0270, ADR-0231, ADR-0123 -
Nothing reports a dropped frame.lateis a reading now, and the pacer is what can count it. Both halves the entry named are in one number: the frames the loop never reached, which leave no record in a ring that only holds frames that were painted, and the frames the platform refused after they were painted, which were in the mean as though somebody had seen them. The arithmetic ismax(pendingSince, lastFrame + interval)— andpendingSinceis the whole of why this is honest, because the naive version (the gap since the last frame, over the interval) reports a window nobody touched for a minute as three and a half thousand dropped frames. It drew every frame it was asked for. §1.7’s idle loop is the common case, so a count of missed refreshes that does not know when the request arrived is a count of how long the user was away. A window rather than a total, like every other number onFrameStats: a total only ever goes up, and somebody watching a HUD while they work would never see it come back to zero. — ADR-0271, ADR-0146, ADR-0101 -
“Pixel-precise wheel deltas” is not reachable through SDL.A difference rather than an agreement, and the entry that outlived its own resolution: §7.1 asked for pixel-precise deltas with a line-based fallback, SDL reports only detents as floats, and the two platforms with a pixel axis underneath do not surface it. What §2.4 actually wanted from “pixel-precise” is scrolling that does not quantize, and a fractional line delivers that without the mechanism the sentence named — so the honest change was to the document rather than to the backend. This had already been settled and said so in this file’s own introduction while the entry sat on in the list below it. — ADR-0115, ADR-0056 -
Every pointer event now costs anMeasured: 8.71 ns a call, so 34.8 µs per second of dragging. Polled per event rather than carried on it, because SDL’s mouse events have noSDL_GetModState.modfield where its keyboard events do. The entry said “not measured — named here so it can be if a profile ever points at it”, which is whatModifierPollBenchmark(./gradlew :natives:benchmark) is: at a generous 4000 events a second that is 0.0035% of one core, spread across a whole second of frames whose budget is 16.7 ms each. Nothing to do, and nothing to carry on the event either — ADR-0089’s reason for polling stands, since latching the mask from the last key event leaves it stuck down when a window loses focus mid-chord. The number is the answer: the next person to wonder should find the measurement rather than repeat the worry. — ADR-0089 -
A guard at the top ofIt warns now, once per kind per node type. The entry’s own last sentence was the design: a default that is “right foronPointeris a guard on every pointer kind, and nothing warns.dragX’sNaNand quietly wrong for a nullbutton”, because the two are not the same kind of default.NaNis arithmetic — the meaninglessness propagates and every comparison against it is false in both directions, so a caller cannot act on it by accident. A null button is a reference: it is unequal to everything, sobutton() != PRIMARYis true for a move and the guard fires backwards, keeping the press it was written for and dropping every drag. It stays null rather than throwing — an input handler that threw would turn a lost drag into a window that falls over — andButton.NONEwas the other shape and fixes nothing, since the guard still fires backwards against a value that now looks deliberate. All ninebutton()reads in the toolkit are already inside a kind check, so it fires on the mistake and on nothing else. — ADR-0266, ADR-0168 -
A widget can reach its window, and the in-window overlay layer is still the application’s.Toasts.of(context)is the door, and it is a map. The entry had already shrunk once —BuildContext.host()answered the popup half — and what was left was atoastraised from a handler deep in the tree, with every layer above it carrying a callback.findAncestorStatecannot do it and it is worth knowing why: a toast stack is mounted in the overlay layer, which is a sibling of the application’s root underwindow-rootand not an ancestor of anything inside it, so walking up reacheswindow-rootand stops.host()gives the window andToasts.atis the one place that knows which stack is on it, so the mechanism is a weak map from the first to the second — kept in:widgets, because:corelearning what a toast is would undo the reasonToasts,MenusandDialogsare three classes there rather than three methods on the window. A generalhost.service(Class)is the shape to reach for ifMenusorDialogsever want the same thing, which is ADR-0140’s own rule about one consumer not being enough. — ADR-0264, ADR-0140, ADR-0100 -
The enter/exit lifecycle is a tab’s own, not the toolkit’s.A tour has no arrival or exit.The promotion happened two records ago, and the tour uses it now. The first entry asked forTabPhaseto be promoted “when the second consumer arrives”; it iswidgets.core.Phase, moved there by ADR-0166 — whose own javadoc says “there was never anything tab-shaped in it” — with theclosing → removedhalf extracted intoDepartureby ADR-0234. Six families use one or both, includingtoastanddialog, which is to say both of the consumers the entry named as wanting it. So the second entry’s “that isTabPhaseagain” was pointing at a wall that had been a door for milestones, and what was missing was atourwalking through it. It has an arrival now (opacityand a 4px rise, §3.1’s popover row bar the scale, whichpopoveritself also lacks and for the sametransform-originreason) and a travelling cut-out (one rectangle interpolated, so the ring, the hole and the card cannot disagree mid-flight).AnimationSweepTestcaught two real gaps within a minute — aPhaseonTourVeilwith noisAnimating, and a package with no test naming it — neither of which an image would have shown. The goldens did not move:TourGoldenTesthas a virtual clock now, so the settled tour is the picture it always was. — ADR-0269, ADR-0166, ADR-0109 -
A tour card’s height is estimated, not measured.It is measured, and the mechanism was already in the file. The entry said measuring “needs the measure-then-place machinery ADR-0104 built, which works on windows rather than on boxes” — andTourStopalready banks the window’s own rectangle from the frame before, throughLocated. The card is one node further in andMeasuredis the same door.ESTIMATED_HEIGHTsurvives with a narrower meaning: what the first frame decides with, before anything has been laid out and had a height to report.Measured’s third rule holds by construction — the card’s width is fixed and its content is the stop’s own text, so the height does not depend on whether it was placed above or below — which ismasonry’s argument rather than the scrollbar’s, and is asserted rather than claimed. — ADR-0268, ADR-0121 -
A tooltip’s 500ms delay is a constant, and the token that would replace it cannot be read.Both blockers expired, and one was never true. The first — “nothing above the cascade can read a resolved custom property” — isBuildContext.token(ADR-0254), and the launcher holds anElement, which is aBuildContext. The second asked whether the design system should carry a duration that is not motion, anddesign-system.md§3’stooltiprow had already answered: “delay 500ms show / 100ms move-between”. So the question was settled before it was asked — and the code had built the first number as a constant and the second not at all, so a user reading along a toolbar was served the full sentence of hover intent at every button. That is a specified behaviour that was never built, hiding inside an entry about tokens.BuildContext.durationistoken’s sibling with the cascade’s ownms/sparser where its length parser is — made public rather than written twice, because two readers for one syntax disagree the day either grows a unit. — ADR-0262, ADR-0105 -
A focus ring is only ever pictured on the dark theme, apart from one.There is a rule now, and it is a test. The entry asked for “a rule about which states are worth a second theme rather than one more image”, and the rule is narrow on purpose: §2.2’s ring is the one mark in the system with no second means of being seen — a hover has a wash, a checked control has a fill, a disabled one has its opacity, and each of those is drawn in colours some other golden already covers. A ring is only a ring, and--gb-focusdiffers per theme.FocusGoldenPairTestreads the resource directory rather than a list, so a focus golden added next month is checked next month; it also asserts that the sweep found something, because a discovering test’s own failure mode is passing by seeing nothing. Three images came with it —menu-focus-light,menubar-focus-light,tabs-focus-light— and doubling the whole corpus was the alternative and is not a rule so much as the absence of one. — ADR-0261, ADR-0240 -
An icon-only segment has no accessible name, and neither does an icon-only button.name=is onAttributesnow, so every widget has one. The entry separated two things that had drifted together: “§13’s semantics are M5’s” is true of the AccessKit bridge, and was never true ofSemantics.role()andaccessibleName(), which have shipped for milestones with a sweep enforcing them. What was missing was somewhere to put a name a widget cannot work out — an icon-only control’s label is the empty string by construction, soButton.accessibleName()answered"": a control a reader cannot announce, passing a sweep that only checked for null. It sits besidetooltipandcontext-menufor their reason, which the entry had already written (“a gap the whole catalog shares”), and the label wins where there is one. — ADR-0260 -
It has two, and has had since--gb-list-row-heighthas no consumer.listshipped.ListStatereads the token throughBuildContext.token(ADR-0254) andlist-rowwritesheight: var(--gb-list-row-height), so the number the density decides is the number the rows are and the number the spacers are spaced by — which is now checked, since a disagreement between the last two is reported (ADR-0257). The entry’s closing line, “listis M3”, is the other half that expired:listis built. — ADR-0257, ADR-0254, ADR-0074 -
ABoth paths refuse one by name, and both refusals are tested. The entry was right that it cannot work — the woven path would generatestatic@Actionis still unsupported, and nothing refuses one explicitly.target::methodfor a method with no target, and the reflective one writesfindVirtual— and wrong that nothing says so.ModelWeaverthrows aWeaveExceptionandRuntimeBindinganIllegalStateException, both reading “is static; an action changes a model, and a static one has no model to change”, andModelWeaverTest.staticActionandRuntimeBindingTest.staticActionare the two tests. Nothing was built to close this; it had been closed and the entry was not updated, which is the argument for reading an entry against the code before believing it. — ADR-0098 -
Nothing warns when a declaration is dropped for being unsupported, in an application’s stylesheet.An application’s stylesheet can still be all classes, and nothing says so.Both are asked for now, which is what both entries said the answer had to be.StyleLintisSupportedPropertyTest’s machinery with the test taken off it: every rule through the real cascade, every declaration to the realComputedStyle, andFindingvalues back with the line and column the parser saw. Neither entry wanted a louder log and ADR-0216 is why — a dropped value already warned at WARN andgroup-box-titledrew square corners for months anyway. The engine side is four lines:withreturnsthisin exactly two places and both are failures, so identity is the answer and it cannot drift from the behaviour because it is the behaviour. What it deliberately does not do is tell the two failures apart, which would mean the engine reporting rather than being asked — thirty edited switch arms in the frame loop for a difference the author reads off §8’s list either way. An unresolvablevar()is not a finding either: it is already the resolver’s report, once, which is ADR-0243’s shape. The test that used to do this lost a logback appender, two sentence-matching filters and its own two guard tests, and gained two sweeps it could not afford — the light theme and the compact density, either of which can resolve avar()the other does not. — ADR-0257, ADR-0249, ADR-0216, ADR-0215 -
It says so now, once per axis. The entry expected this to be hard — “a diagnostic would have to know that aflex-growmeans nothing inside ascroll, and nothing says so.growresolved against an unbounded main axis, which Yoga knows and does not report” — and it is a field comparison:ScrollContent.renderis handed its children as boxes, withflex-growalready resolved, and the content box’s main axis is the scrolling axis by construction. Nothing had to be asked of Yoga. It also catches a widget that set the growth itself, which no rule in any stylesheet would have shown. ADR-0251’swarnIfNestedOnTheSameAxisis the shape, down to the static set that keeps it a message rather than a stream. — ADR-0257, ADR-0251, ADR-0116 -
A row height that disagrees with the stylesheet is a silent layout error.It is reported now, and by a simpler mechanism than the entry predicted. It asked for “aMeasuredassertion on the first built row”; what it got is the cascade, becauselist-rowdeclaresheight: var(--gb-list-row-height)and that number is resolved before the row is laid out — so the check is exact, free and a frame earlier than a measurement. Two things it turned up. The mismatch has to be seen twice, because the first build of a tree has no cascade (ADR-0254): a list reading the token answers the default on that build and the stylesheet’s value on the next, so under a compact density there is one frame of a real disagreement that settles by itself — and reported naively, the form that cannot be wrong would have been the noisiest one. And that forced the second: the check runs on one row of the window, since twenty rows resolving the same height would report twenty times a frame and “seen twice” could not then tell a frame from a sibling. The entry’s last sentence was already stale — reading--gb-list-row-heightis ADR-0254’s door and it is open. — ADR-0257, ADR-0254, ADR-0213 -
A slider’s value label is left-aligned in its box, because §8’s subset has noIt istext-align.text-align: endnow, and nothing had to be added toBox. The entry’s reason was quoting §8’s own note — “Boxcannot express them” — and that note was right aboutbackdrop-filterandletter-spacing, wrong about this one, and has since been overtaken onbox-shadowtoo (ADR-0310: aDecorationcomponent and a stack of rounded rectangles, noBoxfield required).Paragraph.paintis already handed the box’s width, because it has to be or the text could not wrap to it, and everyTextLinehas already measured itself: the two numbers an alignment needs were in the same method the whole time, and what was missing was a keyword saying what to do with them.slider-valueiswidth: 40pxby declaration (ADR-0080), which is exactly the condition under which an alignment means anything — and the four goldens that moved are all the same readout,slider-value.pngplus the showcase’s Basic screen in its three variants, with9%,50%and100%finally lining up on their trailing edge.leftandrightare refused, for ADR-0247’s reason: they are not the same asstart/endunder RTL.justifyis refused for a different one — it is a respacing rather than a placement, and a paragraph shaped once has nowhere to put the extra advance. — ADR-0256, ADR-0255, ADR-0080 -
A menu row overflows rather than ellipsising, and the missing property iswhite-space: nowrap.A segment’s label overflows its cell when it is longer than 1/n of the bar.Both are cut now, and so areoptionandselect-value. The entry was right about the property and right about why three attempts at clipping had failed: a box with text is a measured leaf, so narrowing it re-measures the paragraph and wraps it, and there is then nothing overflowing to clip. §8’s subset haswhite-space: normal|nowrapandtext-overflow: clip|ellipsisnow, andwhite-spaceis the whole mechanism — undernowrapthe measure function ignores the width Yoga offers and reports the width the text wants, so a box may be laid out narrower than its own content, which is the state the clip and the ellipsis were always waiting for. Three things worth keeping.text-overflowis read only at paint time: an ellipsised line is drawn short and measured long, because a paragraph whose measurement shrank from being truncated would let the ellipsis decide the width that caused it. The cascade carries the two properties apart and hands out one value, because CSS inheritswhite-spaceand does not inherittext-overflowand a bundle cannot be half-inherited — sowhiteSpacehad to joininheritsSameAsas well asinheritingFrom, which is ADR-0248’s standing warning. And ADR-0148’sflex-shrink: 0came off the label rather than being reverted: it stopped the wrap by stopping the shrink, andnowrapstops the wrap without it. The accelerator keeps its own, because half ofCtrl+Shift+Kis not a shortcut. What ADR-0235 left that is not closed isprogress’s indeterminate sweep, which is a design decision about a shipped animation. — ADR-0255, ADR-0235, ADR-0148 -
--gb-list-row-heighthas no consumer, and no widget can read a resolved custom property at build time.BuildContext.tokenis the other door, and it was three lines.Elementalready implemented bothBuildContextandStyleElement, andElementTreehas held aStyleResolversince ADR-0149 — what was missing was the method.WidgetRenderer.prepareis the new part and it is about ordering:renderhands the tree its resolver on the way in, which is a frame too late for a reader inbuild. The stakes were higher than a repeated number:density-compact.csssets--gb-list-row-height: 26px, so a list writtenvirtualized(32)virtualizes on the wrong pitch the moment an application switches density. Two things measuring turned up. The first build of a tree has no cascade — aStatefulwidget builds inside theElementTreeconstructor, before any renderer exists — so a token there answers its default and the second build is the first that can see the stylesheet; a virtualized list settles by construction, and that is now written down rather than assumed. And the token must be declared at or above the list, becauseListViewis a composition node whose state builds thelistelement — which is where it ships, on:root. — ADR-0254, ADR-0251, ADR-0213 -
It is a token now, and it was an accessibility gap rather than a styling question. The entry’s diagnosis was right — a--gb-caret-widthis not a token and the caret is one logical pixel.caret { width: 3px }is overwritten rather than honoured, because the caret’s box is set after the cascade — and the token was not shipped only because nothing could read one, which ADR-0251 changed. A thicker caret is a low-vision aid, which is why §13 lists that kind of switch. Two things the entry did not mention.text-inputandtext-areaeach had their ownCARET_WIDTH = 1, the second’s comment saying it was the first’s — one constant inwidgets.form.Caretsnow, with a test that says they agree. And there is a third consumer:TextInputState.laidOutreserves “the caret’s own width of room” so a field does not scroll short of showing it, hard-coded to 1 — a three-pixel caret against a one-pixel reserve is a caret clipped at the end of the text, which is the failure that would have looked like a text-rendering bug. — ADR-0253, ADR-0251, ADR-0167 -
A window’s maximized state is write-once and cannot be read back.All three are built, and the question the entry left open has an answer that follows from what maximizing is.Window.maximize(),restore()andisMaximized()ship, andSDL_EVENT_WINDOW_MAXIMIZED/RESTOREDarrive as oneBackendEvent.MaximizedChanged—FocusChanged’s shape, because SDL sends two and every consumer wants the boolean.isMaximized()answers what the platform last reported, not what was last asked. ADR-0221 had already established that maximized is a state rather than a size, and every platform routes the ask through a window manager that may refuse it — so a flag set on the way out would be a lie the moment one did. The cost is stated rather than hidden and is asserted by a test: between the request and the event,isMaximized()is still false, because that is a window which has been asked and has not yet agreed. It is also what makes the interesting half work — an application can learn that the user maximized it, which no amount of tracking one’s own calls can produce. — ADR-0252, ADR-0221 -
A widget cannot read a resolved custom property, soIt can, and the entry was half stale when it was written.scroll’s line height is a constant.Paints.Context.colorhas read one since ADR-0195 — that is how a chart gets--gb-chart-1…8— so what was missing was the same door for a number, andlengthis it. The interesting half is that reading it is not enough: the wheel arrives where there is no context to ask, soScrollViewportreads the token inrenderand banks it intoScrollStatethrough the shapeonMeasuredalready had. That makes it a frame late, which is ADR-0117’s bargain unchanged — a paint always precedes an input. What is left islist, and it needs a different door; it is above. — ADR-0251, ADR-0195, ADR-0116 -
Nested same-axis scrollers are banned in the canon and nothing enforces it.A nested pair says so now, once.BuildContext.findAncestorStateis the whole implementation — it exists forscrollIntoViewand answers this question with nothing added, which is why it is asked inScrollState.buildrather than by teaching the renderer about scroll views. It stays a diagnostic and not a refusal: chaining already makes the arrangement work, and turning a canon rule into a crash is worse than the rule going unheard — the author’s problem was that nobody told them. Deduplicated by axis for ADR-0243’s reason, becausebuildruns per element per invalidation and a document that nests in four places has one mistake. — ADR-0251, ADR-0243, ADR-0116 -
It is built, and it positions nothing. The entry’s own last sentence had become “whatstackis still owed.stackstill wants isstack” once ADR-0244 took its last blocker. It is nine lines: the first child stays in flow so the box has a size — a stack whose children are all out of flow is a box of nothing, and this is what makes wrapping an existing widget in one a change that cannot move it — and every child after it isposition: absoluteso an overlay cannot resize what it sits on. §1’s “positioned by alignment or absolute insets” needed no code at all: both already worked, and this is the caseComputedStyle.INITIAL’s inset comment has been describing since before anything could reach it — “the difference only shows on an absolute node, where zero would stretch it and undefined leaves it where the alignment put it”. Z-order is document order, which is the painter’s existing rule for siblings. — ADR-0250, ADR-0244, ADR-0100 -
A style that really changes still re-resolves its whole subtree, and only the inherited properties can matter.It compares the inherited half now, and the notion the entry wanted already existed.ComputedStyledoes have a list of what inherits —inheritingFromis two lines,colorandtypography, and its comment even enumerates what is deliberately not there. What was missing was reading it twice. The difficulty was not the comparison:stableStyle’s return did two unrelated jobs, the children’s cache key and what the node paints, so loosening it would have handed back an older instance with last frame’s transform and then painted with it — a scrolling viewport frozen at its first offset while every child cached happily. Two variables, because there are two jobs. The test for that is the one that matters and it was written against the mistake: folding the roles back together fails it. — ADR-0248, ADR-0142 -
The rule buckets are only as good as the stylesheet, and nothing enforces that the toolkit’s own stay type-first.They are enforced, and measuring found the assumption was already half wrong. 16 of 340 rules named no type, and they were two families rather than a scattering. Seven weretour’s parts — built from plainTextandButtonwidgets carrying a class, so every one was matching a known type and simply not saying so;text.tour-titlematches exactly what.tour-titlematched and lands in a bucket. Seven rules, one word each, and no golden moved, which is the evidence that it changed what the cascade looks at rather than what it finds. The other eight cannot be qualified and should not be: the typography scale is ranks an application puts on whatever it likes, and:rootis the theme’s token layer.RuleBucketTestholds those eight as an exact set — a threshold is a number somebody raises. — ADR-0249, ADR-0152 -
It stays, and the trap is an asserted fact now. The look the entry asked for found five readers and not one of them wants a direction: a default--gb-surface-2has been mistaken for an elevation three times, and it is unresolved whether it should keep existing.badge’s fill, ascrollbaron hover, agroup-box-titleband, askeleton-barand a collapsedsplit-divider. Every one wants a plate merely distinct from what is under it, which is what the token promises — the three that were wrong wanted “raised” or “sunken” and have their own tokens now. Renaming it would not have stopped a single one of them. What does isThemeTest:--gb-surface-raisedis never darker than--gb-surfaceand--gb-surface-sunkennever lighter, on both themes — and--gb-surface-2takes opposite directions in the two files, a step up on dark and down on light, which is exactly why each consumer looked right to whoever wrote it and wrong to everybody on the other theme. The theme files carry the same sentence at the definition. — ADR-0245, ADR-0168 -
AIt works open, and the condition the entry set for adding a capture phase was met. The entry named the fix —select’s typeahead works closed and not open.Handleshad anonKeyCaptureand noonTextCapture— and refused to add it on spec, “because a capture phase is a routing rule and inventing one for a single consumer is how a router grows two”. There is a consumer now.textInputcaptures root-first then bubbles, which isdispatchKey’s shape exactly and removes an asymmetry nobody had written down: one event kind had a phase the other did not.SelectListreads the letters on the way down and calls the sametypeaheadthe closed control calls, son,n,ncycles the same options in the same order either way. It consumes what it acted on, leaves blank text alone — a space means “pick this one” everywhere else — and atreegets none, which is above. — ADR-0246, ADR-0141 -
Whether to accept CSS’s alignment aliases is open.They are taken, because they are not aliases.align-items: startis CSS — Box Alignment Level 3 — and Yoga has onlyflex-start, so the toolkit was dropping a declaration the specification allows and telling the author they had made a typo. It filled the Panels screen’s console for long enough to need deduplicating before anybody asked whether the declaration was actually wrong. Two entries in one map, applied inkeywordafter the enum’s own lookup so a constant namedSTARTcould never be shadowed by it.leftandrightstay refused: they arejustify-contentonly and are notstart/endunder RTL, so §2.4’s bidi support means the toolkit cannot promise they stay equivalent. Two tests that encoded the old decision were rewritten, both in the group that exists because of this typo. — ADR-0247, ADR-0216 -
It is built, and the entry was wrong twice in the toolkit’s favour. §8 had listedalign-selfis not in §8’s subset.align-items/self/contentall along and named onlyflex-basisas unimplemented — so the document claimed this worked, and what was missing was the implementation rather than the sanction. AndAlign.AUTOwas already waiting for it: the enum’s own comment says “AUTOonly means anything foralign-self”, a value that existed for a property that did not. The price the entry quoted was real — 47 positional argument lists across two records, withalignItemsandalignSelfthe same type, so a swap between them compiles and runs — and it was already insured.RecordWitherTesthas existed since ADR-0181 for exactly this: it asks every wither to set its component to what it already holds and requires the record back unchanged, which no transposition survives. The clean sites were scripted and the four carrying inline commas edited by hand.stackis one blocker lighter; what it still wants isstackitself. — ADR-0244, ADR-0181, ADR-0111 -
Nothing warns that aIt says it once, and the field is the resolver’s rather than a static. The entry named the fix — “once per property per stylesheet would make it a diagnostic” — andvar()resolved to nothing — it logs, per node, per frame.ComputedStylehad already met the same problem one stage later and answered it (ADR-0216): a stylesheet is static, so a declaration that cannot resolve cannot resolve next frame either, and “that is not a louder warning, it is a quieter log”. The difference worth having is where the set lives.ComputedStyle’s is static and needs a publicforgetReportedDrops()for tests; aStyleResolveris built per stylesheet set and lives as long as its renderer, so once per resolver is once per stylesheet — a theme swap builds a new one and legitimately reports what the new theme is missing, and a test is isolated by constructing its own rather than by remembering a static hook. Keyed by property and element type, which is a refinement of the entry: the same token failing onbuttonand ontextis two facts, and which types it reaches is the blast radius somebody debugging it wants. A cycle is keyed by the property alone, because that is a fact about the property. The drop is unchanged and still happens every time; only the report is once. — ADR-0243, ADR-0216, ADR-0121 -
emandremdo not resolve against the node’s ownfont-size.emdoes now, in two passes, and the fix needed no plumbing at all.CssLength.Contextwas always the right shape; what was missing is that nothing built one per element —WidgetRendererholds one for the whole tree and handed the same instance to every node, soemwas one constant at every depth. The two passes are CSS’s own rule rather than a refinement: onfont-sizeanemis the parent’s size, because the value being computed cannot be its own input, and on everything else it is the element’s own. Both were already in hand —parent.typography().size()is passed for inheritance anyway. Measuring it turned up a number the entry did not mention:Context.DEFAULTis 16 andTypography.INITIALis 13, so1emwas not the parent’s size, not the element’s own, and not any size the toolkit renders text at.Transformwas the same bug in a second place and said so in a comment; it takes aContextnow. What is left isrem, and it is above. — ADR-0242, ADR-0066 -
Nothing validates an application’s own theme.ThemeAuditdoes, and the pairs are found by convention rather than listed. The arithmetic was nine private lines inContrastTest; it iscss.contrast.Contrastnow, in:coreand exported, because a theme is core and an application should not need the widget catalog to learn its colours are unreadable. The part the entry did not anticipate is what makes it worth having: a hard-coded list of the toolkit’s own pairs would check a custom theme’s overrides and miss everything it added, so the rule is every--gb-<name>-bgwith a matching--gb-<name>-text— which the design system already follows, and which audits--gb-mycard-bgfor free. Two details decide whether it works on a real theme: values are substituted, so--gb-badge-warning-bg: var(--gb-warning)is measured rather than skipped as “not a colour”; and a translucent pair is skipped rather than scored, because what it composites over decides the answer —--gb-hud-bgis#1c212ae6and is the shipped example.ContrastTestnow calls the same code, so the number CI asserts and the number an application audits against cannot drift. It does not cover the non-text floor: which token is a mark is not something a naming convention can tell, and those sixteen are above. — ADR-0241, ADR-0239, ADR-0087 -
§2.2’s focus ring is below §1.2’s floor on every surface of the light theme.It follows the accent now, which is what the dark theme always did. Opened by ADR-0239 the moment the non-text floor was first measured, and the cause was a ramp left behind rather than a colour anyone chose: both themes set the ring to their accent, except that the light theme’s accent had moved down the Frost ramp to--nord10for contrast and the ring kept the pale--nord8. One token, 1.64:1 → 3.31:1 at worst, and a palette value rather than an invented one. What it exposed is the more useful half and is above: changing a shipped colour moved no golden, because every focus golden in the catalog wasNORD_DARK. — ADR-0240, ADR-0239 -
Non-text contrast is not checked at all.It is measured now, and the question the entry could not answer had a simpler answer than it looked. What counts as the background of a mark drawn onto its own box is its own box: a mark is coloured by thecolorof the element it is drawn in and that element supplies its ownbackground, and for every mark in the catalog the same rule sets both — a checked tick is--gb-checkbox-mark-checkedon--gb-checkbox-bg-checked, both fromcheck-indicator:checked. So the pair is oneComputedStyle’s two properties and nothing needs the painted frame. Three sweeps: a mark against its box, a ring against the surface behind it, and a control against that surface by the better of its fill and its edge — a maximum rather than two measurements, because §1.2 asks that some means identifies a component, and measuring separately reported--gb-borderfailing everywhere when a decorative divider is supposed to be subtle. What the sweeps find is above and is not this entry’s any more. — ADR-0239, ADR-0088 -
A wheel over a disabled control is swallowed outright, and the scroll view above it never gets a turn.It chains now, and the answer was “per event kind” — for one kind. The entry stated the question correctly and the resolution is the narrow half of it:dispatchstill refuses a press, a release and a click aimed into a disabled subtree, for ADR-0059’s unchanged reason — that argument is about the thing being aimed at, and a click on a disabled button must not become a click on the row holding it. A wheel is not aimed at a control; it is aimed at whatever scrolls, which is what every platform does with one. So for a wheel the chain is built and its disabled prefix dropped rather than the whole dispatch abandoned: the dead subtree still handles nothing, and what changes is only who gets a turn afterwards.isInputis untouched — takingWHEELout of “the user doing something” would have made the disabled knob start turning. What this also records is why nothing caught it:DisabledPropagationTest’s tree had nothing above the disabled container, so “refused” and “swallowed” logged identically. — ADR-0238, ADR-0236, ADR-0059 -
Nothing recomputes the cursor when the tree changes under a still pointer.It does now, and the entry named half of it. The cursor half is exactly as written — a fourth position field, remembered from every entry point that carries one rather than frompointerMovedalone, andcursorAtre-run fromupdateRegionsafter each paint.NaNis the whole of “we do not know”, and it means it twice: before the pointer has arrived and after it has left, which is another window’s pointer and not a place to ask about. The capture freeze is reached through rather than around, so a repaint during a drag does not thaw the shape. What measuring it turned up is that:hoverand:activehad the same staleness, and thatmark’s own comment denied it — “a control that was hovered before it became disabled does not keep the state” was describing an intention as an achievement, because clearing is not suppressed but nothing called it. Fixing only the cursor would have shipped a control drawing its hover wash while its cursor saidnot-allowed, so both halves went together; §2.1 left no decision to defer. — ADR-0237, ADR-0057, ADR-0059 -
A knob inside a scroll view is still untested, andBoth cases are tested, and the one the entry predicted would fail did. These were two entries saying the same thing from either end, and they close together. The wheel route had been covered since ADR-0061 — a fabricatedKind.WHEELhad exactly one consumer for a long time.SDL_MouseWheelEventthrough the real translate and the real sink — but untilscrollshipped there was nothing above a knob for an unconsumed wheel to reach, so the half of the contract that is about not consuming had never been run.Knob.wheelconsumed unconditionally; it now consumes what it moved, which isScrollViewport’s existing rule applied to a second widget rather than a new one invented for it.KnobChainingTestis the first test in the catalog to drive a wheel through a real bubble between two widgets, and the arrangement is what took the work: the list has to be scrolled off its top first, or the viewport’s own edge rule refuses the wheel and the test passes before the fix. What the exercise turned up is one entry it did not close, above: a disabled control swallows a wheel for a reason that is the router’s rather than the knob’s. — ADR-0236, ADR-0089, ADR-0116 -
The overlay enter/exit lifecycle is a specification without a subject, and the imperativeThe survey is done, and the answer is two objects rather than one controller. The arrival needs nothing shared —AnimationControllerhas now lost all three of its own.Phaseis already the whole of it, and six widgets use it without wanting more. The departure was the same code twice:dialogandmessageeach held two flags, a timer and six lines, and independently got the same four rules right — idempotence, two flags that mean different things, stop-drawing-before-telling, and gone-at-once under reduced motion. That is a mechanism waiting to be named, and it isDeparture. It is still not anAnimationController: it drives no value, interpolates nothing and owns no clock. What the survey also settles is thattoastandtabmust not be converted — a toast’s departure ends when its stack’s queue says so and a tab’s ends insiderender— which is ADR-0092’s rule about generalising from examples that already agree. — ADR-0234, ADR-0178, ADR-0081 -
Overlays do not animate in or out.They do, and this was fixed by the widgets rather than by the layer — which is what the entry itself predicted. §1.7’s overlay curve wanted “a toast to arrive rather than appear, which is a transition on the widget and not on the layer”: a toast slides 16px from its edge and reflows when a sibling goes ([ADR-0177], [ADR-0178]), a dialog scales from 0.96 and fades ([ADR-0176]), a banner rises 2px ([ADR-0175]). Thestackhalf of this entry stays open above, because it is a layout widget and this was never about one. — ADR-0100 -
Two popups do not know about each other.They still do not, and they do not have to: what was wrong is thatEscapeand a press outside were the same code. A press that lands somewhere else is the user pointing at something other than the menu, and the whole chain goes;Escapeis the user stepping back out of what they opened, one menu at a time. The launcher randismissPopups()for both, so openingFile → Recentand pressingEscapetook the parent with the submenu. Finding which handler was at fault was most of the work — aPopupwatches its own window and closes only itself, which is correct and never runs, because since ADR-0189 no popup holds the platform keyboard and the key reaches the owner. One more thing the entry did not say: the innermost popup is not always the one that goes, because a tooltip refuses light dismissal — so the walk looks past it rather than stopping. — ADR-0233, ADR-0103 -
A tooltip is plain text, has no maximum width of its own and does not follow the pointer.All three are what §7 specifies for v1, so they are boundaries and not gaps — and an entry that records a boundary belongs here rather than on a list of what is missing. All three are what “rich content” would change, and none of them has a caller asking. The delay is a different matter and stays open above: it is a number this toolkit chose rather than one §7 gave, and the token that would let an application change it cannot be read. — ADR-0105 -
That is the design, stated, and there is nothing here to build. A popup taller than the work area is clamped to the near edge;Placementstill clamps, and now two callers have stopped asking it to.menuandselectcap their own content first, from a measurement rather than a guess ([ADR-0179]). Any other caller that opens an oversized popup and offers noHost.Fitgets the clamp — which is right for a facility that cannot know what its content means: a tooltip that scrolled would be a tooltip that should have been a dialog. The entry read as a gap because it opens with “still”, and what follows it is a division of responsibility rather than a shortfall. — ADR-0179, ADR-0104, ADR-0118 -
Nothing hit-tests an overlay by rule.Both halves are rules now, and the second was two mechanisms pretending to be one. The topmost painted region taking the pointer was already true — the capture is in paint order and is scanned backwards — and is written onelementAtwith a test that fails if it stops being. The modal half was worse than unwritten:Handles.isModalsaid in as many words that “the pointer is not this flag’s business”, because a dialog is unreachable by mouse through its scrim. That is modality by geometry, and a widget that declared itself modal without a scrim trapped the keyboard and let every click through — the two halves of “modal” disagreeing in an accessibility feature. It is one flag now: the pointer reaches the modal’s subtree and its ancestors, and nothing else. The ancestors are the point rather than a loophole, because a scrim is the panel’s parent and a click on it is what closes the dialog. — ADR-0232 -
It is a list, and the decision the entry was waiting for is that there is nothing to decide. What is delivered is a notification and not an event: nothing is passed, nothing can be consumed, and each listener readsPointerRouterhas one listener slot, not a list.hovered()orfocused()from the router for itself — so no listener can change what another sees and order is not a policy. An event — one carrying a target, or consumable — would be the shape worth refusing, and is the one ADR-0105’s objection was aimed at. The slot also had a bug the entry had not noticed: a setter namedonPointingChangedreads like a registration and behaved like an assignment, so a second caller silently dropped the first and a tooltip simply stopped appearing. It hands back aSubscriptionnow, which the slot could not express at all. — ADR-0230 -
The survey is done, and it found a rank missing rather than a rank unused. Six rules drew ink in a bare semantic hue and only one of them was a line — the--gb-*-linehas one consumer, and the widgets that should be next have not been looked at.field:invalidedge the entry named. The other four were words, and §1.2’s floor for words is 4.5:1 where-lineis derived against 3:1, so pointing them at-linewould have moved them from clearly wrong to quietly wrong:--gb-danger-lineis 3.53:1 on the dark theme’s surface. So a hue has a fourth rank,--gb-<hue>-text, and the HUD has two tokens of its own because its plate is the same in both themes and a theme-varying red on a near-black plate is an absence rather than a warning. Thebadgehalf of the entry was a false memory: a badge is a filled chip with its own pair and has no border. Eight golden images changed, every one of which had been recording a colour below §1.2’s floor. And the survey is a lint now —noBareHueDrawsInkreads the stylesheet, which is the one question a contrast measurement cannot answer. — ADR-0229, ADR-0175 -
They ask theircollapseandcarouselnever stop asking for frames.Phasenow, and it was the same three lines the entry predicted. The question the frame loop asks is are you still moving, and both were answering were you built in a state where you could move — acarousel’s was never even conditional, so every window with one on it repainted at the refresh rate for ever. A phase settles itself on the frame that finishes it; aDoubleUnaryOperatorclosing over one cannot say whether it has. Two things the entry did not predict: a section shut half way through an arrival keeps a phase nothing will ever settle, soCollapseSectionguards onopen; andCarouselTest’s ownanimating()case asserted the bug, because it had been written against the implementation rather than against §1.7. — ADR-0228 -
Every clock-driven arrival costs one wasted frame.It does not, and “harmless and worth writing down” was one line short. The entry had the diagnosis exactly right — the renderer asked whether a node was animating before it drew it — and drew the wrong conclusion from it: a phase learns it has finished by being read, and the only place a widget is handed the frame clock isrender. Asked afterwards, the answer is current. One line moved, and it is worth one frame of every animation in the toolkit.TabMotionTestdocumented the waste in a comment and now asserts its absence. — ADR-0228 -
AIt takes one, and the thing that was missing was a name rather than a mechanism. The entry had already named the fix — “a way for a widget to describe nothing, which the element tree has no word for” — and what looking at the renderer showed is that the tree could always do it: a node that is neithermessagetakes nobind=, so a banner whose text comes from a model has to be described away rather than emptied.StylednorPaintsand has no children contributes no box, which is how every composition node already works. SoWidget.nothing()is a singleton leaf and no new branch anywhere. A bound banner whose value is blank is not there, and comes back when the value does — the same element, the same subscription, the same arrival, which is what makes this a value change rather than a node being rebuilt. The dismissed case converged on it and stopped leaving a gap. What is not converted isfield-message: doing so changes the spacing of every form, which five golden images say is a design decision about §4’s “message slot” rather than a bug fix. — ADR-0227, ADR-0175 -
A closing overlay used not to animate at all, and every golden passed.The lesson has somewhere to live now, and it is a test rather than a paragraph. The entry was right twice — the corpus cannot catch this by construction, and an assertion onisAnimatingis the only thing that can — and stopped one step short: the second half is a specification for a test, and left as prose it is read by people who already know.AnimationSweepTestis that test, in two rules: a widget holding aPhasedeclaresisAnimating(structural, scoped to things that actually paint, or it names six false positives and gets deleted), and every declaration ofisAnimatinghas a test beside it that names the method (which catches the animations aPhasedoes not describe — a tab’s number, a scrollbar’s idle clock). It found a real gap on its first run:ScrollViewportandScrollFadehad no such assertion anywhere. — ADR-0226, ADR-0176 -
A right-click does not select what it is over.It does, and the premise was half right. The toolkit still has no notion of what “select” means for an arbitrary widget — but the widget under the pointer does, and what was missing was never a concept of selection: it was a moment at which a row could be told the gesture had happened to it. The launcher’s existing walk supplies one, because it already goes from what was clicked up to whatever named the menu; it now remembers the deepest widget on that walk that can answer and asks it, once, immediately before opening. The rule the entry did not state is the one that makes it worth having: a row already in the selection leaves it alone, so right-clicking one of five chosen files opens a menu about the five. — ADR-0224 -
A bareIt does, and the entry had already written the design. “Key-release tracking with a nothing-happened-in-between rule, at the window level” is exactly what shipped — the part it did not predict is where the keycode has to be read.Alttap does not activate the menu bar, andF10does.Keynames no modifier on purpose, soAltreaches the router asKey.UNKNOWNand is indistinguishable there from every letter that arrives as text; the platform keycode is the only place the distinction survives, andWindowis the last component that holds one. What is bound is not aShortcutat all but a gesture, with its own tiny vocabulary and an exhaustive list of what spoils it — another key, a repeat, a second modifier, a press, a wheel, a focus change, and deliberately not pointer motion.F10stays beside it as the binding that survives a compositor which eatsAlt, and both toggle now. — ADR-0223 -
Both halves ship, and the model that outlives an opening turned out to be the one the author already wrote. What is built and discarded per opening is the popup; amenubaris not built, and it wants a menu that outlives one opening.Menuis a value, so amenubarholding one holds it for as long as the bar is mounted.Acceleratorswalks that description and binds every command with a key on it, with no menu on screen and none needed. A bar’s children areitems and a nesteditemis a heading, so no markup was added. — ADR-0163, ADR-0106 -
They do, and fixing either did fix both. The missing item-to-popup callback isLeftandRightdo not move between menus while one is showing.MenuSignals, and a bar hands its root menu aMenus.Siblingssaying what the two arrows that leave it mean — wrapping at the ends and skipping a separator or a disabled heading. A submenu gets none, which is what keepsLeftin one going back a level rather than leaping along the bar. — ADR-0219, ADR-0163 -
An accelerator is unbound by key, so aThe map remembers owners now, andmenubargoing away can take somebody else’s binding with it.menubaris the only thing that uses it — which is what the entry predicted. A binding is(action, owner)compared by identity;removeShortcut(key)still removes whatever is there, andremoveShortcut(key, owner)is a no-op when somebody else has taken the key since. The bind side is unchanged: two commands on one key is still last-writer-wins, and what changed is that the loser cannot unbind the winner. A displaced binding is still not restored — that needs a stack per key, and nothing has asked for one. — ADR-0220, ADR-0163 -
The keyboard menu key does not open a context menu.It does, and so doesShift+F10. The entry named both pieces correctly:Key.MENUis SDL’sSDLK_APPLICATION, and the element-wise anchor turned out to already exist asanchorOf, which the tooltip path had been using since ADR-0111.Shift+F10is bound beside it because a Mac keyboard has no menu key; bareF10is deliberately left to themenubar(ADR-0163). The walk up to the widget that named the menu is now one method both halves call, because “a right-click on a label is a right-click on the button” and “the menu key on a focused button is that button’s menu” are the same rule. — ADR-0208, ADR-0108 -
A keyboardIt opens in the same frame.Rightinto a submenu waits 150ms.Itemcan tell a hover from a keypress now:hovered()is what the pointer did andopen()is what a key did, and the delay is for the pointer — it stops a submenu dropping out of one travelling past three rows, and a keypress has travelled past nothing. — ADR-0219, ADR-0112 -
It does, and it is the arrow that opened it, undone. In a submenu it closes back to the menu above; at the root of a bar’s menu it moves along the bar; at the root of a context menu it does nothing, deliberately — a menu that vanished on an arrow key would be a menu nobody could navigate, andLeftdoes not close a submenu.Escapeis the key that means “put this away”. — ADR-0219, ADR-0112 -
Nothing marks the row whose submenu is showing.item.opendoes. The row keeps:hover’s wash for as long as its branch is on screen, which is what the pointer moving into the submenu made visible: the row it came from went plain while its submenu was still showing. A class rather than a pseudo-class, because “the branch that is showing” is a menu’s own bookkeeping and not a state the element tree tracks — the shapemenu-title.openalready used. — ADR-0219, ADR-0113 -
AIt can, by reversing the order: the × fades the banner while it is still described and tells the application when the fade is over, so nothing has to outlive the description.messagecannot go away with a fade. -
The sibling reflow is still not built, andIt is built, and which toasts move turned out to be a fact about the overlay layer rather than about the widget. Atoastis now the thing that could build it.toasteris pinned to a corner andcontrols.cssputs the newest toast at that end, so the column is anchored by its newest member: a hole in the middle leaves everything between it and the corner alone, and the older half travels in to close it. The ordinary case therefore moves nothing — a stack that shares a timeout loses its oldest first, and the oldest has nothing older to move. Neither number wasHost.anchor(id)in the end: the height comes fromMeasured, banked every frame because the toast is gone by the time it is wanted, and the gap comes fromtoaster { gap }through the channel ADR-0177 opened for the frame clock. A column ofmessagees still cannot have it, for ADR-0175’s unchanged reason: a banner has no owner to hold the list. — ADR-0178 -
A toast cannot be dismissed by clicking itThe plate is the affordance now. What §7’s omission of a × meant is that a toast does not need a second affordance competing with its action for a 360×40 plate — not that a persistent one should be undismissable. The click was already being swallowed, because the plate is hit-testable and a click on it reached nothing and did nothing. The trade-off, stated: a click aimed at the action button that misses it dismisses without acting; the button is told first, so a hit is never lost. — ADR-0182 -
A tooltip is plain text and has no maximum width of its ownIt has one now: 320, and the number is a judgement rather than a specification. §2’s metrics row gives a tooltip a padding, a radius and two delays and no width, so this isToaster.DEFAULT_MAXIMUM’s kind of decision — without it a sentence of help text is a ribbon across the window that is harder to read than no tooltip. The other three “consumers” ofmax-widthturned out not to be:toastkeeps its width on the design argument its own note already made — the same 360 on every toast is what makes a stack read as a stack, and a maximum would give the ragged pile back;popover’sminimumWidthis a runtime measurement (field.size().width()) that no declaration can express (ADR-0145); andtext-area’s max rows is built and is a row count rather than a length. — ADR-0181 -
A menu caps itself by estimate, not by measurement.It measures now, and so doesselect. The popup facility takes aHost.Fit— a callback handed what the content measured and the room it has, between the measure and the place — so the guess and the second copy of--gb-menu-item-heightare both gone. A twenty-row menu measures 667px where the estimate said 696, which is 29px of menu needlessly wrapped on a short display and nothing at all on a tall one. Returning the content unchanged costs nothing; returning something else costs a second element tree, which is the right way round because nearly every popup fits. — ADR-0179, ADR-0118 -
A field refuses right-to-left text outright.It takes it, and draws it mirrored. The interim this entry asked for is chosen: a paragraph shapes bidi text with the direction forced toLTR, so the glyphs are right, their order is not, and every width, caret and hit test agrees with what is on screen. It says so —Paragraph.isBidiApproximate()and one warning per distinct string — because the alternative to a crash should not be a silence. What is still ahead is the real thing:java.text.Bidirun splitting, which is several runs per line, visual reordering within a line, and a caret that knows which run it is in and which side of it. That last part is why the half-measure of handling uniformly right-to-left paragraphs was not taken — it changes whatwidthBetweenmeans to every caller, which is the same change full bidi needs. M5, with the IME preedit it sits beside. — ADR-0218, ADR-0167 -
Markup cannot hand a controller to a widget.It can, through a fourth registry — and the first attempt was refused by the codebase itself. This entry guessed the answer was “a registry besideactionsandbindings”, and it was; what it did not guess was that the binding registry would settle the question. A@Bindfield holding a controller is refused with “a value that cannot change is not something to subscribe to”, which is exactly what a controller is.Namedis the registry for objects that are neither methods, resources, nor values that change.scrollstill has the gap — aScrollControllercould be named the same way and nothing has done it. — ADR-0170 -
Nothing can ask for focus, soA container can hand focus down now.fieldhas no click-to-focus.Handles.delegatesFocus()turns the router’s walk round: a press that finds no focusable ancestor takes the first focusable descendant of a container that claims one. It has one consumer, which is one fewer than a mechanism should have —group-boxandcardare candidates and neither has asked. — ADR-0170 -
It does, andHost.focusstill does not exist.dialogis what needed it:host.focus(id, fromKeyboard), by id forHost.anchor’s reason — a widget has no element and never will. The rule that makes it useful was not the obvious one: a node that cannot take focus resolves to the first focusable thing inside it, so “focus this dialog” and “focus this form” mean what a caller intends. It is refused for anything outside an open modal. A form jumping to its first error is now two lines an application writes, and nothing in the toolkit writes them. — ADR-0176, ADR-0170 -
Nothing restores focus when a modal closes.It does, and the entry understated the problem. “The keyboard lands nowhere in particular” was the visible half;Element.unmounttells the element tree and nothing else, so the router went on holding the element that had left it — an unmounted node receiving key events and keeping its dead subtree reachable. So the fix is two rules: the router never holds an element that is not in the tree (right for a switched tab and a shortened list as much as for a dialog), and if there is somewhere to put the keyboard back, it goes there. The remembered element is indeed the first state the trap has held, kept to one slot, written at exactly one moment, and allowed to go stale on purpose. — ADR-0180 -
All four are, andmin-widthandmax-widthare not in the CSS subsetdialoghas the two numbers §2 asks it for. One value rather than four components — the four are only meaningful together, and they are the same question asked four ways, so a caller that handled three would have a bug nobody would find. The trick for “80% of the window” was the scrim: a percentage resolves against the containing block, so the scrim’s padding across had to go or the maximum would have been 80% of the window less 48px — measured at 330 in a window where §2 permits 339. The four consumers this entry named are all resolved, and only two of them by being built.tooltiphas amax-widthof 320 — a judgement rather than a specified number, because §2’s metrics row gives it no width at all, and 320 so a label cannot reach adialog’s minimum.text-area’s max rows shipped with the widget (ADR-0171).toaststays a width: giving every toast the same width is what makes a stack of three read as a stack, and a maximum would size each one to its own string, which is the ragged pile. Andpopover’sminimumWidthis not amin-widthconsumer at all — it is “at least as wide as the control this dropped from” (ADR-0145), a runtime measurement of a different node, which no stylesheet can state. — ADR-0181 -
Afield’s error summary is a list and not a widget.messageis built andMessage.summary(errors)is the summary — onedangerbanner with a line per failure, and empty when nothing is wrong, because a summary of no errors is not an empty banner. It is a factory rather than a childformadds, for two reasons that were not obvious until the widget existed: a form does not know where its summary belongs (above the fields is the convention, below is what a long form wants, a dialog’s header is what a dialog wants), and a form that drew one would have to rebuild whenever any field’s message changed — which is a notification fromValidatedtoFormAccessthat nothing else needs. What is still open is aform summary=#truethat does exactly that, and it is waiting on that notification rather than on the banner. — ADR-0175, ADR-0169 -
A golden of aBoth halves are fixed now. The first was the gallery rendering twice and asserting on the second, 200ms in, which §7’stext-areais a golden of its first frame.messageforced. The second — feeding the hit-test regions back between those frames — stayed open because nothing needed it badly enough, andmasonrydid: a layout that reads last frame cannot be photographed at all without it (ADR-0196).Measuredis delivered by the router, from the rectangles a laid-out frame produced, so a harness that only rendered gave every self-measuring widget a first-frame answer for ever. The harness runs render → lay out → hand the router the regions, twice, which is what a window does — and the Forms image is now the one the running application shows, with itstext-areawrapped at the width it actually has. — ADR-0175, ADR-0171 -
All five of the leftovers are built, and one of them was never actually blocked. The keyboard three —treeis built in a first cut, and §3 asks for more.Home/End,*, type-to-select — needed rows the focused one cannot see, which is why they waited and why each is a callback the tree hands down (ADR-0209). The other two arecheckable=and the selection models (ADR-0210). Multi-selection was recorded here as blocked onlistand that reading was too strict:treedefined the node model itself for the same reason, and wrote down thatlistwill have to agree — the selection models are the shape every desktop list has, which makes it a small promise to make onlist’s behalf.listis built now and the promise was kept:Selectionmoved to it andtreeimports it, and nothing about the shape changed on the way (ADR-0212). The checkbox needed a different question answered first, and it was in the design document rather than in the code: §3 spends the wordcheckabletwice, on which rows are an answer and on whether rows carry a box. Both ship, under two names, and the disagreement is now inARCHITECTURE.md§17.1. §2’s chevronrotateis two marks instead, because §8’s subset has notransformon a mark — the wallselect’s chevron hit — so a closed row draws>and an open onev, and the cost is the animation. — ADR-0210, ADR-0209, ADR-0184 -
Both are built, and the promise held. The item-factory did survive contact:listrenders every row, andtableis still waiting on the recycler neither has.virtualized(rowHeight)calls the same function with the same items and nothing about the API moved (ADR-0213).tableturned out not to be waiting for the recycler at all but forlist— a table’s rows are a list’s rows with more than one thing in them, so it composes one (ADR-0214). What is still out is rows of varying height, which the arithmetic rules out rather than merely lacks:index × heightis only a position if every row is that height, and the usual way round it — an estimate corrected as rows are measured — makes the scrollbar drift under the reader’s thumb. -
AIt scrolls. The popup facility reports what it measured, so neither caller has to guess, and both give the same answer from the same helper —select’s list is clamped rather than scrolled when it is taller than the screen.Fitted, which wraps content taller than the room in a viewport of the room’s height and leaves everything else alone. — ADR-0179, ADR-0141 -
It is. The selection is a set,select multiple=… is not builtchangeis a toggle in that mode — the set is the application’s, so asking for a value it already holds can only mean taking it out — and the list stays open while values are picked, which needed a popup whose content can change while it is open (Popup.content, new). §4’s free-text autocomplete is built too:TextInput.suggesting(options)offers aSelectListunder the field, the rows commit onEnterrather than following the focus, and the field’s text is never rewritten without the user choosing.select autocomplete=#trueis built too (ADR-0183): the closed control holds a realtext-input, so the editing model, the undo history, the clipboard and the caret stay where their rules already are; the field stops being a Tab stop and delegates focus, so a combobox is one stop;Escrestores and a free-typed value is refused unlessfree, both off one nullable string of offered text thatTextInputState.followalready knew how to honour.tree=#trueis built too (ADR-0184), and so is a first cut oftreeitself, whichlisthad to agree with since §3 says the two share an item-factory — and does, now thatlistis built and the selection models have moved to it (ADR-0212). — ADR-0182, ADR-0141 -
AThe field never loses it. Measured rather than reasoned about: the owner window’s router is not touched by a popup opening or closing, so the field keeps both its focus and its ring for as long as the list is up. What remains is the platform-level question above, which is not a control’s problem and not this control’s in particular. — ADR-0180, ADR-0104selectopened from the keyboard does not give focus back to the field. -
A gradient fill needs a symbol the export list does not have.It has six now, and the entry was right about which commit came first. The widening is the whole of the interesting part:bl_gradient_init_as,_destroyand_add_stop_rgba32build one,bl_context_set_fill_styleand its_rgba32companion put it on the context and take it off, andbl_context_fill_path_d— the plain fill, with no_rgba32suffix — is the only styleless drawing call on the list and the only way a ramp reaches a path. The OKLCH in the original wording turned out to be vacuous: a fade between two alphas of one hue is the same curve in every perceptual space, and what makes it correct is premultiplied interpolation plus repeating the colour at the far stop, because0x00000000is transparent black and a green fading to it goes through grey.goldberry-htmlandgoldberry-vectorboth start one commit further along. — ADR-0207 -
split-paneis not built.Both ship, and §5 is complete. The divider turned out to wantcarouselis not built.knob’s gesture anchor rather thanslider’s position — the pointer is somewhere inside a six-point bar, and reading its position would snap the divider under the finger on every press — and the carousel’s rotation is one one-shot timer rescheduled after each slide, so that a pause is a timer not scheduled rather than one suspended. What did not ship is one of the carousel’s three brakes; see the entry below. — ADR-0165 -
AThe third brake ships, and it cost one line because something else needed the same thing. This entry guessed the price wrong in an instructive direction: it said closing the gap meant “carouseldoes not pause when focus lands inside a slide.:focus-withinin the selector engine, the matcher and the router’s focus bookkeeping”. None of that was needed. A carousel does not want to style itself on focus-within, it wants to be told — and so does afield, which validates when the keyboard leaves it. So what shipped isHandles.onFocusWithin, a notification rather than a selector, reporting only the moves that cross a subtree’s boundary. The selector-engine version is still unbuilt and now has no consumer asking for it. — ADR-0169, ADR-0165 -
It ships, as a widget rather than as a flag oncollapse’saccordion=is not built.column. The flag belongs on the container — “one open at a time” is a rule about siblings — but honouring it needs state, and statefulness is a property of the type: putting it oncolumnwould give every column in every document aStateit never uses.column accordion=#trueinflates to anAccordionthat reportscolumnas its own CSS type, so the document writes what §5 says and an ordinary column pays nothing. — ADR-0166 -
Per-corner radii do not exist, andThey exist,segmentedis the second control that wanted one.segmenteduses them, and the fourth asking is what built them.button.squareasked first,segmentedsecond — both went round the outside, the bar keeping the radius and the segment inset.group-box-titlecould not: its top corners meet a rounded frame and its bottom ones meet the body, and no arrangement of nodes fakes that. It had been writingborder-radius: 7px 7px 0 0since it shipped, and the engine had been dropping the declaration with a warning nobody read.Cornersis four numbers over CSS’s 1-4 shorthand, the uniform case emits the drawing it always did, and elliptical corners are still refused.SegmentedTest’s pinned numbers did what they were pinned for: the bar is drawn joined again, with §3’s hairline between its cells, and the design system’s row is amended back.button.square’s joined buttons andtabsare the two callers ofCorners.inRowthat have not arrived yet. — ADR-0217, ADR-0216, ADR-0097 -
It asserts the platform’s own half of the contract now. One test read the realWaylandDecorationsTestasserted/procexists./proc/thread-selfand assertedOptional.of(false)unconditionally — true on Linux and false everywhere else, in a suite all three OS legs run (macos.ymlandwindows.ymlboth run:core:testunfiltered). The fix is not a skip: where/proccan answer it is still the live check that the parsing works against a real symlink, and where it cannot the assertion is that the answer is empty — which isonInitialThread’s documented contract, “a machine that cannot say must produce silence rather than a guess”, and the branch macOS and Windows actually take. Gating with@EnabledOnOs(LINUX)would have left two of the three platforms asserting nothing about the call that runs on them. — ADR-0084 -
A popup hangs on screen when the application loses focus to another windowNo popup of any kind holds the platform keyboard now, soanyWindowFocusedmeans what it says: the application is focused exactly when one of its own real windows is. A popup never relied on focus anyway — the owner has forwarded keys to whatever popup is open since ADR-0104, precisely because SDL focusesPOPUP_MENUwindows on some drivers and not others. — ADR-0189 -
(was) — still open, and one candidate is eliminated.
anyWindowFocused()counts popup windows, so a popup holding platform focus keeps the whole check true. AMENU-kind popup is focusable and is the likely culprit; the suggestion panels areTOOLTIP-kind andNOT_FOCUSABLEsince ADR-0186, so they can no longer be it. The next step is a real window and a log ofFocusChangedper window id, which the headless backend cannot produce. (earlier) The window hides and the popup stays where it was. The mechanism ADR-0144 describes is wired — the launcher watchesFocusChangedand callsdismissPopupsafter a settle delay if no window of the application is focused — so this is a fault inside it rather than a missing feature. Two candidates and no evidence yet: a popup window still reporting focused, soanyWindowFocusednever goes false; or the platform not sendingFocusChangedat all when the owner is hidden rather than deactivated. Diagnosing it needs a real window and a real compositor. — ADR-0185 -
It is, and it took the shape this entry predicted — one component onflex-wrapis not in §8’s subset.Box, one onComputedStyle, one line in the render tree, and 48 positional reconstructions. What it did not predict is the half that mattered: putting the property on the field wraps the chevron onto a second line under the chips, so the chips needed a box of their own. A golden image is what said so; nothing in the CSS looked wrong. — ADR-0192 -
Nothing drives §3’s select family through the real loop.SelectLoopTestdoes, and found a seventh defect on its first run: a click opened the list and closed it again in the same gesture, because the press focused the editor (which opens it) and the click then toggled from a staleopenflag. One signal opens an editable control now, and the signal is focus. What the harness still cannot reach is the platform’s window flags — revertingNOT_FOCUSABLEfails nothing, because the headless backend has none. — ADR-0188 -
(was) Nothing drives §3’s select family through the real loop. All three defects ADR-0185 fixed were found by running the application and were green in CI, because every test drives the widget by hand and each fault lives in the seam between the widget and a running window — the application’s rebuild, the platform’s focus, the pointer.
MenusTestdoes drive the real launcher against the headless backend and is the shape that would have caught them. Closing this is worth more than the three bugs were. — ADR-0185 -
The HUD’s budgets assume a 60 Hz display.They are shares of the display’s own frame now.SDL_GetCurrentDisplayMode’s refresh rate was already bound for the frame pacer; it reaches ahudthroughFrameStats.displayHertz(), and a platform that will not say falls back to 60 with the reading showing dashes rather than a number it does not have. — ADR-0153 -
A click costs one node’s style.It cost the whole tree’s, and the HUD is what found it. Hover and active apply to the ancestor chain, and every node in that chain invalidated its entire subtree in case a descendant combinator read the state — 74 of 78 elements per click on the showcase. This was never on this list because nothing could see it until the frame had a breakdown. — ADR-0149 -
The style cache makes a settled frame free.It did not, and had not sincescrollshipped. ADR-0070 measured style resolution as the largest term in a frame and cached it; the cache was keyed on the parent’s style by identity, and the style a parent hands down is not the one it caches —restyleruns afterwards and allocates. Every node under ascroll, atabor asegmentedre-resolved on every frame, which in the showcase is every node on the screen: 10 069 µs to render 77 unchanged elements. This was never on this list, because nothing had measured it. — ADR-0142 -
Nothing tells the toolkit its window lost focus.FocusChangeddoes. A menu left open while the user switched applications stayed on screen over the one they switched to, because a popup is always-on-top by kind and light dismissal only ever saw a press inside the owner window. — ADR-0144 -
It lives inoptionlives in…controls.segmentedandselectwill want it.…controls.option, andselectwants exactly whatsegmentedwanted. The guess this entry refused to make — “a model, possibly a tree node, a popup to render in” — turned out to be wrong in every part: a row in a dropdown is the same record as a cell in a bar, and the whole difference between them is a stylesheet’s ancestor selector and one flag saying whether the keyboard chooses or merely moves. ADR-0092’s rule paid for itself twice over — it stopped a generalisation that would have been made from the wrong example. — ADR-0141 -
A press that dismisses a popup also activates what it lands on.It does not, and this was never written down as a gap because nothing had hit it. With a list open, the press on the field that dismisses it was also read as “open it”, so aselecttoggled twice and stayed open. The launcher already took the press for the secondary button (ADR-0108); it now takes any press that actually closed something, which is what the click that puts a menu away does everywhere. — ADR-0140 -
A popup does not size itself to its content.It does, in two passes.RenderTree.measurelays a tree out with no surface; the second pass exists because Yoga lays a root out at exactly the available size when that size is definite — there is no parent for it to be “at most” of — so measuring against the window returns the window. Nothing definite first, then the width pinned only if the natural width overflows. — ADR-0104 -
Placement is not policy.Placementis, and it is arithmetic. Preferred side, flip only when the preferred side does not fit and the opposite one does, then shift along the cross axis; clamped to the near edge when it fits nowhere. Computed against the display’s work area —SDL_GetDisplayUsableBounds, reached throughBackendWindow.workArea()and translated byposition()— which is the rectangle that excludes the taskbar a menu would otherwise open under. — ADR-0104 -
Focus does not travel into a popup.The keyboard belongs to the open popup. Its router focuses the first item after the first frame, and keys the owner window receives are forwarded to the topmost popup before the owner’s own router sees them. Forwarded rather than delegated to platform focus, because whether a popup gets the keyboard is per-driver and a tooltip must never have it. What is still owed is the return: §7’s “restores focus on close” is the widgets’ to keep, and nothing yet remembers what had focus before a menu opened. — ADR-0104 -
Answered: it runs, through the real SDL, on every CI run. A test cannot turn a wheel — butSdl3Backend.translate’sMOUSE_WHEELbranch has never run.SDL_PushEventcan, which is what the call is for. A fabricatedSDL_MouseWheelEvent, written at the offsets the layout probe has already checked against the compiled C, goes onto SDL’s own queue, comes back out of the ordinary pump and takes the shipping route: the realtranslate, the real window lookup, the real sink. The tests assert the sign is inverted exactly once (SDL’s y is positive away from the user, the SPI’s is positive down the document), that “natural scrolling” is undone before that rather than after, that a touchpad’s fractions survive, and that the position comes from the wheel arm’s own fields — reading it through the motion arm’s accessor returns 3.0 where the answer is 120.0, because the vertical delta lands at exactly that offset. Under SDL’sdummyvideo driver, so it needs no display and runs on all three platforms. The cursor half was already answered: the showcase setsCursor.CROSSHAIRat start-up, soSDL_CreateSystemCursorandSDL_SetCursorreally run. — ADR-0061, ADR-0056, ADR-0057 -
Group opacity is a multiply, not a layer.Answered: it is a layer. A node withopacity < 1and children is composited through an offscreen raster drawn at full strength and faded once, which is what CSS specifies.group-opacity.pngis two overlapping squares under a parent at 50%, and the test asserts the overlapping pixel equals the non-overlapping one — true for a layer, false for a multiply. A translucent leaf keeps the cheap path deliberately: its own shapes can overlap each other, but by a fraction of a level on an antialiased edge, and an allocation and a blit per faded label is a poor trade. Three goldens with a:disabledcontrol at 45% moved, and the diff is confined to that control — the correction, reviewed rather than accepted. — ADR-0071, ADR-0064 -
Answered: a weight is a face.body-strongis not drawn, and no control uses a weight.Inter-SemiBold.ttfis extracted beside the variable file,font-weightresolves to one of two shipped faces in the cascade, and a button’s label is Inter 600 at 13/18. Instancing thewghtaxis would have been the smaller download and needed symbols in both HarfBuzz and Blend2D — three export branches, answered only by a CI run across four targets — while §1.4 ships exactly two weights. The axis stays a real optimisation for the day an intermediate weight is specified. — ADR-0066 -
There is no italic face, and an application has asked for one.Built, as two files rather than one.docs/gaps.mdG27 wanted italic beside underline and strikethrough; the other two came with ADR-0321 and the faces with ADR-0323. It was ADR-0066’s question one step on — an italic is a face, because Inter’s italic is drawn rather than slanted, and shearing the upright glyphs is a type-design decision rather than a workaround. Two faces, so the matrix closes: one would have left semibold italic resolving to the nearest of three, which is how a design system acquires a weight nobody chose. Matching is CSS’s order (family, style, weight), so italic code stays upright code;obliqueis dropped with a warning. The variable-axis answer ADR-0066 deferred stays deferred, and stays the right change the day an intermediate weight is specified — which is still nothing. — ADR-0323 -
Seven shippedFixed, and the worst of them was a rule applied where it does not hold. §1.2 had always said “every text/surface pair meets WCAG 4.5:1 […] validated in CI against both themes”; nothing validated anything untilbuttoncolour pairs are below §1.2’s 4.5:1 floor.badgeforced the question, and the first run ofContrastTestfound--gb-button-danger-texton--gb-button-danger-bgat 3.55:1 —--nord6on--nord11, unchanged since the first control shipped. Two things in the numbers were the shape of the fix rather than its size. Every ramp’s darkest step already passed (button.danger:activeis 5.11:1 on light), so nothing needed a new colour system — the ramps needed sliding, and the value that was:activeis roughly where rest belongs. And the worst pair was a hover state that was worse than the rest state one step from it:button.danger:hoverat 2.95:1 on dark, below the 3.55 it moved from. The dark theme lightens on hover, correctly, for a surface moving one step toward the light — and a danger button is not a surface, it is a saturated fill carrying--nord6, so lightening moved it toward its own text. Stated as a rule it already described three of the four filled variants: a fill that carries text moves away from it. Sobutton.dangeron dark now darkens on hover, against that theme’s usual direction and alone in the toolkit in doing so.--gb-danger-filland--gb-accent-fillreplace the aliases to--nord11and--nord10, and the danger ramp is now identical on both themes, because the hue is and the text on it is. The one piece of collateral was worth catching:--gb-checkbox-bg-checked-hoverand its radio and toggle counterparts aliased the button’s ramp, on the argument that a checked control and a primary button share the accent — true until a button’s fill started being chosen for its label. A checked glyph carries a mark, which §1.2 asks 3:1 of, so--gb-accent-bg-hover/-activeare split out holding the values the button’s ramp used to, and every checkbox, radio, toggle, slider, progress and spinner golden is byte-identical — two button images are the only ones that moved, which is what says the split landed where it was aimed.KNOWN_FAILURESis now empty and stays, asserted equal to the measured failures and asserted empty by name: nothing is exempt, and re-exempting a pair fails a test that says what happened — ADR-0088, ADR-0087, ADR-0082 -
Nothing animates.Answered for the properties that can. The frame clock, the curves, the overlay, the whitelist, OKLCH interpolation and reduced motion all ship, and the frame loop goes idle the frame after a transition ends. What is left of §1.7 is listed below rather than here. — ADR-0067 -
Answered, and the trap it named is what the change is about.transformis in §1.7’s whitelist and is not implemented.transformandtransform-originparse, cascade, apply down the box subtree the wayopacitydoes, animate through the overlay, and — the part worth the separate record — route input through the inverse of the matrix the painter used, computed once while painting rather than re-derived on the input path. A transform the painter applies and hit testing ignores produces no error and no wrong pixel: the control is drawn where the stylesheet asked and simply does not respond where it looks like it should. No new native symbol crosses the boundary:bl_context_apply_transform_opwas already exported for the display scale, andBL_TRANSFORM_OP_ASSIGNreplaces the context’s matrix rather than composing onto it — so the stack is accumulated in Java, which is also what makes it invertible. Blend2D’ssave/restoreare not exported and turned out not to be needed. A computedtransformis the function list, not a matrix, becausetranslate(50%)and the50% 50%origin default are proportions of a box that has no size until Yoga has run — and because halfway betweenrotate(0)androtate(180deg), interpolated entry by entry, is a collapsed box rather than a right angle. — ADR-0068 -
The check mark still does not scale.Answered, andtransformwas never what was missing. §1.7 and §3.1 specify the checkbox tick and the radio dot as “scale 0.6→1 + opacity”; the opacity half shipped with ADR-0067 and the scale did not arrive withtransform. The reason is that aBox.Markis drawn onto the box carrying it, so scaling the indicator scaled the 16px glyph with it — the ring grew with the tick. The mark is now a cascade node of its own (check-mark,radio-dot), which makes them the third and fourth parts and the first justified by something other than “two surfaces need two backgrounds”: two things must move independently, and the unit of independent movement is a node. The mark is built in every state and hidden withopacity, because a node that appears with the value has no previous style to move from and would snap.radio-group-scaling.pngis the frame at 80 ms of 160, one dot growing in and the one it replaced shrinking out, and what it asserts is that all three rings are the same 16px circle — which is precisely what the naive fix gets wrong. §3.1 now has no unimplemented row for any shipped control. — ADR-0073, ADR-0068, ADR-0065 -
Fixed.:activewas set on one element, so no control had a pressed state.:hoverwalked the ancestor chain from the beginning;:activewas set on the single deepest element the press landed on — so pressing a checkbox’s 16px glyph lit upcheck-indicator, pressing its label lit uptext, andcheckboxitself matched only in the sliver of padding between them.checkbox:activehad been incontrols.csssince the control shipped and was very nearly a dead rule. §2.1 requires every control to render a pressed state, and one that depends on which of its own parts you hit does not have one. Found by trying to write the radio’s pressed appearance, not by a test — and the test that now covers it asserts the ancestor, which is the half the original test never looked at. — ADR-0073 -
An unnamed key crashed the window.Fixed.keyPressedbuilt aShortcutfrom every key that reached it, to use as a map key.Shortcutrefuses to holdKey.UNKNOWN— an accelerator on it could never fire — so theIllegalArgumentExceptionwent up the UI thread with nothing above it. Not an edge case:Keynames the keys a shortcut might use, so every letter, digit and punctuation mark that arrives as text isUNKNOWN, and the crash was one keystroke away at all times. The accelerator tests never saw it because they only ever pressed keys that had names. — ADR-0073 -
A checkbox was invisible on the surface it normally sits on.Fixed, and the reason CI missed it is the interesting half.--gb-checkbox-bgwasnord1, which is--gb-surface; the light theme’s was#ffffff, which is its--gb-surface. The token’s own comment gives the mistake away — “one step up from the window” was measured against--gb-bg, and almost nothing sits directly on the window. Both glyphs now take the button’s ramp on each theme rather than one of their own, which is the scale §2.1’s “one surface step” is already defined by. Every golden image in this repository paints on--gb-bg, so a control that disappears on--gb-surfacewas invisible to the entire suite;controls-on-surface-{dark,light}.pngadd the missing axis rather than one more scene. — ADR-0073, ADR-0050 -
Answered, and deliberately at four controls rather than at thirteen. §1.3’s--gb-densityis not implemented.regular | compactships: every control sizes itself from--gb-control-height, anddensity-compact.cssis a three-token:rootblock in the theme layer — the same slot asnord-light, because that layer is defined by what it holds rather than by what it is called, and a fifth cascade layer would differ from the fourth in its name and nothing else. The layer is also what makes the override work: both blocks are:root, so specificity ties andlayeris the only term left to separate them, which is why the test asserts the layer rather than the resolved height.Density.REGULARships no stylesheet at all — regular is not something an application applies, it is what the toolkit already is, and adensity-regular.cssrestating 32 would be one number in two files, which is the arrangement that produced both the §10.1 typography table and the checkbox’s private surface ramp. Padding, gap and radius stay literal, asserted so: §1.3’s density row names heights and list rows, and tokenising the rest “for symmetry” invents a scale the design system does not have.--gb-densityitself is a marker rather than the mechanism, because a keyword cannot select a number in §8’s subset. Every existing golden is byte-identical, which is the check that the token swap was a refactor; two new ones are the same scene at both densities. The showcase switches onCtrl+Dand not one widget in that file mentions a height, which is the whole of what “token-conformant apps adapt with zero code” claims. Named rather than implied: compact is below §1.3’s own 32×32 hit-target floor, deliberately — the floor is the regular default rather than an invariant, the trade is what a density preference is, and it is bounded by the glyph staying 16px so compact costs margin around the target rather than a smaller target. — ADR-0074,docs/design-system.md§1.3 -
A slider’s groove was invisible on a surface.Fixed, and it is the fourth instance of one defect.--gb-slider-track-bgwasnord1on the dark theme, which is--gb-surface— so the unfilled groove vanished on any panel, which is where the showcase’s options live. A slider hides this better than anything before it: the fill and the thumb still show, so the control looks like a control and merely appears to have no track. It is--gb-bordernow, because a 4px groove is an edge. What is different this time is thatcontrols-on-surface-{dark,light}already existed — ADR-0073 added it for exactly this — and had not been extended to the new control, so the axis was covered and the control was not.everySurfacelessControlIsCoverednow asserts every entry inControls.controlTypes()is in that scene, withbuttonexempt and saying why, and the scene is one helper the golden and the guard share. Verified by deleting the slider from the scene and watching it fail by name. — ADR-0079, ADR-0073 -
A slider has no tick marks and no value label.Both ship, and the label needed exactly the mechanism this entry predicted. A widget can name the part its pointer position is measured against —Handles.localPart(), a CSS type resolved by the router — because a label at the end of the row takes its width off the track and a value mapped along the control is short by that width at every position, drawn correctly and reported nowhere. The marks hang out of a zero-height row, moved clear of the thumb by atransformso that adding a scale does not move the groove. — ADR-0080, ADR-0079 -
It ships, as a value rather than a function.fader’s dB scale is not implemented.Scaleis a sealed interface with two inverse methods and two records — the obviousDoubleUnaryOperatorspelling is the wrong one, because §11’s parity invariant compares two control records for equality and two lambdas doing the same arithmetic never are.knob’s taper is what it was built general for. What it does not have is a second curve: §3 names dB and nothing else, and inventing alogor anexpfor symmetry would be inventing a scale the design system does not have (Principle 3). — ADR-0080 -
Arrow-key group navigation inside composites does not exist.Answered, as a mechanism rather than as a radio group.Handles.focusScope()makes a subtree one Tab stop with the arrows roving inside it, andtabs,menu,select’s popup list and a toolbar all get it by returningtruefrom one method. The router owns both halves, by the argument already written on Tab — traversal is a property of the tree and not of any node in it — and the test is written against bare widgets in:corerather than againstradio, because the next three users will look nothing like a radio. — ADR-0073 -
A focus scope has no axis.Answered.Handles.focusScope()returns aFocusScope—NONE,HORIZONTAL,VERTICALorBOTH— andradio-groupis the one composite in the catalog that legitimately answersBOTH, because its direction is its stylesheet’s and.inlineflips it. The axis is the widget’s even though traversal stays the router’s: the router cannot know what a widget means by the other pair, and the widget cannot see its own siblings. It only matters on the path where the widget declines the key, which is why a boolean survived four controls — arrows reach the focused chain first, so a menu bar that handlesDownitself works either way. The failure it prevents is a menu item with no submenu decliningRightand aBOTHscope quietly sliding focus to the next item: the user asked to open something and the selection moved instead, with no error anywhere.HomeandEndbelong to no axis and reach the ends of any scope, because they name a position in the set rather than a direction on screen. Four widgets unblocked by an enum. — ADR-0078, ADR-0073 -
A disabled group fades correctly only by an explicit undo.Answered, and the undo is deleted rather than generalised. A rule whose only job was to undo its own mechanism was the mechanism saying it was the wrong one. — ADR-0077 -
Layer promotion does not exist, so every animating frame repaints the window.Answered. A promoted subtree is rasterized at full strength and untransformed, so its alpha and matrix apply to the blit — and a group that is only fading or moving now keeps its raster, which is the case §1.7 wanted promotion for and which ADR-0071 shipped without. One flag had been answering three questions: does the screen differ (damage), does an ancestor’s raster differ (yes, it bakes in this node’s finished blit), does this raster differ (no, alpha and matrix are the composite’s). A descendant’s opacity is baked in, which is why it could not be fixed by droppingopacityfrom one comparison. Measured on the showcase’s tree at 45%: a frame of the fade is 199 µs against 554 µs, 2.8×.RenderTree.layersRepainted()is public because a cached raster and a fresh one produce the same image, so no pixel assertion can tell them apart — which is exactly how the bug survived a test file written about layer caching. — ADR-0072, ADR-0071 -
Damage tracking says what to upload, not what to paint.It paints what changed now.bl_context_clip_to_rect_dandbl_context_restore_clippingare the third and fourth new exports, andRenderTree.paint(frame, damage)clips to the damage — 367 µs to 117 µs on a frame where one small box changed. Read that carefully: the damaged area was 0.23% of the window and the saving is 3.1×, not 400×, because the clip saves rasterization while the tree walk still visits every box for Blend2D to clip away. Skipping the traversal too is a further change and is not made. Correctness rests on a promise the SPI now makes:BackendWindow.retainsFrameContents(), false by default so a backend that says nothing gets a full repaint.Windowchecks three things that fail independently — the promise, the buffer’s identity (a backend may retain and still rotate between two), and the size — plus a fourth case where the backend lends nothing and the buffer isWindow’s own, which retains by construction. A clipped repaint is asserted pixel-identical to a full one across a whole frame, because otherwise damage is a rendering bug with a performance excuse. — ADR-0072 -
A disabled container does not disable its descendants.Answered, and the sentence turned out to have two halves that pull apart.docs/core-widgets.mdsays “disables its descendants for input and semantics” — and deliberately not for paint, which is where the double-fade came from. Input propagates: no press, click, wheel, focus or key reaches a descendant of a disabled container. Paint does not::disabledstays on the node that declared it, because the container’s own 45% already fades everything under it (opacity multiplies down a subtree) and a descendant that also matched would land at 20%. It costs nothing in expressiveness, since §2.1 requires disabled to be opacity and never a colour remap. The effective value is derived by walking up the ancestors, not stored — ADR-0073’s lesson applied again: a second copy of a fact the tree already holds disagrees the first time something changes without telling the thing that cached it. The router is the choke point, one guard indispatchplusisFocusable, so a control written without its owndisabledcheck is still unavailable — and the keyboard needed no guard at all, because focus is the only route a key has, so one line about focus coversonKey,onKeyCaptureandonTexttogether. The cut is input versus observation: enter, exit, motion, hit testing and the cursor all still work, which is what keeps ADR-0059’s two cases — a click that must not fall through, and a tooltip explaining why something is unavailable.form,group-boxand adialogin itsclosingphase all get this for free. — ADR-0077, ADR-0059 -
The state and rebuild API.Answered. The stateful-widget lifecycle, rebuild scheduling and dirty-marking are settled: state lives on the element,setStatemutates immediately and defers the rebuild, and the tree flushes dirty elements once per frame. — ADR-0052, ADR-0004 -
KDL 2.0 Java parser.Answered, by writing one. No third-party parser was adopted: the tokenizer and parser are hand-written for the §9 subset, with the §9 example document as a test. — ADR-0051, ADR-0005 -
Answered, and now driven by Yoga itself. A Java upcall returningYGSizestruct-by-value upcall returns.YGSizeby value is called from C and arrives intact; the return segment is allocated once per callback rather than per call, and an exception thrown by a measure function is held and rethrown in Java instead of taking the process with it. The node API is bound, so the callback is invoked by real layout passes with the constraints the flexbox algorithm arrived at — not by a C probe written for the purpose. Proven on linux-x64; the checks run on every target in CI, so the other five are answered by the next run rather than by argument. — ADR-0017, ADR-0029 -
Windows has never been built.Answered. All four targets link, and all three export branches are now exercised rather than argued about: the ELF version script on both Linux targets, the Mach-O-u,_symbol/-exported_symbols_listpair onmacos-aarch64, and the MSVC/INCLUDE:and.defbranch onwindows-x64. The Windows leg buildsgoldberry.dll, runs:natives:testagainst it withgoldberry.native.required=trueso a skipped test cannot pass for a passing one, and matches the golden images — which is also what answers Win64’s 4-bytelong, the one thing no other target could catch. What Windows has not done is open a window: the leg links the library and runs the Java tests, exactly the hole ADR-0039 describes for macOS. The showcase image workflow is what would close it. — ADR-0012, ADR-0041 -
Live resize stalls on Windows and macOS.Taken, and half proven. Both platforms run a modal loop during a resize gesture, so SDL does not return from event pumping until the drag ends and frames stopped with it. Goldberry now installs anSDL_AddEventWatchcallback and draws from inside it: SDL keeps pumping events within the platform’s loop, and a watch is called from inside that pump, so it is the one place a frame can be produced while the platform holds the thread. Four guards decide whether it does anything — the UI thread, an active sink, re-entrancy, and the event type — and each is there because a watch is called in circumstances a pump never is; the resize the queue then delivers a second time is coalesced away rather than laid out twice. What CI proves is the whole mechanism except the platform: a test pushes an event from inside an event handler, which is the same state a modal loop creates, and asserts that the resize and a frame come out of the watch re-entrantly. What is left is that Windows’ and macOS’ loops really do pump during a drag — SDL’s own documented behaviour, and a human with a mouse is what would confirm it. — ADR-0060, ADR-0024 -
Blend2D and AsmJit have no release tags.Answered. Neither upstream has ever cut one, so both are pinned by commit SHA instead — Blend2D at6dbc2ceand AsmJit at0bd5787, the pair that has actually built, linked and passed the tests. All six upstreams now resolve to exactly one commit, so the build is reproducible. What remains before publishing is the licence texts. — ADR-0030 -
Shaping itself is unverified: there is no font to shape with.Answered. Inter, JetBrains Mono and OpenMoji are fetched at build time, pinned by version and SHA-256, and packaged intogoldberry-core(ADR-0033). Shaping now runs against real outlines: real glyph ids rather than.notdef, a proportional face measurably different from a monospace one, and emoji resolving through OpenMoji. Right-to-left glyph reordering is still unchecked — it needs a script the bundled faces cover. — ADR-0032 -
Nothing draws a glyph or an icon yet.Both do.bl_font_*andbl_context_fill_glyph_run_d_rgba32were bound first; the path API followed — seventeen symbols, one per SVG command, plus the three stroke options an icon needs because Lucide is drawn in strokes rather than fills.SvgPathreads the table’s path data with SVG’s own number grammar, and every one of the 1544 icons is asserted to parse and produce geometry. What is still open is that an icon is not aBox: the showcase draws them over its sidebar rather than laying them out in it, because nothing decides an icon’s intrinsic size until the widget model does. — ADR-0043, ADR-0004 -
ATwo copies per face now, not per size.Fontcosts two copies of the font file, and there is one per size.FontFaceholds HarfBuzz’s whole font — which is size-independent because Goldberry never scales the shaper — and Blend2D’s data and face;Font.on(face, size)adds only the object the size lives on. A second size measures at 4.4 µs against 681, and four sizes of Inter cost three megabytes rather than twelve. Faces are owned explicitly rather than cached globally, because these objects are thread-confined and a per-thread cache of native memory has no hook that would ever free it. What remains is the two copies themselves: each library owns its own memory, and neither takes a borrowed buffer for font data. — ADR-0044 -
Nothing measures text for layout yet.It does. AParagraphshapes once and wraps with arithmetic, and its measure function reports a height to Yoga through theYGSizeupcall. What is still ahead is bidi run splitting — right-to-left text is shaped in logical order and therefore drawn mirrored, because HarfBuzz returns those glyphs in visual order and prefix sums taken in logical order would otherwise measure the wrong ones (it was refused outright until ADR-0218, which cost a window every time somebody pasted Arabic into a field). Font fallback between the UI and emoji slots — the thing that makes a paragraph several runs rather than one — is built: the itemizer splits emoji out by UTS #51’s sequence rules and a paragraph takes one measurement over up to two shapings, with the emoji face’s advances rescaled into the base font’s design units. — ADR-0218, ADR-0393, ADR-0036 -
The paragraph cache is a one-entry memo.Both caches exist, and the numbers say why.ParagraphCacheholds shaped paragraphs keyed by(font, text); the width memo stays inside eachParagraph. Shaping is 56 µs and a cache hit is 0.05 µs, while a memoised wrap is already 0.02 µs — so shaping is the only part worth a cache, and caching layouts would save nothing. The cache has no consumer yet, because nothing rebuilds a widget tree; it exists because the measurement says it will be needed the moment something does. §6’s third key component, the width bucket, is the per-paragraph memo, and the “resolved text style” is aFontuntil the CSS engine has something better. — ADR-0037 -
A fresh upcall stub per text box per frame is the largest cost of text in a layout pass.Answered: the render tree is retained.RenderObjectowns aYGNodethat survives the frame and keeps its measure callback for as long as the paragraph behind it is the same instance. Measured on a showcase-shaped tree with seven measured leaves at 960×640: layout and walk fall from 190 µs to 7.2 µs, and a whole frame from 354 µs to 148 µs. The 7.2 µs row is the one that had to be won — it hands over a fresh box tree every frame, as a real application produces, and it matches the do-nothing case because every Yoga setter is guarded by a comparison against the box already applied. Yoga dirties a node when a style is set, not when it changes, so an unguarded retained tree would cost exactly what a thrown-away one costs plus the memory management. Retention also introduced this repository’s first keep-state bug, caught by its own equivalence test: Yoga does not dirty a node when its measure function is replaced, so a paragraph swapped for longer text reported the height cached for the old one — six lines of prose laid out as one, with no error anywhere. — ADR-0069, ADR-0037, ADR-0004 -
The cascade is now the largest term in a frame.Answered: it resolves invalidated nodes, which is what §5 always said it did. A node’s resolved style is cached on its element and checked by identity against two things — the resolver, so a theme swap or a hot reload invalidates everything at once with no event to remember to fire; and the inherited style, so a parent that re-resolved hands its children a different instance and they re-resolve without being told. Invalidation is a subtree, because a descendant combinator means a node’s own match depends on an ancestor’s state:checkbox:hover check-indicatorrestyles the indicator while the checkbox’s own style need not change at all, and that rule is incontrols.csstoday. One hook —setPseudoClass— covers:hover,:active,:focus,:disabled,:checkedand:indeterminate, and fires only on an actual change, which matters because the renderer mirrors three of them onto every styled element every frame. The CPU a frame spends before rasterizing falls from 148 µs to 3.5 µs — 354 µs to 3.5 µs taken with the retained render tree, a factor of a hundred. — ADR-0070, ADR-0052 -
The isolated paint benchmark and the in-app paint number disagree by 10×.Answered: it ispresent. A frame that follows a present costs about four times what the same frame costs painted back-to-back — 2.19 ms against 0.57 ms, measured by skipping present and changing nothing else — and the benchmark never presents. It was not the borrowed compositor buffer, which was the standing hypothesis: painting into a heap buffer measured 2.28 ms against the surface’s 2.22 ms. Nor the icons (+0.01 ms), the display server (Wayland 2.22, X11 2.07), the compositor (SDL’sdummydriver 2.00), or the environment at all — the benchmark’s own loop, run inside the live application between two real frames, came out at 0.49 ms while those frames cost 2.06 and 2.25. The mechanism is cache and TLB pollution; a synthetic 96 MB eviction between iterations reproduces 1.6× of the 3.8×. — ADR-0045 -
Every frame damages the whole window.Answered for the upload. Something now knows which parts changed: the retained render tree remembers each node’s rectangle and reports the union of old and new for whatever moved. What is still true is that the painting is full-frame — see the damage entry above for why that needs an SPI change rather than more code here. — ADR-0071, ADR-0004 -
CMake arguments live in five places.The refs do not any more.CMakeLists.txtreadsgradle/libs.versions.tomlitself, so a ref bump is one edit and there is no default to drift from; a floating ref is refused at configure time. The manylinux container never needed a JDK to read the catalog, only something that can parse a text file.checkPinnedRefsis inverted — it asserts no copy has come back, across every workflow rather than three, which is what would have caughtexample.ymlpinning Blend2D to a floatingmaster. The rest of the argument list — build type, install prefix, target id — is still kept in step by hand. — ADR-0035 -
Nothing warns at run time that a window came up undecorated.Answered:WaylandDecorationswarns, once, with the command that fixes it. Not by asking SDL, which cannot answer —libdecor_newsucceeds even when every plugin failed, so SDL marks the surfaceWAYLAND_SHELL_SURFACE_TYPE_LIBDECORand exposes nothing to say the frame is empty. It is inferred from which plugin files are installed, which works because the GTK plugin is guaranteed to fail in a JVM. The verdict is three-valued and stays silent when it cannot locate a plugin directory: a warning that is sometimes wrong is worse than none. — ADR-0084 -
A Goldberry window on GNOME/Wayland has no titlebar out of the box.Answered: X11 is the Linux default now. On a Wayland session the backend asks SDL forx11,wayland, unconditionally — under XWayland the window manager decorates the window itself, which is the only configuration today that produces a titlebar matching the desktop. Wayland stays behind X11 rather than being dropped, so a session without XWayland still gets a window, and the ADR-0084 warning still fires there.-Dgoldberry.backend.videoDriver=waylandasks for Wayland anyway. The cost is ADR-0027’s resize quality and fractional scaling, given up for as long as decorations are unobtainable on the better axis. — ADR-0086 -
A window on GNOME/Wayland had no titlebar and could not be resized.Answered: SDL was built without libdecor. Wayland has no decoration protocol of its own, GNOME’s compositor declines to draw them server-side, and every use of the client-side path in SDL sits behind#ifdef HAVE_LIBDECOR_H. Withoutlibdecor-0-devSDL builds a complete Wayland driver that opens an undecorated toplevel — and since a Wayland resize is client-initiated from the decoration’s own edge, the same missing header removes resizing too. The Java side was never involved:WindowSpec.ofasks for decorated and resizable andSdl3Backend.createWindowpasses exactly that. It only became visible when ADR-0082 addedegland the Wayland driver started being built at all. — ADR-0083 -
Answered: the table it checked had drifted from what SDL demands. It probedcheckToolchainpassed and the build died two minutes later.pkg-config --exists xss, a module no distribution ships — SDL’s own spec isxscrnsaver— so the row returned “absent” whether the package was installed or not, and it was marked optional besides, while SDL’sCheckX11treats XScrnSaver as aFATAL_ERROR. XTest, the next hard stop in line, was not in the table at all. Both CI workflows already knew all of this, in comments, written by whoever hit it there twice. The table is nowLinuxDependenciesin build-logic with a three-valuedNecessity, andLinuxDependenciesTestasserts it against the packages the workflows install — the invariant that broke. — ADR-0082