max / makeover-layout
| 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 | |
| 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 | |
| 225 | /// The `makeover` intent token this resolves against. |
| 226 | ; |
| 227 | |
| 228 | |
| 229 | |
| 230 | |
| 231 | |
| 232 | |
| 233 | |
| 234 | |
| 235 | |
| 236 | |
| 237 | |
| 238 | |
| 239 | |
| 240 | |
| 241 | |
| 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; |
| 247 | pub use ; |
| 248 | pub use ; |
| 249 | pub use ; |
| 250 | pub use ; |
| 251 | pub use ; |
| 252 | pub use ; |
| 253 | pub use ; |
| 254 | pub use ; |
| 255 | pub use ; |
| 256 | pub use ; |
| 257 | pub use ; |
| 258 | pub use ; |
| 259 | |
| 260 | |
| 261 | |
| 262 |