An unpainted background emits no background, so transparency survives#520
Conversation
|
CI infra unblocked: your The only failures left are golden/snapshot mismatches, which are the intended result of this PR -- it changes how backgrounds are emitted, so the committed render references no longer match:
To clear them, regenerate the references, eyeball the diff to confirm the new output is what you intend, then commit: Unrelated aside: the run logs a background |
There is no alpha in the terminal protocol: a cell is transparent precisely when the application never set a background for it. Emitting any background -- including one that matches the terminal's -- makes the cell opaque. Unpainted backgrounds defaulted to the atom `:black`, which the renderer maps to `\e[40m`: an opaque black cell. On an opaque black terminal that is invisible, so it shipped. On a transparent terminal each of those cells punches an opaque rectangle through the window, while cells whose background is genuinely nil stay see-through -- hence the same UI rendering at two different opacities. Default an unpainted background to nil instead. The renderer already emits no SGR for a nil background, and every run is `\e[0m`-terminated, so nothing bleeds into it. This also un-overloads the sentinel. `:black` had to mean both "unpainted" and "actually black", indistinguishable by the time it reached the renderer; nil now means unpainted and `:black` is free to be a real, dimmable color. CellDim.dim_bg passes nil through: there is nothing to dim, and dimming it would paint it. The one test that asserted an unpainted background rendered as `:black` was pinning the bug.
Two more places turned an unpainted background into a painted one, so cells stayed opaque even after defaulting the background to nil: ThemeResolver.resolve_bg_color/3 fell back to the theme's global background (and then to :black), so every element was handed an explicit background. Renderer.build_ansi_prefix/2 fell back to the theme's default background when a cell's background was nil, painting the very cells that had asked for none. Both are the same mistake: the theme's "background" is an assumption about the terminal's own, and painting it is a no-op on an opaque terminal and an opaque rectangle on a transparent one. This is also what CellDim already assumes. It detects the real ground with OSC 11 and *solves* colours against it -- the terminal owns the background and we adapt to it. Painting our assumed ground over the real one contradicts that. So: themes colour content (foreground, accents, borders); the terminal owns the canvas. An app that genuinely wants a different canvas still sets a background explicitly, and that still paints -- foreground keeps its theme fallback, since text does need a colour. The theme tests asserted that an unpainted box inherits and paints the theme's background. That was the behaviour being removed, so they now assert nil; the test that overrides a background explicitly still expects it painted.
The FATE corpus hashes the cell grid, and a cell carries its background, so every fixture that never asked for one moves from `bg: :white` to `bg: nil`. The button snapshots move the same way at the ANSI level: the `\e[40m` they used to carry is simply gone. Both sets of references were recorded against the behaviour this branch removes -- they encode "an element with no background gets one anyway". The render diff is exactly that and nothing else (verified cell-by-cell against master: only the bg token changes; glyphs, positions, foregrounds and attrs are byte-identical).
c258077 to
89b28c2
Compare
|
Rebased onto current master, which brought the FATE golden corpus (#441) into range. Every reference hash in The render diff is only that. Verified cell-by-cell against master: Glyphs, positions, foregrounds and attributes are byte-identical; the only token that Those references were recorded against the behaviour this branch removes — they were |
ADR-0029 rule 2: an unpainted background emits no background. This clause defaulted bg to :black on its own, unlike every other element in the paint path -- element_renderer and border_renderer both went through this fix already (#520), the divider was missed. Visible on a modal: the surface fills its interior with the theme's :surface colour, but the divider punched a black cell through it because :black overrides the fill instead of showing it.
Reported from iTerm with a transparent background: some elements are opaque and some are see-through, in the same UI.
Why
There is no alpha in the terminal protocol. A cell is transparent precisely when the application never set a background for it. Emitting any background — including one that matches the terminal's own — makes that cell opaque.
Raxol defaulted an unpainted background to the atom
:black:and the renderer maps
:black→@bg_color_codes[:black]→\e[40m. So every "unpainted" cell was painted with an opaque black.On an opaque black terminal that is invisible — black on black — which is exactly why it shipped. On a transparent terminal, each of those cells punches an opaque rectangle through the window, while cells whose background is genuinely
nilstay see-through. Hence: one UI, two opacities.Fix
Default an unpainted background to
nil. The renderer already emits no SGR for anilbackground, and every run is\e[0m-terminated (renderer.ex:199), so nothing bleeds into it.This also un-overloads the sentinel.
:blackwas forced to mean both "unpainted" and "actually black" — indistinguishable by the time it reached the renderer (CellDim's moduledoc says so explicitly: ":blackis the de facto unpainted-bg sentinel"). Nownilmeans unpainted and:blackis free to be a real, dimmable color.CellDim.dim_bg(nil, _)passes through: there is nothing to dim, and dimming an unpainted background would paint it — silently re-introducing the bug on every dimmed cell (i.e. the whole modal backdrop).Tests
New
unpainted_background_test.exslocks the actual invariant end-to-end: a fg-only style resolves its bg tonil; a painted bg is preserved; anilbg emits no48;/\e[40mSGR; a painted bg does;dim_bgpassesnilthrough but still dims a real color.One existing test asserted an unpainted background rendered as
:black— it was pinning the bug, and is updated.Sweep (ui + cross_terminal + playground + renderer): 1798/1798.