max / makeover-layout
| 1 | use crate::; |
| 2 | |
| 3 | // Names this module's prose links to, resolved for rustdoc. |
| 4 | |
| 5 | use crate::; |
| 6 | |
| 7 | /// What a region is saying, when it is saying something. |
| 8 | /// |
| 9 | /// The one intent family shared by badges, notices and nothing else. Kept |
| 10 | /// separate from [`Fill`] because a surface is where a thing sits and a tone is |
| 11 | /// what it means, and the three apps agree on the four statuses: |
| 12 | /// `info_banner` / `warning_banner` in audiofiles, `.toast-info` / |
| 13 | /// `.toast-success` / `.toast-error` in goingson, `.toast.success` / |
| 14 | /// `.toast.error` in Balanced Breakfast. |
| 15 | /// |
| 16 | /// The per-tag palette (`category-one` through `category-six`) is deliberately |
| 17 | /// not here. Which colour a *particular* tag takes is app domain, and both |
| 18 | /// webview apps already carry it as a `data-color` attribute. |
| 19 | |
| 20 | |
| 21 | /// No status. |
| 22 | /// |
| 23 | /// Ordinary content, at full weight. It does not also mean muted: a |
| 24 | /// badge reads quiet because [`Token::Badge`] answers no click, which is |
| 25 | /// the renderer's knowledge and not this axis's. A renderer wanting a |
| 26 | /// muted badge reaches for [`Token::interactive`] itself rather than |
| 27 | /// expecting `Neutral` to have muted it. |
| 28 | Neutral, |
| 29 | /// Something worth knowing and nothing to do about it. |
| 30 | Info, |
| 31 | /// Something finished and it worked. |
| 32 | Success, |
| 33 | /// Something the user should look at before continuing. |
| 34 | Warning, |
| 35 | /// Something broken, or something about to be destroyed. |
| 36 | Danger, |
| 37 | |
| 38 | |
| 39 | |
| 40 | |
| 41 | match self |
| 42 | // Neutral has no status token of its own, so it takes the plain |
| 43 | // content intent. It used to answer `content-muted`, which read |
| 44 | // "no status" as "de-emphasised" and muted every figure value in |
| 45 | // the webview. Muting is a renderer's call about a particular |
| 46 | // token, not something the status axis knows. |
| 47 | SelfNeutral => "content", |
| 48 | SelfInfo => "info", |
| 49 | SelfSuccess => "success", |
| 50 | SelfWarning => "warning", |
| 51 | SelfDanger => "danger", |
| 52 | |
| 53 | |
| 54 | |
| 55 | |
| 56 | /// A small labelled thing that sits inside something else. |
| 57 | /// |
| 58 | /// Two members, because the three apps drew three taxonomies and only one line |
| 59 | /// runs through all of them: does it answer a click. audiofiles has |
| 60 | /// `classification_badge` (a label) against `tag_chip`, `tag_chip_removable` |
| 61 | /// and `selectable_tag` (all of which do). Balanced Breakfast has `.tag` and |
| 62 | /// `.badge` against `.tag-chip`. goingson is the one that has to move: its |
| 63 | /// `.tag` and `.badge` are a single CSS rule, so every call site has to be read |
| 64 | /// to decide which of the two it always was. |
| 65 | /// |
| 66 | /// The evidence that a chip is a real concept rather than a badge with a |
| 67 | /// cursor: audiofiles inverts its bevel on press and Balanced Breakfast latches |
| 68 | /// `.tag-chip.active` with the inset bevel. Two independent arrivals at "a chip |
| 69 | /// holds itself down", which is exactly what [`Depth::pressed`] already says. |
| 70 | |
| 71 | |
| 72 | /// Non-interactive status or count. Answers no click. |
| 73 | Badge, |
| 74 | /// An interactive or removable token. Answers a click, and latches if it |
| 75 | /// stands for a filter that is either on or off. |
| 76 | Chip |
| 77 | /// Whether it carries its own remove affordance. |
| 78 | removable: bool, |
| 79 | , |
| 80 | |
| 81 | |
| 82 | |
| 83 | /// Whether this answers a click. |
| 84 | /// |
| 85 | /// The whole difference between the two members, and the reason a renderer |
| 86 | /// with no hover (a touch surface, a terminal) can still tell them apart. |
| 87 | |
| 88 | pub const |
| 89 | matches! |
| 90 | |
| 91 | |
| 92 | /// How it sits, given whether it is currently latched down. |
| 93 | /// |
| 94 | /// A badge is flat: it is a label, and giving it an edge would say it can |
| 95 | /// be pressed. A chip is raised, and inset while latched. |
| 96 | |
| 97 | pub const |
| 98 | match self |
| 99 | SelfBadge => Flat, |
| 100 | SelfChip if latched => Well, |
| 101 | SelfChip => Raised, |
| 102 | |
| 103 | |
| 104 | |
| 105 | |
| 106 | /// Something the app is telling the user, unprompted. |
| 107 | /// |
| 108 | /// Two concepts, not one with a placement. They differ in more than where they |
| 109 | /// sit: a toast is transient, stacked and self-dismissing, and a banner is |
| 110 | /// persistent, in flow, one per region, and dismissed by fixing the condition |
| 111 | /// it reports. Folding them into one member with a placement parameter would |
| 112 | /// make lifetime, stacking and dismissal all placement-dependent, which is the |
| 113 | /// description leaking renderer policy. |
| 114 | /// |
| 115 | /// All three apps have banners: `info_banner` and `warning_banner` in |
| 116 | /// audiofiles, five of them in goingson (sync, sync-result, vacation-day, |
| 117 | /// timer-active, past-review), `.update-banner` in Balanced Breakfast. The two |
| 118 | /// webview apps also have toasts. So neither member is speculative, and no app |
| 119 | /// gains a concept it lacks except audiofiles, whose renderer may legitimately |
| 120 | /// decline to draw a toast at all. |
| 121 | |
| 122 | |
| 123 | /// Transient, stacked, dismisses itself. |
| 124 | Toast, |
| 125 | /// Persistent, in flow, one per region, dismissed by fixing the cause. |
| 126 | Banner, |
| 127 | |
| 128 | |
| 129 | |
| 130 | /// Whether it goes away on its own. |
| 131 | |
| 132 | pub const |
| 133 | matches! |
| 134 | |
| 135 | |
| 136 | /// How it sits. |
| 137 | /// |
| 138 | /// A toast floats above the page rather than resting on it, which is |
| 139 | /// [`Fill::Overlay`]'s whole reason to exist. A banner is a card in the |
| 140 | /// flow. Both are raised, and they are raised off different things. |
| 141 | |
| 142 | pub const |
| 143 | match self |
| 144 | SelfToast => Overlay, |
| 145 | SelfBanner => Raised, |
| 146 | |
| 147 | |
| 148 | |
| 149 | |
| 150 | /// The parts of a list row. |
| 151 | /// |
| 152 | /// `#[non_exhaustive]`: a renderer carries a wildcard arm, so a new part is not |
| 153 | /// a lockstep event across three renderers. |
| 154 | /// |
| 155 | /// # Meta against Tokens |
| 156 | /// |
| 157 | /// The line is whether the thing has its own standing. `Meta` is one short |
| 158 | /// trailing fact about the row, written as text: a count, a size, a date. |
| 159 | /// `Tokens` is a set of small labelled things, each of which can be toned and |
| 160 | /// can answer a click. "3 files" is meta. A status badge that is amber, and a |
| 161 | /// tag you can click to filter by, are tokens. |
| 162 | /// |
| 163 | /// Keeping them apart is what a single widened slot would have foreclosed. A |
| 164 | /// renderer can right-align one string and cannot usefully do the same to a |
| 165 | /// strip of chips, and a fact that is not clickable should not be drawn as |
| 166 | /// though it were. |
| 167 | /// How much vertical room a part's text may take. |
| 168 | /// |
| 169 | /// A row is an inline run and every part in it is a leaf, so a part's text has |
| 170 | /// always been drawn on one line and no description could say otherwise. Two |
| 171 | /// apps say otherwise in their own stylesheets, both to the same number and |
| 172 | /// both with a comment explaining it: Balanced Breakfast clamps a feed row's |
| 173 | /// title to two lines (`.row--article .row-primary`, whose comment reads |
| 174 | /// "overrides .row-primary's single flex line"), and goingson clamps a |
| 175 | /// problem's body to two ("two lines is enough to recognize one, and the full |
| 176 | /// text is in the task once promoted"). |
| 177 | /// |
| 178 | /// Two named tiers rather than a line count, and the count is what the measured |
| 179 | /// demand argues against. Both sites want exactly one tier past the default, |
| 180 | /// and a number invites a row whose primary is a paragraph, which is a block |
| 181 | /// and has no business in a run. A third tier is a decision, made here, rather |
| 182 | /// than something a call site can reach for. |
| 183 | /// |
| 184 | /// What a renderer owes it: `Tight` is what a run already does and needs no |
| 185 | /// answer. `Relaxed` is at most two lines and then truncation, however that |
| 186 | /// renderer truncates -- a webview clamps, a terminal wraps into two rows of |
| 187 | /// cells, an immediate-mode renderer caps the galley. A renderer that cannot |
| 188 | /// give two lines may draw one; what it may not do is grow without bound, |
| 189 | /// because the run is a line and the row's neighbours are relying on that. |
| 190 | |
| 191 | |
| 192 | |
| 193 | /// One line. What every part did before this type existed. |
| 194 | |
| 195 | Tight, |
| 196 | /// Up to two lines, then truncated. |
| 197 | Relaxed, |
| 198 | |
| 199 | |
| 200 | |
| 201 | /// How many lines the part may take. |
| 202 | /// |
| 203 | /// A number here rather than in the enum, because a renderer needs one and |
| 204 | /// a call site does not. That asymmetry is the whole argument for the |
| 205 | /// tiers: the description says how much room the thing deserves and this |
| 206 | /// says what that costs, so a third tier changes one line rather than every |
| 207 | /// consumer's arithmetic. |
| 208 | |
| 209 | pub const |
| 210 | match self |
| 211 | SelfRelaxed => 2, |
| 212 | // Including any tier added later: one line is the safe reading of |
| 213 | // an unknown flow, since it is what the run guaranteed before flows |
| 214 | // existed. |
| 215 | _ => 1, |
| 216 | |
| 217 | |
| 218 | |
| 219 | |
| 220 | /// How deep a row sits inside a set: a tree, an outline, a threaded list. |
| 221 | /// |
| 222 | /// [`RowPart`] below already names what is *in* a row; nothing named where a |
| 223 | /// row sits relative to its siblings, so every consumer that had a hierarchy |
| 224 | /// carried a bare number and every renderer decided for itself what one was |
| 225 | /// worth. |
| 226 | /// |
| 227 | /// # Not `Depth`, and the collision is the reason |
| 228 | /// |
| 229 | /// [`Depth`] is taken and means something else entirely: surface bevel -- |
| 230 | /// `Flat`, `Raised`, `Well`, `Sunken`, `Overlay` -- a fact about a surface |
| 231 | /// rather than a position in a hierarchy. Two meanings under one word in one |
| 232 | /// crate is the collision that costs a reader an hour, and the word this |
| 233 | /// concept wants is the one that would cause it. |
| 234 | /// |
| 235 | /// # The magnitude is the renderer's, and that is the precedent |
| 236 | /// |
| 237 | /// [`Awaiting`] is the shape: this crate names the fact and declines to name |
| 238 | /// what it is worth. makeover-webview writes the rule as a custom property with |
| 239 | /// a fallback -- the way it writes `margin-inline-start: var(--awaiting-gap, |
| 240 | /// 0.5ch)` -- so a level has one answer per renderer and an app can override |
| 241 | /// it. A terminal spends columns, a browser spends inline space, and neither |
| 242 | /// number belongs in a description. |
| 243 | /// |
| 244 | /// # Zero is a real answer |
| 245 | /// |
| 246 | /// [`top`](Self::top) is the default and is what a flat list says: every row is |
| 247 | /// at the top level, which is true and is the reading a renderer needs. An |
| 248 | /// `Option` here would make "not nested" and "nested at zero" two spellings of |
| 249 | /// one thing, the same argument `Discovery`'s `indexable` makes about defaults |
| 250 | /// that are meaningful. |
| 251 | |
| 252 | |
| 253 | |
| 254 | /// How many levels in, counting from zero. |
| 255 | /// |
| 256 | /// `u8` because a hierarchy a reader can follow is not 256 deep, and a |
| 257 | /// renderer indenting by a level has to multiply it by something -- a wider |
| 258 | /// integer here is a wider integer in every renderer's arithmetic for a |
| 259 | /// range nothing will use. |
| 260 | pub level: u8, |
| 261 | |
| 262 | |
| 263 | |
| 264 | /// The top level: not nested. The default, and what a flat list says. |
| 265 | |
| 266 | pub const |
| 267 | Self |
| 268 | |
| 269 | |
| 270 | /// A row this many levels in. |
| 271 | |
| 272 | pub const |
| 273 | Self |
| 274 | |
| 275 | |
| 276 | /// Whether this row sits under another. |
| 277 | /// |
| 278 | /// The question every renderer asks before it spends anything on indenting, |
| 279 | /// answered once here rather than by a `> 0` in each -- which is |
| 280 | /// [`Awaiting::is_determinate`]'s reason too. |
| 281 | |
| 282 | pub const |
| 283 | self.level > 0 |
| 284 | |
| 285 | |
| 286 | |
| 287 | |
| 288 | |
| 289 | |
| 290 | /// The thing itself. What the row is called. |
| 291 | Primary, |
| 292 | /// Supporting text under the primary. |
| 293 | Secondary, |
| 294 | /// A short trailing fact: a count, a size, a date. |
| 295 | Meta, |
| 296 | /// Controls that act on this row. |
| 297 | Actions, |
| 298 | /// Small labelled things belonging to the row: badges, chips, tags. |
| 299 | /// |
| 300 | /// Each carries its own [`Token`] kind and [`Tone`], so a renderer with no |
| 301 | /// colour still has the kind to work with, and one with no chips still has |
| 302 | /// the label. That is the constrained-consumer test this vocabulary exists |
| 303 | /// to pass, and it is why the tone lives on the token rather than on the |
| 304 | /// part. |
| 305 | Tokens, |
| 306 | /// How much of a set the row's thing has done: a [`Meter`] in the row. |
| 307 | /// |
| 308 | /// A row holds no nodes, by the rule that a row part may not carry an |
| 309 | /// arbitrary node, which is the door through which a description becomes a |
| 310 | /// templating language. So the part carries the *description of a bar* rather than a node, exactly as |
| 311 | /// `Tokens` carries tags rather than nodes. |
| 312 | /// |
| 313 | /// Without it a row flattens the proportion into [`Meta`](Self::Meta) as |
| 314 | /// "3/7 subtasks", which keeps both numbers and loses the reading, the same |
| 315 | /// way a toned status badge read as prose before `Tokens`. |
| 316 | Proportion, |
| 317 | |
| 318 | |
| 319 | |
| 320 | /// What the part is worth when the run does not fit. |
| 321 | /// |
| 322 | /// The default only. A part may say otherwise, and a renderer reads the |
| 323 | /// part rather than the role; this is what a description that has never |
| 324 | /// heard of [`Priority`] means, which is every description written before |
| 325 | /// the field existed. |
| 326 | /// |
| 327 | /// Deriving it from the role is the thing this vocabulary has otherwise |
| 328 | /// been moving away from, and it is right here for one reason: the roles |
| 329 | /// already encode this ranking and every consumer already assumes it. |
| 330 | /// [`Primary`](Self::Primary) is what the row is called, and |
| 331 | /// [`Priority::Essential`]'s own doc was written about exactly that -- |
| 332 | /// "without it the row does not identify itself". |
| 333 | /// |
| 334 | /// [`Actions`](Self::Actions) is `Essential` and it is the interesting one. |
| 335 | /// A control is not a fact, so dropping it does not cost the reader a |
| 336 | /// detail; it costs them the only way to act on the row, and in a terminal |
| 337 | /// it silently removes something focus had already been claimed for. A |
| 338 | /// renderer that needs room takes it from what the row *says*, never from |
| 339 | /// what it *offers*. |
| 340 | /// |
| 341 | /// An unknown member reads as [`Priority::Secondary`]: droppable, but not |
| 342 | /// first, since guessing `Optional` for something this crate has not been |
| 343 | /// taught would make a new member the first thing to vanish. |
| 344 | |
| 345 | pub const |
| 346 | match self |
| 347 | SelfPrimary | SelfActions => Essential, |
| 348 | SelfMeta | SelfProportion => Optional, |
| 349 | _ => Secondary, |
| 350 | |
| 351 | |
| 352 | |
| 353 | /// The content intent the part takes. |
| 354 | |
| 355 | pub const |
| 356 | match self |
| 357 | SelfPrimary => "content", |
| 358 | SelfSecondary => "content-secondary", |
| 359 | SelfMeta => "content-muted", |
| 360 | // Actions carry controls rather than text, so they inherit. |
| 361 | SelfActions => "content", |
| 362 | // So do tokens: each one carries its own tone, and a part-level |
| 363 | // intent underneath it would fight the token that sits on it. |
| 364 | SelfTokens => "content", |
| 365 | // And so does a proportion, for the same reason: the meter carries |
| 366 | // the tone, and it is about the ratio rather than about the row. |
| 367 | SelfProportion => "content", |
| 368 | |
| 369 | |
| 370 | |
| 371 | |
| 372 | /// How far down the heading tree a title sits. |
| 373 | /// |
| 374 | /// Three, and only the three that are actually headings. The bands those used |
| 375 | /// to be filed with (goingson's `.page-header`, Balanced Breakfast's `.header` |
| 376 | /// and `.detail-header`) are arrangement, not type, and live at |
| 377 | /// [`Region::Band`]. One of them contains no text at all. |
| 378 | |
| 379 | |
| 380 | /// Names the whole screen. One per screen. |
| 381 | Page, |
| 382 | /// Names a block within the screen. |
| 383 | Section, |
| 384 | /// Names a sub-block inside an already-named section. |
| 385 | Subsection, |
| 386 | |
| 387 | |
| 388 | |
| 389 | /// Whether a rule follows the heading. |
| 390 | /// |
| 391 | /// audiofiles' `section_header` draws a separator and its |
| 392 | /// `subsection_label` deliberately does not, which is the only thing |
| 393 | /// distinguishing the two once weight and colour are deferred. |
| 394 | |
| 395 | pub const |
| 396 | matches! |
| 397 | |
| 398 | |
| 399 | |
| 400 | /// A control that picks between things. |
| 401 | /// |
| 402 | /// Three, because three distinct behaviours are in play and collapsing any two |
| 403 | /// loses something. A segmented control picks a value; a tab picks a pane; a |
| 404 | /// toggle picks nothing and simply holds itself on or off. |
| 405 | |
| 406 | |
| 407 | /// Exactly one of N, and the options abut. |
| 408 | Segmented, |
| 409 | /// Independent on or off, on its own. |
| 410 | Toggle, |
| 411 | /// Navigation between panes. The folder semantic. |
| 412 | Tabs, |
| 413 | |
| 414 | |
| 415 | |
| 416 | /// How the chosen option sits. |
| 417 | /// |
| 418 | /// Held in for a segmented control and a toggle, which is the same shape |
| 419 | /// pressing produces and the whole economy of the idiom: one appearance, |
| 420 | /// two reasons to wear it. A tab is the exception, because the selected |
| 421 | /// folder tab comes *forward* to join the pane it opens. |
| 422 | |
| 423 | pub const |
| 424 | match self |
| 425 | SelfSegmented | SelfToggle => Well, |
| 426 | SelfTabs => Raised, |
| 427 | |
| 428 | |
| 429 | |
| 430 | /// How the options that were *not* picked sit. |
| 431 | /// |
| 432 | /// Describing only [`Selector::chosen`] left the unchosen option falling |
| 433 | /// through to [`Depth::Flat`], which says it is level with the strip it |
| 434 | /// sits in, and no renderer emitted anything for it. That is wrong in both |
| 435 | /// directions and goingson proved it: its unchosen tabs are recessed by |
| 436 | /// hand, and being recessed is *why* the chosen one reads as coming |
| 437 | /// forward. Against a flat strip, a raised chosen tab is a bevel drawn on |
| 438 | /// the strip's own colour, which is a much weaker folder effect than the |
| 439 | /// contrast the idiom is named after. |
| 440 | /// |
| 441 | /// Each member is the inverse of its chosen state, which is the whole |
| 442 | /// content of "picked" once colour is deferred: |
| 443 | /// |
| 444 | /// - Tabs recede, so the chosen one comes forward. |
| 445 | /// - A segment and a toggle stand up, so the chosen one is held in. |
| 446 | |
| 447 | pub const |
| 448 | match self |
| 449 | SelfTabs => Sunken, |
| 450 | SelfSegmented | SelfToggle => Raised, |
| 451 | |
| 452 | |
| 453 | |
| 454 | /// Whether the options touch. |
| 455 | /// |
| 456 | /// The gap is the entire difference between a segmented control and a row |
| 457 | /// of buttons that happen to sit near each other, which is what audiofiles' |
| 458 | /// `segmented_control` says in its own comment and why it zeroes the |
| 459 | /// spacing by hand. |
| 460 | |
| 461 | pub const |
| 462 | matches! |
| 463 | |
| 464 | |
| 465 |