Raxol. UI. ColorResolver
(Raxol v2.6.1)
View Source
The single whole-list resolution pass that turns Raxol.UI.ColorIntent
structs (and the {:fixed, color} wrapper) surviving in a cell list's
fg/bg slots into concrete literal colors -- hex strings, {r, g, b}
tuples, ANSI atoms, or 256-palette integers -- exactly once, as close to
the terminal writer as this codebase gets.
See docs/core/RENDERING.md's "Region prominence" section for the model
this implements.
Test-ID legend
Comments and tests below cite short IDs (RP-N-02, RP-P-01, F2, A6,
...) rather than re-deriving each contract inline: RP- is the region-
prominence spec docs/core/RENDERING.md numbers negative (N, a
falsifier -- something that must NOT happen) and positive (P, a
guarantee) assertions under; bare letter+digit IDs (F2, A6) are that
same document's numbered functional/architectural requirements. Each ID
maps to one test/raxol/ui/*.exs describe/test name carrying it, which
is the authoritative expansion of what the ID claims.
Raxol.UI.Layout.Engine's stamp_region_prominence/2 stamps a
per-element region_prominence float -- computed by
Raxol.UI.RegionPolicy.region_prominence/4 from the region paths present
this frame, the focused path, and any mounted dimming overlays -- into a
transient {:region_prominence, p} marker in each cell's attrs list
(see Raxol.UI.Renderer's per-element render step). This module reads
that marker, composes it into effective_p (own_p * region_p), and
fades both fg and bg, literal or intent-resolved, toward the frame's
terminal ground -- mirroring Raxol.UI.CellDim.dim_fg/2 / dim_bg/2
exactly (including the :black fg-vs-bg sentinel asymmetry documented
there) but parametrized by the composed p and a module chroma exponent
@region_gamma instead of CellDim's fixed (contrast_keep 0.45, chroma_keep 0.65) pair. @region_gamma is the closed-form solve
ln(0.65) / ln(0.45) (~0.5395), chosen to reproduce CellDim's
chroma_keep to floating-point precision at the one p
(Raxol.UI.Layout.Engine's @overlay_keep, 0.45) the modal dim
composes. See the @region_gamma module attribute comment for the full
derivation and test/raxol/ui/region_prominence_test.exs for the golden
(RP-N-02: 0 of 964 painted cells differ on the modal_demo fixture).
The {:region_prominence, p} marker is a resolver-internal implementation
detail, never a real cell attribute: resolve_cells/2 always strips it
before a cell leaves this pass (see resolve_cell/3), so no downstream
consumer (buffer diff, terminal writer, MCP structured screenshot, ...)
ever sees it. p == 1.0 (the default, and every cell's value absent a
mounted dialog or other de-prominent region) short-circuits every
dimming clause to identity, so the pass is neutral (RP-P-01) whenever no
region drops below full prominence.
Local-ground bookkeeping
Cell lists arrive flattened -- tree containment is already erased by
the time render_to_cells/2 concatenates every element's cells, and
overlapping absolute layers make "enclosing element" ambiguous. So the
ground a bg intent resolves against, and the ground a fg intent (or
the legibility floor) resolves against, cannot come from containment --
it comes from paint order: a sparse %{{x, y} => resolved_bg} grid,
folded once over the cell list exactly as Raxol.UI.CellManager.put_cell/2
and Backends.inherit_background/2 already fold overlapping cells when
merging into the buffer. A cell reads the grid (its own under-layer, or
the terminal ground if nothing painted there yet) before resolving, and
writes its own resolved bg into the grid after -- but only if that
bg is non-nil (opaque). A nil (transparent) bg never writes, so a
stack of transparent cells all fall through to the deepest painted bg,
identical to how the buffer itself inherits background today.
What actually resolves
nil-- the unpainted/transparent sentinel, passes through unchanged (and never writes the grid).{:fixed, color}-- unwraps tocolor, exempt from everything (seeRaxol.UI.ColorIntentmoduledoc).%Raxol.UI.ColorIntent{}in abgslot -- resolved viaRaxol.UI.Theming.Salience.solve/4against the enclosing ground (the grid's under-layer at this coordinate, else the terminal ground), then region-dimmed (below) -- the RESULT becomes the local ground for this cell'sfgand for every later cell painted at the same coordinate. bg intent resolution itself has no prominence term -- it always resolves at the intent's own tier's full-strength target (tierdefaults to:baselinewhen unset); the region term is layered on after, identically to a literal bg (below).%Raxol.UI.ColorIntent{}in afgslot -- resolved ateffective_p = clamp(intent.prominence || 1.0) * clamp(region_p)(own_pcomposed with the per-cell region prominence) against the local ground (this cell's own region-dimmedbg, else the grid's under-layer, else terminal ground), then clamped to the intent's:floorclass against the local resolved bg's actual color (not a synthesized gray of its lightness) via bisection along the same fade line. The floor clamp runs after the fulleffective_pcomposition, so a region dim can never push an already-floored color back under its floor.- Any other term (atom, hex string,
{r, g, b}, integer, ...) -- a literal. Passes through completely unchanged (RP-P-01, the byte-identity contract) when this cell's region prominence is1.0; region-dimmed otherwise, exactly likeRaxol.UI.CellDimdims literals for the modal case -- literals participate in region dimming, not just intents.
Region prominence
Each cell's attrs list may carry a transient {:region_prominence, p}
marker (p :: float() in 0.0..1.0), stamped per-element by
Raxol.UI.Renderer from Raxol.UI.Layout.Engine's
stamp_region_prominence/2 output before cells reach this module.
region_prominence_of/1 reads it (default 1.0 when absent); the marker
is always stripped from the emitted cell's attrs before it leaves
resolve_cell/3 -- it is never a real terminal/buffer attribute like
:bold or :dim, only a resolver-internal composition input.
At p == 1.0 every dimming clause below is the identity (an explicit
guard, not just an arithmetic no-op) -- byte-identical output, matching
the neutrality contract that holds whenever no region drops below full
prominence. At p < 1.0:
- fg -- an intent's
effective_pfolds inpbefore the existing fade+clamp pipeline runs (so the floor clamp sees the fully composed prominence -- no upstream stage can push an already-floored class back under the floor). A literal fg fades viaregion_dim_fg/3-- the fg-flavored half of the CellDim mirror: a paintedfg: :blackis dimmed as a real color (CellDim.dim_fg/2's documented asymmetry). - bg -- both an intent-resolved bg and a literal bg pass through
region_dim_bg/3--nil(the unpainted sentinel) and the literal atom:black(the unpainted-bg sentinel, seeCellDimmoduledoc) both pass through completely untouched, matchingCellDim.dim_bg/2exactly. - Both dimming paths fade apparent lightness toward the terminal
ground (
ground + (apparent - ground) * p) and chroma by `p @region_gamma** -- the same two-channel OKLCH interpolationCellDim.dim_oklch/4performs, parametrized instead of hardcoded.@region_gammais chosen so the shipped modal look survives through the unified formula exactly (see moduledoc intro and the@region_gamma` attribute comment).
Reuse, not reimplementation
The fade/clamp math is not duplicated here. Raxol.UI.Theming.Salience's
solve/4, hex_to_oklch/1, and apparent_lightness/3 do all OKLCH/H-K
work; Raxol.UI.Harness.Prominence's fade/3 and wcag_ratio/2 do the
ground-aware fade and the external WCAG contrast check. The only new code
here is the bisection driver -- adapted from Prominence.clamp_to_floor/7
but walking the fade line against the LOCAL bg's real hex rather than a
gray reconstruction of the ground's lightness.
The writer guard (RP-N-03)
resolve_cells/2 always emits literal fg/bg for every cell -- every
clause below terminates in a literal. enforce_resolved!/1 is the
defense-in-depth postcondition check run as the pass's last step: if any
cell still carries a %ColorIntent{} or {:fixed, _} (a producer bug, or
a code path that skipped render_to_cells/2 entirely), it raises in
dev/test and maps the offending slot to :default plus a
[:raxol, :ui, :color_resolver, :unresolved_intent] telemetry event in
prod -- fail visible-but-safe, never a crashed render in production.
Summary
Functions
The RP-N-03 writer guard: any cell whose fg/bg is still a
%ColorIntent{} or {:fixed, _} after resolution raises (dev/test) or is
mapped to :default with a telemetry event (prod). Called automatically
as the last step of resolve_cells/2; exposed publicly so it can be
exercised directly (e.g. injecting an unresolved intent to prove the
guard fires -- RP-N-03's falsifier).
The chroma exponent region-prominence dimming uses -- see the
@region_gamma module attribute comment for the RP-N-02 rationale.
Exposed so tests/callers can compute the exact expected region-dimmed
chroma for a given p without duplicating the constant.
Resolves every ColorIntent/{:fixed, _} in cells' fg/bg slots to
literal colors, folding a paint-order local-ground grid across the whole
list. Literal-only input passes through unchanged.
Types
Functions
The RP-N-03 writer guard: any cell whose fg/bg is still a
%ColorIntent{} or {:fixed, _} after resolution raises (dev/test) or is
mapped to :default with a telemetry event (prod). Called automatically
as the last step of resolve_cells/2; exposed publicly so it can be
exercised directly (e.g. injecting an unresolved intent to prove the
guard fires -- RP-N-03's falsifier).
@spec region_gamma() :: float()
The chroma exponent region-prominence dimming uses -- see the
@region_gamma module attribute comment for the RP-N-02 rationale.
Exposed so tests/callers can compute the exact expected region-dimmed
chroma for a given p without duplicating the constant.
Resolves every ColorIntent/{:fixed, _} in cells' fg/bg slots to
literal colors, folding a paint-order local-ground grid across the whole
list. Literal-only input passes through unchanged.
Options
:ground- ground (background) OKLCH lightness. Default:Raxol.UI.Theming.SalienceTheme.detect_ground/0-- never a hardcoded constant; the ground is always live-detected, never assumed. A caller that needs a fixed ground for a test passes it explicitly here rather than this module defaulting to one.:color_depth- the capability-tier downgrade rung (:truecolor | :ansi256 | :ansi16 | :none). Default: a guarded lazy read ofRaxol.Terminal.Capabilities.color_depth/0that falls back to:truecolor(NOT that function's own:ansi16no-record default) whenever no capability record has been cached for this session -- seedefault_color_depth/0for the full rationale. A caller that needs to force a rung (tests; an app that already knows its terminal) passes it explicitly here.