makeover_layout/lib.rs
1//! The renderer-agnostic half of the make-family design system.
2//!
3//! <!-- wiki: makeover-layout -->
4//!
5//! `makeover` answers *what colour*, and varies by theme. `makeover-geometry`
6//! answers *how much space*, and varies by density and surface. This crate
7//! answers *what the thing is*, and varies by nothing.
8//!
9//! # The deferral rule
10//!
11//! A description names intents and relationships, never values. Say
12//! [`Fill::Raised`], never `#D9DDF4`. Say `Gap::Peer`, never `6px`. What is
13//! left once colour and spacing are deferred is **composition**: which edges
14//! are lit, what inverts on press, what nests in what.
15//!
16//! The constraint that shapes all of it: a renderer that can only paint
17//! rectangles has to be able to express the result. egui has no
18//! `box-shadow: inset` and one stroke per widget with no per-side control; a
19//! terminal has box-drawing characters and one cell of resolution, and cannot
20//! draw a two-tone lit edge at all. A description that assumes per-side edges
21//! is a CSS description wearing a neutral name. So this crate names the
22//! *intent* (this region is a well) and each renderer chooses an expression
23//! it can actually produce, including dropping half of one.
24//!
25//! # Scope
26//!
27//! - **Depth.** [`Bevel`], [`Edge`], [`Fill`], [`Depth`]: the bevel and the
28//! surfaces it shapes. Fill and bevel are named together, so a raised bevel
29//! over a recessed fill is unrepresentable.
30//! - **Components.** [`Token`] (badge against chip), [`Notice`] (toast against
31//! banner), [`Prominence`], [`CellPart`], [`Heading`], [`Selector`],
32//! [`Readiness`], [`Awaiting`], [`Progress`], [`Track`], and [`Tone`], the
33//! one intent family they share.
34//! - **Schemas.** [`FieldKind`] for what a form field takes, [`ColumnRoom`]
35//! for what a table column asks of the sizer, [`Facet`] for the dimensions a
36//! set is narrowed by.
37//!
38//! The things a screen is built from (a field, an option, a column, a control,
39//! a meter, a figure, a fact) are `quasi-router`'s, which owns them and the
40//! reasoning behind each. This crate keeps the facts those members are stated
41//! in: kinds, tones, priorities, widths and the arithmetic a sizer shares.
42//! - **Structure.** [`Region`] for the parts of a screen, [`Arrangement`] for
43//! how a screen is put together, [`Showing`] for how many of a region's
44//! children are visible at once.
45//! - **Adaptation.** [`Affordance`] for what exists at a given input class and
46//! window size class, read against `makeover-geometry`'s `Density` and
47//! `SizeClass`. The one part of the vocabulary that varies, and it varies
48//! only by those two axes, which it borrows rather than defines.
49//!
50//! # What a member is admitted on
51//!
52//! A member is added when an app needs a fact the vocabulary cannot state, and
53//! refused when what it wants is presentation it should be asking a renderer
54//! for. Three tests, all of which have to pass:
55//!
56//! - **Generic against bespoke.** Is this furniture any app would have, or is
57//! it this app's own? A rule that withholds a word until a second app has
58//! duplicated the code guarantees the duplication. What the app owns
59//! keeps [`Region::Handover`] and [`Region::Ceded`].
60//! - **Every host has an honest answer.** A member no renderer can express
61//! without borrowing one host's idiom is not a description.
62//! - **It can be laid out before it is filled.** See *First paint is final
63//! paint* below.
64//!
65//! Count the members a thing needs before refusing it. A refusal is only worth
66//! as much as the measurement under it.
67//!
68//! # What this crate cannot say
69//!
70//! - **An address.** What a control calls, and where a button goes.
71//! - **A current value.** A webview reads it out of the DOM and an
72//! immediate-mode renderer holds a `&mut` to the app's own field. A
73//! description carrying it would be a form model.
74//! - **What has focus.** See below.
75//! - **A duration or a clock.** No estimate of time remaining, no autosave
76//! interval, no animation length.
77//! - **A colour, a size or a position.** The deferral rule.
78//!
79//! # Reach, focus and the focus ring
80//!
81//! Three terms, and no others, for what sits outside the description.
82//! **Reach** is which things can take focus and in what order; a browser reads
83//! it off the document, a TUI derives it from draw order, egui from its own id
84//! stack. **Focus** is which reached thing has the keyboard right now: the
85//! renderer's, live, never described and never round-tripped through a
86//! description. The **focus ring** is the visible cue; the token (`focus-ring`,
87//! derived by `makeover` from the action colour) is the one shared artifact and
88//! the drawing is the renderer's. Retired as names for any of this: "focus
89//! stroke", "focus cue", "wants focus". "Caret" is a different thing, the text
90//! cursor inside a field, and keeps its name.
91//!
92//! # The four tones, and what a colour claims
93//!
94//! One rule for how colour says whether a thing can be
95//! used. Every renderer answers to it, and it is stated here because the
96//! description is what names the intents.
97//!
98//! | the thing | intent |
99//! |-----------|--------|
100//! | active, emphasised, the thing itself | `content` |
101//! | inactive but usable: it still answers a press | `content-secondary` |
102//! | never a control: a caption, a hint, a placeholder, an axis label | `content-muted` |
103//! | a control that will not answer | `content-disabled` |
104//!
105//! `content-disabled` is the one with a claim in it. [`State::Disabled`]
106//! resolves to it, so a live control wearing it is telling the user it will
107//! not answer, and being wrong about that is worse than being quiet, because
108//! the user's response is to stop trying. A sortable column heading that was
109//! never sorted, and every unchosen option in a radio group, read as dead
110//! lists if they wear it. `content-muted` holds body text's floor because a
111//! hint is read; the disabled tier holds only the quiet floor, so it stays
112//! fainter than any hint.
113//!
114//! The tones are one ramp and not four colours. `makeover` derives the quieter
115//! steps from the ink, so "one step back" means the same distance in
116//! every theme and a renderer cannot land between them by picking its own.
117//!
118//! # First paint is final paint
119//!
120//! Nothing may change size or position after it is
121//! first drawn, and nothing may stand in for content that has not arrived yet.
122//! Both halves are absolute.
123//!
124//! It is stated here, rather than left to each renderer, because a renderer can
125//! only reserve space the description gave it enough to size. A member whose
126//! size depends on its content therefore owes whatever makes it sizeable while
127//! the content is still absent, and that is the second admission test for a new
128//! member: not only does it compose something this crate already names, it can
129//! be laid out before it is filled.
130//!
131//! The mechanism is a reservation, and [`Sort`]'s caret is the worked example.
132//! The caret is drawn into a box its own width whether or not the column is
133//! sorted, so pressing a heading cannot reflow the row it sits in. The box names
134//! no magnitude, which is what keeps it out of `makeover-geometry`'s territory.
135//! Reserve from what is known; never discover geometry from what has not
136//! arrived.
137//!
138//! The trap is an `Option` that means "not yet". [`Readiness::Pending`] is the
139//! honest way to say a region is still waiting. An optional *measurement* is
140//! not: a count that shows up later widens the text that prints it and moves
141//! everything beside it, which is the reflow this rule exists to forbid. So an
142//! `Option` on a measurement means the host will never know it, a property of
143//! the query, fixed for the life of the screen, and a renderer sizes for the
144//! answer it was handed rather than for the one it hopes is coming.
145//!
146//! # Any width, one answer
147//!
148//! The sibling of the rule above. That one is
149//! independence from *when*; this one is independence from *how you got here*.
150//!
151//! A rendering is a pure function of the description and the viewport. The same
152//! description at the same width is the same output, whatever widths came
153//! before it. No renderer may carry geometry across frames, and none may narrow
154//! by counting.
155//!
156//! The failure this forbids is ordinary enough to be the default everywhere
157//! else: a page that hides its sidebar below some width, remembers that it hid
158//! it, and does not bring it back the same way. Layout there is a function of
159//! `(width, history)`, so dragging a window to 900 wide is a different screen
160//! depending on whether you came from 1400 or from 600. Nobody chose that; it
161//! is what measuring and remembering produce.
162//!
163//! The mechanism is [`Width`] for what grows and [`Priority`] for what drops.
164//! Both are declared, both are read off the description, and neither needs a
165//! measurement. A renderer narrows by raising a cutoff over a total order,
166//! never by counting what fits and stopping, `makeover-tui`'s table states
167//! that as its own rule and tests it, and `makeover-webview` reaches the same
168//! place with `@media` and `display: none`, which is path-independent by
169//! construction because CSS has nowhere to keep the previous width.
170//!
171//! Two things follow for anything new. A member that would need last frame's
172//! size to lay out this frame is refused, the same way a member that cannot be
173//! sized before it is filled is refused. And a fact about what disappears
174//! belongs in the description, because a host that has to infer it can only
175//! infer it from a measurement.
176//!
177//! # Where the description stops
178//!
179//! A member is added when an app needs a fact the vocabulary cannot state, and
180//! refused when what it wants is presentation it should be asking a renderer
181//! for. It is not a quota, and the goal is every screen described.
182//!
183//! A timeline is describable. What it needs and could not previously get is two
184//! integers, where a thing starts and how long it lasts, which is [`Track`].
185//! Slot heights, gridline colour, how overlapping things stack and which hour
186//! scrolls into view stay the renderer's, and `Track` carries none of them.
187//!
188//! A kanban board is describable, and the member is [`Region::Columns`]. Every
189//! card fact is already sayable through `Row`'s parts; what nothing else could
190//! say is that the columns are *peers*, since [`Arrangement`] offers only
191//! list-detail and sidebar-content and a board described as either is a lie
192//! about the screen. Dragging a card between columns does not enter into it: a
193//! drop's effect is "set status", a discrete action `Row`'s menu already
194//! carries, and the drag itself is affordance.
195//!
196//! A calendar takes no members. The month grid's primacy in calendar apps is an
197//! artifact of paper: paper cannot be queried, so it has to show every day at
198//! once as a fallback index, and routes, search and ranking do that job better.
199//! Three jobs survive that reasoning, and only one of them needs a grid:
200//!
201//! 1. **Spans across days**, a stretch of leave, a trip, a sprint. You cannot
202//! see "away the 3rd to the 17th" in a list without diffing dates. This is
203//! [`Track`] with [`Unit::Days`], and [`Track::days`] is it.
204//! 2. **Density at a glance**, which weeks were heavy. That is a heatmap, and a
205//! heatmap describes as a list.
206//! 3. **Weekday periodicity**, "every other Tuesday", "the 15th is a Saturday".
207//! This is the only job that needs the seven-column wrap, because alignment
208//! is the whole of what makes it visible.
209//!
210//! Job 3 is the only open question, and nothing in the tree asks for it. A
211//! month grid otherwise renders as a table: seven weekday
212//! columns, weeks as rows, blanks for the offset. If a screen wants one,
213//! measure the members it needs before adding any.
214//!
215//! [`Region::Handover`] and [`Region::Ceded`] remain for the genuinely
216//! app-owned. The description
217//! names the *place* and the app owns the contents, so a screen containing a
218//! timeline is still a whole screen and still routable. Without it, the screens
219//! that make an app worth using would need a second, undescribed path beside
220//! the router, and two paths is how a vocabulary drifts from its app.
221//!
222//! [`Region::Widget`] sits between that limit and the primitives, and it does
223//! not move the limit. A widget is an assembly of members this crate *already*
224//! has, under a name a renderer may or may not recognise. Anything that needs a
225//! member the vocabulary does not have is still a finding about the vocabulary
226//! or still bespoke; naming an assembly buys no new expressive power, which is
227//! why it is safe to let the set grow outside this crate.
228
229#![forbid(unsafe_code)]
230
231/// A colour intent this crate refers to but never resolves.
232///
233/// The string is the token name `makeover` publishes, so a renderer can look
234/// it up without this crate knowing what colour came back.
235pub trait Intent {
236 /// The `makeover` intent token this resolves against.
237 fn token(self) -> &'static str;
238}
239
240mod affordance;
241mod column;
242mod component;
243mod depth;
244mod facet;
245mod field;
246mod picture;
247mod readiness;
248mod region;
249mod text;
250mod theme_choice;
251mod track;
252
253/// A tag tree as a facet, from tagtree's outline, selection and rollup. Behind
254/// the `tagtree` feature.
255#[cfg(feature = "tagtree")]
256pub mod tag_facet;
257
258// The crate's entire API is these names at the root. Named rather than glob so
259// a member added to a module and left out here fails at the first call site
260// outside the crate instead of quietly leaving the root API.
261pub use affordance::{Affordance, AffordanceAxis};
262pub use column::{
263 Budget, Caret, CellPart, ColumnKind, ColumnRoom, Fallback, MIN_CEILING, Priority, Sizing, Sort,
264 Tiling, Width,
265};
266pub use component::{
267 Flow, HEAT_BANDS, Heading, Heat, Nesting, Notice, Prominence, Selector, Token, Tone,
268};
269pub use depth::{Bevel, Depth, Edge, Fill, State};
270pub use facet::{Facet, FacetValue, GroupRank, Join, Selecting, Standing};
271pub use field::{DATE_FORMAT, DATETIME_FORMAT, DENSITY_CHOICES, Family, FieldKind, Value};
272pub use picture::{Extent, Fit};
273pub use readiness::{Awaiting, Loading, Progress, Readiness, progress_reading};
274pub use region::{Arrangement, Measure, PageKind, Paging, Region, Share, Showing, Strip, Window};
275pub use text::{Change, Syntax};
276pub use theme_choice::{Contrast, ThemeVariant};
277pub use track::{Placement, Span, Track, Unit};
278
279#[cfg(test)]
280mod tests;