Expand description
The terminal renderer for makeover_layout.
Named for the target and not for ratatui, the same way
makeover-immediate is named for the mode and not for egui.
§What a terminal actually costs you
Not colour. That was the original assumption here and it is wrong on any
terminal built this decade. Measured across the 31 shipped themes
(makeover’s well_fidelity example):
| ANSI-16 | ANSI-256 | truecolor | |
|---|---|---|---|
| a well collapses onto its face | 18/31 | 4/31 | 2/31 |
| at least one bevel edge vanishes into its face | 31/31 | 4/31 | 0 |
The threshold is 256, not 24-bit, and the two failures that survive at
truecolor are not terminal failures at all: they are the themes whose
raised surface is already white, so the lightening clamps and the well
lands exactly on its face. Those render identically in a browser.
makeover’s own well_is_distinct_from_its_face test already names them.
What a terminal costs is geometry, and no amount of colour fixes it.
An edge occupies a whole cell on each side. A cell is roughly 8x17 pixels,
so a one-pixel bevel becomes something an order of magnitude heavier, which
is why frame hands back a shrunk Rect instead of pretending the
region survived intact. There is nowhere to put a corner radius, so
radius_control and radius_container mean the same thing here. A fill
can only begin and end on a cell boundary.
That is the constraint worth designing against. It does not improve, it is not detectable, and it applies equally to the best terminal ever written.
What it does not mean is that the shape inside the cell stops mattering.
Half of a cell is still addressable, and a bevel drawn in half-blocks reads
as a lit edge where the same bevel in box-drawing reads as a line: ─ and
│ are one stroke through the middle, identical on all four sides, saying
nothing about where the light is. Half-blocks also make the two corners
where light meets shadow expressible, since a glyph that fills half a cell
leaves the other half to the second tone.
§Where fidelity does matter
At Fidelity::Ansi16 the depth vocabulary collapses outright: a well
cannot be filled distinctly on most themes and a bevel loses an edge on
every one of them, so a raised card and a well both read as a single-tone
box. Colour cannot carry the distinction, so frame carries it with the
glyphs instead.
Above that, colour carries it and the glyph fallback never fires.
Palette::shows is worth reading correctly in light of the numbers: it
is not a low-colour workaround. It is a correctness check that a fill
will be visible against what is behind it, and at truecolor it fires on
exactly the two clamping themes, which is precisely when it should.
§0.13.0: a modal, and two cues four ports were about to each invent
Depth::Overlay arrives in
makeover-layout 0.14.0 and needed nothing here: Palette::fill has
answered Fill::Overlay since this crate had a palette, so what was missing
was the route from a description rather than the drawing. A test asserts it,
because a route nothing exercises is one a refactor can quietly lose.
[Theme::selection_on] and [Theme::focus_ring] are the other half, and
both are DERIVED rather than authored. Every consumer measured did selection
with REVERSED, for want of an on-accent foreground; every one that wanted
a focus ring either spent makeover’s border-strong on it, which is a
divider at 1.63:1 on Akari Dawn, or derived its own the way alloy_tui
does. Four terminal ports were each about to answer that separately.
Derived, not authored, because the direction is one-way. An authored key can fall back to a derivation and break no theme on disk; a key this crate started requiring would break every theme that lacks it. So the theme format does not change and nothing on disk grows, and the promotion stays available for a theme that ever needs to tune either.
§0.14.0: the first structural widget
table is the first thing here that draws content rather than a surface,
and it exists because 14 call sites across mnw-cli and viewer were
already drawing one. mnw-cli had written the mapping layer by hand
(src/tui/widgets.rs: a muted bold header, a selected row carried by the
background alone) and viewer had written a smaller one, which is two
answers to a question this crate is supposed to answer once.
It is a mapping layer over ratatui::widgets::Table rather than a table
implementation, because ratatui already lays tracks out, draws a header,
highlights a row and scrolls. What it has no answer for is content
measurement and narrowing, and those are what the module is.
§0.19.0: widget is piece, because the word went to the description
makeover-layout 0.20.0 added Region::Widget, the third tier between a
primitive and Region::Bespoke: a named assembly of primitives that each
renderer draws its own way. That is host-agnostic and sits above every
renderer.
This crate’s widget module is the opposite end of the same stack —
renderer-local, the answer to what a meter looks like in cells, taking a
description plus what only a terminal knows. Two different things wearing
one word, and the collision would have been worst exactly here, in a crate
that has to implement the tier.
So this half moved and the description’s half kept the word. That direction
is not arbitrary: a second or third party naming a widget is naming the
layout kind, and nothing outside this tree ever needed a word for a drawing
routine. WidgetStyle is PieceStyle.
§0.27.0: a bar for a bounded number, and an option that is not offered yet
makeover-layout 0.28.0’s form findings, at the renderer that has the bar
already. A makeover_layout::FieldKind::Range is drawn as piece::meter’s
cells with its two ends read out either side, because the ends are what the
question means and a terminal is where it would be easiest to quietly show a
figure instead. An unbounded range falls back to the text path rather than
to bounds this crate invented.
Choice::unavailable is the one place the three-tone convention’s muted is
the truth rather than the lie it warns about: that option will not answer,
and the reason it will not now sits on its row.
Field::placeholder on a chooser, the third finding, is already answered
here and needed nothing: this renderer draws every option of a select at
once, so an unanswered one is a list with no mark against any row rather
than an empty box with nothing in it.
§The correction this renderer forced
makeover_layout::Fill briefly carried a fallback method, returning
Page for Well so a consumer without surface-well had something to
use. That is an answer for a renderer that can always paint a colour. Here
it is actively wrong: page is the surface a well is usually cut into, so
falling back to it produces the exact invisibility the fallback was meant
to avoid.
Substituting one intent for another is renderer policy, not description.
The fallback moved out of the description and into
makeover-immediate, where it belongs, which is the first thing a second
renderer was built to find.
Re-exports§
pub use makeover_layout;
Modules§
- piece
- A meter, a badge, a control, a figure and a form field.
- table
- Columns, narrowing, cell parts and the sort caret, over ratatui’s own
Table. - text
- Word wrapping that answers a height and a drawing from the same measurement.
Structs§
- Palette
- The resolved colours this renderer needs.
Enums§
- Fidelity
- How many colours the terminal can actually show.
Functions§
- frame
- Draw a region at a given
Depthand return the area left for content. - paint_
bevel - Paint a two-tone edge around the outside of
area.