Skip to main content

Crate makeover_layout

Crate makeover_layout 

Source
Expand description

The renderer-agnostic half of the make-family design system.

makeover answers what colour, and varies by theme. makeover-geometry answers how much space, and varies by density and surface. This crate answers what the thing is, and varies by nothing.

§The deferral rule

A description names intents and relationships, never values. Say Fill::Raised, never #D9DDF4. Say Gap::Peer, never 6px. What is left once colour and spacing are deferred is composition: which edges are lit, what inverts on press, what nests in what.

The constraint that shapes all of it: a renderer that can only paint rectangles has to be able to express the result. egui has no box-shadow: inset and one stroke per widget with no per-side control; a terminal has box-drawing characters and one cell of resolution, and cannot draw a two-tone lit edge at all. A description that assumes per-side edges is a CSS description wearing a neutral name. So this crate names the intent — this region is a well — and each renderer chooses an expression it can actually produce, including dropping half of one.

§Scope

Depth came first: the bevel and the surfaces it shapes. That much was settled the hard way — the vocabulary here was read off audiofiles’ ui::theme and ui::widgets, which are the only implementation written by a consumer with no CSS, then checked against both webview apps. All three agreed once Balanced Breakfast’s fills were corrected.

0.2.0 adds the rest of the description, each member drawn the same way, from what the three apps already hand-write rather than from a taxonomy:

Validation was absent on purpose here, on the grounds that neither app had a shared story. That reasoning is retired — see 0.11.0 below, which is where the constraints arrived and why the argument did not survive contact with what the apps were measured to do.

0.3.0 closes a gap the first real adoption found, which is what adopting against goingson first was for. Selector described only the chosen option, so an unchosen one fell through to Depth::Flat and no renderer drew it; goingson’s tab strip recesses its unchosen tabs by hand and could not delete the line, because being recessed is why the chosen tab reads as coming forward. So Selector::unchosen joins chosen, and saying it needed Fill::Sunken and Depth::Sunken: a surface set back by colour with no edge, which is neither a well nor level-with.

0.7.0 adds State, the interaction axis, closing the gap that adopting against three apps rather than one made visible. The description named rest and, through Depth::pressed, pressed. It named neither focus nor disabled, so makeover-webview emitted a hover rule and stopped, and each consumer completed the primitive from outside by out-specifying a rule it did not own: 19 such rules in goingson, 21 in the MNW server, a further set in Balanced Breakfast, and three focus rings that do not match. The axis is deliberately two members wide, because hover and pressed belong where they already are. State’s own docs carry that argument.

0.8.0 finishes Field, which described a field well enough to label it and not well enough to draw it. Writing makeover-webview’s form emitter found three things missing and the renderer supplied all three from outside: the current value, a select’s options, and the placeholder. Two of those move here and one does not.

  • Field::placeholder is user-facing text sitting beside label and hint. There was never a reading on which it was renderer state; it was outside only because adding a field to a published struct is breaking.
  • Field::options moves because every renderer needs them and each was going to invent its own shape. Choice is the shape makeover-webview already arrived at, taken as-is rather than redesigned.
  • The current value stays renderer-side and is not coming here. It is the one of the three that is genuinely state: a webview reads it out of the DOM, an immediate-mode renderer holds a &mut to the app’s own field, and a description that carried it would be a form model.

0.9.0 opens RowPart, which was the last closed enum in the vocabulary, and adds RowPart::Tokens. Both halves come from the same finding, made by the first two real screens described through the router rather than by reading a stylesheet.

A goingson project card carries two trailing badges, a type and a toned status; a contact card carries a primary email and a strip of tags. Meta is one slot and one string, so both ports joined their facts with a separator and lost what the second one was: a status reads as text where it used to read as colour. Token already says exactly the right thing — a small labelled thing with a kind, a tone and an optional action — and could only ever be a node in its own right, never inside a row.

