Skip to main content

max / makeover-layout

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