max / makeover-layout
| 1 | use crateDepth; |
| 2 | |
| 3 | // Names this module's prose links to, resolved for rustdoc. |
| 4 | |
| 5 | use crate::; |
| 6 | |
| 7 | /// A named part of a screen. |
| 8 | /// |
| 9 | /// The thing `makeover-geometry` deliberately does not name: it names the space |
| 10 | /// *between* things by relationship, and nothing named the things. Six named |
| 11 | /// members, taken from what the two webview apps actually use, plus |
| 12 | /// [`Region::Handover`] and [`Region::Ceded`] for the parts no description |
| 13 | /// should reach. Both apps' |
| 14 | /// `layout.css` currently names exactly two things, `.raised` and `.well`, so |
| 15 | /// this layer is absent rather than divergent, which makes it the cheapest of |
| 16 | /// the schemas to add and the easiest to over-build. |
| 17 | /// |
| 18 | /// `#[non_exhaustive]`, for [`RowPart`]'s and [`Readiness`]' reason: the member |
| 19 | /// after this one should not be a lockstep event across three renderers. |
| 20 | |
| 21 | |
| 22 | |
| 23 | /// A full-width strip with a title slot and an actions cluster, either of |
| 24 | /// which may be empty. goingson's `.page-header`, Balanced Breakfast's |
| 25 | /// `.header` and `.detail-header` are all this, differing only in which |
| 26 | /// slots they fill. |
| 27 | Band, |
| 28 | /// A persistent column beside the content, holding navigation. |
| 29 | Sidebar, |
| 30 | /// A region of content with its own scroll. |
| 31 | Pane, |
| 32 | /// Things that belong together, and nothing else. |
| 33 | /// |
| 34 | /// The block [`Heading::Section`] names, which the vocabulary otherwise |
| 35 | /// cannot contain. A section heading is a leaf sitting |
| 36 | /// *beside* the things it names, so nothing said where a section started or |
| 37 | /// ended and a renderer learned one had ended only because the next heading |
| 38 | /// arrived. |
| 39 | /// |
| 40 | /// # The measurement |
| 41 | /// |
| 42 | /// 41 [`Heading::Section`] sites across the ten screens described through |
| 43 | /// the router, not one of them contained. audiofiles' settings screen is the |
| 44 | /// clearest: one pane holding a heading, a field, a heading, two toggles, a |
| 45 | /// heading, a toggle and a heading, which is four sections and no |
| 46 | /// containers. Under the hand-written CSS the ports are replacing the same |
| 47 | /// block is spelled `.settings-section` in goingson, `.form-section` and |
| 48 | /// `.content-section` in the MNW server, `.help-section` in Balanced |
| 49 | /// Breakfast: three apps, four names, one shape. |
| 50 | /// |
| 51 | /// # Why the existing members were the wrong answer |
| 52 | /// |
| 53 | /// [`Pane`](Self::Pane) is what apps reached for, and it is 28 of the 45 |
| 54 | /// regions in the described screens. It claims a scroll of its own and |
| 55 | /// [`Depth::Well`], so four settings groups inside a pane are four wells |
| 56 | /// inside a well and four scroll contexts. Neither claim is true of a group. |
| 57 | /// |
| 58 | /// [`Widget`](Self::Widget) is wrong from the other side. Its own docs say a |
| 59 | /// widget is never how a primitive gets added by the back door, and a run of |
| 60 | /// related controls under a heading is furniture any app would have, which |
| 61 | /// is the generic-against-bespoke bar a primitive has to clear. |
| 62 | /// |
| 63 | /// # What it does not carry |
| 64 | /// |
| 65 | /// **A heading.** A group usually has one and it is an ordinary node in the |
| 66 | /// body, the way it already was. A group of related toggles with no heading |
| 67 | /// is a real thing and a mandatory slot would forbid it. |
| 68 | /// |
| 69 | /// **A depth.** [`Depth::Flat`], on [`Handover`](Self::Handover)'s reasoning: |
| 70 | /// it inherits, and an app that wants its group in a well puts it in a |
| 71 | /// [`Pane`](Self::Pane), which composes rather than adding a knob here. |
| 72 | /// |
| 73 | /// **A colour.** Distinguishing sibling groups by colour is the thing this |
| 74 | /// member was asked for and it is deliberately not stated here. The |
| 75 | /// description says these things belong together; which of the theme's |
| 76 | /// categorical colours a renderer reaches for, and whether it reaches for |
| 77 | /// one at all, is derived from sibling order at the renderer. A terminal |
| 78 | /// that tints nothing and separates with a rule is honouring this. |
| 79 | Group, |
| 80 | /// Two panes side by side, where the left chooses what the right shows. |
| 81 | Split, |
| 82 | /// Peer regions across, all of them equals. |
| 83 | /// |
| 84 | /// A kanban board's columns, and the shape [`Split`](Self::Split) is not: |
| 85 | /// a split's two panes stand in a master-detail relationship, where the |
| 86 | /// left chooses what the right shows. These choose nothing about each |
| 87 | /// other. Each is a whole region and the set is the arrangement. |
| 88 | /// |
| 89 | /// # What it does not carry |
| 90 | /// |
| 91 | /// **How many.** The children say, and a count here would be a second |
| 92 | /// source for something the description already states by containing them. |
| 93 | /// |
| 94 | /// **How wide.** Peers are equal by definition, so there is no [`Share`] to |
| 95 | /// state. A board whose columns wanted different widths would be a |
| 96 | /// different member, and no app has one. |
| 97 | /// |
| 98 | /// **What happens when there is no room.** Scroll across, wrap, or collapse |
| 99 | /// to one column at a time: all three are right on some host, none is |
| 100 | /// derivable from the description, and every one of them is presentation. |
| 101 | /// A terminal that stacks them vertically is honouring this, not degrading |
| 102 | /// it. |
| 103 | /// |
| 104 | /// # Why it is not an `Arrangement` |
| 105 | /// |
| 106 | /// [`Arrangement`] is the page's shape, and a board is usually a region |
| 107 | /// *inside* a page that also has a band over it. Naming it here composes; |
| 108 | /// naming it there would make a screen either a board or a list-detail and |
| 109 | /// never a band above a board. It also keeps [`Arrangement::share`] |
| 110 | /// meaningful, which a peer arrangement has no answer for. |
| 111 | Columns, |
| 112 | /// A set of panes, one visible at a time, and a [`Selector::Tabs`] that |
| 113 | /// chooses between them. |
| 114 | /// |
| 115 | /// Says nothing about where the strip sits. A row over the panes, a column |
| 116 | /// beside them, a wrapped run of links under them: all three are the same |
| 117 | /// member drawn by a renderer that knows its host, the way the strip's |
| 118 | /// overflow is. |
| 119 | TabGroup, |
| 120 | /// Content over a scrim, taking input until dismissed. |
| 121 | Modal, |
| 122 | /// A region this crate names the *place* of, whose contents the app still |
| 123 | /// owes every host. |
| 124 | /// |
| 125 | /// The escape hatch, and the thing that keeps the description honest about |
| 126 | /// its own limits. A day-plan timeline, a kanban board, a calendar and the |
| 127 | /// paint interaction over the timeline are not describable here and are not |
| 128 | /// going to become describable: a description expressive enough to produce |
| 129 | /// a timeline is a widget library wearing a description's name. |
| 130 | /// |
| 131 | /// But a screen containing one still has to be a screen. Without this |
| 132 | /// member the description covers only the boring screens, and the four that |
| 133 | /// make goingson worth using would need a second, undescribed path beside |
| 134 | /// the router. Two paths is how the vocabulary starts drifting from the app |
| 135 | /// again, which is the exact failure this crate exists to end. |
| 136 | /// |
| 137 | /// So the description says "a thing called `day-plan` goes here" and stops. |
| 138 | /// The name is opaque: this crate never interprets it, and no renderer is |
| 139 | /// expected to know what it means beyond handing the space over. |
| 140 | /// |
| 141 | /// # What separates it from [`Ceded`](Self::Ceded) |
| 142 | /// |
| 143 | /// **A fill is owed here in every host's currency.** A renderer handed one |
| 144 | /// of these and given nothing to put in it is looking at a hole the app |
| 145 | /// meant to fill, and saying so is the honest drawing. [`owed`](Self::owed) |
| 146 | /// is how it asks. |
| 147 | /// |
| 148 | /// That is the whole of the split. Before it there was one opaque member, |
| 149 | /// so a region the description had given up on and a region nobody had |
| 150 | /// converted yet were the same value, and both drew as a silently empty |
| 151 | /// box on the two renderers that answer no fill. |
| 152 | Handover |
| 153 | /// What the app calls it. Never interpreted here. |
| 154 | name: &'a str, |
| 155 | , |
| 156 | /// A region this crate names the place of, whose contents no host is owed. |
| 157 | /// |
| 158 | /// The other half of the old single opaque member. The app has decided this |
| 159 | /// space is not the description's to fill and is not going to become so: |
| 160 | /// a chart, a waveform, a rendered picture of domain data with marks |
| 161 | /// painted over it at positions no description knows. |
| 162 | /// |
| 163 | /// **Silence is the correct drawing.** A renderer with no fill for this |
| 164 | /// draws nothing and is right to; unlike [`Handover`](Self::Handover) there |
| 165 | /// is nothing missing. That is what makes the pair worth two members rather |
| 166 | /// than a flag: the two want opposite behaviour from a renderer that cannot |
| 167 | /// fill them, and one name cannot carry both. |
| 168 | /// |
| 169 | /// The measured sites are MNW's analytics charts, which already carry the |
| 170 | /// ruling that a bar chart is not describable and should not be, and |
| 171 | /// audiofiles' waveform, whose exclusion had no vocabulary to live in and |
| 172 | /// was recorded in a doc comment instead. |
| 173 | Ceded |
| 174 | /// What the app calls it. Never interpreted here. |
| 175 | name: &'a str, |
| 176 | , |
| 177 | /// A named assembly of things the vocabulary already says. |
| 178 | /// |
| 179 | /// The third tier, between a primitive and the two opaque members. |
| 180 | /// |
| 181 | /// # What separates it from the two members either side |
| 182 | /// |
| 183 | /// A primitive is a thing every renderer draws from scratch, and the test |
| 184 | /// it has to pass is that every host has an honest answer. A carousel fails |
| 185 | /// that test — a terminal has no carousel — which is the same refusal |
| 186 | /// `Node::Html` got and is why the carousel sat unsayable for months. |
| 187 | /// |
| 188 | /// [`Handover`](Self::Handover) fails it from the other side. It is for |
| 189 | /// what one app owns and nobody will build twice, and it carries *no* |
| 190 | /// contents: the description names the place and stops. A carousel is |
| 191 | /// furniture any app would have, and every part of it — an ordered set of |
| 192 | /// frames, a position, prev and next, a strip of position indicators — is |
| 193 | /// already sayable. Only the assembly had no name. |
| 194 | /// |
| 195 | /// So this member is the pair the other two are not: a name **and** |
| 196 | /// contents. The contents are the assembly, in the region's own body, said |
| 197 | /// in members that already exist. |
| 198 | /// |
| 199 | /// # Why the name does not have to be understood |
| 200 | /// |
| 201 | /// A renderer that recognises the name draws it the way its host does it: a |
| 202 | /// carousel in a webview, a pager with a count in a terminal, a selector in |
| 203 | /// egui. A renderer that does not recognise it walks the body, which is |
| 204 | /// primitives all the way down and which it can already draw. |
| 205 | /// |
| 206 | /// That is what lets the widget set be **open** without every renderer |
| 207 | /// knowing every widget. An unrecognised widget degrades to its assembly |
| 208 | /// instead of failing, so a second or third party can name one without |
| 209 | /// three renderers releasing in lockstep to accept it. Contrast |
| 210 | /// [`Handover`](Self::Handover), which no renderer can degrade: there is |
| 211 | /// nothing under it to fall back to. |
| 212 | /// |
| 213 | /// # What it does not do |
| 214 | /// |
| 215 | /// A widget is an assembly of things the vocabulary *already* says, so it |
| 216 | /// buys no expressive power. Anything needing a member the vocabulary does |
| 217 | /// not have is a finding about the vocabulary, and the answer to a finding |
| 218 | /// is to add the member. A widget is never the way a primitive gets added |
| 219 | /// by the back door. A timeline is describable because [`Track`] was added |
| 220 | /// to say it, not because a screen was dressed up as an assembly. |
| 221 | Widget |
| 222 | /// What the assembly is called. This crate never interprets it, and a |
| 223 | /// renderer is free not to know it. |
| 224 | name: &'a str, |
| 225 | , |
| 226 | |
| 227 | |
| 228 | |
| 229 | /// How the region sits on what is behind it. |
| 230 | |
| 231 | pub const |
| 232 | match self |
| 233 | SelfBand | SelfSidebar | SelfSplit | SelfTabGroup => Flat, |
| 234 | // Flat, and it inherits. A group says its contents belong together |
| 235 | // and says nothing about the surface they sit on, so a group in a |
| 236 | // pane is in a well and a group on the page is on the page. An app |
| 237 | // wanting one lifted puts it in a `Pane`. |
| 238 | SelfGroup => Flat, |
| 239 | // Flat, and it is the container rather than the columns. Each |
| 240 | // column is its own region and brings its own depth; a well here |
| 241 | // would put a second edge around a row of wells. |
| 242 | SelfColumns => Flat, |
| 243 | // A pane is looked into, the same as a table body or a tag tree. |
| 244 | SelfPane => Well, |
| 245 | SelfModal => Raised, |
| 246 | // Flat because it inherits: a bespoke region takes the depth of |
| 247 | // whatever frames it. An app that wants its timeline in a well puts |
| 248 | // it in a `Pane`, which composes rather than adding a knob here. |
| 249 | // |
| 250 | // A widget inherits for the same reason and it matters more here, |
| 251 | // because a widget is drawn by whichever renderer recognises it. A |
| 252 | // depth set here would be this crate deciding that a carousel is |
| 253 | // raised on every host, which is the kind of value the deferral |
| 254 | // rule exists to refuse. |
| 255 | SelfHandover | SelfCeded | SelfWidget => Flat, |
| 256 | |
| 257 | |
| 258 | |
| 259 | /// Whether this crate can say anything about the region's contents. |
| 260 | /// |
| 261 | /// A renderer walks the description and hands every region it understands |
| 262 | /// to the right drawing code. This is how it tells the two apart, and the |
| 263 | /// reason it is a method rather than a `matches!` at each renderer: there |
| 264 | /// is exactly one opaque member and there should stay exactly one. |
| 265 | /// |
| 266 | /// [`Widget`](Self::Widget) is described, and that is the whole of what |
| 267 | /// separates it from the two opaque members here. All three carry a name |
| 268 | /// this crate never interprets; only the widget carries contents under it. |
| 269 | /// A renderer that does not recognise a widget's name still walks its body, |
| 270 | /// so there is nothing for it to hand over and nothing it cannot draw. |
| 271 | |
| 272 | pub const |
| 273 | !matches! |
| 274 | |
| 275 | |
| 276 | /// Whether a fill is owed here, for a renderer that has none. |
| 277 | /// |
| 278 | /// The question the single opaque member could not answer. True for |
| 279 | /// [`Handover`](Self::Handover): the app meant to fill this and a renderer |
| 280 | /// with nothing to put in it should say so. False for everything else, |
| 281 | /// [`Ceded`](Self::Ceded) included, where silence is the correct drawing |
| 282 | /// because nothing is missing. |
| 283 | /// |
| 284 | /// A method rather than a `matches!` at each renderer, for |
| 285 | /// [`described`](Self::described)'s reason: three renderers writing the |
| 286 | /// same match is three chances to disagree about what an empty region |
| 287 | /// means. |
| 288 | |
| 289 | pub const |
| 290 | matches! |
| 291 | |
| 292 | |
| 293 | /// The name an app gave this region, if it gave one. |
| 294 | /// |
| 295 | /// [`Handover`](Self::Handover), [`Ceded`](Self::Ceded) and |
| 296 | /// [`Widget`](Self::Widget) are the members that carry a name, for two |
| 297 | /// different purposes: the first two say what the app puts in the space, |
| 298 | /// the third says what the assembly under it is called. A renderer dispatching on either wants the string without |
| 299 | /// caring which member it came from, and writing that `matches!` at each |
| 300 | /// renderer is how the two drift apart. |
| 301 | |
| 302 | pub const |
| 303 | match self |
| 304 | SelfHandover | SelfCeded | SelfWidget => Some, |
| 305 | // Spelled out rather than a wildcard, so a member added later has |
| 306 | // to answer whether it carries a name instead of inheriting `None` |
| 307 | // by sitting under a `_`. |
| 308 | SelfBand |
| 309 | | SelfSidebar |
| 310 | | SelfPane |
| 311 | | SelfGroup |
| 312 | | SelfSplit |
| 313 | | SelfColumns |
| 314 | | SelfTabGroup |
| 315 | | SelfModal => None, |
| 316 | |
| 317 | |
| 318 | |
| 319 | |
| 320 | /// How many of a region's children are visible at once. |
| 321 | /// |
| 322 | /// One sentence covering three shapes: *this region holds several children and |
| 323 | /// shows some of them, and the reader can change which.* A tab group, a |
| 324 | /// carousel and a disclosure all need it, and without it a renderer has two |
| 325 | /// moves: hardcode a widget name, or draw every child. That is what puts |
| 326 | /// per-widget code in renderers. |
| 327 | /// |
| 328 | /// # What is here and what is not |
| 329 | /// |
| 330 | /// The *kind*, and only the kind. Which child is currently up is the current |
| 331 | /// answer, and a layer that defers every address does not hold the current |
| 332 | /// answer either — the split [`Selector`] already makes, where this crate says |
| 333 | /// what kind of chooser a thing is and the router says which option is picked. |
| 334 | /// So a holder of regions carries the index and the per-child label beside this. |
| 335 | /// |
| 336 | /// # What a renderer does with it |
| 337 | /// |
| 338 | /// Derives its chrome, once, for every widget rather than per name: |
| 339 | /// |
| 340 | /// - Children carrying labels get a strip of the labels, the current one marked. |
| 341 | /// - Children carrying none get previous, position, next. |
| 342 | /// - [`AtMostOne`](Self::AtMostOne) over one child gets a summary line that |
| 343 | /// opens. |
| 344 | /// |
| 345 | /// The name on [`Region::Widget`] survives as app vocabulary, for a renderer |
| 346 | /// that wants to do something *special* with one, which is what it should have |
| 347 | /// been from the start. |
| 348 | /// |
| 349 | /// Degradation runs the way it already did: a renderer ignoring this draws every |
| 350 | /// child, which is more content rather than less. |
| 351 | |
| 352 | |
| 353 | |
| 354 | /// Every child, in order. What every region did before this existed. |
| 355 | |
| 356 | All, |
| 357 | /// Exactly one. A carousel, a tab group. |
| 358 | One, |
| 359 | /// One, or none. A disclosure, which is closed until it is opened. |
| 360 | AtMostOne, |
| 361 | |
| 362 | |
| 363 | |
| 364 | /// Whether the reader can change which child is up. |
| 365 | /// |
| 366 | /// The question every renderer's region arm asks before deriving any |
| 367 | /// chrome, and a method rather than a `matches!` at each renderer for |
| 368 | /// [`Region::name`]'s reason: three renderers writing the same comparison is |
| 369 | /// how they come to disagree about a member added later. |
| 370 | |
| 371 | pub const |
| 372 | !matches! |
| 373 | |
| 374 | |
| 375 | /// Whether showing nothing is a legal state. |
| 376 | /// |
| 377 | /// True only for [`AtMostOne`](Self::AtMostOne). A renderer needs this to |
| 378 | /// know whether its control closes as well as moves: a carousel's row moves |
| 379 | /// between frames and never reaches empty, and a disclosure's summary line |
| 380 | /// is the same control wearing its closed state. |
| 381 | |
| 382 | pub const |
| 383 | matches! |
| 384 | |
| 385 | |
| 386 | |
| 387 | /// A window onto a sequence: where it starts, how much it covers, and how long |
| 388 | /// the sequence is when that is known. |
| 389 | /// |
| 390 | /// The mechanism under two things the vocabulary deliberately keeps apart. A |
| 391 | /// carousel is a window of one frame over children that are all present; a |
| 392 | /// paged list is a window of a page over rows most of which were never fetched. |
| 393 | /// Those are different facts and they stay different types — [`Showing`] says |
| 394 | /// which child is up, [`Paging`] says where a reader is in a query — but the |
| 395 | /// arithmetic underneath is one piece of code, so a terminal and a browser |
| 396 | /// cannot come to disagree about which frame is last. |
| 397 | /// |
| 398 | /// # Why `of` is optional and `count` is not |
| 399 | /// |
| 400 | /// `count` is what is on screen and is therefore always known. `of` is the |
| 401 | /// length of the thing being windowed, and a host that cannot count says so by |
| 402 | /// leaving it empty **for the life of the screen**. It is never "not counted |
| 403 | /// yet": see "First paint is final paint" in the crate header. A total that |
| 404 | /// turns up on a later pass widens the text that prints it. |
| 405 | /// |
| 406 | /// # Clamping |
| 407 | /// |
| 408 | /// Every derivation clamps rather than refusing, and a zero `count` answers |
| 409 | /// `None` rather than dividing. A window past the end is a bug in the host, and |
| 410 | /// a renderer that answered it by drawing nothing would report a region that |
| 411 | /// vanished, which is the hardest kind of bug to find from what is on screen. |
| 412 | /// [`Share::percent`] clamps for the same reason. |
| 413 | |
| 414 | |
| 415 | /// The index into the sequence where the window starts. |
| 416 | pub from: usize, |
| 417 | /// How many the window covers. One, for a carousel. |
| 418 | pub count: usize, |
| 419 | /// How long the sequence is, when the host can say. |
| 420 | pub of: , |
| 421 | |
| 422 | |
| 423 | |
| 424 | /// A window of `count`, starting at `from`, over a sequence of unknown |
| 425 | /// length. |
| 426 | |
| 427 | pub const |
| 428 | Self |
| 429 | from, |
| 430 | count, |
| 431 | of: None, |
| 432 | |
| 433 | |
| 434 | |
| 435 | /// How long the sequence is. |
| 436 | |
| 437 | pub const |
| 438 | self.of = Some; |
| 439 | self |
| 440 | |
| 441 | |
| 442 | /// One item of a sequence whose length is known. A carousel frame. |
| 443 | |
| 444 | pub const |
| 445 | Self |
| 446 | from: at, |
| 447 | count: 1, |
| 448 | of: Some, |
| 449 | |
| 450 | |
| 451 | |
| 452 | /// Which window this is, counting from zero. |
| 453 | /// |
| 454 | /// `None` when `count` is zero, which is the only input with no answer |
| 455 | /// rather than a clamped one. |
| 456 | |
| 457 | pub const |
| 458 | if self.count == 0 |
| 459 | return None; |
| 460 | |
| 461 | Some |
| 462 | |
| 463 | |
| 464 | /// How many windows the sequence holds. |
| 465 | /// |
| 466 | /// `None` unless both the length and a non-zero `count` are known. A |
| 467 | /// partial answer here would be a renderer drawing "of 0". |
| 468 | |
| 469 | pub const |
| 470 | match self.of |
| 471 | Some if self.count > 0 => Some, |
| 472 | _ => None, |
| 473 | |
| 474 | |
| 475 | |
| 476 | /// Whether anything sits before this window. |
| 477 | |
| 478 | pub const |
| 479 | self.from > 0 |
| 480 | |
| 481 | |
| 482 | /// How many sit after this window, when the length is known. |
| 483 | /// |
| 484 | /// Here rather than in each renderer for [`Showing::selective`]'s reason: |
| 485 | /// three of them writing the same subtraction is how they come to disagree, |
| 486 | /// and this one has an underflow in it for whoever writes it fourth. |
| 487 | |
| 488 | pub const |
| 489 | match self.of |
| 490 | Some => Some, |
| 491 | None => None, |
| 492 | |
| 493 | |
| 494 | |
| 495 | /// Whether anything sits after it. |
| 496 | /// |
| 497 | /// `true` when the length is unknown: a host that cannot count cannot rule |
| 498 | /// out more, and offering a way forward that turns out to be empty is the |
| 499 | /// cheaper of the two mistakes. |
| 500 | |
| 501 | pub const |
| 502 | match self.of |
| 503 | Some => self.from.saturating_add < of, |
| 504 | None => true, |
| 505 | |
| 506 | |
| 507 | |
| 508 | /// The window with `from` brought inside the sequence. |
| 509 | /// |
| 510 | /// A no-op when the length is unknown, since there is nothing to clamp |
| 511 | /// against. |
| 512 | |
| 513 | pub const |
| 514 | if let Some = self.of |
| 515 | && self.from >= of |
| 516 | |
| 517 | // `max(1)` by hand: `Ord::max` is not const yet, and a zero-count |
| 518 | // window would otherwise clamp onto the end rather than inside it. |
| 519 | let step = if self.count == 0 else ; |
| 520 | self.from = of.saturating_sub; |
| 521 | |
| 522 | self |
| 523 | |
| 524 | |
| 525 | |
| 526 | /// Where a reader is in a set that arrived in parts. |
| 527 | /// |
| 528 | /// A [`Window`] wearing the paged reading of itself. Distinct from a carousel's |
| 529 | /// window at the top level on purpose, because the intent differs and a call |
| 530 | /// site should say which one it means, while the arithmetic below is shared so |
| 531 | /// the two cannot drift apart. |
| 532 | /// |
| 533 | /// # The two idioms, and which one a renderer may draw |
| 534 | /// |
| 535 | /// Load-more and numbered pages are both this type. Which is honest is |
| 536 | /// [`paged`](Self::paged): a set whose page size is known can be drawn as |
| 537 | /// "Page 3 of 8", and one without can only be drawn as "150 of 400" and a way |
| 538 | /// forward. Saying it here rather than letting each renderer guess is the point |
| 539 | /// — three renderers inferring it from the numbers is how they come to disagree. |
| 540 | /// |
| 541 | /// # What it does not carry |
| 542 | /// |
| 543 | /// No addresses. `makeover-layout` cannot name an action, and the way to ask for |
| 544 | /// the next part is the host's: `quasi_router` pairs this with the addresses the |
| 545 | /// same way `Row` pairs its parts with `Row::activate`. That split is the reason |
| 546 | /// this type is reusable by a carousel, which has nothing to ask. |
| 547 | |
| 548 | |
| 549 | /// The window onto the set. |
| 550 | pub window: Window, |
| 551 | /// Whether the parts are a fixed size, and so whether pages are countable. |
| 552 | /// |
| 553 | /// `false` for load-more, where the window simply grew and "page 2" would |
| 554 | /// name nothing. |
| 555 | pub paged: bool, |
| 556 | |
| 557 | |
| 558 | |
| 559 | /// A page of `per`, starting at `from`. |
| 560 | |
| 561 | pub const |
| 562 | Self |
| 563 | window: new, |
| 564 | paged: true, |
| 565 | |
| 566 | |
| 567 | |
| 568 | /// The first `shown`, with more behind them. |
| 569 | /// |
| 570 | /// The load-more shape: the window starts at the beginning and grows, so |
| 571 | /// there is no page to number. |
| 572 | |
| 573 | pub const |
| 574 | Self |
| 575 | window: new, |
| 576 | paged: false, |
| 577 | |
| 578 | |
| 579 | |
| 580 | /// How many there are altogether. |
| 581 | /// |
| 582 | /// Left unsaid by a host that cannot count, and left unsaid **for good**: |
| 583 | /// a total arriving later widens whatever prints it. See "First paint is |
| 584 | /// final paint" in the crate header. |
| 585 | |
| 586 | pub const |
| 587 | self.window = self.window.of; |
| 588 | self |
| 589 | |
| 590 | |
| 591 | /// Which page this is, counting from one, when pages are countable. |
| 592 | /// |
| 593 | /// One-based because it is read aloud. [`Window::index`] is the zero-based |
| 594 | /// form for anyone indexing with it. |
| 595 | |
| 596 | pub const |
| 597 | if !self.paged |
| 598 | return None; |
| 599 | |
| 600 | match self.window.index |
| 601 | Some => Some, |
| 602 | None => None, |
| 603 | |
| 604 | |
| 605 | |
| 606 | /// How many pages there are, when that is countable. |
| 607 | |
| 608 | pub const |
| 609 | if !self.paged |
| 610 | return None; |
| 611 | |
| 612 | self.window.windows |
| 613 | |
| 614 | |
| 615 | /// How many are on screen. |
| 616 | |
| 617 | pub const |
| 618 | self.window.count |
| 619 | |
| 620 | |
| 621 | /// How many there are, when the host counted. |
| 622 | |
| 623 | pub const |
| 624 | self.window.of |
| 625 | |
| 626 | |
| 627 | /// How many are not shown yet, when the host counted. |
| 628 | /// |
| 629 | /// The figure a load-more control puts in its label. `None` is the honest |
| 630 | /// and common case: a set that cannot say how many more there are still has |
| 631 | /// a way to ask for them. |
| 632 | |
| 633 | pub const |
| 634 | self.window.after |
| 635 | |
| 636 | |
| 637 | /// Whether there is anything further on. |
| 638 | |
| 639 | pub const |
| 640 | self.window.has_after |
| 641 | |
| 642 | |
| 643 | /// Whether there is anything back the other way. |
| 644 | |
| 645 | pub const |
| 646 | self.window.has_before |
| 647 | |
| 648 | |
| 649 | |
| 650 | /// How much of the width an arrangement's first region takes. |
| 651 | /// |
| 652 | /// Nothing said how much room a region got, so every renderer invented its own |
| 653 | /// number and two hosts showing one screen disagreed about its proportions. A |
| 654 | /// webview never noticed, because the stylesheet answered once for every |
| 655 | /// consumer; a terminal has no stylesheet to inherit from, so `quasi-tui` |
| 656 | /// picked 24 columns for a sidebar and 40% for a list pane and neither had |
| 657 | /// anything behind it. |
| 658 | /// |
| 659 | /// # A proportion, never a unit |
| 660 | /// |
| 661 | /// Held as a percentage, and that is the only form it comes in. A description |
| 662 | /// carrying columns would be describing a terminal and one carrying pixels a |
| 663 | /// webview, and the whole point is that both honour the same fact: a terminal |
| 664 | /// resolves it against a column count, a webview writes it into a grid, and |
| 665 | /// neither has to know what the other did. |
| 666 | /// |
| 667 | /// It is not [`makeover_geometry::Ratio`]'s job either, which was the first |
| 668 | /// guess. Geometry is scales that answer the same for every screen and takes |
| 669 | /// no input that would let a sidebar screen differ from a list-detail one. |
| 670 | /// |
| 671 | /// [`makeover_geometry::Ratio`]: https://docs.rs/makeover-geometry |
| 672 | |
| 673 | ; |
| 674 | |
| 675 | |
| 676 | /// What a sidebar takes, when nobody says otherwise. |
| 677 | /// |
| 678 | /// A quarter. `quasi-tui` drew 24 columns, which is a quarter of a |
| 679 | /// 96-column terminal and about a fifth of a wide one; a quarter is that |
| 680 | /// number said in the form a webview can honour too. |
| 681 | pub const SIDEBAR: Self = Self; |
| 682 | |
| 683 | /// What the list side of a list-detail takes, when nobody says otherwise. |
| 684 | /// |
| 685 | /// `quasi-tui`'s 40%, which was already a proportion and is the one number |
| 686 | /// this member did not have to invent. |
| 687 | pub const LIST: Self = Self; |
| 688 | |
| 689 | /// A share of the width, as a percentage. |
| 690 | /// |
| 691 | /// Clamped to 5..=95 rather than refused. A description that asked for a |
| 692 | /// region of nothing is a bug in the app, and a renderer drawing a region |
| 693 | /// zero cells wide reports it as a region that vanished, which is the |
| 694 | /// hardest kind of bug to find from what is on the screen. |
| 695 | |
| 696 | pub const |
| 697 | Self |
| 698 | 5 |
| 699 | else if percent > 95 |
| 700 | 95 |
| 701 | else |
| 702 | percent |
| 703 | |
| 704 | |
| 705 | |
| 706 | /// The share as a percentage. |
| 707 | |
| 708 | pub const |
| 709 | self.0 |
| 710 | |
| 711 | |
| 712 | /// This share of a width, rounded to the nearest whole unit. |
| 713 | /// |
| 714 | /// What a terminal calls to turn the proportion into columns. At least one, |
| 715 | /// because a region the description named should be visible: a screen |
| 716 | /// 3 columns wide is unusable either way, and a sidebar that is there is a |
| 717 | /// truer picture of the description than a sidebar that is not. |
| 718 | |
| 719 | pub const |
| 720 | let taken = .div_ceil; |
| 721 | if taken == 0 else |
| 722 | |
| 723 | |
| 724 | |
| 725 | /// How a screen is laid out. |
| 726 | /// |
| 727 | /// Three, and no one of them is a variant of another. goingson is list-detail, |
| 728 | /// Balanced Breakfast is sidebar plus content, and MNW's embeds are one region |
| 729 | /// filling the document. The tab group is a modifier rather than a member, |
| 730 | /// because goingson uses it *inside* the same content region rather than |
| 731 | /// instead of one. |
| 732 | /// |
| 733 | /// This exists at all because the router has to be able to express a screen |
| 734 | /// rather than only a control. Discovering the arrangement layer missing after |
| 735 | /// the renderers exist is a redesign; naming two now is a morning. |
| 736 | /// |
| 737 | /// # Why the share rides here |
| 738 | /// |
| 739 | /// A share is per-arrangement: how much a sidebar takes and how much a list |
| 740 | /// side takes are different questions, and this enum is the only thing that |
| 741 | /// knows which one is being asked. Geometry would have had to invent a channel |
| 742 | /// to be told. |
| 743 | /// |
| 744 | /// [`list_detail`](Self::list_detail) and |
| 745 | /// [`sidebar_content`](Self::sidebar_content) build these with the default |
| 746 | /// shares, so a screen that has no opinion does not have to have one. |
| 747 | |
| 748 | |
| 749 | /// A list that chooses what the detail beside it shows. |
| 750 | ListDetail |
| 751 | /// Whether the detail side is a [`Region::TabGroup`]. |
| 752 | tabbed: bool, |
| 753 | /// How much of the width the list side takes. |
| 754 | share: Share, |
| 755 | , |
| 756 | /// Navigation down the side, content filling the rest. |
| 757 | SidebarContent |
| 758 | /// How much of the width the sidebar takes. |
| 759 | share: Share, |
| 760 | , |
| 761 | /// One region, filling the document. |
| 762 | /// |
| 763 | /// The other two are both about dividing a width between two regions, so a |
| 764 | /// screen that is one region had to borrow one of them and then undo it: |
| 765 | /// MNW's five embeds said `list_detail(title, false)` and the host spent a |
| 766 | /// `display: block` cancelling the grid that produced. A host writing CSS |
| 767 | /// to contradict the description rather than to add to it is the thing |
| 768 | /// this member ends. |
| 769 | /// |
| 770 | /// Carries no [`Share`], because there is no division to describe. That is |
| 771 | /// why [`share`](Self::share) answers `None` here. |
| 772 | Single, |
| 773 | |
| 774 | |
| 775 | |
| 776 | /// A list and a detail beside it, at the default share. |
| 777 | |
| 778 | pub const |
| 779 | SelfListDetail |
| 780 | tabbed, |
| 781 | share: LIST, |
| 782 | |
| 783 | |
| 784 | |
| 785 | /// A sidebar and content beside it, at the default share. |
| 786 | |
| 787 | pub const |
| 788 | SelfSidebarContent |
| 789 | share: SIDEBAR, |
| 790 | |
| 791 | |
| 792 | |
| 793 | /// How much of the width the first region takes, when two regions divide it. |
| 794 | /// |
| 795 | /// `None` for [`Single`](Self::Single): one region takes the width, and a |
| 796 | /// renderer that asked how to divide it was asking the wrong question. It |
| 797 | /// answers `Option` rather than a full-width `Share` so that a host cannot |
| 798 | /// quietly draw a one-region screen as a grid with an empty second column. |
| 799 | |
| 800 | pub const |
| 801 | match self |
| 802 | SelfListDetail | SelfSidebarContent => Some, |
| 803 | SelfSingle => None, |
| 804 | |
| 805 | |
| 806 | |
| 807 | /// The same arrangement, at this share. |
| 808 | /// |
| 809 | /// [`Single`](Self::Single) is returned unchanged: it has no division to |
| 810 | /// set, so a share named for it is a statement about nothing rather than an |
| 811 | /// error worth refusing a screen over. |
| 812 | |
| 813 | pub const |
| 814 | match self |
| 815 | SelfListDetail => SelfListDetail , |
| 816 | SelfSidebarContent => SelfSidebarContent , |
| 817 | SelfSingle => SelfSingle, |
| 818 | |
| 819 | |
| 820 | |
| 821 | |
| 822 | /// How wide the content of a whole screen runs. |
| 823 | /// |
| 824 | /// Both are the description's, which is what answering the two together |
| 825 | /// settled. |
| 826 | /// |
| 827 | /// Measured in the MNW server, where 69 of 72 templates carry exactly one of |
| 828 | /// three mutually exclusive classes and the choice is per screen. GoingsOn |
| 829 | /// reaches for `max-width` 56 times and Balanced Breakfast 12, neither with a |
| 830 | /// token for it, so three apps were solving one thing by hand. |
| 831 | /// |
| 832 | /// # Named for the measure, not for MNW's classes |
| 833 | /// |
| 834 | /// A renderer that is not a browser has to answer this too, and `padded-page` |
| 835 | /// tells a terminal nothing. The three say how wide the text runs, which is a |
| 836 | /// question every renderer can answer: a webview with a `max-width`, a terminal |
| 837 | /// with gutters, an immediate-mode frame with its own width. |
| 838 | /// |
| 839 | /// `#[non_exhaustive]` for [`Fill`]'s reason. The set is closed today because |
| 840 | /// the measurement found three, and a fourth arriving should not be a lockstep |
| 841 | /// release across nine repos. |
| 842 | |
| 843 | |
| 844 | |
| 845 | /// The whole width, with gutters. The default, and 53 of the 69. |
| 846 | /// |
| 847 | /// What a dashboard, a table and a settings screen want: the content is |
| 848 | /// wide because the content *is* wide, and constraining it would waste the |
| 849 | /// window. |
| 850 | |
| 851 | Wide, |
| 852 | /// Capped at a comfortable page width, centred. 13 of the 69. |
| 853 | /// |
| 854 | /// A form, a sign-in, a purchase. Content that does not get better by |
| 855 | /// getting wider, but is not prose either. |
| 856 | Contained, |
| 857 | /// Capped at a line length that reads well. 3 of the 69. |
| 858 | /// |
| 859 | /// Prose. The narrowest of the three, and the one with a reason outside |
| 860 | /// taste: a line of text past roughly 75 characters costs the reader the |
| 861 | /// return sweep. |
| 862 | Reading, |
| 863 | |
| 864 | |
| 865 | |
| 866 | /// A stable name, for a renderer that needs to spell it. |
| 867 | /// |
| 868 | /// Here rather than in each renderer for [`Sort::as_str`]'s reason: three |
| 869 | /// renderers spelling one enum is three chances to spell it differently. |
| 870 | |
| 871 | pub const |
| 872 | match self |
| 873 | SelfWide => "wide", |
| 874 | SelfContained => "contained", |
| 875 | SelfReading => "reading", |
| 876 | |
| 877 | |
| 878 | |
| 879 |