So the missing thing was permission rather than a concept. Tokens is that permission, and #[non_exhaustive] arrives with it so the next member is not a lockstep event across three renderers. The pairing is the point: this enum’s own consumer in makeover-webview carried a comment predicting it would stop compiling one day, which is a lockstep break written down and waited for rather than prevented.

Balanced Breakfast was checked before the member was added, because one consumer wanting something is not evidence. It packs a count and two icon buttons into the same single Meta slot while leaving Actions empty, so the slot was already straining under a second consumer for a different reason.

0.10.0 adds Meter, a proportion carried as a pair rather than as a percentage. Its own docs carry the argument; the short form is that the percentage shape had already been tried in goingson and had already needed a companion flag to recover what rounding and clamping threw away.

0.11.0 is four members from the quasi proving ground, batched into one release because pre-1.0 a minor is breaking and a cascade is nine repos. Three findings that arrived with them turned out not to belong here at all: this crate has no notion of an action, a route or a destination, so anything asking what a control calls was never the vocabulary’s to say.

  • Figure, a value with a caption. goingson had five of them across five screens with five class vocabularies for the one shape, which is the divergence this crate exists to end, sitting in plain sight and counted for the first time.
  • RowPart::Proportion, so a Meter can sit in a row. Meter reached two of its seven sites at 0.10.0 and the other five are row-shaped. Exactly RowPart::Tokens’s problem with a different payload, and it takes Tokens’ answer: the part carries the description of a bar, not a node.
  • Field::max_length, Field::min and Field::max, joining Field::required, which had been sitting here as the sole constraint while the header above claimed there were none. The set stops before pattern, which fails the renderer test and is one site in one app.
  • FieldKind::File. Every host has an honest answer — a native picker, an <input type="file">, a path prompt, an argument — and it carries no accepted-types list because accept appears at zero sites in either app.

The evidence rule changed under these, and it is worth recording because four earlier decisions were made under the old one. The two-app test said a shape earns a word once a second app wants it. It is backwards: a rule that withholds a word until a second app has duplicated the code guarantees the duplication, and app three writes it a third time. The bar is now generic against bespoke — is this furniture any app would have, or is it this app’s own? Bespoke keeps Region::Bespoke, which already carries a completion heatmap and is the right answer for a calendar nobody will build twice.

§Where the description stops

The bespoke widgets, a day-plan timeline and a kanban board and a calendar, are not describable here and will not become describable. A description expressive enough to produce a timeline is a widget library wearing a description’s name. Generate the boring 80% so the bespoke 20% gets the attention.

Region::Bespoke is how that limit is stated rather than hidden. The description names the place and the app owns the contents, so a screen containing a timeline is still a whole screen and still routable. Without it, the four goingson screens that make the app worth using would need a second, undescribed path beside the router, and two paths is how a vocabulary starts drifting from its app again.

Structs§

Choice
One option offered by a field FieldKind::offers_options accepts.
Column
One column of a table.
Field
One field of a form.
Figure
One figure with a caption: a number and what it counts.
Meter
How much of a set is done.

Enums§

Arrangement
How a screen is laid out.
Bevel
Which way the light falls across a two-tone edge.
Depth
How a region sits relative to the surface behind it.
Edge
One side of a bevel, named by the intent it takes.
FieldKind
What kind of value a form field takes.
Fill
A surface intent a region is filled with.
Heading
How far down the heading tree a title sits.
Notice
Something the app is telling the user, unprompted.
Priority
What a column is worth when there is not room for all of them.
Readiness
Whether the content of a region has arrived.
Region
A named part of a screen.
RowPart
The parts of a list row.
Selector
A control that picks between things.
State
An interaction state a region can be in, beside whatever Depth it is.
Token
A small labelled thing that sits inside something else.
Tone
What a region is saying, when it is saying something.
Width
How much room a column asks for.

Traits§

Intent
A colour intent this crate refers to but never resolves.