Skip to main content

max / quasi

503.2 KB · 11990 lines History Blame Raw
1 //! The screen tree: what a route answers with.
2 //!
3 //! Decision 3 on the wiki note puts this in quasi rather than in
4 //! `makeover-layout`, for two reasons. The tree will churn while the router is
5 //! being proven against a second host, and churning it here costs nothing where
6 //! churning it there is a breaking release against three adopters. And a route
7 //! is an address, which is the one thing `makeover-layout`'s deferral rule says
8 //! it never names.
9 //!
10 //! # What this crate adds, and what it does not
11 //!
12 //! It adds three things: an [`Action`], which is an address; a [`Slot`], which
13 //! is a region with a name a fragment can be aimed at; and ownership.
14 //!
15 //! Everything else is `makeover-layout`'s. Every member of [`Node`] composes a
16 //! vocabulary that already exists there, and that is the admission test for a
17 //! new one: if the thing being drawn has no name in the description layer, it
18 //! does not get a node here, it gets a [`Region::Handover`] or it gets named
19 //! there first. Without that rule this file becomes a widget library, which is
20 //! the failure `makeover-layout` was extracted to prevent.
21 //!
22 //! # Why these are owned when the description layer is borrowed
23 //!
24 //! `makeover-layout`'s structs borrow, because a description is built, read
25 //! once and dropped inside one frame. A router's answer outlives its handler by
26 //! construction: it is returned from a function, and its text is usually built
27 //! from state rather than found in it. So [`Field`], [`Choice`] and [`Column`]
28 //! have owned mirrors here, each with a conversion back, and the conversion is
29 //! what keeps them from drifting: adding a field over there stops the mirror
30 //! compiling over here.
31
32 use std::collections::BTreeSet;
33
34 use makeover_layout as layout;
35
36 use crate::containment::{Containment, Element};
37 use crate::request::{Method, Params};
38
39 /// Where an action goes.
40 ///
41 /// The port put the URL in the row's trailing text, which made it something to
42 /// copy rather than something to follow.
43 ///
44 /// # Why this and not a separate link node
45 ///
46 /// Both were on the table. A destination keeps one concept where there would
47 /// have been two, and the cost is that every renderer now branches: a webview
48 /// emits an anchor rather than a button, and a terminal has to decide whether
49 /// it can open a browser or should show the address. That branch is honest
50 /// work, and it is work each renderer must do anyway once external addresses
51 /// exist at all.
52 ///
53 /// What it must never become is a guess. The renderer branches on this enum and
54 /// never on the shape of the string, because "starts with https" is how a route
55 /// named `/https-setup` ends up opening a browser.
56 ///
57 /// # Locality
58 ///
59 /// [`Local`](Self::Local) is for what happens without a request: arrow keys
60 /// moving a highlight, a toast dismissing itself, a price recomputing as a
61 /// slider is dragged.
62 ///
63 /// The mark hangs here, per element, because an [`Action`] does, and because
64 /// which interactions are local varies within one behaviour: typing asks the
65 /// route for suggestions, arrowing through what came back does not. Both are
66 /// actions on the same field and only one leaves.
67 ///
68 /// **What happens locally is named by the member carrying the action, never by
69 /// this variant.** A suggestion source's pick action being local says picking
70 /// sets the field; a notice's dismiss action being local says the notice goes
71 /// away. A bare [`Act`] with a local destination says only "press this and the
72 /// renderer's own affordance happens", which is a description bug everywhere
73 /// except inside a [`Region::Handover`](layout::Region::Handover) or a
74 /// [`Region::Ceded`](layout::Region::Ceded). This is the
75 /// same rule that keeps the other two variants off string-shape guessing, and
76 /// it is what stops this becoming `data-action="doTheThing"` in a new hat.
77 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
78 pub enum Destination {
79 /// A path this app's router answers.
80 Route(String),
81 /// An address outside the app, offered as a reference. Nothing here will
82 /// ever call it.
83 ///
84 /// The reader is expected to come back, so a windowing host puts it aside
85 /// rather than in front: a webview opens a tab. See
86 /// [`Leaving`](Self::Leaving) for the other half, and read the two
87 /// together -- what separates them is the description's, and what a host
88 /// does about it is the host's.
89 External(String),
90 /// An address outside the app that this document hands the reader on to.
91 ///
92 /// The reader is finished here, so a host navigates in place rather than
93 /// opening anything: nothing is being kept.
94 ///
95 /// The measured consumer is MNW's custom-pages host. `u.makenot.work`
96 /// serves a creator's page with a platform strip on it, and every link in
97 /// that strip -- the brand, "View on makenot.work", the footer credit --
98 /// goes to the apex in the same tab, because going to the apex is the
99 /// whole point of the strip. Said as [`External`](Self::External) all
100 /// three would open tabs, which is a satellite document refusing to let go
101 /// of a reader who asked to leave.
102 ///
103 /// # The split is intent, and the mechanism is still the renderer's
104 ///
105 /// A tab is window management, which is placement, which this vocabulary
106 /// has held is the host's throughout -- the same call [`Frame`] placement
107 /// and the clock a toast expires on both got. What a renderer cannot infer
108 /// from an address alone is which of the two kinds of leaving it is, and
109 /// that is what these two variants say. A terminal that has no tabs
110 /// follows both the one way it can, and is not wrong.
111 ///
112 /// [`Frame`]: crate::chrome::Frame
113 Leaving(String),
114 /// Nothing leaves the machine. The renderer performs it from what it
115 /// already holds, and no route is asked.
116 ///
117 /// Not "cheap" and not "fast", both of which are judgements. What it says
118 /// is that there is no request, which is a fact the description knows and
119 /// no renderer can infer.
120 ///
121 /// Who is obliged to read it is [`Renderer`](crate::Renderer)'s business: a
122 /// client renderer redraws every frame from memory and gets this for free,
123 /// a hybrid one has to be told. Read that type before implementing a
124 /// renderer against this variant.
125 Local,
126 /// Wherever the reader came from.
127 ///
128 /// A route cannot name this, because which route it is depends on history
129 /// the runtime holds and the description does not. That is what makes it a
130 /// destination rather than a path some screen computes: the description
131 /// says "back" and the host says where that is.
132 ///
133 /// # Whose history
134 ///
135 /// The runtime's, and this is the part worth stating. Each host runtime
136 /// keeps what it has been asked for -- `Outcome::Screen` pushes through its
137 /// own `remember` -- and answers this by popping it. A webview maps that
138 /// onto the browser's history, not the other way round: the description
139 /// cannot see a browser and must not be written against one.
140 ///
141 /// # Why not [`Local`](Self::Local)
142 ///
143 /// `Local` says no request is made at all. Going back usually makes one:
144 /// the runtime pops an address and calls it. What this variant says is that
145 /// the *address* is the host's to supply, which is a different fact, and
146 /// [`is_local`](Self::is_local) stays false for it.
147 ///
148 /// One thing to say it with, so a key, a visible Close and a system gesture
149 /// cannot disagree:
150 ///
151 /// ```
152 /// # use quasi_router::{Act, Action, Chrome};
153 /// Chrome::new().bind("escape", "Back", Action::back());
154 /// Act::new("Close", Action::back());
155 /// ```
156 Back,
157 }
158
159 /// An empty route rather than an empty external address, so a half-built
160 /// [`Action`] is something this app would answer rather than somewhere it would
161 /// send a user. Same reasoning as [`Method`]'s default being the safe verb.
162 ///
163 /// Written out because `#[default]` only applies to unit variants.
164 impl Default for Destination {
165 fn default() -> Self {
166 Self::Route(String::new())
167 }
168 }
169
170 impl Destination {
171 /// The route path, if it is one.
172 ///
173 /// `None` for an external address, which is the answer a host wants when it
174 /// is deciding whether it can dispatch something.
175 #[must_use]
176 pub fn route(&self) -> Option<&str> {
177 match self {
178 Self::Route(path) => Some(path),
179 // `Back` has no path *here*. The runtime supplies one when it pops
180 // its history, and that is a route like any other by then.
181 Self::External(_) | Self::Leaving(_) | Self::Local | Self::Back => None,
182 }
183 }
184
185 /// The address as written, whichever kind it is.
186 ///
187 /// For rendering only. A host deciding whether to dispatch wants
188 /// [`route`](Self::route), which cannot hand back something uncallable.
189 ///
190 /// Empty for [`Local`](Self::Local), which has no address to write: the
191 /// absence of one is the whole of what that variant says. A renderer
192 /// reaching here for a local action has skipped the branch it owes.
193 #[must_use]
194 pub fn as_str(&self) -> &str {
195 match self {
196 Self::Route(address) | Self::External(address) | Self::Leaving(address) => address,
197 Self::Local | Self::Back => "",
198 }
199 }
200
201 /// Whether it leaves the app.
202 ///
203 /// True for both ways of leaving it. What separates them is where the
204 /// reader ends up, which is [`Leaving`](Self::Leaving)'s subject; a host
205 /// asking this is asking whether it can dispatch the thing, and it cannot
206 /// either way.
207 #[must_use]
208 pub const fn is_external(&self) -> bool {
209 matches!(self, Self::External(_) | Self::Leaving(_))
210 }
211
212 /// Whether performing it takes no request at all.
213 ///
214 /// The complement of neither of the others: an external address leaves the
215 /// app and is still a request, and this is the case where nothing is asked.
216 #[must_use]
217 pub const fn is_local(&self) -> bool {
218 matches!(self, Self::Local)
219 }
220
221 /// Whether it means "wherever I came from".
222 ///
223 /// The one question a host asks before dispatching, because it is the one
224 /// destination whose address the host has and the description does not.
225 #[must_use]
226 pub const fn is_back(&self) -> bool {
227 matches!(self, Self::Back)
228 }
229 }
230
231 /// Where a call's answer lands, when the responder cannot say.
232 ///
233 /// The value of [`Action::replaces`], which carries the whole of when this is
234 /// set at all: only for a route the description layer does not serve. Read that
235 /// first. This type is about what may be named once you are already in that
236 /// case.
237 ///
238 /// # Why three
239 ///
240 /// Measured on the MNW server's dashboard, across the two conversions that
241 /// stopped on this. A region id covered neither.
242 ///
243 /// - Two acts target the repeated element they sit in and nothing else
244 /// (`hx-target="closest .link-row"`, `closest .tag`). Both are shared
245 /// partials called from several parents, so there is no id to name.
246 /// - Fifteen acts target nothing at all: they fire and the surface they sit on
247 /// is refetched, navigated away from, or reloaded whole (`data-after` in
248 /// `frontend/src/core/dispatch.ts`).
249 ///
250 /// # Deliberately not `#[non_exhaustive]`
251 ///
252 /// [`Outcome`](crate::Outcome)'s reasoning, and for the same reason: every
253 /// renderer has to decide what to draw for each of these, and a wildcard arm
254 /// has nothing sensible to put in it. A member added here should break every
255 /// host on purpose.
256 #[derive(Debug, Clone, PartialEq, Eq)]
257 pub enum Replaces {
258 /// The region with this [`Slot::id`].
259 ///
260 /// What this was before it was three things, and still the common case.
261 Region(String),
262 /// The repeated element this control is drawn inside.
263 ///
264 /// A row of a list or of a table, without either side naming the other.
265 /// The containment is already in the description — a [`Row`] holds its acts
266 /// — so the alternative was making every repeated element author a unique
267 /// id whose only purpose is to be pointed at from within itself. That is
268 /// the reasoning [`Field::suggests`] used when it gave the field its own
269 /// list rather than having two elements point at each other by id.
270 ///
271 /// Replaces the element rather than filling it: "remove this row" is what
272 /// both measured sites mean, and a row that answered into itself would
273 /// nest.
274 Enclosing,
275 /// Nothing on screen contains it, so what is showing is stale.
276 ///
277 /// The act fires and the surface it sits on is no longer trustworthy. A
278 /// statement about staleness rather than a verb naming a mechanism, which
279 /// is the same choice [`Invalidated`](crate::Invalidated) made and for the
280 /// same reason: "reload the page" is a webview sentence, and two of the
281 /// three renderers have no page to reload.
282 ///
283 /// What each renderer does with it: a webview asks the host to load the
284 /// document again, a terminal redraws, an immediate-mode host does nothing
285 /// because it was going to redraw anyway.
286 ///
287 /// This is the *control's* half. A described route says the same thing by
288 /// answering [`Outcome::Screen`](crate::Outcome::Screen), which is the
289 /// member for an effect no one region contains.
290 Everything,
291 }
292
293 /// An address a control calls when it acts.
294 ///
295 /// Decision 2: an action is a route. The webview emits this as an `hx-get` or
296 /// `hx-post`, the terminal binds a key to it, egui calls it directly. All three
297 /// are calling the same path with the same verb.
298 ///
299 /// A route is not the only thing it can be: see
300 /// [`Destination`]. Decision 2 still holds for everything the app answers, and
301 /// an external address is the case it never covered.
302 ///
303 /// It does not carry a target. What a response replaces is the *response's*
304 /// business, per decision 7, because the router is the only party that knows
305 /// what it just changed.
306 #[derive(Debug, Clone, PartialEq, Eq, Default)]
307 pub struct Action {
308 /// Asking or telling.
309 ///
310 /// Meaningless for a [`Destination::External`], which is nobody's route to
311 /// answer. Left on the struct rather than moved inside `Destination`
312 /// because a method that is ignored is simpler than two shapes of action.
313 pub method: Method,
314 /// Where it goes.
315 pub destination: Destination,
316 /// Values the control sends that are not in the path.
317 ///
318 /// A webview emits these as `hx-vals`; a terminal passes them straight
319 /// through. Here so that no app hand-builds a query string, which is where
320 /// escaping bugs live.
321 ///
322 /// Empty on a read. A read has nothing to send: its values are its address,
323 /// so [`Action::with`] puts them in [`Self::carried`] instead. See
324 /// [`Request`](crate::Request) for the whole of why the two are separate.
325 pub params: Params,
326 /// The view this control was offered under.
327 ///
328 /// A filtered list sends its filters on every control it draws, so that
329 /// pressing one answers with the list you were looking at rather than with a
330 /// default. Kept apart from [`Self::params`] because a screen that filters
331 /// on `status` and also writes a `status` would otherwise have one name for
332 /// two things, and the handler would read whichever landed first.
333 ///
334 /// Emitted as the query string, on the address itself, which is where a view
335 /// belongs: the link is then the view, and a middle-click reaches the same
336 /// place the control does.
337 pub carried: Params,
338 /// The region this call's answer replaces, when the responder cannot say.
339 ///
340 /// Normally nothing sets this and nothing should: a described route answers
341 /// with a `Response::Fragment` naming the region it changed, `quasi-http`
342 /// turns that into the transport's retarget header, and the router is the
343 /// only party that knows what it just changed. That is decision 7 and it is
344 /// unchanged.
345 ///
346 /// **Decision 7 assumes the responder is described, and a control may call
347 /// a route that is not.** Every write on the MNW server's dashboard goes to
348 /// a plain API route that quasi never sees and that answers with a status or
349 /// a hand-rendered fragment. Those routes cannot name a region, so if the
350 /// control does not either, nobody does: the answer lands wherever the
351 /// transport's default puts it, which for htmx is inside the button that was
352 /// pressed. That is not a second party deciding one thing. It is the only
353 /// party that can decide, because the other one is outside the description
354 /// layer.
355 ///
356 /// So: leave it unset when calling a described route, and set it when
357 /// calling something else. A screen that sets it against a described route
358 /// is overriding an answer that already knew better, and that is the misuse
359 /// decision 7 was guarding against.
360 ///
361 /// What it may name is [`Replaces`]. A region is still the common case;
362 /// the other two are the shapes the MNW server's undescribed routes were
363 /// measured to want and a region id could not say.
364 pub replaces: Option<Replaces>,
365 /// The name to keep the answer under, when the answer is a file.
366 ///
367 /// `Some` means the response is not a view: nothing swaps, and the reader
368 /// ends up holding a file called this. A webview makes that a browser
369 /// download; a terminal writes it to disk; either way the screen said what
370 /// it meant rather than a class name on a button implying it.
371 ///
372 /// Counted before adding it. Nine sites in the MNW server: five CSV export
373 /// buttons across four dashboard templates, a sixth in the item-sales tab's
374 /// own script, and three anchors carrying a `download` attribute. Six of the
375 /// nine are writes, which is what makes this a property of the action rather
376 /// than a kind of destination: a write cannot be a plain link, so the host
377 /// has to be told, and until now it was told by
378 /// `data-action="exportCsvButton"` plus two positional arguments.
379 ///
380 /// Independent of [`method`](Self::method). A read that saves is an anchor
381 /// the browser downloads instead of navigating to; a write that saves has to
382 /// be performed and then handed to the reader. Both are the same sentence
383 /// here and differ only in the emitting.
384 pub saves: Option<String>,
385 /// That this call waits on something which resolves once, in expected
386 /// finite time.
387 ///
388 /// The mark itself is
389 /// [`layout::Awaiting`] and its docs carry the whole of what is described
390 /// and what is not. It rides on the action rather than on the control
391 /// because the wait is a fact about the call, and because the same call is
392 /// what a region is fed by: one mark, read twice.
393 ///
394 /// A control carrying one goes busy when it is pressed and refuses a second
395 /// press until the answer lands, which is the double-submit guard the MNW
396 /// server writes by hand twice against 57 spinners. A region carrying one
397 /// through [`Slot::fed_by`] stands in and fills.
398 ///
399 /// Not remoteness, which [`Destination`] would already answer and which
400 /// misses a heavy local query. Not slowness, which is a judgement. What it
401 /// says is that something is outstanding and will finish.
402 pub awaiting: Option<layout::Awaiting>,
403 /// That the host makes this call, and the renderer does not.
404 ///
405 /// The same shape as
406 /// [`saves`](Self::saves) and for the same reason it is stated rather than
407 /// inferred: there are calls a renderer cannot make, and the alternative to
408 /// saying so is a host reaching around the description to a control the
409 /// description already owns.
410 ///
411 /// The case it was ruled on is an upload. One described destination, and
412 /// three requests behind it: MNW asks its own server to sign a URL, PUTs the
413 /// file to S3 with it, then tells the server the file landed. A renderer
414 /// posting the field to the first of those would be wrong about the response
415 /// shape and about where the bytes go, and the sequence is not describable —
416 /// it is one address in the description because it is one thing to the
417 /// reader, and three calls in the host because that is how the bytes get
418 /// there.
419 ///
420 /// # What this costs, said out loud
421 ///
422 /// **A call marked this way is not portable.** Every other action in this
423 /// vocabulary is a sentence any host can carry out; this one is a sentence
424 /// only a host that already knows the chain can. A terminal meeting a
425 /// described upload learns the accept list, the multiplicity and that it
426 /// waits, and has nothing to perform.
427 ///
428 /// That is a real limit rather than a temporary one, and it is the price the
429 /// ruling accepted for keeping the file going browser-to-storage instead of
430 /// through the server. It is named here so the next host to meet one finds
431 /// the reason rather than the gap.
432 ///
433 /// Independent of [`method`](Self::method) and of everything else on this
434 /// type. The destination, the parameters and [`awaiting`](Self::awaiting)
435 /// all still mean what they mean; what changes is who acts on them.
436 pub by_host: bool,
437 /// That this call's answer belongs in a mount of its own, not in this one.
438 ///
439 /// The same family as
440 /// [`saves`](Self::saves) and [`replaces`](Self::replaces): all three say
441 /// where the answer lands, and this one says it lands somewhere that is not
442 /// here.
443 ///
444 /// The case it was ruled on is a compose window. goingson draws the same
445 /// compose screen in the main window and in a window of its own, and
446 /// [`Frame`](crate::Frame) already says what a mount puts around a screen.
447 /// What was missing was the sentence that asks for the second mount at all,
448 /// and without it the only ways left were a host script reaching around the
449 /// description or a menu item the description cannot see.
450 ///
451 /// # What a mount of its own means, per host
452 ///
453 /// Deliberately not "a window". A mount is whatever the renderer puts a
454 /// screen up in, and each host already has one:
455 ///
456 /// ```text
457 /// webview a second document, which the host opens as a window
458 /// immediate a viewport of its own
459 /// terminal nothing; the call is performed where it stands
460 /// ```
461 ///
462 /// The terminal's answer is the interesting one and it is not a gap. A
463 /// second mount in a terminal would be a split or a tab, and both are that
464 /// renderer's furniture rather than the description's: a screen asking for
465 /// one would be asking for a layout. So a terminal reads this and navigates,
466 /// which is the honest degradation and is what
467 /// [`Renderer`](crate::Renderer) exists to allow.
468 ///
469 /// # Why it is not a [`Destination`]
470 ///
471 /// A destination says where the call goes. This says where its answer is
472 /// put, and the two are independent: the same address is the main window's
473 /// compose screen and the compose window's, which is the whole point of
474 /// [`Frame`]. One address, two mounts, and a screen that does not know
475 /// which one it is in.
476 ///
477 /// Independent of [`method`](Self::method), and mutually exclusive with
478 /// [`saves`](Self::saves) and [`replaces`](Self::replaces) by meaning
479 /// rather than by type: an answer cannot land in a region here, be kept as
480 /// a file, and be put up in a mount of its own. Nothing enforces that,
481 /// because a type that made it impossible would have to be a fourth
482 /// vocabulary for "where an answer goes".
483 pub elsewhere: bool,
484 /// That performing this replaces the whole document, rather than a region
485 /// of it.
486 ///
487 /// The plain "go there" that every list of links on a public site is made
488 /// of, and nothing said it until now:
489 /// [`replaces`](Self::replaces) names a region, and the whole document is
490 /// not one.
491 ///
492 /// On [`Action`] rather than as a fourth [`Destination`], knowingly against
493 /// the precedent `f35aafee` set when it put the sibling fact on the
494 /// destination. What differs between a navigation and a fragment swap is
495 /// what happens to the document, and the document is the action's business;
496 /// where it goes is the same place either way. A member would also foreclose
497 /// stacking navigation with a later destination kind, since a value can only
498 /// be one of them.
499 ///
500 /// # What each renderer does with it
501 ///
502 /// ```text
503 /// webview the anchor and no verb, so the browser navigates
504 /// terminal pushes a screen: anything open over it is put away first
505 /// immediate swaps its view, the same way
506 /// ```
507 ///
508 /// The webview case is a narrowing rather than an addition. A read of a
509 /// route already emits an `href` beside the verb, for middle-click,
510 /// copy-link, crawlers and the page with JS off; a navigating act keeps that
511 /// anchor and drops the htmx swap, which would otherwise put a whole screen
512 /// inside the page it was meant to leave.
513 ///
514 /// Independent of [`method`](Self::method) on the type, and a read in
515 /// practice: navigation is asking for a place. A renderer meeting it on a
516 /// write performs the write as it otherwise would, since an anchor there
517 /// would ask where the description said to tell.
518 ///
519 /// Mutually exclusive with [`replaces`](Self::replaces),
520 /// [`saves`](Self::saves) and [`elsewhere`](Self::elsewhere) by meaning
521 /// rather than by type, for the reason `elsewhere` records: an answer cannot
522 /// land in a region here, be kept as a file, go up in a mount of its own,
523 /// and be the whole document.
524 pub navigates: bool,
525 }
526
527 impl Action {
528 /// A read.
529 pub fn get(path: impl Into<String>) -> Self {
530 Self {
531 method: Method::Get,
532 destination: Destination::Route(path.into()),
533 params: Params::new(),
534 carried: Params::new(),
535 saves: None,
536 replaces: None,
537 awaiting: None,
538 by_host: false,
539 elsewhere: false,
540 navigates: false,
541 }
542 }
543
544 /// A write.
545 pub fn post(path: impl Into<String>) -> Self {
546 Self {
547 method: Method::Post,
548 destination: Destination::Route(path.into()),
549 params: Params::new(),
550 carried: Params::new(),
551 saves: None,
552 replaces: None,
553 awaiting: None,
554 by_host: false,
555 elsewhere: false,
556 navigates: false,
557 }
558 }
559
560 /// A write that removes what is at the address.
561 ///
562 /// Reach for it when the route the app already answers is a `DELETE`, not
563 /// to editorialise about what a `POST` means: the verb here exists to
564 /// address an interface, and a route that deletes over `POST` is still
565 /// [`post`](Self::post).
566 pub fn delete(path: impl Into<String>) -> Self {
567 Self {
568 method: Method::Delete,
569 destination: Destination::Route(path.into()),
570 params: Params::new(),
571 carried: Params::new(),
572 saves: None,
573 replaces: None,
574 awaiting: None,
575 by_host: false,
576 elsewhere: false,
577 navigates: false,
578 }
579 }
580
581 /// A write that replaces what is at the address.
582 pub fn put(path: impl Into<String>) -> Self {
583 Self {
584 method: Method::Put,
585 destination: Destination::Route(path.into()),
586 params: Params::new(),
587 carried: Params::new(),
588 saves: None,
589 replaces: None,
590 awaiting: None,
591 by_host: false,
592 elsewhere: false,
593 navigates: false,
594 }
595 }
596
597 /// Somewhere outside the app.
598 ///
599 /// [`Method::Get`], because following a link asks and does not tell, and a
600 /// host that ignores the method loses nothing by it.
601 pub fn external(url: impl Into<String>) -> Self {
602 Self {
603 method: Method::Get,
604 destination: Destination::External(url.into()),
605 params: Params::new(),
606 carried: Params::new(),
607 saves: None,
608 replaces: None,
609 awaiting: None,
610 by_host: false,
611 elsewhere: false,
612 navigates: false,
613 }
614 }
615
616 /// Somewhere outside the app, and the reader is going there.
617 ///
618 /// [`external`](Self::external)'s other half. See [`Destination::Leaving`]
619 /// for the split: a reference is kept aside, and this replaces the page.
620 ///
621 /// [`Method::Get`] for `external`'s reason.
622 pub fn leaving(url: impl Into<String>) -> Self {
623 Self {
624 method: Method::Get,
625 destination: Destination::Leaving(url.into()),
626 params: Params::new(),
627 carried: Params::new(),
628 saves: None,
629 replaces: None,
630 awaiting: None,
631 by_host: false,
632 elsewhere: false,
633 navigates: false,
634 }
635 }
636
637 /// Wherever the reader came from.
638 ///
639 /// See [`Destination::Back`], which carries the whole argument: the address
640 /// is the host's and the description does not have it. One thing to say it
641 /// with, so a key binding, a visible Close and a system gesture cannot
642 /// disagree about where back goes.
643 ///
644 /// [`Method::Get`], for [`external`](Self::external)'s reason: what the
645 /// host performs is a read of somewhere it has been, and the safe verb is
646 /// the honest default.
647 #[must_use]
648 pub fn back() -> Self {
649 Self {
650 method: Method::Get,
651 destination: Destination::Back,
652 params: Params::new(),
653 carried: Params::new(),
654 saves: None,
655 replaces: None,
656 awaiting: None,
657 by_host: false,
658 elsewhere: false,
659 navigates: false,
660 }
661 }
662
663 /// Something that happens without a request.
664 ///
665 /// See [`Destination::Local`], and read it before reaching for this: what
666 /// happens is named by the member this action is set on, never by the
667 /// action itself. A [`Node::Act`] built straight from this says only "press
668 /// this and something local happens", which no renderer can perform.
669 ///
670 /// [`Method::Get`], for [`external`](Self::external)'s reason: nothing is
671 /// asked, so nothing reads the verb, and the safe one is the honest
672 /// default. [`params`](Self::params) still carries what the behaviour acts
673 /// on — which suggestion was picked, which notice was dismissed — because
674 /// that is a value and not an address.
675 #[must_use]
676 pub fn local() -> Self {
677 Self {
678 method: Method::Get,
679 destination: Destination::Local,
680 params: Params::new(),
681 carried: Params::new(),
682 saves: None,
683 replaces: None,
684 awaiting: None,
685 by_host: false,
686 elsewhere: false,
687 navigates: false,
688 }
689 }
690
691 /// Put this call's answer into the region with this id.
692 ///
693 /// For a route the description layer does not serve. See
694 /// [`replaces`](Self::replaces) before reaching for it.
695 #[must_use]
696 pub fn replacing(mut self, region: impl Into<String>) -> Self {
697 self.replaces = Some(Replaces::Region(region.into()));
698 self
699 }
700
701 /// Put this call's answer in place of the repeated element it sits in.
702 ///
703 /// [`Replaces::Enclosing`], and the same caveat as
704 /// [`replacing`](Self::replacing): only for a route the description layer
705 /// does not serve.
706 #[must_use]
707 pub fn replacing_enclosing(mut self) -> Self {
708 self.replaces = Some(Replaces::Enclosing);
709 self
710 }
711
712 /// Say this call's effect is contained by nothing on screen.
713 ///
714 /// [`Replaces::Everything`], and the same caveat as
715 /// [`replacing`](Self::replacing): only for a route the description layer
716 /// does not serve. A described route answers
717 /// [`Outcome::Screen`](crate::Outcome::Screen) instead.
718 #[must_use]
719 pub fn invalidating(mut self) -> Self {
720 self.replaces = Some(Replaces::Everything);
721 self
722 }
723
724 /// Keep the answer as a file with this name, rather than showing it.
725 #[must_use]
726 pub fn saving(mut self, filename: impl Into<String>) -> Self {
727 self.saves = Some(filename.into());
728 self
729 }
730
731 /// Say that this call waits on something which resolves once, with nothing
732 /// countable about the wait.
733 ///
734 /// The common case: a round trip to a payment provider, a report the server
735 /// assembles, a heavy local query. The renderer draws it indeterminate,
736 /// because manufacturing a figure for it is the prediction
737 /// [`layout::Awaiting`] refuses.
738 #[must_use]
739 pub const fn awaiting(mut self) -> Self {
740 self.awaiting = Some(layout::Awaiting::unmeasured());
741 self
742 }
743
744 /// Say that it waits, and how much there is to get through.
745 ///
746 /// Only with a measured figure. An upload knows its file length; nothing
747 /// else here may guess one, since a renderer cannot tell a measurement from
748 /// an estimate once it is written down.
749 #[must_use]
750 pub const fn awaiting_amount(mut self, amount: u64) -> Self {
751 self.awaiting = Some(layout::Awaiting::of(amount));
752 self
753 }
754
755 /// Whether this call waits on something.
756 #[must_use]
757 pub const fn awaits(&self) -> bool {
758 self.awaiting.is_some()
759 }
760
761 /// Put this call's answer up in a mount of its own.
762 ///
763 /// See [`elsewhere`](Self::elsewhere) for what a mount is on each host and
764 /// for why a terminal is allowed to ignore it.
765 #[must_use]
766 pub const fn elsewhere(mut self) -> Self {
767 self.elsewhere = true;
768 self
769 }
770
771 /// Performing this replaces the whole document.
772 ///
773 /// See [`navigates`](Self::navigates) for what each renderer does with it,
774 /// and reach for it where the act is a plain "go there": a search result
775 /// standing for a page, a name standing for the profile behind it.
776 #[must_use]
777 pub const fn navigating(mut self) -> Self {
778 self.navigates = true;
779 self
780 }
781
782 /// The host makes this call, not the renderer.
783 ///
784 /// See [`by_host`](Self::by_host) for what it means and for the
785 /// portability it costs. Reach for it only where a renderer genuinely
786 /// cannot make the call, which today is one case: a destination the host
787 /// reaches through a sequence of requests rather than one.
788 #[must_use]
789 pub const fn by_host(mut self) -> Self {
790 self.by_host = true;
791 self
792 }
793
794 /// The route this calls, if it calls one.
795 #[must_use]
796 pub fn route(&self) -> Option<&str> {
797 self.destination.route()
798 }
799
800 /// Send a value along with the call.
801 ///
802 /// On a write this is the payload: what the control is telling the route.
803 /// On a read it is the address, because a read sends nothing and its values
804 /// are where it goes — so this lands in [`Self::carried`] rather than in
805 /// [`Self::params`], and a read's two bags are never both populated.
806 ///
807 /// That is what keeps the rule one sentence at the reading end: a filter is
808 /// in `carried` whichever verb offered it.
809 ///
810 /// A [`Destination::Local`] takes the write's side of that whatever its
811 /// method says. The read rule rests on the values being the address, and a
812 /// local action has no address for them to be: putting them in `carried`
813 /// would file them as the view a call was made under, and nothing is called.
814 /// So they are the payload — which suggestion was picked, which notice was
815 /// dismissed — and that is the bag every renderer reads for one.
816 #[must_use]
817 pub fn with(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
818 if self.method.mutates() || self.destination.is_local() {
819 self.params.insert(name, value);
820 } else {
821 self.carried.insert(name, value);
822 }
823 self
824 }
825
826 /// Keep this control pointed at the view it was offered under.
827 ///
828 /// What a filtered screen puts on every control it draws. Distinct from
829 /// [`Self::with`] on a write, and the same thing as it on a read.
830 #[must_use]
831 pub fn carrying(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
832 self.carried.insert(name, value);
833 self
834 }
835 }
836
837 /// A small labelled thing: a badge, a chip, a tag.
838 ///
839 /// [`Node::Token`]. Extracted because a row can carry these now
840 /// ([`Row::tokens`], against `makeover-layout`'s `RowPart::Tokens`), and the
841 /// alternative was defining the same fields twice and watching them drift.
842 ///
843 /// The tone rides on the tag rather than on whatever holds it, which is what
844 /// lets a strip of them say different things: a neutral type and an amber
845 /// status, side by side in one row.
846 ///
847 /// No `Hash`, because it can hold an [`Action`], which holds [`Params`], which
848 /// is a `Vec`. Same derive set as `Action` for that reason.
849 #[derive(Debug, Clone, PartialEq, Eq)]
850 pub struct Tag {
851 /// Whether it answers a click, and whether it can be removed.
852 pub kind: layout::Token,
853 /// What it says.
854 pub label: String,
855 /// What it is saying.
856 pub tone: layout::Tone,
857 /// Whether it is currently held down. Only meaningful for a chip.
858 pub latched: bool,
859 /// What clicking it calls, if it answers a click.
860 pub action: Option<Action>,
861 /// The detail behind the label, for a renderer that has somewhere to put
862 /// it.
863 ///
864 /// A badge's label is short because a badge is small, and the shipped
865 /// goingson board says the short thing and carries the long one: "Blocked"
866 /// with the block depth behind it, "Unblocks 3" with the wording, "Cycle"
867 /// with the repair instruction. A described card said the label and
868 /// dropped the detail, so the description could not say what the shipped
869 /// markup already did.
870 ///
871 /// Standing detail, not a message: it is true whenever the tag is on
872 /// screen, which is what makes it a property of the tag rather than
873 /// something a response says. **Every renderer may drop it**, and dropping
874 /// is the graceful degradation this vocabulary keeps choosing rather than a
875 /// gap; what each one does is stated in its own docs. Never put anything
876 /// here that is the only place a fact appears.
877 pub hint: Option<String>,
878 }
879
880 impl Tag {
881 /// A neutral badge: it says something and answers nothing.
882 pub fn badge(label: impl Into<String>) -> Self {
883 Self {
884 kind: layout::Token::Badge,
885 label: label.into(),
886 tone: layout::Tone::Neutral,
887 latched: false,
888 action: None,
889 hint: None,
890 }
891 }
892
893 /// A chip that calls a route when clicked.
894 ///
895 /// Not removable. A removable chip is a different control with a different
896 /// affordance, so it says so rather than being inferred from carrying an
897 /// action.
898 pub fn chip(label: impl Into<String>, action: Action) -> Self {
899 Self {
900 kind: layout::Token::Chip { removable: false },
901 label: label.into(),
902 tone: layout::Tone::Neutral,
903 latched: false,
904 action: Some(action),
905 hint: None,
906 }
907 }
908
909 /// A chip the reader can take off again.
910 ///
911 /// The affordance a removable chip has and a plain one does not, said
912 /// rather than inferred, which is the same reason [`chip`](Self::chip) is
913 /// not removable by virtue of carrying an action.
914 pub fn removable(label: impl Into<String>, action: Action) -> Self {
915 Self {
916 kind: layout::Token::Chip { removable: true },
917 ..Self::chip(label, action)
918 }
919 }
920
921 /// Set what it is saying.
922 #[must_use]
923 pub const fn tone(mut self, tone: layout::Tone) -> Self {
924 self.tone = tone;
925 self
926 }
927
928 /// Hold it down. Only meaningful for a chip.
929 #[must_use]
930 pub const fn latched(mut self, latched: bool) -> Self {
931 self.latched = latched;
932 self
933 }
934
935 /// The detail behind the label; see [`hint`](Self::hint).
936 ///
937 /// A renderer with nowhere to put it drops it, so this must never be the
938 /// only place a fact appears.
939 #[must_use]
940 pub fn hinted(mut self, hint: impl Into<String>) -> Self {
941 self.hint = Some(hint.into());
942 self
943 }
944 }
945
946 /// One option offered by a field, owned.
947 ///
948 /// The borrowed original is `makeover-layout`'s [`layout::Choice`]. Two strings
949 /// rather than one for the reason recorded there: the submitted value and the
950 /// read label are different facts, and every renderer that collapsed them has
951 /// had to un-collapse them later.
952 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
953 pub struct Choice {
954 /// What is submitted.
955 pub value: String,
956 /// What is read.
957 pub label: String,
958 /// Why it cannot be picked right now, when it cannot.
959 ///
960 /// The borrowed original is [`layout::Choice::unavailable`], and everything
961 /// it says applies. One member rather than a flag beside a reason, so an
962 /// option greyed out with no explanation stays unsayable here too.
963 pub unavailable: Option<String>,
964 /// The line under the label that says what picking this means.
965 ///
966 /// The borrowed original is [`layout::Choice::detail`], where the
967 /// measurement and the per-host placement live. Here it matters for the
968 /// reason `picks` does not exist on the layout type: an owned mirror is
969 /// what a handler builds, and a tier list built from the database carries a
970 /// price and a description that were never `&'static str`.
971 pub detail: Option<String>,
972 }
973
974 impl Choice {
975 /// An option whose submitted value is also its label.
976 pub fn plain(value: impl Into<String>) -> Self {
977 let value = value.into();
978 Self {
979 label: value.clone(),
980 value,
981 unavailable: None,
982 detail: None,
983 }
984 }
985
986 /// An option that reads differently from what it submits.
987 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
988 Self {
989 value: value.into(),
990 label: label.into(),
991 unavailable: None,
992 detail: None,
993 }
994 }
995
996 /// The same option, not pickable yet, and why.
997 #[must_use]
998 pub fn unless(mut self, reason: impl Into<String>) -> Self {
999 self.unavailable = Some(reason.into());
1000 self
1001 }
1002
1003 /// The same option, with the line that says what picking it means.
1004 ///
1005 /// See [`detail`](Self::detail). Two different sentences from
1006 /// [`unless`](Self::unless), and an option carrying both has said two
1007 /// things: what the tier is, and that it is not available yet.
1008 #[must_use]
1009 pub fn detailing(mut self, detail: impl Into<String>) -> Self {
1010 self.detail = Some(detail.into());
1011 self
1012 }
1013
1014 /// Whether the option can be picked right now.
1015 #[must_use]
1016 pub const fn available(&self) -> bool {
1017 self.unavailable.is_none()
1018 }
1019
1020 /// Borrow as the description layer's own type.
1021 #[must_use]
1022 pub fn as_layout(&self) -> layout::Choice<'_> {
1023 let mut choice = layout::Choice::new(&self.value, &self.label);
1024 if let Some(detail) = self.detail.as_deref() {
1025 choice = choice.detailing(detail);
1026 }
1027 match self.unavailable.as_deref() {
1028 Some(reason) => choice.unless(reason),
1029 None => choice,
1030 }
1031 }
1032 }
1033
1034 /// One theme a picker offers, owned.
1035 ///
1036 /// The borrowed original is `makeover-layout`'s [`layout::ThemeChoice`], and
1037 /// the reason it is not [`Choice`] is recorded there: a theme is four facts and
1038 /// an option is two. The two extra ones — which group it sits in and how
1039 /// legible it measured — are resolved by the theme layer and neither survives
1040 /// being written into a label.
1041 ///
1042 /// Built from `makeover::ThemeOption` at each adopter. That conversion is the
1043 /// seam the layering costs: `quasi-router` takes only `makeover-layout`, which
1044 /// takes nothing at all, so the crate that reads theme files off disk is not in
1045 /// this graph and the app is what joins them.
1046 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1047 pub struct ThemeChoice {
1048 /// The id stored, and what the picker submits.
1049 pub id: String,
1050 /// What the picker reads.
1051 pub name: String,
1052 /// Which group it belongs to.
1053 pub variant: layout::ThemeVariant,
1054 /// How legible its muted text measured.
1055 pub contrast: layout::Contrast,
1056 }
1057
1058 impl ThemeChoice {
1059 /// A theme, with everything a picker needs to place and mark it.
1060 ///
1061 /// Every fact is an argument, matching [`layout::ThemeChoice::new`] and for
1062 /// its reason: a theme with no variant has no group and a theme with no
1063 /// tier has no badge, so both are the control rather than trimmings on it.
1064 pub fn new(
1065 id: impl Into<String>,
1066 name: impl Into<String>,
1067 variant: layout::ThemeVariant,
1068 contrast: layout::Contrast,
1069 ) -> Self {
1070 Self {
1071 id: id.into(),
1072 name: name.into(),
1073 variant,
1074 contrast,
1075 }
1076 }
1077
1078 /// Borrow as the description layer's own type.
1079 #[must_use]
1080 pub fn as_layout(&self) -> layout::ThemeChoice<'_> {
1081 layout::ThemeChoice::new(&self.id, &self.name, self.variant, self.contrast)
1082 }
1083 }
1084
1085 /// One entry in a field's suggestion list, owned.
1086 ///
1087 /// The borrowed original is `makeover-layout`'s [`layout::Candidate`], and the
1088 /// reason it is not [`Choice`] is recorded there: an option and a candidate are
1089 /// submitted the same way and **read differently**. An option is picked out of
1090 /// a set the user can see whole; a candidate is offered out of a set nobody can
1091 /// see, so it carries the line that tells it from its neighbours.
1092 ///
1093 /// # What this mirror carries that the borrowed type cannot
1094 ///
1095 /// [`picks`](Self::picks). What happens when a candidate is chosen is an
1096 /// [`Action`], and `Action` is not a word the description layer has — exactly
1097 /// as [`Field::suggests`] has no counterpart on [`layout::Field`]. So the
1098 /// member lives here, on the same terms and for the same reason.
1099 /// No `Hash`, unlike [`Choice`]: [`Action`] is not hashable and a candidate
1100 /// carrying one could not be. Nothing in the tree hashes a suggestion row.
1101 #[derive(Debug, Clone, PartialEq, Eq)]
1102 pub struct Candidate {
1103 /// What is submitted, and what picking writes into the field by default.
1104 pub value: String,
1105 /// What is read.
1106 pub label: String,
1107 /// The second line: what orients this candidate among rows that read alike.
1108 ///
1109 /// The borrowed original is [`layout::Candidate::detail`]. [`None`] draws
1110 /// one line rather than an empty second one.
1111 pub detail: Option<String>,
1112 /// What picking this candidate does instead of writing its value.
1113 ///
1114 /// **Picking is local by default, not by definition.** Absent an action,
1115 /// picking writes
1116 /// [`value`](Self::value) into the field that owns the list, which is what
1117 /// every site that exists today wants and what ownership buys. A candidate
1118 /// that carries one has that performed instead.
1119 ///
1120 /// The two measured sites are why. MNW's search box navigates — each
1121 /// candidate is a project, item or creator page, nothing is written into
1122 /// the box, and the typed value is discarded — so its candidates carry
1123 /// `Action::get(url)`. MNW's tag box adds a facet and re-reads, which is
1124 /// the same route its drill-down checkbox already calls, so the handover
1125 /// `choose()` in hand-written JS disappears rather than moving. Neither
1126 /// wants the one behaviour ownership gives for free.
1127 ///
1128 /// It is on this type rather than on [`Choice`] deliberately. `Choice` is
1129 /// the most-consumed struct in the vocabulary and a per-row action there
1130 /// would land on every option list in the tree the day it shipped. Confined
1131 /// here, the objection this raises against itself — that the route decides
1132 /// where a pick goes, per row, which is more than a description says about
1133 /// any other control — is confined with it.
1134 pub picks: Option<Action>,
1135 }
1136
1137 impl Candidate {
1138 /// A candidate whose submitted value is also its label.
1139 pub fn plain(value: impl Into<String>) -> Self {
1140 let value = value.into();
1141 Self {
1142 label: value.clone(),
1143 value,
1144 detail: None,
1145 picks: None,
1146 }
1147 }
1148
1149 /// A candidate that reads differently from what it submits.
1150 pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
1151 Self {
1152 value: value.into(),
1153 label: label.into(),
1154 detail: None,
1155 picks: None,
1156 }
1157 }
1158
1159 /// The same candidate, with the line that tells it from its neighbours.
1160 #[must_use]
1161 pub fn detailed(mut self, detail: impl Into<String>) -> Self {
1162 self.detail = Some(detail.into());
1163 self
1164 }
1165
1166 /// The same candidate, picking it performs this instead of writing.
1167 #[must_use]
1168 pub fn picking(mut self, action: Action) -> Self {
1169 self.picks = Some(action);
1170 self
1171 }
1172
1173 /// Borrow as the description layer's own type.
1174 ///
1175 /// [`picks`](Self::picks) does not survive the borrow, and cannot: the
1176 /// description layer has no [`Action`]. A renderer reading a candidate
1177 /// through this sees the row and not what picking it does, so a renderer
1178 /// that performs picks reads this type rather than the borrowed one.
1179 #[must_use]
1180 pub fn as_layout(&self) -> layout::Candidate<'_> {
1181 let candidate = layout::Candidate::new(&self.value, &self.label);
1182 match self.detail.as_deref() {
1183 Some(detail) => candidate.detailed(detail),
1184 None => candidate,
1185 }
1186 }
1187 }
1188
1189 /// One entry in a file field's accept list, owned.
1190 ///
1191 /// The borrowed original is [`layout::Accepted`], and everything it says
1192 /// applies: three shapes because all three are in the measured sites, and a
1193 /// suffix names no family because a suffix-to-family table rots. This is the
1194 /// same split [`Choice`] makes, and for the same reason — a screen is built by
1195 /// a handler and outlives the strings it was built from.
1196 ///
1197 /// [`layout::Family`] is used directly rather than mirrored. It borrows nothing,
1198 /// so there is no owned counterpart to write and a second spelling would only be
1199 /// a second place to add the fourth family to.
1200 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1201 #[non_exhaustive]
1202 pub enum Accepted {
1203 /// Every file of a family: `image/*` and its siblings.
1204 Family(layout::Family),
1205 /// One media type: `image/jpeg`, `text/csv`.
1206 Type(String),
1207 /// One file-name suffix, with its leading dot: `.zip`, `.tar.gz`.
1208 Suffix(String),
1209 }
1210
1211 impl Accepted {
1212 /// Every file of a family.
1213 #[must_use]
1214 pub const fn family(family: layout::Family) -> Self {
1215 Self::Family(family)
1216 }
1217
1218 /// One media type.
1219 #[must_use]
1220 pub fn media_type(media_type: impl Into<String>) -> Self {
1221 Self::Type(media_type.into())
1222 }
1223
1224 /// One file-name suffix, written with its leading dot.
1225 #[must_use]
1226 pub fn suffix(suffix: impl Into<String>) -> Self {
1227 Self::Suffix(suffix.into())
1228 }
1229
1230 /// Borrow as the description layer's own type.
1231 #[must_use]
1232 pub fn as_layout(&self) -> layout::Accepted<'_> {
1233 match self {
1234 Self::Family(family) => layout::Accepted::Family(*family),
1235 Self::Type(media_type) => layout::Accepted::Type(media_type),
1236 Self::Suffix(suffix) => layout::Accepted::Suffix(suffix),
1237 }
1238 }
1239 }
1240
1241 /// A picture and where it is.
1242 ///
1243 /// One type, since makeover-layout 0.42.0. The borrowed twin this used to
1244 /// mirror carried the same five fields and a single method, had no consumer
1245 /// anywhere in the makeover suite, and was built in exactly one place: here.
1246 /// What survives of that split is the rule which produced it, and [`Act`] still
1247 /// carries it: makeover-layout defers every address, so [`src`](Self::src) is
1248 /// this crate's and a renderer wanting the bytes comes here for them.
1249 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1250 pub struct Image {
1251 /// Where the picture is. A URL, or whatever the host resolves.
1252 ///
1253 /// Never interpreted here. A renderer escapes it for wherever it is putting
1254 /// it, the way it does every other app-supplied string.
1255 pub src: String,
1256 /// What the picture says, for anything not showing it.
1257 ///
1258 /// Empty means decorative. `layout::Image` carries the argument for why
1259 /// this is a `String` and not an `Option<String>`.
1260 pub alt: String,
1261 /// A visible line under it, where the app wants one.
1262 pub caption: Option<String>,
1263 /// How it sits in the box it is given.
1264 pub fit: layout::Fit,
1265 /// The picture's own dimensions, where the app knows them.
1266 ///
1267 /// `layout::Image::intrinsic` carries the argument. The short form: without
1268 /// it a renderer cannot hold the picture's place, so the picture takes no
1269 /// room until it arrives and then shoves the page down.
1270 pub intrinsic: Option<layout::Extent>,
1271 /// Whether the picture is needed with the screen, or can arrive later.
1272 pub loading: layout::Loading,
1273 }
1274
1275 impl Image {
1276 /// A picture at a source, carrying its own proportions.
1277 pub fn new(src: impl Into<String>, alt: impl Into<String>) -> Self {
1278 Self {
1279 src: src.into(),
1280 alt: alt.into(),
1281 caption: None,
1282 fit: layout::Fit::Natural,
1283 intrinsic: None,
1284 loading: layout::Loading::Eager,
1285 }
1286 }
1287
1288 /// The picture's own dimensions, so a renderer can hold its place.
1289 #[must_use]
1290 pub const fn intrinsic(mut self, width: u32, height: u32) -> Self {
1291 self.intrinsic = Some(layout::Extent::new(width, height));
1292 self
1293 }
1294
1295 /// This picture is not on screen yet; it can arrive when it is near.
1296 #[must_use]
1297 pub const fn lazy(mut self) -> Self {
1298 self.loading = layout::Loading::Lazy;
1299 self
1300 }
1301
1302 /// A visible line under it.
1303 #[must_use]
1304 pub fn caption(mut self, caption: impl Into<String>) -> Self {
1305 self.caption = Some(caption.into());
1306 self
1307 }
1308
1309 /// How it sits in its box.
1310 #[must_use]
1311 pub const fn fit(mut self, fit: layout::Fit) -> Self {
1312 self.fit = fit;
1313 self
1314 }
1315
1316 /// Whether the alt text says anything.
1317 ///
1318 /// An empty `alt` is a claim that the picture adds nothing to the text
1319 /// beside it, so a renderer that cannot show the bytes draws nothing rather
1320 /// than standing in for it. Came off `layout::Image` when the two merged in
1321 /// makeover-layout 0.42.0.
1322 #[must_use]
1323 pub fn speaks(&self) -> bool {
1324 !self.alt.is_empty()
1325 }
1326 }
1327
1328 /// One figure with a caption, owned.
1329 ///
1330 /// The borrowed original is [`layout::Figure`], and everything it says applies:
1331 /// the value is text because only the app knows whether the number is a
1332 /// percentage, a duration or a ratio, and the tone is carried because no
1333 /// renderer can work out that a streak of zero is worth colouring.
1334 ///
1335 /// What it calls, if it calls anything, is not here. That is an address, which
1336 /// `makeover-layout` never names, and it rides beside the figure in
1337 /// [`Node::Stats`] the way [`Row::activate`] rides beside a row's parts.
1338 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1339 pub struct Figure {
1340 /// The number, formatted the way the app means it to read.
1341 pub value: String,
1342 /// What it counts. The caption under the value.
1343 pub caption: String,
1344 /// How the value has moved, if the app is tracking that.
1345 ///
1346 /// Mirrors `layout::Figure::change`, added there at 0.13.0. Text for the
1347 /// same reason [`value`](Self::value) is: only the app knows whether a move
1348 /// reads as `+12.5%`, `+3` or `2x`.
1349 ///
1350 /// This is what [`tone`](Self::tone) was for. Counted before adding it: the
1351 /// MNW server has four screens whose stat card is a label, a value and a
1352 /// delta, and on all four the delta is the toned part while the number
1353 /// itself is an ordinary fact. Without it the delta folds into the caption,
1354 /// which loses the tone and turns a second smaller line into a longer first
1355 /// one.
1356 pub change: Option<String>,
1357 /// What the figure means. [`layout::Tone::Neutral`] is an ordinary fact.
1358 ///
1359 /// Applies to [`change`](Self::change) where there is one, and to the value
1360 /// where there is not. The renderer decides which element that lands on.
1361 pub tone: layout::Tone,
1362 }
1363
1364 impl Figure {
1365 /// A figure that is an ordinary fact.
1366 pub fn new(value: impl Into<String>, caption: impl Into<String>) -> Self {
1367 Self {
1368 value: value.into(),
1369 caption: caption.into(),
1370 change: None,
1371 tone: layout::Tone::Neutral,
1372 }
1373 }
1374
1375 /// How the value has moved.
1376 #[must_use]
1377 pub fn change(mut self, change: impl Into<String>) -> Self {
1378 self.change = Some(change.into());
1379 self
1380 }
1381
1382 /// What the figure means.
1383 #[must_use]
1384 pub const fn tone(mut self, tone: layout::Tone) -> Self {
1385 self.tone = tone;
1386 self
1387 }
1388
1389 /// Borrow as the description layer's own type.
1390 #[must_use]
1391 pub fn as_layout(&self) -> layout::Figure<'_> {
1392 layout::Figure {
1393 value: &self.value,
1394 caption: &self.caption,
1395 change: self.change.as_deref(),
1396 tone: self.tone,
1397 }
1398 }
1399 }
1400
1401 /// What a list has that it is not showing.
1402 ///
1403 /// Deliberately not virtual scrolling, which is the neighbouring thing and is
1404 /// not a description concern: goingson's `virtual-scroller.js` windows rows the
1405 /// app already holds, which is a renderer performance technique. This is a fact
1406 /// about the data — there are rows that were never fetched — and only the thing
1407 /// that fetched them knows it.
1408 /// # The position is the description layer's, the addresses are this one's
1409 ///
1410 /// [`layout::Paging`] holds where the reader is and how big the set is, and it
1411 /// is the same [`layout::Window`] a carousel instantiates. What it cannot hold
1412 /// is the way to ask for the next part, because `makeover-layout` names no
1413 /// actions at all. So this is the pairing, the way [`Row`] pairs its parts with
1414 /// [`Row::activate`] and [`Node::Stats`] pairs a figure with an address.
1415 ///
1416 /// That split is the reason the two can share an implementation. A carousel's
1417 /// frames are already in the description and moving between them asks nobody
1418 /// anything; a page's rows were never fetched and moving costs a round trip.
1419 /// Presence is the difference, and it shows up here as whether there are
1420 /// actions rather than as a second copy of the arithmetic.
1421 ///
1422 /// # Both directions, and why neither is required
1423 ///
1424 /// A host that pages forward only supplies [`forward`](Self::forward) alone and
1425 /// gets a load-more control. One that pages both ways supplies both and gets
1426 /// prev/next. A renderer offers what it was given and never invents the other,
1427 /// because an address this crate made up would not resolve.
1428 #[derive(Debug, Clone, PartialEq, Eq)]
1429 pub struct Rest {
1430 /// Where the reader is, and how much there is.
1431 pub paging: layout::Paging,
1432 /// What asking for the next part calls.
1433 pub forward: Option<Action>,
1434 /// What asking for the previous part calls.
1435 pub back: Option<Action>,
1436 /// The pages a reader may go straight to, each with its own address.
1437 ///
1438 /// Empty is the common case and is prev/next paging.
1439 ///
1440 /// # An address per page, not a count
1441 ///
1442 /// A renderer cannot build page 5's address out of
1443 /// [`forward`](Self::forward) and [`back`](Self::back) without knowing the
1444 /// address grammar, and that grammar is the private vocabulary a conversion
1445 /// exists to retire. So the router names the addresses and the renderer
1446 /// draws them, which is the split [`Row::activate`] and [`Node::Stats`]
1447 /// already make. A number alone would put URL construction in three hosts.
1448 ///
1449 /// # Which pages, and why that is the description's call too
1450 ///
1451 /// A set of 400 pages is not a strip of 400 controls, so somebody windows
1452 /// it, and it is not this crate: MNW's `build_pagination_range` already
1453 /// windows to five around the reader, and a renderer inventing its own
1454 /// would give a different answer per host for one list. What arrives here
1455 /// is the pages being offered, in the order they should read.
1456 ///
1457 /// Which of them the reader is on is not stated twice:
1458 /// [`paging`](Self::paging) says it, and a renderer marks the jump whose
1459 /// [`Jump::page`] matches.
1460 pub jumps: Vec<Jump>,
1461 }
1462
1463 /// One page a reader can go straight to.
1464 ///
1465 /// The number is carried rather than implied by position, because what a strip
1466 /// offers is a window around the reader -- pages 8 through 12 of 20 -- and an
1467 /// index into that list is not the page it names.
1468 #[derive(Debug, Clone, PartialEq, Eq)]
1469 pub struct Jump {
1470 /// Which page this reaches, counting from one, the way
1471 /// [`layout::Paging::page`] counts.
1472 pub page: usize,
1473 /// What going there calls.
1474 pub action: Action,
1475 }
1476
1477 impl Jump {
1478 /// Going to this page calls this route.
1479 #[must_use]
1480 pub const fn new(page: usize, action: Action) -> Self {
1481 Self { page, action }
1482 }
1483 }
1484
1485 impl Rest {
1486 /// The first `shown` of something longer, and how to ask for more.
1487 ///
1488 /// The load-more shape. The window starts at the beginning and grows, so
1489 /// there is no page to number and no way back to offer.
1490 #[must_use]
1491 pub const fn more(shown: usize, action: Action) -> Self {
1492 Self {
1493 paging: layout::Paging::more(shown),
1494 forward: Some(action),
1495 back: None,
1496 jumps: Vec::new(),
1497 }
1498 }
1499
1500 /// The first `shown` of something longer, with nothing to press.
1501 ///
1502 /// [`more`](Self::more) without the address, which is a real shape rather
1503 /// than a degenerate one: audiofiles' bulk-rename preview caps its table at
1504 /// fifty rows because fifty is all a modal can show, and the rename acts on
1505 /// every name whether or not it was drawn. There is nowhere to ask for the
1506 /// rest because the rest were never missing -- the cap is a rendering
1507 /// budget, and what the reader needs to know is that there are more than
1508 /// these.
1509 ///
1510 /// A constructor and not a struct literal, which is what those two sites
1511 /// were: a literal names every field, so it breaks on each one this type
1512 /// gains, and it broke on `jumps`.
1513 #[must_use]
1514 pub const fn showing(shown: usize) -> Self {
1515 Self {
1516 paging: layout::Paging::more(shown),
1517 forward: None,
1518 back: None,
1519 jumps: Vec::new(),
1520 }
1521 }
1522
1523 /// One page of `per`, starting at `from`.
1524 ///
1525 /// Arrives with no addresses; [`forward`](Self::forward) and
1526 /// [`back`](Self::back) add whichever of them exists. A first page has no
1527 /// back and a last page has no forward, and saying so by leaving one off is
1528 /// how a renderer knows to draw the control disabled rather than absent.
1529 #[must_use]
1530 pub const fn page(from: usize, per: usize) -> Self {
1531 Self {
1532 paging: layout::Paging::pages(from, per),
1533 forward: None,
1534 back: None,
1535 jumps: Vec::new(),
1536 }
1537 }
1538
1539 /// How many there are altogether.
1540 ///
1541 /// Left unsaid by a host that will not pay for the count, and then left
1542 /// unsaid for good: a total arriving on a later pass widens the text that
1543 /// prints it. See "First paint is final paint" in `makeover-layout`'s
1544 /// header, and "What a handler owes the first paint" in this crate's.
1545 #[must_use]
1546 pub const fn of(mut self, total: usize) -> Self {
1547 self.paging = self.paging.of(total);
1548 self
1549 }
1550
1551 /// What asking for the next part calls.
1552 #[must_use]
1553 pub fn forward(mut self, action: Action) -> Self {
1554 self.forward = Some(action);
1555 self
1556 }
1557
1558 /// What asking for the previous part calls.
1559 #[must_use]
1560 pub fn back(mut self, action: Action) -> Self {
1561 self.back = Some(action);
1562 self
1563 }
1564
1565 /// Offer a jump straight to this page.
1566 ///
1567 /// See [`jumps`](Self::jumps). Adds rather than replaces, because a strip
1568 /// is built one page at a time out of whatever window the host chose, and
1569 /// the order of the calls is the order they read.
1570 #[must_use]
1571 pub fn jumping(mut self, page: usize, action: Action) -> Self {
1572 self.jumps.push(Jump::new(page, action));
1573 self
1574 }
1575
1576 /// Whether one of the offered jumps is the page the reader is on.
1577 ///
1578 /// Asked by every renderer that draws a strip, so the comparison is here
1579 /// rather than three times: a host that windowed its pages badly can offer
1580 /// a strip the reader is not in, and each renderer answering that for
1581 /// itself is how they come to disagree about what "current" means.
1582 #[must_use]
1583 pub fn is_here(&self, jump: &Jump) -> bool {
1584 self.paging.page() == Some(jump.page)
1585 }
1586
1587 /// Borrow as the description layer's own type.
1588 #[must_use]
1589 pub const fn as_layout(&self) -> layout::Paging {
1590 self.paging
1591 }
1592 }
1593
1594 /// Prose in a row part: what it says, and whether it is markdown.
1595 ///
1596 /// `secondary` has always been a `String`, and three call sites had markdown to
1597 /// put in it: the goingson projects card's description, the mail list's body
1598 /// preview, and a contact's note next. Each put the **source** in, so a row read
1599 /// `**Ships Q3.** See [the brief](https://...)` where the screen it stands in
1600 /// for reads the sentence. Flattening at the call site fixes what the user sees
1601 /// and loses the fact on the way: a renderer receiving the row cannot tell text
1602 /// an author typed from markdown somebody already flattened, so it cannot decide
1603 /// for itself, and the flattening is copied per site.
1604 ///
1605 /// This is [`Meter`]'s answer, not [`Node`]'s. The row still holds no node it
1606 /// holds a two-case value saying which of two things its string is. A webview
1607 /// renders the markdown inline, a terminal can emit bold, and a renderer that
1608 /// wants neither flattens it, each from the same description.
1609 ///
1610 /// [`Text`](Self::Text) is the default in every sense: `From<&str>` and
1611 /// `From<String>` both produce it, so `.secondary("...")` means what it always
1612 /// meant and no existing call site changes.
1613 #[derive(Debug, Clone, PartialEq, Eq)]
1614 pub enum Prose {
1615 /// Text as written. A renderer escapes it and draws it, and nothing in it
1616 /// is markup however it is punctuated.
1617 Text(String),
1618 /// Markdown source, carried as source for the reason [`Node::Rich`] does:
1619 /// every renderer has an honest answer because each renders it its own way,
1620 /// and nothing here is markup a renderer has to trust.
1621 Rich(String),
1622 }
1623
1624 /// How much of the markdown format a source may use.
1625 ///
1626 /// One of the two axes [`Node::Rich`] carries, and the one about *shape*.
1627 /// [`Trust`] is the other, about *provenance*. They correlate -- the sources an
1628 /// app does not vouch for are usually the ones it also wants to keep plain --
1629 /// and they are not the same question, which is why fusing them was a mistake
1630 /// worth undoing: a platform's own policy page is prose it wrote and wants
1631 /// followed, and a creator's long-form description is a document it did not
1632 /// write and still wants tables in.
1633 ///
1634 /// Nothing here says *phrase*. One line of markdown inside a row is decided by
1635 /// where the node sits rather than by what it declares: a row part holds no
1636 /// node, and the run a cell holds is rendered inline whatever this says. See
1637 /// the renderer's row handling.
1638 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1639 pub enum Richness {
1640 /// Paragraphs, lists, emphasis, code and links.
1641 ///
1642 /// The default, and what ordinary prose needs. No tables, no footnotes, no
1643 /// task lists, no images -- a page's own sentence does not want them and a
1644 /// stranger's paragraph should not have them by default.
1645 #[default]
1646 Sentence,
1647 /// Everything the format has: tables, task lists, footnotes, strikethrough,
1648 /// smart punctuation and images.
1649 ///
1650 /// For a source that is a document rather than a sentence. A creator's
1651 /// long-form item description is the measured consumer.
1652 Document,
1653 }
1654
1655 /// How far the app vouches for a source.
1656 ///
1657 /// [`Node::Rich`]'s other axis. See [`Richness`] for why they are two.
1658 ///
1659 /// **The default is [`Untrusted`](Self::Untrusted)**, written out rather than
1660 /// derived, for the reason `Discovery::indexable` is: the unsafe direction has
1661 /// to be the one somebody types. A description that said nothing about
1662 /// provenance and got the permissive treatment would be a hole nobody could see
1663 /// in a diff.
1664 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1665 pub enum Trust {
1666 /// Somebody the app does not vouch for wrote it.
1667 ///
1668 /// A renderer hardens it: links get `nofollow`, raw markup in the source is
1669 /// dropped rather than passed through, and schemes a host would fetch are
1670 /// filtered. Forum posts, item descriptions, anything a reader typed.
1671 #[default]
1672 Untrusted,
1673 /// The app wrote it.
1674 ///
1675 /// The screen's own copy, in the same repository as the screen. A renderer
1676 /// takes it at its word: its links are the app's own and are followed, and
1677 /// its markup is the app's own.
1678 ///
1679 /// This is not a claim about the *reader*; it is a claim about the author.
1680 /// A string that reached the screen from a database is untrusted however
1681 /// well-behaved it has been.
1682 Trusted,
1683 }
1684
1685 impl Prose {
1686 /// Markdown, to be rendered by whoever draws it.
1687 pub fn rich(source: impl Into<String>) -> Self {
1688 Self::Rich(source.into())
1689 }
1690
1691 /// The string, whichever case this is.
1692 ///
1693 /// For a renderer that treats both the same, and for a test that does not
1694 /// care. A renderer that draws this without looking at the case is drawing
1695 /// markdown as text, which is the bug this type exists to make visible
1696 /// rather than impossible.
1697 #[must_use]
1698 pub fn source(&self) -> &str {
1699 match self {
1700 Self::Text(text) | Self::Rich(text) => text,
1701 }
1702 }
1703
1704 /// Whether there is anything to draw.
1705 #[must_use]
1706 pub fn is_empty(&self) -> bool {
1707 self.source().is_empty()
1708 }
1709 }
1710
1711 impl From<String> for Prose {
1712 fn from(text: String) -> Self {
1713 Self::Text(text)
1714 }
1715 }
1716
1717 impl From<&str> for Prose {
1718 fn from(text: &str) -> Self {
1719 Self::Text(text.to_owned())
1720 }
1721 }
1722
1723 impl From<&String> for Prose {
1724 fn from(text: &String) -> Self {
1725 Self::Text(text.clone())
1726 }
1727 }
1728
1729 /// How much of a set is done, owned.
1730 ///
1731 /// The borrowed original is [`layout::Meter`], which arrived at 0.10.0 for this.
1732 /// Before it, a screen with a progress bar concatenated the two numbers into its
1733 /// heading — "Subtasks 3/7" — which keeps both facts and loses the reading, the
1734 /// same way a toned status badge read as prose before [`Row::tokens`].
1735 ///
1736 /// Its own struct as of 0.11.0, having been the inline payload of
1737 /// [`Node::Meter`]. Extracted for the reason [`Tag`] was: a row can carry one
1738 /// now ([`Row::meter`], against `makeover-layout`'s `RowPart::Proportion`), and
1739 /// the alternative was defining the same four fields twice and watching them
1740 /// drift.
1741 ///
1742 /// Carries the pair rather than a percentage for the reason [`layout::Meter`]
1743 /// gives: a bar that is full because it landed exactly and one that is full
1744 /// because it ran over are the same width and not the same fact.
1745 ///
1746 /// This carries a proportion, and one answer never carries motion. That is the
1747 /// whole of the restriction: **a meter does not tick**, so no renderer animates
1748 /// one between answers.
1749 ///
1750 /// The proportion need not be of a static set. Files written of files to write
1751 /// and subtasks done of subtasks are, and audiofiles' transport is not: it
1752 /// states a playback position against a duration, re-answered as the host
1753 /// redraws. That is the same member under the same rule, and the doc used to
1754 /// call the fact static, which was true of the first consumers and never of the
1755 /// restriction.
1756 ///
1757 /// Live progress is described by re-answering. Each answer states the
1758 /// proportion as it stood when the route was asked, and the host asks again
1759 /// through its runtime's `reload`, which is where the cadence belongs: how
1760 /// often a fact goes stale is a property of the app holding it. So an export
1761 /// reporting files written is a meter, re-answered, rather than something a
1762 /// description was refusing.
1763 ///
1764 /// What still has no proportion to state gets [`layout::Readiness::Pending`],
1765 /// and what has finished and wants saying gets a [`layout::Notice::Toast`].
1766 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
1767 pub struct Meter {
1768 /// How much is done. May exceed [`total`](Self::total).
1769 pub done: u32,
1770 /// How much there is to do.
1771 pub total: u32,
1772 /// What the proportion means. No renderer can derive this.
1773 pub tone: layout::Tone,
1774 /// What is being counted: "subtasks", "tasks". The noun, not the ratio.
1775 pub label: Option<String>,
1776 }
1777
1778 impl Meter {
1779 /// A proportion, untoned and unlabelled.
1780 #[must_use]
1781 pub const fn new(done: u32, total: u32) -> Self {
1782 Self {
1783 done,
1784 total,
1785 tone: layout::Tone::Neutral,
1786 label: None,
1787 }
1788 }
1789
1790 /// What the proportion means.
1791 #[must_use]
1792 pub const fn tone(mut self, tone: layout::Tone) -> Self {
1793 self.tone = tone;
1794 self
1795 }
1796
1797 /// What is being counted. The noun, not the ratio.
1798 #[must_use]
1799 pub fn label(mut self, label: impl Into<String>) -> Self {
1800 self.label = Some(label.into());
1801 self
1802 }
1803
1804 /// Borrow as the description layer's own type.
1805 #[must_use]
1806 pub fn as_layout(&self) -> layout::Meter<'_> {
1807 layout::Meter {
1808 done: self.done,
1809 total: self.total,
1810 tone: self.tone,
1811 label: self.label.as_deref(),
1812 }
1813 }
1814 }
1815
1816 /// How a slider's position becomes its value, and how finely it moves.
1817 ///
1818 /// The borrowed original is [`layout::Curve`], and the reasoning lives there:
1819 /// the data of a slider is a fraction and a function taking numbers to numbers,
1820 /// so [`Field::min`] and [`Field::max`] are `f(0)` and `f(1)` rather than the
1821 /// control's extent. This is the owned mirror, holding its step as a `String`
1822 /// for the reason every other member here does.
1823 #[derive(Debug, Clone, PartialEq, Eq)]
1824 #[non_exhaustive]
1825 pub enum Curve {
1826 /// Constant slope. The default, and what every described range meant before
1827 /// the curve existed.
1828 Linear {
1829 /// The granularity, in the value's own units.
1830 step: Option<String>,
1831 },
1832 /// Constant ratio, for an extent spanning orders of magnitude.
1833 Logarithmic {
1834 /// The granularity, in the value's own units.
1835 step: Option<String>,
1836 },
1837 }
1838
1839 impl Default for Curve {
1840 fn default() -> Self {
1841 Self::Linear { step: None }
1842 }
1843 }
1844
1845 impl Curve {
1846 /// Read this curve as the description layer's own type.
1847 #[must_use]
1848 pub fn as_layout(&self) -> layout::Curve<'_> {
1849 match self {
1850 Self::Linear { step } => layout::Curve::Linear {
1851 step: step.as_deref(),
1852 },
1853 Self::Logarithmic { step } => layout::Curve::Logarithmic {
1854 step: step.as_deref(),
1855 },
1856 }
1857 }
1858
1859 /// The same curve with its granularity replaced.
1860 #[must_use]
1861 pub fn with_step(self, step: Option<String>) -> Self {
1862 match self {
1863 Self::Linear { .. } => Self::Linear { step },
1864 Self::Logarithmic { .. } => Self::Logarithmic { step },
1865 }
1866 }
1867 }
1868
1869 /// A question answered zero or more times, with the reader adding and removing
1870 /// the slots.
1871 ///
1872 /// **A repeating group enters the vocabulary, submitting once.** Every other
1873 /// member of [`layout::FieldKind`] is one field holding one value or one
1874 /// choice, and nothing said "this question is answered N times". A description
1875 /// that needs one says it here, and every renderer draws the slots, the
1876 /// control that adds one and the control that takes one away.
1877 ///
1878 /// # What it is not
1879 ///
1880 /// Not a list of forms, which is N submits. This is one submit
1881 /// carrying N values under one question, which is what makes the two hard parts
1882 /// hard: the names have to come back apart, and a refusal has to be able to say
1883 /// *which* answer is wrong.
1884 ///
1885 /// Not [`Field::multiple`], which was the near miss and was rejected in the
1886 /// same ruling. That is [`layout::FieldKind::File`]'s pick-several and a
1887 /// multi-select: one control taking a set, with one value, one error and no
1888 /// slots for the reader to add. Widening it would have put two shapes behind
1889 /// one word and left every renderer disambiguating them from the kind beside
1890 /// it.
1891 ///
1892 /// # The names on the wire
1893 ///
1894 /// `name[0]`, `name[1]`, and [`Repeat::at`] is the only place that is spelled.
1895 /// Picked once here rather than per renderer, because the three of them have to
1896 /// agree with each other and with whatever reads the submission back:
1897 /// [`Params::repeated`](crate::Params::repeated) is that reader.
1898 ///
1899 /// The index rather than N values under one bare name, which the wire already
1900 /// allows and [`Params::get_all`](crate::Params::get_all) already reads. An
1901 /// index survives a slot the reader emptied and a slot a host dropped: an error
1902 /// reported against the third answer means the third box on the way back
1903 /// whatever happened to the second, where a positional list renumbers itself
1904 /// silently and attaches the message to a different value.
1905 ///
1906 /// Holes are legitimate for the same reason and every reader here tolerates
1907 /// them: a browser removing the second of three slots may leave `0` and `2`
1908 /// standing rather than renumbering, and the answers are still the answers.
1909 ///
1910 /// # One consumer, which is enough
1911 ///
1912 /// goingson's event form, whose `Event.reminder_offsets_seconds` is a
1913 /// `Vec<i64>` capped at eight by `sanitize_reminder_offsets`. Nothing else in
1914 /// the described screens submits a variable-length set under one question.
1915 ///
1916 /// # A slot is one question, or several named ones
1917 ///
1918 /// One question is the ordinary slot and the one goingson's reminders use:
1919 /// [`Instance::value`] is the answer and [`Instance::error`] is what is wrong
1920 /// with it.
1921 ///
1922 /// Several is [`Instance::parts`], and it exists because a picked-file queue is
1923 /// a slot that is a name, a size and a failure of its own. The growth this
1924 /// type's header left open is the one that was taken: `name[0].size` beside
1925 /// `name[0]`, so a slot that is one question submits exactly as it always did.
1926 /// [`Answer`] is one of the several and [`Progress`] is the per-slot status,
1927 /// which is the half [`Repeating`] deliberately does not carry.
1928 #[derive(Debug, Clone, PartialEq, Eq, Default)]
1929 pub struct Repeat {
1930 /// The answers standing right now, in the order they are asked.
1931 ///
1932 /// Empty is a question nobody has answered yet, which is what "zero or
1933 /// more" means and what a renderer draws as the add control alone.
1934 pub instances: Vec<Instance>,
1935 /// The fewest slots the reader may leave standing.
1936 ///
1937 /// `0` is the ordinary answer and is why removing the last slot is allowed.
1938 /// A question that must be answered at least once says `1`, which is
1939 /// [`Field::required`]'s reading for a repeating question: the flag is
1940 /// about one box holding a value, and this is about how many boxes there
1941 /// are.
1942 pub least: usize,
1943 /// The most slots the reader may add, if there is a ceiling.
1944 ///
1945 /// `None` is no ceiling. goingson's reminders carry `Some(8)`, which is the
1946 /// cap `sanitize_reminder_offsets` already enforces on the way in: stating
1947 /// it here is what stops the reader filling in a ninth slot that the write
1948 /// path silently drops.
1949 pub most: Option<usize>,
1950 /// The named questions each slot is made of, when a slot is several.
1951 ///
1952 /// Empty is a slot that is one question, which is every repeating question
1953 /// written before this member existed. See [`Question`] for why the shape
1954 /// is declared here and only the answers are per slot.
1955 pub parts: Vec<Question>,
1956 /// What adds a slot to this question.
1957 ///
1958 /// [`Adds::Control`] is the ordinary answer and is a control of the
1959 /// question's own. [`Adds::Elsewhere`] is a question whose slots arrive
1960 /// from another control on the same screen, which is MNW's version queue:
1961 /// the reader picks files and each picked file is a slot, so a control
1962 /// offering a blank row has nothing to offer.
1963 pub add: Adds,
1964 /// What the control that takes one away is called. "Remove".
1965 pub remove: String,
1966 }
1967
1968 /// One answer to a repeating question.
1969 ///
1970 /// The value and the error, and nothing else. Both have the lifecycle
1971 /// [`Field::value`] and [`Field::error`] have and for the same reasons: what to
1972 /// re-offer after a refusal, and what whoever validated said about it.
1973 ///
1974 /// **This is the per-instance validation the ruling asked for.**
1975 /// [`Field::error`] is one string on one field, so a repeating question with
1976 /// only that could say the whole question was wrong and never which answer was.
1977 /// A message here belongs to this slot, and [`Field::error`] keeps the fact
1978 /// about the set: "at most eight reminders" is not about any one of them.
1979 #[derive(Debug, Clone, PartialEq, Eq, Default)]
1980 pub struct Instance {
1981 /// What this slot holds, on [`Field::value`]'s terms.
1982 ///
1983 /// The slot's own answer, for a slot that is one question. A slot that is
1984 /// several leaves this `None` and fills [`parts`](Self::parts) instead: a
1985 /// slot answers one way or the other, never both, and
1986 /// [`grouped`](Self::grouped) is which one it is.
1987 pub value: Option<String>,
1988 /// What is wrong with *this* answer. Supplied by whoever validated.
1989 ///
1990 /// About the slot as a whole either way. A slot that is several questions
1991 /// puts what is wrong with *one* of them on that [`Answer::error`], and
1992 /// keeps this for what is wrong with the slot: the upload that failed, not
1993 /// the name that was too long.
1994 pub error: Option<String>,
1995 /// What this slot answered for each of [`Repeat::parts`], in their order.
1996 ///
1997 /// Empty is a slot that is one question, and also a slot of a grouped
1998 /// question that has answered nothing yet. [`Repeat::parts`] is what says
1999 /// which of the two, because that is where the shape lives.
2000 pub parts: Vec<Answer>,
2001 /// What this slot is called, when it is not called by its position.
2002 ///
2003 /// `None` is [`Repeat::ordinal`]: "Reminder 1", "Reminder 2", which is
2004 /// right for slots that differ only by where they are in the list.
2005 ///
2006 /// A picked file is the case that is not. A queue's slots are named by
2007 /// what is in them -- "track.wav" -- and numbering them would put a second,
2008 /// less useful name where the useful one goes. A slot that names itself
2009 /// keeps that name when the slots above it are removed, which is the whole
2010 /// reason it is on the slot rather than derived from its index.
2011 pub named: Option<String>,
2012 /// How far the work on this slot has got, when work happens to it.
2013 ///
2014 /// [`Progress::Idle`] is a slot nothing is happening to, which is the
2015 /// default and every slot of a question that carries no work.
2016 pub progress: Progress,
2017 }
2018
2019 impl Instance {
2020 /// A slot holding this value and nothing wrong with it.
2021 #[must_use]
2022 pub fn new(value: impl Into<String>) -> Self {
2023 Self {
2024 value: Some(value.into()),
2025 error: None,
2026 parts: Vec::new(),
2027 named: None,
2028 progress: Progress::Idle,
2029 }
2030 }
2031
2032 /// An empty slot.
2033 #[must_use]
2034 pub const fn blank() -> Self {
2035 Self {
2036 value: None,
2037 error: None,
2038 parts: Vec::new(),
2039 named: None,
2040 progress: Progress::Idle,
2041 }
2042 }
2043
2044 /// A slot answering [`Repeat::parts`], in their order.
2045 ///
2046 /// [`value`](Self::value) stays `None`: the answers are on the parts, and a
2047 /// slot holding both would be two shapes behind one word, which is what
2048 /// [`Field::multiple`] was refused for.
2049 #[must_use]
2050 pub fn grouped(parts: impl IntoIterator<Item = Answer>) -> Self {
2051 Self {
2052 value: None,
2053 error: None,
2054 parts: parts.into_iter().collect(),
2055 named: None,
2056 progress: Progress::Idle,
2057 }
2058 }
2059
2060 /// What this slot answered for the `at`th of [`Repeat::parts`].
2061 ///
2062 /// A slot that has not answered that far is blank there, which is what a
2063 /// slot the reader just added is.
2064 #[must_use]
2065 pub fn part(&self, at: usize) -> Answer {
2066 self.parts.get(at).cloned().unwrap_or_default()
2067 }
2068
2069 /// The same slot, called this rather than called by its position.
2070 #[must_use]
2071 pub fn called(mut self, name: impl Into<String>) -> Self {
2072 self.named = Some(name.into());
2073 self
2074 }
2075
2076 /// The same slot, with the work on it standing where this says.
2077 #[must_use]
2078 pub fn getting(mut self, progress: Progress) -> Self {
2079 self.progress = progress;
2080 self
2081 }
2082
2083 /// The same slot, with this said about it.
2084 #[must_use]
2085 pub fn wrong(mut self, message: impl Into<String>) -> Self {
2086 self.error = Some(message.into());
2087 self
2088 }
2089 }
2090
2091 /// One of the named questions a slot is made of.
2092 ///
2093 /// A slot of a repeating field is ordinarily one answer to one question, and
2094 /// [`Instance::value`] is it. A picked-file queue is the shape that is not: a
2095 /// slot is a file, and a file is a name and a size and something that may have
2096 /// failed on its own.
2097 ///
2098 /// This is the *question* half and it lives on [`Repeat::parts`], declared once
2099 /// for the whole repeating question, for the reason [`Field::kind`] and the
2100 /// bounds live there: the question is what repeats, and only the answer is per
2101 /// slot. [`Answer`] is the other half. Declaring it once is also what gives a
2102 /// renderer the blank slot to offer when the reader adds one, which a shape
2103 /// living only on the answers could not.
2104 ///
2105 /// # The wire
2106 ///
2107 /// `name[0].size`, written by [`Repeat::part_at`] and read back by
2108 /// [`Repeat::part_of`] and [`Params::repeated_part`]. Beside `name[0]` rather
2109 /// than instead of it, which is the growth [`Repeat`]'s header sketched and
2110 /// left open: a slot that is one question still submits under the bare indexed
2111 /// name, so nothing written before this existed changed shape.
2112 ///
2113 /// [`Params::repeated_part`]: crate::Params::repeated_part
2114 #[derive(Debug, Clone, PartialEq, Eq, Default)]
2115 pub struct Question {
2116 /// What this question is called within the slot: the `size` of
2117 /// `name[0].size`.
2118 ///
2119 /// A suffix rather than a whole name. The repeating question's own name and
2120 /// the slot index are the caller's, and [`Repeat::part_at`] is the only
2121 /// place the three are joined.
2122 pub name: String,
2123 /// What it is called on screen.
2124 ///
2125 /// Not numbered. [`Repeat::ordinal`] numbers the slot, and numbering the
2126 /// questions inside it as well would read as "Name 2" for the second file.
2127 pub label: String,
2128 }
2129
2130 impl Question {
2131 /// One named question of a slot.
2132 #[must_use]
2133 pub fn new(name: impl Into<String>, label: impl Into<String>) -> Self {
2134 Self {
2135 name: name.into(),
2136 label: label.into(),
2137 }
2138 }
2139 }
2140
2141 /// What one slot answered for one of [`Repeat::parts`].
2142 ///
2143 /// The answer half of [`Question`], positional against it: the `n`th answer is
2144 /// to the `n`th question. Fewer answers than questions is a slot that has not
2145 /// answered the rest, which is what a slot the reader just added is, and every
2146 /// reader here tolerates it.
2147 #[derive(Debug, Clone, PartialEq, Eq, Default)]
2148 pub struct Answer {
2149 /// What this question of this slot holds, on [`Field::value`]'s terms.
2150 pub value: Option<String>,
2151 /// What is wrong with *this* question of this slot.
2152 ///
2153 /// The per-slot error one level finer. [`Instance::error`] stays what is
2154 /// wrong with the slot as a whole.
2155 pub error: Option<String>,
2156 }
2157
2158 impl Answer {
2159 /// An answer holding this.
2160 #[must_use]
2161 pub fn new(value: impl Into<String>) -> Self {
2162 Self {
2163 value: Some(value.into()),
2164 error: None,
2165 }
2166 }
2167
2168 /// An unanswered question of a slot.
2169 #[must_use]
2170 pub const fn blank() -> Self {
2171 Self {
2172 value: None,
2173 error: None,
2174 }
2175 }
2176
2177 /// The same, with this said about it.
2178 #[must_use]
2179 pub fn wrong(mut self, message: impl Into<String>) -> Self {
2180 self.error = Some(message.into());
2181 self
2182 }
2183 }
2184
2185 /// What adds a slot to a repeating question.
2186 ///
2187 /// A repeating question used to assume one answer: the reader presses a
2188 /// control of the question's own and a blank slot appears. That is still the
2189 /// ordinary case and still the default.
2190 ///
2191 /// MNW's version-upload queue is the case it could not say. Its slots are
2192 /// picked files, made by the upload field above the table, and a blank row is
2193 /// not something a reader can fill: there is no way to type a file. A question
2194 /// there wants no add control at all, and no renderer may invent one.
2195 ///
2196 /// # Not [`Repeating::add`]
2197 ///
2198 /// That is an [`Act`], because a repeating *group*'s slots are the app's and
2199 /// adding one is a route. Neither of these is: a repeating field's slots live
2200 /// in the renderer's own view until they submit, so what makes one is either
2201 /// this renderer's own control or another control on the same screen. No
2202 /// route either way.
2203 #[derive(Debug, Clone, PartialEq, Eq)]
2204 pub enum Adds {
2205 /// A control of the question's own, called this. "Add reminder".
2206 Control(String),
2207 /// Another control on the screen, named by whatever the renderer addresses
2208 /// controls by, which in a form is the field's name.
2209 ///
2210 /// The question draws no add control of its own. What the named control
2211 /// does with the slots it makes is that control's business and this says
2212 /// nothing about it: a renderer that cannot reach the named control draws
2213 /// the slots it was given and no way to make more, which is the honest
2214 /// answer for a terminal asked to display a file queue.
2215 Elsewhere(String),
2216 }
2217
2218 impl Default for Adds {
2219 fn default() -> Self {
2220 Self::Control(Repeat::ADD.to_owned())
2221 }
2222 }
2223
2224 impl Adds {
2225 /// What the control is called, when the question offers one.
2226 ///
2227 /// `None` is a question whose slots come from elsewhere, and is every
2228 /// renderer's signal to draw no add control.
2229 #[must_use]
2230 pub fn label(&self) -> Option<&str> {
2231 match self {
2232 Self::Control(label) => Some(label),
2233 Self::Elsewhere(_) => None,
2234 }
2235 }
2236
2237 /// The control the slots come from, when they come from another.
2238 #[must_use]
2239 pub fn from(&self) -> Option<&str> {
2240 match self {
2241 Self::Control(_) => None,
2242 Self::Elsewhere(name) => Some(name),
2243 }
2244 }
2245 }
2246
2247 /// How far the work on one slot has got.
2248 ///
2249 /// [`Instance::error`] says what is wrong with a slot and cannot say *when*:
2250 /// an answer that failed to upload and an answer nobody has started uploading
2251 /// are both "no value yet" to a renderer reading the slot alone. A queue of
2252 /// picked files is the screen that needs the difference, and needs it per slot
2253 /// rather than per question, because one file failing says nothing about the
2254 /// other six.
2255 ///
2256 /// Not [`layout::Readiness`], which is a region's word for whether its
2257 /// *content* arrived and carries an `Empty` that means nothing about a slot.
2258 /// Two meanings behind one word is what [`Field::multiple`] was refused for.
2259 #[derive(Debug, Clone, PartialEq, Eq, Default)]
2260 pub enum Progress {
2261 /// Nothing is happening to this slot.
2262 ///
2263 /// The default, and every slot of every repeating question that carries no
2264 /// work of its own.
2265 #[default]
2266 Idle,
2267 /// Work on this slot began and has not finished.
2268 ///
2269 /// The [`Meter`] is how far, for a host that can say. `None` is work whose
2270 /// extent nobody can state yet, which is a file that has been handed over
2271 /// and has reported no bytes.
2272 Working(Option<Meter>),
2273 /// The work finished and this slot holds what it produced.
2274 Done,
2275 /// The work did not finish.
2276 ///
2277 /// What went wrong is [`Instance::error`]. Kept apart from it so that a
2278 /// slot may carry a message without being a failure, which is an ordinary
2279 /// validation refusal on a queue nobody has submitted yet.
2280 Failed,
2281 }
2282
2283 impl Progress {
2284 /// Whether this slot is waiting on work that has not finished.
2285 ///
2286 /// Asked by every renderer before it draws a slot's remove control: a slot
2287 /// mid-flight is one the reader may not pull out from under the work.
2288 #[must_use]
2289 pub const fn busy(&self) -> bool {
2290 matches!(self, Self::Working(_))
2291 }
2292
2293 /// Whether the work on this slot ended badly.
2294 #[must_use]
2295 pub const fn failed(&self) -> bool {
2296 matches!(self, Self::Failed)
2297 }
2298 }
2299
2300 impl Repeat {
2301 /// What the control that adds a slot is called when nothing else is said.
2302 pub const ADD: &'static str = "Add";
2303 /// What the control that removes one is called when nothing else is said.
2304 pub const REMOVE: &'static str = "Remove";
2305
2306 /// A question nobody has answered yet, with no floor and no ceiling.
2307 #[must_use]
2308 pub fn new() -> Self {
2309 Self {
2310 instances: Vec::new(),
2311 least: 0,
2312 most: None,
2313 parts: Vec::new(),
2314 add: Adds::default(),
2315 remove: Self::REMOVE.to_owned(),
2316 }
2317 }
2318
2319 /// The named questions each slot is made of.
2320 ///
2321 /// A slot answers these rather than answering once itself, and
2322 /// [`Instance::grouped`] is how one supplies them.
2323 #[must_use]
2324 pub fn of(mut self, parts: impl IntoIterator<Item = Question>) -> Self {
2325 self.parts = parts.into_iter().collect();
2326 self
2327 }
2328
2329 /// The slots this question already stands in, supplied whole.
2330 ///
2331 /// [`answered`](Self::answered) for a grouped question, where a slot is an
2332 /// [`Instance::grouped`] rather than a single value, and for any slot that
2333 /// carries a [`Progress`] the plain values cannot express.
2334 #[must_use]
2335 pub fn instances_of(mut self, instances: impl IntoIterator<Item = Instance>) -> Self {
2336 self.instances = instances.into_iter().collect();
2337 self
2338 }
2339
2340 /// Whether a slot of this question is several questions rather than one.
2341 #[must_use]
2342 pub fn grouped(&self) -> bool {
2343 !self.parts.is_empty()
2344 }
2345
2346 /// A question already answered these many times.
2347 ///
2348 /// What a form being offered for editing carries: one slot per value the
2349 /// record holds.
2350 #[must_use]
2351 pub fn answered(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
2352 Self {
2353 instances: values.into_iter().map(Instance::new).collect(),
2354 ..Self::new()
2355 }
2356 }
2357
2358 /// Leave at least this many slots standing.
2359 #[must_use]
2360 pub const fn least(mut self, least: usize) -> Self {
2361 self.least = least;
2362 self
2363 }
2364
2365 /// Take at most this many answers.
2366 #[must_use]
2367 pub const fn most(mut self, most: usize) -> Self {
2368 self.most = Some(most);
2369 self
2370 }
2371
2372 /// What the control that adds a slot is called.
2373 #[must_use]
2374 pub fn adding(mut self, label: impl Into<String>) -> Self {
2375 self.add = Adds::Control(label.into());
2376 self
2377 }
2378
2379 /// The slots come from another control on the screen, named here.
2380 ///
2381 /// The question offers no add control of its own. See [`Adds::Elsewhere`].
2382 #[must_use]
2383 pub fn added_by(mut self, control: impl Into<String>) -> Self {
2384 self.add = Adds::Elsewhere(control.into());
2385 self
2386 }
2387
2388 /// What the control that takes a slot away is called.
2389 #[must_use]
2390 pub fn removing(mut self, label: impl Into<String>) -> Self {
2391 self.remove = label.into();
2392 self
2393 }
2394
2395 /// Say what is wrong with one answer, leaving the rest alone.
2396 ///
2397 /// Grows the list to reach it, because a refusal naming the fourth answer
2398 /// of a form that came back with three is a description bug worth seeing on
2399 /// the screen rather than a message silently dropped.
2400 #[must_use]
2401 pub fn wrong(mut self, at: usize, message: impl Into<String>) -> Self {
2402 if self.instances.len() <= at {
2403 self.instances.resize(at + 1, Instance::blank());
2404 }
2405 self.instances[at].error = Some(message.into());
2406 self
2407 }
2408
2409 /// The name the `at`th slot submits under: `name[at]`.
2410 ///
2411 /// The one place the wire naming is spelled. See the type's header for why
2412 /// it is an index rather than N values under one name.
2413 #[must_use]
2414 pub fn at(name: &str, at: usize) -> String {
2415 format!("{name}[{at}]")
2416 }
2417
2418 /// The name the `part` of the `at`th slot submits under: `name[at].part`.
2419 ///
2420 /// [`at`](Self::at) with a part on the end, and the only place that join is
2421 /// spelled, for [`at`](Self::at)'s reason: the three renderers and whatever
2422 /// reads the submission back have to agree, and [`part_of`](Self::part_of)
2423 /// is the reader.
2424 #[must_use]
2425 pub fn part_at(name: &str, at: usize, part: &str) -> String {
2426 format!("{name}[{at}].{part}")
2427 }
2428
2429 /// The question, the slot and the part a wire name belongs to, if it is
2430 /// one.
2431 ///
2432 /// [`part_at`](Self::part_at) read backwards. `None` for a bare indexed
2433 /// name, which [`instance_of`](Self::instance_of) is the reader for, and
2434 /// `None` for every ordinary field name: the two never both answer, so a
2435 /// host may ask either of any name it holds.
2436 #[must_use]
2437 pub fn part_of(wire: &str) -> Option<(&str, usize, &str)> {
2438 let (indexed, part) = wire.split_once('.')?;
2439 if part.is_empty() || part.contains('.') {
2440 return None;
2441 }
2442 let (name, at) = Self::instance_of(indexed)?;
2443 Some((name, at, part))
2444 }
2445
2446 /// The question and the slot a wire name belongs to, if it is one.
2447 ///
2448 /// [`at`](Self::at) read backwards, for a host holding a name and asking
2449 /// what it is. `None` for every ordinary field name, which is what makes it
2450 /// safe to ask of any of them.
2451 #[must_use]
2452 pub fn instance_of(wire: &str) -> Option<(&str, usize)> {
2453 let (name, rest) = wire.split_once('[')?;
2454 let index = rest.strip_suffix(']')?;
2455 // Refused rather than parsed loosely: `name[+1]` and `name[ 1]` both
2456 // parse as 1 through `str::parse` on some inputs a caller would not
2457 // expect, and a name this did not write is not a slot.
2458 if index.is_empty() || !index.bytes().all(|byte| byte.is_ascii_digit()) {
2459 return None;
2460 }
2461 Some((name, index.parse().ok()?))
2462 }
2463
2464 /// What the `at`th slot is called on screen: "Reminder 2".
2465 ///
2466 /// Counted from one, because it is read by a person. Spelled here so the
2467 /// three renderers cannot number the same slot differently, which is the
2468 /// same reason [`at`](Self::at) is here: an error reported against the
2469 /// third answer has to name the third box on every host.
2470 #[must_use]
2471 pub fn ordinal(label: &str, at: usize) -> String {
2472 format!("{label} {}", at + 1)
2473 }
2474
2475 /// Whether another slot may be added when this many are standing.
2476 #[must_use]
2477 pub fn more(&self, standing: usize) -> bool {
2478 self.most.is_none_or(|most| standing < most)
2479 }
2480
2481 /// Whether a slot may be taken away when this many are standing.
2482 #[must_use]
2483 pub const fn fewer(&self, standing: usize) -> bool {
2484 standing > self.least
2485 }
2486
2487 /// How many slots stand before the reader has touched anything.
2488 ///
2489 /// The described count, floored at [`least`](Self::least): a question that
2490 /// must be answered twice opens with two boxes rather than with none and a
2491 /// refusal on submit.
2492 #[must_use]
2493 pub fn standing(&self) -> usize {
2494 self.instances.len().max(self.least)
2495 }
2496
2497 /// What the `at`th slot holds, if the description offered anything.
2498 #[must_use]
2499 pub fn holds(&self, at: usize) -> Option<&str> {
2500 self.instances.get(at)?.value.as_deref()
2501 }
2502
2503 /// What is wrong with the `at`th answer, if anything is.
2504 #[must_use]
2505 pub fn amiss(&self, at: usize) -> Option<&str> {
2506 self.instances.get(at)?.error.as_deref()
2507 }
2508 }
2509
2510 /// One field of a form, owned.
2511 ///
2512 /// The borrowed original is [`layout::Field`], and everything it says about
2513 /// what a field carries applies unchanged, with one addition that does not
2514 /// travel down to it: [`value`](Self::value).
2515 ///
2516 /// # Why the value lives here and not in `makeover-layout`
2517 ///
2518 /// [`layout::Field`] refuses to carry the current value, and that refusal is
2519 /// right: an immediate-mode renderer writes through a `&mut String` the app
2520 /// owns, and a terminal keeps an edit buffer, so a description carrying a live
2521 /// value would need a way to write it back and would then be a form model.
2522 ///
2523 /// What is carried here is not a live value. It is what to re-offer after a
2524 /// submission was refused, and it has [`error`](Self::error)'s lifecycle rather
2525 /// than a live value's: per-submission, one way, supplied by whoever validated,
2526 /// gone on the next request. `error` already sits in this struct on exactly
2527 /// those terms.
2528 ///
2529 /// The reason it is this crate's field and not the vocabulary's is that only a
2530 /// stateless request and response destroys the value. In egui and in a terminal
2531 /// the buffer never went anywhere, so nothing is lost and there is nothing to
2532 /// re-offer. This is the layer where the loss happens, so this is the layer that
2533 /// repairs it.
2534 ///
2535 /// # A field's described state is its value, and the caret is the renderer's
2536 ///
2537 /// Nothing here carries a caret position, and nothing in [`layout::FieldKind`]
2538 /// does either. A description names the field and, where it has one, its
2539 /// completion source. Where the caret sits is how a renderer decides what to
2540 /// offer from that source.
2541 ///
2542 /// The question came from goingson's `search.js`, whose completion list depends
2543 /// on which token the caret is inside rather than on the value: it reads
2544 /// `selectionStart`, listens for caret moves that change nothing else, and
2545 /// writes the caret back when a suggestion is applied. That is a real
2546 /// dependency, and it still does not belong here. A caret is where the user is
2547 /// pointing inside a control, the same class of fact as a scroll offset and a
2548 /// focus position, and this stack already puts those in the renderer's view
2549 /// rather than in the description (`quasi-tui`'s `View`).
2550 ///
2551 /// Growing this struct to (value, caret) was rejected: it is the most-consumed
2552 /// member in the vocabulary, every renderer would owe it an answer, and a
2553 /// terminal's answer would be a second cursor concept beside the one the runtime
2554 /// already holds. The measured demand was one file.
2555 ///
2556 /// Reversible if a second consumer appears that needs the caret described rather
2557 /// than held, such as a completion that has to survive a fragment swap. That is
2558 /// a member here and a cascade, the same shape as every other addition.
2559 ///
2560 /// No `Hash`, for the reason [`Tag`] has none: it can hold an [`Action`], which
2561 /// holds [`Params`], which is a `Vec`.
2562 #[derive(Debug, Clone, PartialEq, Eq)]
2563 pub struct Field {
2564 /// What kind of value it takes.
2565 pub kind: layout::FieldKind,
2566 /// The name the value is submitted under, and the name the handler reads
2567 /// back out of [`Params`].
2568 ///
2569 /// The *lower* end's name for a [`layout::FieldKind::Interval`], whose upper
2570 /// end is [`upper_name`](Self::upper_name).
2571 pub name: String,
2572 /// The name a [`layout::FieldKind::Interval`]'s upper end is submitted
2573 /// under.
2574 ///
2575 /// The borrowed original is [`layout::Field::upper_name`], and everything it
2576 /// says applies: stated rather than derived, because the two measured sites
2577 /// disagree about affix order, and which member a name sits in is what says
2578 /// which end it is.
2579 ///
2580 /// `None` for every other kind. [`Field::interval`] is what makes an
2581 /// interval without one unsayable.
2582 pub upper_name: Option<String>,
2583 /// What the user is asked for.
2584 pub label: String,
2585 /// Standing help.
2586 pub hint: Option<String>,
2587 /// What is currently wrong with the value. Supplied by whoever validated;
2588 /// nothing here decides that a value is wrong.
2589 pub error: Option<String>,
2590 /// A consequence of the answer the user has given, carrying its own tone.
2591 ///
2592 /// The third message channel, between [`hint`](Self::hint) and
2593 /// [`error`](Self::error) and overlapping neither: the value is acceptable
2594 /// and choosing it costs something worth saying. It does not make the field
2595 /// [`invalid`](Self::invalid).
2596 ///
2597 /// Precedence for a renderer with room for one line, decided in
2598 /// [`layout::Field::note`]: error, then note, then hint.
2599 pub note: Option<(layout::Tone, String)>,
2600 /// Ghost text shown while the field is empty.
2601 pub placeholder: Option<String>,
2602 /// The options offered, in order. Empty for kinds that offer none.
2603 pub options: Vec<Choice>,
2604 /// The themes offered, in the order they are offered.
2605 ///
2606 /// The borrowed original is [`layout::Field::themes`], and everything it
2607 /// says applies. Empty for every kind
2608 /// [`layout::FieldKind::offers_themes`] rejects, and a real answer for the
2609 /// one that accepts it.
2610 ///
2611 /// **The order is the grouping**, and nothing here sorts. The order arrives
2612 /// from whoever measured the tiers — `makeover::theme_options` is what
2613 /// produces it — and re-sorting at this layer would be deciding a question
2614 /// it cannot see the inputs to.
2615 pub themes: Vec<ThemeChoice>,
2616 /// The entry that follows the ambient mode instead of naming a theme.
2617 ///
2618 /// The borrowed original is [`layout::Field::follows`]. A [`Choice`] rather
2619 /// than a bare label because the value belongs to the app's own store, and
2620 /// `None` is a real answer for a host with no ambient mode to follow.
2621 pub follows: Option<Choice>,
2622 /// What a file field takes. Empty for kinds that take no files, and also a
2623 /// real answer for one that does: a field listing nothing takes any file.
2624 ///
2625 /// The borrowed original is [`layout::Field::accept`]. It filters the picker
2626 /// and it says which disclosure the field earns — a preview, a duration —
2627 /// which is why it is a list of [`Accepted`] rather than the comma-joined
2628 /// string a template holds.
2629 pub accept: Vec<Accepted>,
2630 /// Whether more than one file may be picked at once.
2631 ///
2632 /// The borrowed original is [`layout::Field::multiple`]. Read only by a kind
2633 /// [`layout::FieldKind::takes_files`] accepts.
2634 pub multiple: bool,
2635 /// Whether the form refuses to submit without it.
2636 pub required: bool,
2637 /// The longest the value may be, in characters.
2638 ///
2639 /// The borrowed original's [`layout::Field::max_length`], and everything it
2640 /// says applies: the description carries the rule, the renderer emits its
2641 /// host's idiom, and deciding a value is wrong stays with whoever validated.
2642 pub max_length: Option<u32>,
2643 /// The lowest value accepted, written the way the host writes one.
2644 pub min: Option<String>,
2645 /// The highest value accepted. See [`min`](Self::min).
2646 pub max: Option<String>,
2647 /// The granularity a *typed* value moves in, written the way the host writes
2648 /// one.
2649 ///
2650 /// The borrowed original is [`layout::Field::step`]. Absent means the
2651 /// host's own granularity, which is a real answer rather than a missing
2652 /// one. A [`layout::FieldKind::Range`] keeps its own on
2653 /// [`curve`](Self::curve) instead, as of makeover-layout 0.32.0.
2654 pub step: Option<String>,
2655 /// How a slider's position becomes its value, and how finely it moves.
2656 ///
2657 /// The borrowed original is [`layout::Field::curve`]. A
2658 /// [`layout::FieldKind::Range`] reads its granularity here; every other
2659 /// kind reads [`step`](Self::step). See [`Curve`].
2660 pub curve: Curve,
2661 /// What the number is measured in: `s`, `ms`, `dB`, `GiB`.
2662 ///
2663 /// The borrowed original is [`layout::Field::unit`], and everything it says
2664 /// applies. A fact about the value rather than part of the question's name,
2665 /// which is the distinction the member exists for: the two come apart the
2666 /// moment a handler reads a field back instead of a renderer drawing it.
2667 ///
2668 /// Read only by a kind [`layout::FieldKind::measurable`] accepts. The symbol
2669 /// alone, no brackets and no leading space; the spacing is the renderer's.
2670 pub unit: Option<String>,
2671 /// Whether the field lives behind a "more options" disclosure.
2672 pub extended: bool,
2673 /// Whether this local wall-clock value is submitted as an absolute instant.
2674 ///
2675 /// The borrowed original is [`layout::Field::as_instant`], and everything it
2676 /// says applies. A [`layout::FieldKind::DateTime`] asks for a time the way a
2677 /// person says one, which names a different moment in each zone; this says
2678 /// the description wants the renderer to convert it, because the renderer is
2679 /// the only party that knows what its host's clock and zone are.
2680 ///
2681 /// No wire contract moves when a site adopts it. The route was already
2682 /// receiving an instant; what changes is who computed it.
2683 pub as_instant: bool,
2684 /// How much of its row the control asks for.
2685 ///
2686 /// Fill is determined at the description stage. [`Column::width`] has said
2687 /// this about a table cell since the beginning and [`layout::Share`] says
2688 /// it about a region, so the vocabulary already accepted that an app has
2689 /// an opinion about which of several things expands. A leaf control having
2690 /// no way to say it was an inconsistency in where the line sat rather than
2691 /// a principle being upheld, and this is the correction.
2692 ///
2693 /// What it is not is a measurement. [`layout::Width`] is an intent —
2694 /// content-sized, fixed, or take the rest — and the actual floor stays with
2695 /// `makeover-geometry`, which is the same division `Column` makes. A field
2696 /// that carried pixels would be the thing this replaces: audiofiles'
2697 /// toolbar kept a measured `trailing_width` in renderer memory, corrected
2698 /// it a frame late, and needed two constants to survive the first frame,
2699 /// all to say [`Fill`](layout::Width::Fill).
2700 ///
2701 /// [`layout::Share`]: crate::layout::Share
2702 pub width: layout::Width,
2703 /// What to put back in the box: what was submitted, when a submission was
2704 /// refused and the form is being offered again.
2705 ///
2706 /// `None` on a first showing, which is every form that is not answering a
2707 /// refusal. A checkbox is here by presence, the way HTML submits one: a
2708 /// value means ticked and `None` means not.
2709 ///
2710 /// A [`layout::FieldKind::Secret`] never gets one. [`Field::value`] refuses
2711 /// to set it and every renderer refuses to emit it, so the guarantee does
2712 /// not rest on either alone.
2713 pub value: Option<String>,
2714 /// What to put back in the *upper* box of a
2715 /// [`layout::FieldKind::Interval`], on the same terms as
2716 /// [`value`](Self::value).
2717 ///
2718 /// A second value rather than a separator convention inside the first. An
2719 /// interval submits two names, so a refusal has two values to hand back, and
2720 /// joining them into one string would make this layer own a delimiter that
2721 /// any value could contain.
2722 ///
2723 /// Either end may be absent while the other stands, which is what an
2724 /// open-ended interval is: "over 120 BPM" is a lower end and no upper one,
2725 /// and it is a real answer rather than a half-filled form.
2726 ///
2727 /// `None` for every other kind, and never set for a
2728 /// [`layout::FieldKind::Secret`] for [`value`](Self::value)'s reason.
2729 pub upper_value: Option<String>,
2730 /// What setting this calls, for a control that writes as it is set rather
2731 /// than waiting for a submit.
2732 ///
2733 /// A field inside a [`Node::Form`] submits with the form and needs nothing
2734 /// here. A settings toggle is the other kind: there is no submit, and
2735 /// setting the control *is* the write. goingson had 13 of these and
2736 /// reached them through `dispatch.js`, 109 lines of its own event
2737 /// plumbing, because nothing in the description could say it. No version
2738 /// of spinning up an app quickly has each app hand-rolling a dispatcher.
2739 ///
2740 /// The route receives the value under this field's [`name`](Self::name),
2741 /// which is the same name a submit would have sent it under. Nothing else
2742 /// changes about the field.
2743 ///
2744 /// # What it is for
2745 ///
2746 /// A value that is written as it is set, where no commit point is wanted: a
2747 /// draft field that autosaves, a setting, a per-field select on a detail
2748 /// view. The reader alters one control, and that alteration is the whole
2749 /// interaction.
2750 ///
2751 /// # What it is not for
2752 ///
2753 /// **Applying a value to a selection.** That wants a commit affordance, per
2754 /// wiki `explicit-commit-affordance`: the reader ticks rows, chooses a
2755 /// value, and the write lands on every row ticked. That is a large enough
2756 /// act to deserve a control that says so before it happens.
2757 ///
2758 /// **Anything that does not write.** A filter, a sort, a re-ask: a control
2759 /// whose action fetches a different view of data it leaves alone. Saying it
2760 /// here states something false about the control in every renderer that
2761 /// reads the description, and the falsehood is invisible because the
2762 /// request still goes out and the screen still updates.
2763 ///
2764 /// # Why the name is enough
2765 ///
2766 /// This is the only member on [`Field`] carrying an [`Action`] of its own,
2767 /// so a field without it is written by its form's submit and a field with
2768 /// it writes by itself. The presence of the member is the statement. There
2769 /// is nothing to add and no second member saying a field does not write.
2770 /// A [`Consult`] carries an action too and is not a counterexample: it
2771 /// names a question about the value, and the question is what its type
2772 /// says.
2773 ///
2774 /// # When it fires: the change is *complete*, not on the way to it
2775 ///
2776 /// A value the reader builds up, typed into or dragged across, writes
2777 /// once, when they are finished with the control: a webview on the
2778 /// browser's `change` event, a terminal when the caret walks off the box,
2779 /// egui on `lost_focus` or `drag_stopped`. A value chosen in one go (a
2780 /// select, a radio, a file) was complete the moment it changed and writes
2781 /// then.
2782 ///
2783 /// **This was already what a webview did and what the other two did not.**
2784 /// `quasi-webview` emits `hx-trigger="change"`, so it has meant this since
2785 /// the member existed; `quasi-immediate` fired on every frame the buffer
2786 /// differed and `quasi-tui` on every keystroke. So a search box was one
2787 /// request per letter on two hosts and one per search on the third, and
2788 /// audiofiles' bounded `row_height` posted a row height of 3 on the way to
2789 /// 30, outside the bounds the field's own hint states.
2790 ///
2791 /// Leaving a control nobody altered writes nothing, which is the other half
2792 /// of what `change` promises: walking through a form must not write every
2793 /// box it passes.
2794 ///
2795 /// # A question asked while typing is [`consults`](Self::consults)
2796 ///
2797 /// Nothing above costs live search anything, and that is what makes the
2798 /// rule coherent rather than a restriction. A box that asks a route about
2799 /// what is being typed carries a [`Consult`], which has its own
2800 /// [`after`](Consult::after): a wait the description states rather than a
2801 /// number each renderer picks. Writes complete; questions debounce.
2802 pub writes: Option<Action>,
2803 /// What this asks while the user is still typing, and how long it waits.
2804 ///
2805 /// See [`Consult`]. Distinct from [`writes`](Self::writes) in the two
2806 /// ways that matter: it fires while the value is still being written rather
2807 /// than once it settles, and what comes back is an answer about the value
2808 /// rather than the result of writing it.
2809 ///
2810 /// # Several, because one box can raise more than one question
2811 ///
2812 /// `N8` on MNW's discover screen: `#search-input` carries `hx-
2813 /// get="/discover/results"` with one wait, and the suggestion list beside
2814 /// it is a hand-written `fetch('/discover/suggestions?q=…')` with another.
2815 /// Two questions about one value, asked at two rates, and only one of them
2816 /// was sayable — so the other stayed as JS.
2817 ///
2818 /// Order is the description's and each renderer keeps it, though nothing
2819 /// depends on it: the answers land where each
2820 /// [`Action::replacing`] says, and two consults pointing at one place is a
2821 /// description arguing with itself rather than an ordering question.
2822 ///
2823 /// # "Typing" is the common case, not the rule
2824 ///
2825 /// A select, a radio, a checkbox and a slider all consult, and the
2826 /// majority of the measured sites are selects: a folder picker that re-
2827 /// reads a list of mail asks a route about a value and stores nothing,
2828 /// which is this member entire. It read as typing-only because the first
2829 /// four sites were wizard boxes and the webview hung the question on
2830 /// `keyup`, which a select never raises.
2831 ///
2832 /// Each renderer raises it in its host's idiom, the way it already does for
2833 /// [`Slot::consults`], and the wait stays the description's:
2834 /// [`Consult::after`] of zero is a real number for a control chosen in one
2835 /// gesture rather than a meaningless one.
2836 pub consults: Vec<Consult>,
2837 /// The question whose answer is this field's own list of candidates.
2838 ///
2839 /// The field owns the list. MNW's discover screen spends ~120 lines of
2840 /// `page-discover.js` on exactly that wiring, none of which is about
2841 /// suggestions.
2842 ///
2843 /// # Why a [`Consult`] and not a second kind of member
2844 ///
2845 /// A suggestion source is a route asked as the value is typed, with a wait
2846 /// and a floor, which is [`Consult`] entire. What is added here is not a
2847 /// second mechanism but an owner: the answer to *this* question is a list
2848 /// of candidates for *this* value, and every renderer therefore knows where
2849 /// to draw it, what it is called, and what picking one does.
2850 ///
2851 /// # What comes back, and this one is described
2852 ///
2853 /// [`Outcome::Suggestions`](crate::Outcome::Suggestions), a list of
2854 /// [`Candidate`] — its own type rather than the [`Choice`] that
2855 /// [`options`](Self::options) carries. Both
2856 /// submit one string and read as another; a candidate also says what tells
2857 /// it apart from a row that reads alike, and what picking it does.
2858 /// Deliberately unlike
2859 /// [`consults`](Self::consults), where the answer is undescribed and the
2860 /// renderer picks the wire format: that works for a verdict landing in a
2861 /// region a description already named, and it cannot work here. A list
2862 /// nobody described is a list a terminal cannot draw, and one route
2863 /// answering three wire formats is the per-host branching this stack
2864 /// exists to end.
2865 ///
2866 /// # Picking is local by default
2867 ///
2868 /// [`Destination::Local`] says a suggestion's pick action sets the field.
2869 /// Ownership is what makes that sayable without an action at all: the list
2870 /// belongs to this field, so picking an entry writes its
2871 /// [`Candidate::value`] into this field, and no renderer has to be told
2872 /// which box to write to. Moving the highlight is local for the same
2873 /// reason.
2874 ///
2875 /// By default and not by definition after the two sites this member was
2876 /// designed from were held against it and neither picked locally. A
2877 /// candidate carrying
2878 /// [`picks`](Candidate::picks) has that action performed instead. Local
2879 /// stays the default, so every site that exists today is unchanged and
2880 /// nothing that already works has to say anything new.
2881 ///
2882 /// # Addressed by the field's name
2883 ///
2884 /// No id is authored anywhere. The field already has a
2885 /// [`name`](Self::name) — what a submit sends the value under, what a
2886 /// [`Consult`] sends it under — and that is what the answer names. A
2887 /// renderer that needs a document id derives one from it, which is the
2888 /// difference between a list a field owns and two elements a description
2889 /// has to keep pointing at each other.
2890 ///
2891 /// Beside [`consults`](Self::consults) rather than inside it: MNW's
2892 /// discover box asks two questions about one value, and only one of them is
2893 /// its suggestions. The other re-reads the results under the current
2894 /// filters and lands in a region, which is what [`consults`](Self::consults)
2895 /// has always been for.
2896 ///
2897 /// [`Destination::Local`]: crate::Destination::Local
2898 pub suggests: Option<Consult>,
2899 /// Whether what is in this control belongs to the reader rather than to the
2900 /// answer that drew it.
2901 ///
2902 /// Say it, one member, on the field.
2903 ///
2904 /// The fact is that the server never sent this value and cannot send it
2905 /// again, so a redraw that clears the control loses something only the
2906 /// reader had. A tag typeahead is the measured case: type three letters,
2907 /// tick an unrelated facet, and the surrounding region is swapped out of
2908 /// band with the box in it.
2909 ///
2910 /// # Why it has to be said rather than assumed
2911 ///
2912 /// Because only one host's default is destructive, which is the usual
2913 /// reason a fact enters this vocabulary. A terminal keeps its own buffer
2914 /// and egui keeps widget state by id, so both were already right; a browser
2915 /// replaces the element and takes what was typed with it.
2916 ///
2917 /// # Why not on the region
2918 ///
2919 /// Which is what the original task asked for, and it cannot be
2920 /// implemented. Preservation is per-element, so a renderer told "redraw
2921 /// this region but keep the reader's half" has no way to know *which*
2922 /// elements hold reader state, and "preserve every input in here" would
2923 /// keep a facet control the answer legitimately reset. The field is the
2924 /// only place that knows.
2925 ///
2926 /// # Why not inferred
2927 ///
2928 /// A rule like "no value, plus [`suggests`](Self::suggests) or
2929 /// [`consults`](Self::consults), means reader-owned" would cover the
2930 /// measured site exactly and cost nothing to write. Declined in the same
2931 /// ruling: it changes behaviour when a field gains or loses an unrelated
2932 /// member, so a description that starts preserving because somebody added a
2933 /// consult is a surprise nobody wrote down.
2934 ///
2935 /// # What it is not
2936 ///
2937 /// Not [`value`](Self::value), which is what the answer offers and what a
2938 /// refusal re-offers. A field can have both: the answer says what it
2939 /// starts as, and this says nobody may take it away afterwards.
2940 pub keeps_value: bool,
2941 /// The slots this question is answered in, when it is answered more than
2942 /// once.
2943 ///
2944 /// `None` is the ordinary field, asked once and answering once, which is
2945 /// every other field in the tree. See
2946 /// [`Repeat`] for the wire naming, the per-slot errors and what this
2947 /// deliberately is not.
2948 ///
2949 /// The members around it keep their meanings and apply to every slot: the
2950 /// [`kind`](Self::kind), the bounds, the [`placeholder`](Self::placeholder)
2951 /// and the [`hint`](Self::hint) describe the question, and the question is
2952 /// what repeats. The two that do not are [`value`](Self::value) and
2953 /// [`error`](Self::error), which are per-answer and live on the
2954 /// [`Instance`]; a repeating field's own `error` is what is wrong with the
2955 /// *set*, which is the fact "at most eight reminders" belongs to.
2956 ///
2957 /// [`Field::instance`] is how a renderer gets one slot as an ordinary
2958 /// field, so that everything a renderer already does to a field it does to
2959 /// each slot without a second emitter.
2960 pub repeats: Option<Repeat>,
2961 /// What brings this field out, when it is not simply out.
2962 ///
2963 /// Found by the consumer `079a011e` was ruled for. The region carries the
2964 /// condition and that is the shape for a block of several things; a form's
2965 /// questions are a flat list, so a *single* conditional question inside
2966 /// one had nowhere to put the same fact. goingson's event form is the
2967 /// measured site: `initTzKindConfig` shows one box, "Anchored to", on one
2968 /// of the three zone kinds, and the box has to submit with the form around
2969 /// it.
2970 ///
2971 /// The condition sits on the thing revealed, which is the direction
2972 /// [`Reveal`] was ruled in: what is refused there is the condition living
2973 /// on the *watched* control, and that is refused here too. A renderer
2974 /// answers this exactly as it answers a region's, from what it already
2975 /// holds, and makes no request.
2976 ///
2977 /// A hidden field keeps its value and still submits it, which is what a
2978 /// browser does with an input inside a hidden element and what the client
2979 /// renderers already do with a region the reader has closed.
2980 ///
2981 /// `None` is the ordinary question, which is every other field in the tree.
2982 pub revealed_by: Option<Box<Reveal>>,
2983 }
2984
2985 /// A question asked while the reader is still working, rather than when they
2986 /// are done.
2987 ///
2988 /// N14 and N8: the description names the **source** — a route and how long to
2989 /// wait — and says nothing about what comes back. Both gaps were filed
2990 /// separately, one asking for "pending / ok / taken" and one for a suggestion
2991 /// list, and they are the same member: a field that consults something as it
2992 /// is written. Naming the two shapes instead would have put two mechanisms in
2993 /// the vocabulary for one idea, which is how a vocabulary drifts.
2994 ///
2995 /// # Two places it sits, one mechanism
2996 ///
2997 /// [`Field::consults`] asks about one control's value. [`Slot::consults`] asks
2998 /// about the values of every question inside a region, which is the pricing
2999 /// calculator's shape: five dials that are peers, and a panel that recomputes
3000 /// when any of them moves. Neither is about typing -- a select is the
3001 /// commonest asker of both. — and decided as
3002 /// *this* type rather than as a second member, because a route, a debounce and
3003 /// a floor is `Consult` entire and a second timing mechanism for the same idea
3004 /// is the drift the paragraph above is about.
3005 ///
3006 /// The three fields read the same in both places, against the value that just
3007 /// moved: [`after`](Self::after) is how long it must stand still,
3008 /// [`at_least`](Self::at_least) is how much of it there must be, and
3009 /// [`sends`](Self::sends) is what rides along *beyond* what the position
3010 /// already gathers. What the position gathers differs, and that is the whole of
3011 /// the difference: a field sends its own value, and a region sends the values
3012 /// of the questions it contains.
3013 ///
3014 /// # What comes back is not described, deliberately
3015 ///
3016 /// The route answers with whatever it answers with, and the renderer decides
3017 /// the wire format: a fragment for a webview, values for a terminal or egui.
3018 /// A three-way verdict and a list of suggestions are then the same member with
3019 /// two different routes behind it, and adding a third kind of answer costs
3020 /// nothing here.
3021 ///
3022 /// Where the answer lands is [`Action::replacing`], which already says that
3023 /// about every other action. Nothing new is needed to point a verdict at the
3024 /// status line beside the box.
3025 ///
3026 /// # The accepted costs, so they are not re-argued
3027 ///
3028 /// A field points at a route for the first time in the vocabulary, and a host
3029 /// with nothing to ask — no HTTP, no local responder — cannot honour one. Both
3030 /// were weighed and taken: the alternative was leaving four wizard fields and
3031 /// two comboboxes as host code forever.
3032 ///
3033 /// # Two numbers, not one
3034 ///
3035 /// [`after`](Self::after) says the value has stopped moving and
3036 /// [`at_least`](Self::at_least) says there is enough of it to be worth asking
3037 /// about. Both are the description's for the same reason — a renderer picking
3038 /// either is a renderer the other renderers disagree with — and the measured
3039 /// sites carry both: MNW's tag typeahead waits 150ms above two characters and
3040 /// its search box 200ms above two, while the four wizard fields wait 500ms and
3041 /// ask about a single letter, because one letter can already be taken.
3042 #[derive(Debug, Clone, PartialEq, Eq)]
3043 pub struct Consult {
3044 /// The route asked, receiving the value under the field's
3045 /// [`name`](Field::name) — the same name a submit would send it under.
3046 pub action: Action,
3047 /// How long the value must stand still before asking.
3048 ///
3049 /// A property of the question rather than of the renderer, which is the
3050 /// half quasi-tui went without: its runtime fires a
3051 /// [`writes`](Field::writes) write on every keystroke and its own comment
3052 /// says why it cannot do otherwise — "nothing in the description says a
3053 /// delay is allowed", and a number the renderer picked would be a number
3054 /// the webview and the terminal disagree about.
3055 pub after: std::time::Duration,
3056 /// How many characters the value must carry before the route is asked at
3057 /// all, counted in `char`s.
3058 ///
3059 /// A floor rather than a wait, and the two are not the same question: a
3060 /// debounce says the value has stopped moving, and this says there is
3061 /// enough of it to be worth asking about. Both measured sites spell both
3062 /// numbers and they differ: MNW's tag typeahead waits 150ms and refuses
3063 /// under two characters, its search box waits 200ms and refuses the same.
3064 ///
3065 /// It is the description's for the reason the wait is: a route answering
3066 /// `a%` over every project and item in the catalogue is the expensive
3067 /// question, and neither renderer nor route can know how expensive without
3068 /// being told. Both MNW suggestion routes guard emptiness and nothing else,
3069 /// so a described field that dropped this would ask them on the first
3070 /// letter, which is the query the floor exists to refuse.
3071 ///
3072 /// `0`, the default, asks whatever is there.
3073 pub at_least: usize,
3074 /// Other controls whose values ride along with the question, named the way
3075 /// a submit names them.
3076 ///
3077 /// `N8`. A box that asks about its own value alone is the common case and
3078 /// leaves this empty. MNW's discover search is the other one: the results
3079 /// it re-reads are the results *under the current filters*, so the question
3080 /// is not "what matches `q`" but "what matches `q`, in this mode, sorted
3081 /// this way, under these tags". Asked without them the route answers about
3082 /// a screen the user is not looking at.
3083 ///
3084 /// # Named by field name, not by a group
3085 ///
3086 /// The shipped markup groups them with `hx-include=".discover-filter"`, a
3087 /// CSS class, which is the option this rejects. A class is a fact about the
3088 /// document and a terminal has none; naming the fields names something
3089 /// every host already has, since [`Field::name`] is what a submit sends a
3090 /// value under and what a [`Consult`] sends the typed value under. So a
3091 /// route reads all of them out of one bag, under the names it already
3092 /// expects, whichever host asked.
3093 ///
3094 /// A name that no field on the screen carries sends nothing. That is a
3095 /// description bug and every renderer treats it as one value fewer rather
3096 /// than an error, for [`Screen::replace`]'s reason: a miss is worth being
3097 /// able to see, and is not worth refusing to draw a screen over.
3098 ///
3099 /// # On a region, this is what rides along from *outside* it
3100 ///
3101 /// [`Slot::consults`] gathers the questions the region contains, by
3102 /// containment rather than by name, so the common region consult leaves
3103 /// this empty too. A dial that sits outside the panel it recomputes is what
3104 /// names itself here, and it names itself the same way: by
3105 /// [`Field::name`], which is the one address every host has.
3106 pub sends: Vec<String>,
3107 }
3108
3109 impl Consult {
3110 /// What the four measured sites already wait, and what
3111 /// [`Field::consults`] uses when nothing else is said.
3112 ///
3113 /// All four MNW wizard fields spell `delay:500ms` by hand, so this is the
3114 /// corpus' own number rather than a chosen one.
3115 pub const SETTLES: std::time::Duration = std::time::Duration::from_millis(500);
3116
3117 /// Ask this route once the value has stood still for [`SETTLES`](Self::SETTLES).
3118 #[must_use]
3119 pub const fn new(action: Action) -> Self {
3120 Self {
3121 action,
3122 after: Self::SETTLES,
3123 at_least: 0,
3124 sends: Vec::new(),
3125 }
3126 }
3127
3128 /// Ask this route the moment the value moves.
3129 ///
3130 /// [`new`](Self::new)'s wait is a typing wait, and a control chosen in one
3131 /// gesture has nothing to wait out: a select is at its next value or its
3132 /// last one, never on the way between them. Zero is a real number here
3133 /// rather than a missing one, which is why this is a constructor and not
3134 /// an `Option`.
3135 ///
3136 /// Not the default for a discrete kind. The description says the wait, the
3137 /// renderer says the event ([`Field::consults`]), and a kind deciding the
3138 /// wait would be the third party to the same question -- a radio group
3139 /// inside a [`Slot::consults`] already waits on purpose.
3140 #[must_use]
3141 pub const fn at_once(action: Action) -> Self {
3142 Self::new(action).after(std::time::Duration::ZERO)
3143 }
3144
3145 /// Ask this route once the value has stood still for `after`.
3146 #[must_use]
3147 pub const fn after(mut self, after: std::time::Duration) -> Self {
3148 self.after = after;
3149 self
3150 }
3151
3152 /// Whether a value has enough in it to be worth asking about.
3153 ///
3154 /// The floor stated once rather than in each renderer: three copies of
3155 /// `chars().count() >= n` are three places to disagree about whether the
3156 /// count is bytes or characters, and the terminal is the host where that
3157 /// difference is a wrong answer rather than a slow one.
3158 ///
3159 /// Says nothing about the wait, which is the half a renderer cannot be
3160 /// spared: htmx delays it, the TUI hands it to the host, egui keys it on a
3161 /// deadline it repaints for.
3162 ///
3163 /// On a [`Slot::consults`] the value is the one that just moved, never the
3164 /// gathered set. A floor over a set has no meaning a reader could predict —
3165 /// five dials holding one character each are not five characters — and the
3166 /// browser says the same thing in its own words, where the filter reads
3167 /// `event.target.value`.
3168 #[must_use]
3169 pub fn asks_about(&self, value: &str) -> bool {
3170 value.chars().count() >= self.at_least
3171 }
3172
3173 /// Do not ask at all until the value carries `chars` characters.
3174 ///
3175 /// See [`at_least`](Self::at_least). Combines with
3176 /// [`after`](Self::after): the value has to be long enough *and* to have
3177 /// stood still.
3178 #[must_use]
3179 pub const fn at_least(mut self, chars: usize) -> Self {
3180 self.at_least = chars;
3181 self
3182 }
3183
3184 /// Send these other fields' values with the question.
3185 ///
3186 /// See [`sends`](Self::sends). The typed value goes under the asking
3187 /// field's own name whether or not this is set, so a call names only what
3188 /// it needs *beside* that.
3189 ///
3190 /// Replaces rather than appends, which is the reading a builder has to
3191 /// have: a second call is a correction of the set, and a set built up over
3192 /// two calls would depend on where in the chain each one sat.
3193 #[must_use]
3194 pub fn sending(mut self, names: impl IntoIterator<Item = impl Into<String>>) -> Self {
3195 self.sends = names.into_iter().map(Into::into).collect();
3196 self
3197 }
3198 }
3199
3200 impl Field {
3201 /// A plain optional field of the given kind.
3202 pub fn new(kind: layout::FieldKind, name: impl Into<String>, label: impl Into<String>) -> Self {
3203 Self {
3204 kind,
3205 name: name.into(),
3206 upper_name: None,
3207 label: label.into(),
3208 hint: None,
3209 error: None,
3210 note: None,
3211 placeholder: None,
3212 options: Vec::new(),
3213 themes: Vec::new(),
3214 follows: None,
3215 accept: Vec::new(),
3216 multiple: false,
3217 required: false,
3218 max_length: None,
3219 min: None,
3220 max: None,
3221 step: None,
3222 curve: Curve::Linear { step: None },
3223 unit: None,
3224 extended: false,
3225 as_instant: false,
3226 // `Fill`, matching `Column::new` and matching what every renderer
3227 // did before this member existed. A default of `Content` would have
3228 // been the tidier reading and would have silently narrowed every
3229 // described field in every app on the day it landed, which is the
3230 // one thing an additive member must not do.
3231 width: layout::Width::Fill,
3232 value: None,
3233 upper_value: None,
3234 writes: None,
3235 consults: Vec::new(),
3236 suggests: None,
3237 keeps_value: false,
3238 repeats: None,
3239 revealed_by: None,
3240 }
3241 }
3242
3243 /// A file field taking the given accept list.
3244 ///
3245 /// [`layout::Field::upload`]'s counterpart and it earns a constructor for
3246 /// the same reason: a file field with no list is not broken, it is one that
3247 /// takes anything, so an accidental omission looks exactly like a deliberate
3248 /// choice unless the list is an argument. Pass an empty slice to mean any
3249 /// file and mean it.
3250 #[must_use]
3251 pub fn upload(
3252 name: impl Into<String>,
3253 label: impl Into<String>,
3254 accept: impl IntoIterator<Item = Accepted>,
3255 ) -> Self {
3256 Self {
3257 accept: accept.into_iter().collect(),
3258 ..Self::new(layout::FieldKind::File, name, label)
3259 }
3260 }
3261
3262 /// Say that what is in this control is the reader's and survives a redraw.
3263 ///
3264 /// See [`keeps_value`](Self::keeps_value). The measured case is a box the
3265 /// reader types into whose surrounding region is swapped by something else
3266 /// on the screen.
3267 #[must_use]
3268 pub const fn keeping_value(mut self) -> Self {
3269 self.keeps_value = true;
3270 self
3271 }
3272
3273 /// Several files at once, not one.
3274 #[must_use]
3275 pub const fn many(mut self) -> Self {
3276 self.multiple = true;
3277 self
3278 }
3279
3280 /// Whether anything in [`accept`](Self::accept) names a media family.
3281 ///
3282 /// [`layout::Field::accepts_media`] asked of the owned form: the question a
3283 /// renderer asks before it keeps room for a preview, answered of the whole
3284 /// list because a dropzone taking `image/*,video/*` has two families and
3285 /// still has a disclosure to offer.
3286 #[must_use]
3287 pub fn accepts_media(&self) -> bool {
3288 self.accept
3289 .iter()
3290 .any(|one| one.as_layout().family().is_some())
3291 }
3292
3293 /// How much of its row this asks for.
3294 #[must_use]
3295 pub const fn width(mut self, width: layout::Width) -> Self {
3296 self.width = width;
3297 self
3298 }
3299
3300 /// Changing this writes, without waiting for a submit.
3301 #[must_use]
3302 pub fn writes(mut self, action: Action) -> Self {
3303 self.writes = Some(action);
3304 self
3305 }
3306
3307 /// Ask this route about the value as the reader works on it, once it has
3308 /// stood still for [`Consult::SETTLES`].
3309 ///
3310 /// Not only while it is typed: a select, a radio or a slider asks the same
3311 /// question, and [`consulting`](Self::consulting) with
3312 /// [`Consult::after`] of zero is what one of those usually wants, since its
3313 /// value was complete the moment it moved.
3314 ///
3315 /// Point the answer somewhere with [`Action::replacing`]; what it contains
3316 /// is between the route and the renderer. Use [`consulting`](Self::consulting)
3317 /// for a different interval, or for a floor under which nothing is asked at
3318 /// all — see [`Consult::at_least`].
3319 ///
3320 /// Adds a question rather than replacing the ones already asked, which is
3321 /// what a field with several of them needs and what a builder that took the
3322 /// last call would make unwritable.
3323 #[must_use]
3324 pub fn consults(mut self, action: Action) -> Self {
3325 self.consults.push(Consult::new(action));
3326 self
3327 }
3328
3329 /// [`consults`](Self::consults) with the wait, the floor or what rides
3330 /// along named.
3331 ///
3332 /// Adds, for [`consults`](Self::consults)' reason. Chain it twice for a box
3333 /// that asks two routes at two rates, which is what MNW's discover search
3334 /// does.
3335 #[must_use]
3336 pub fn consulting(mut self, consult: Consult) -> Self {
3337 self.consults.push(consult);
3338 self
3339 }
3340
3341 /// This field owns a suggestion list, filled from this route once the value
3342 /// has stood still for [`Consult::SETTLES`].
3343 ///
3344 /// [`suggests`](Self::suggests). Use [`suggesting`](Self::suggesting) to
3345 /// name the wait, the floor, or what rides along — both measured MNW sites
3346 /// spell a floor, and a suggestion route asked on the first letter is the
3347 /// `ILIKE 'a%'` over the whole catalogue the floor exists to refuse.
3348 ///
3349 /// Replaces rather than adds, unlike [`consults`](Self::consults): a field
3350 /// owns one list, and a second call is a description changing its mind
3351 /// rather than asking a second question.
3352 #[must_use]
3353 pub fn suggests(mut self, action: Action) -> Self {
3354 self.suggests = Some(Consult::new(action));
3355 self
3356 }
3357
3358 /// [`suggests`](Self::suggests) with the wait, the floor or what rides
3359 /// along named.
3360 #[must_use]
3361 pub fn suggesting(mut self, consult: Consult) -> Self {
3362 self.suggests = Some(consult);
3363 self
3364 }
3365
3366 /// This question is answered zero or more times, in slots the reader adds
3367 /// and removes.
3368 ///
3369 /// See [`Repeat`], which carries the answers standing now, the floor and
3370 /// ceiling on how many there may be, and what the two controls are called.
3371 #[must_use]
3372 pub fn repeating(mut self, repeats: Repeat) -> Self {
3373 self.repeats = Some(repeats);
3374 self
3375 }
3376
3377 /// This question only applies while another control holds a value.
3378 ///
3379 /// [`revealed_by`](Self::revealed_by). The counterpart of
3380 /// [`Slot::revealed_by`], for the one question inside a form rather than
3381 /// for a block of several things, and it is answered the same way by every
3382 /// renderer.
3383 ///
3384 /// Calling it twice replaces the condition, which is the reading every
3385 /// builder here has: the second call is a correction.
3386 ///
3387 /// ```
3388 /// use quasi_router::{Field, Reveal, layout::FieldKind};
3389 ///
3390 /// let zone = Field::new(FieldKind::Text, "timezone", "Anchored to")
3391 /// .revealed_by(Reveal::holding("tz_kind", "local"));
3392 ///
3393 /// assert!(zone.revealed(Some("local")));
3394 /// assert!(!zone.revealed(Some("relative")));
3395 /// assert_eq!(zone.watches(), Some("tz_kind"));
3396 /// ```
3397 #[must_use]
3398 pub fn revealed_by(mut self, reveal: Reveal) -> Self {
3399 self.revealed_by = Some(Box::new(reveal));
3400 self
3401 }
3402
3403 /// Whether this question applies, given what its control is holding.
3404 ///
3405 /// `true` for a field that named no condition, which is nearly all of
3406 /// them. [`Slot::revealed`]'s counterpart, and what a renderer does with
3407 /// `false` is the renderer's on the same terms.
3408 #[must_use]
3409 pub fn revealed(&self, held: Option<&str>) -> bool {
3410 self.revealed_by
3411 .as_ref()
3412 .is_none_or(|reveal| reveal.satisfied_by(held))
3413 }
3414
3415 /// The name of the control this question is watching, if it watches one.
3416 #[must_use]
3417 pub fn watches(&self) -> Option<&str> {
3418 self.revealed_by
3419 .as_ref()
3420 .map(|reveal| reveal.control.as_str())
3421 }
3422
3423 /// One slot of a repeating question, as an ordinary field.
3424 ///
3425 /// The name is [`Repeat::at`] of this field's, the label is
3426 /// [`Repeat::ordinal`] of this field's, and the value and the error are
3427 /// that slot's own. Everything else is the question's and is carried
3428 /// through unchanged, which is the point: a renderer draws a slot with
3429 /// whatever it already does to a field, and there is no second field
3430 /// emitter anywhere in the stack.
3431 ///
3432 /// [`repeats`](Self::repeats) is cleared on the way out, so a renderer that
3433 /// loops over the slots cannot recurse into them.
3434 ///
3435 /// A slot past the end of what the description offered is an empty box
3436 /// under the right name, which is exactly what a slot the reader has just
3437 /// added is. That is why this takes an index rather than an
3438 /// [`Instance`]: the reader's slots outnumber the description's the moment
3439 /// the add control is pressed, and both are drawn the same way.
3440 ///
3441 /// Answered for a field that repeats nothing too, and the answer is the
3442 /// field itself with its name indexed. Nothing calls it that way, and
3443 /// refusing would make every renderer branch before it could loop.
3444 #[must_use]
3445 pub fn instance(&self, at: usize) -> Self {
3446 let held = self
3447 .repeats
3448 .as_ref()
3449 .and_then(|repeat| repeat.instances.get(at));
3450 Self {
3451 name: Repeat::at(&self.name, at),
3452 label: held
3453 .and_then(|slot| slot.named.clone())
3454 .unwrap_or_else(|| Repeat::ordinal(&self.label, at)),
3455 value: held.and_then(|slot| slot.value.clone()),
3456 error: held.and_then(|slot| slot.error.clone()),
3457 repeats: None,
3458 // The condition belongs to the question, and a renderer that has
3459 // reached the slots has already answered it once for the whole
3460 // group. A slot carrying it would have every renderer asking the
3461 // same question once per box.
3462 revealed_by: None,
3463 ..self.clone()
3464 }
3465 }
3466
3467 /// One named question of one slot, as an ordinary field.
3468 ///
3469 /// [`instance`](Self::instance) one level finer, and for its reason: a
3470 /// renderer draws a part with whatever it already does to a field, so a
3471 /// slot that is several questions needs no second emitter either. The name
3472 /// is [`Repeat::part_at`], the label is the part's own and is not numbered,
3473 /// and the value and the error are the part's.
3474 ///
3475 /// [`repeats`](Self::repeats) is cleared for
3476 /// [`instance`](Self::instance)'s reason, and so is
3477 /// [`revealed_by`](Self::revealed_by).
3478 #[must_use]
3479 pub fn instance_part(&self, at: usize, part: usize) -> Self {
3480 let repeat = self.repeats.as_ref();
3481 let question = repeat.and_then(|repeat| repeat.parts.get(part));
3482 let answered = repeat
3483 .and_then(|repeat| repeat.instances.get(at))
3484 .map(|slot| slot.part(part))
3485 .unwrap_or_default();
3486 Self {
3487 name: Repeat::part_at(
3488 &self.name,
3489 at,
3490 question.map_or("", |question| question.name.as_str()),
3491 ),
3492 label: question.map_or_else(String::new, |question| question.label.clone()),
3493 value: answered.value,
3494 error: answered.error,
3495 repeats: None,
3496 revealed_by: None,
3497 ..self.clone()
3498 }
3499 }
3500
3501 /// Every field one slot draws: the slot itself, or one per part.
3502 ///
3503 /// The hinge the three renderers turn on, so that none of them carries the
3504 /// branch. A slot of an ordinary repeating question is one field and this
3505 /// answers one; a slot of a grouped one is [`Repeat::parts`] fields in
3506 /// their order. Four walks in the TUI alone read this -- the height, the
3507 /// drawing, the caret and the submitted names -- and they agree because
3508 /// they ask the same question rather than each looping the parts.
3509 #[must_use]
3510 pub fn instance_fields(&self, at: usize) -> Vec<Self> {
3511 match self.repeats.as_ref() {
3512 Some(repeat) if repeat.grouped() => (0..repeat.parts.len())
3513 .map(|part| self.instance_part(at, part))
3514 .collect(),
3515 _ => vec![self.instance(at)],
3516 }
3517 }
3518
3519 /// How many slots this question stands in right now, as described.
3520 ///
3521 /// `1` for the ordinary field, which is the count a caller with no interest
3522 /// in repetition can loop over without asking whether there is any.
3523 #[must_use]
3524 pub fn slots(&self) -> usize {
3525 self.repeats.as_ref().map_or(1, Repeat::standing)
3526 }
3527
3528 /// This field as a control asks it: a question, and nothing that writes.
3529 ///
3530 /// What every renderer draws for a member of [`Act::asks`]. A field there
3531 /// is answered by the press that asked for it, so a
3532 /// [`writes`](Self::writes) route on it would fire a second write for the
3533 /// same value. [`consults`](Self::consults) survives: asking whether a tag
3534 /// slug is taken is a question about the value, not a write of it.
3535 #[must_use]
3536 pub fn as_asked(&self) -> Self {
3537 Self {
3538 writes: None,
3539 ..self.clone()
3540 }
3541 }
3542
3543 /// A select offering the given options.
3544 /// The options offered, appended to the ones already there.
3545 ///
3546 /// The accreting half of [`select`](Self::select) and [`radio`](Self::radio),
3547 /// which take the whole list as an argument. Beside them for the reason
3548 /// [`Table::column`] sits beside `Table::new`: a caller with no expression
3549 /// to hold a list in still has to be able to offer options, and `options`
3550 /// was reachable only by assigning the field.
3551 ///
3552 /// Says nothing about the kind. A kind that offers no options ignores them,
3553 /// which is what [`layout::FieldKind::offers_options`] already decides.
3554 #[must_use]
3555 pub fn options(mut self, options: impl IntoIterator<Item = Choice>) -> Self {
3556 self.options.extend(options);
3557 self
3558 }
3559
3560 /// Offer one more option, chaining.
3561 ///
3562 /// The accreting half of [`Self::options`], which takes the whole list. The
3563 /// fifth constructor of exactly this shape, after [`Table::column`],
3564 /// [`Row::cell`], [`Self::options`] itself and [`Node::figure`]: a caller
3565 /// building its options one at a time has no expression to hold a list in.
3566 /// MNW's repository bar is the site -- its ref chooser offers a branch or a
3567 /// tag per ref, and which of the two decides only the label.
3568 #[must_use]
3569 pub fn option(mut self, option: Choice) -> Self {
3570 self.options.push(option);
3571 self
3572 }
3573
3574 pub fn select(name: impl Into<String>, label: impl Into<String>, options: Vec<Choice>) -> Self {
3575 Self {
3576 options,
3577 ..Self::new(layout::FieldKind::Select, name, label)
3578 }
3579 }
3580
3581 /// A radio group offering the given options.
3582 pub fn radio(name: impl Into<String>, label: impl Into<String>, options: Vec<Choice>) -> Self {
3583 Self {
3584 options,
3585 ..Self::new(layout::FieldKind::Radio, name, label)
3586 }
3587 }
3588
3589 /// A theme picker over the themes the host resolved.
3590 ///
3591 /// A constructor for [`layout::Field::theme`]'s reason: the list is the one
3592 /// thing this kind takes that a call site can get wrong by *substitution*,
3593 /// since [`options`](Self::options) is right there and reads as if it would
3594 /// work. A renderer walking `options` for a theme picker draws an empty
3595 /// control.
3596 ///
3597 /// [`following`](Self::following) is a builder rather than a fourth
3598 /// argument: a picker with no follow-the-system row is a real picker.
3599 pub fn theme(
3600 name: impl Into<String>,
3601 label: impl Into<String>,
3602 themes: Vec<ThemeChoice>,
3603 ) -> Self {
3604 Self {
3605 themes,
3606 ..Self::new(layout::FieldKind::Theme, name, label)
3607 }
3608 }
3609
3610 /// Offer these themes, chaining.
3611 ///
3612 /// The accreting half of [`theme`](Self::theme), which takes the whole list
3613 /// as an argument, and the same gap [`options`](Self::options) fills beside
3614 /// [`select`](Self::select): a caller with no expression to hold a list in
3615 /// still has to be able to offer themes. GoingsOn's Appearance section is
3616 /// the site.
3617 ///
3618 /// Says nothing about the kind, exactly as `options` does not. A renderer
3619 /// reads these only for [`layout::FieldKind::Theme`].
3620 #[must_use]
3621 pub fn themes(mut self, themes: impl IntoIterator<Item = ThemeChoice>) -> Self {
3622 self.themes.extend(themes);
3623 self
3624 }
3625
3626 /// The same picker, offering a row that tracks the ambient mode.
3627 ///
3628 /// The [`Choice`] carries the value the app's own store spells it with —
3629 /// `makeover::FOLLOW` for every store in the family today, and none of them
3630 /// is obliged to keep it.
3631 #[must_use]
3632 pub fn following(mut self, follow: Choice) -> Self {
3633 self.follows = Some(follow);
3634 self
3635 }
3636
3637 /// A bounded number the user drags across its whole extent.
3638 ///
3639 /// The bounds are arguments for [`layout::Field::range`]'s reason: they are
3640 /// not a rule the answer is checked against, they are the control, so a
3641 /// range that forgot them has nothing to slide across.
3642 pub fn range(
3643 name: impl Into<String>,
3644 label: impl Into<String>,
3645 min: impl Into<String>,
3646 max: impl Into<String>,
3647 ) -> Self {
3648 Self {
3649 min: Some(min.into()),
3650 max: Some(max.into()),
3651 ..Self::new(layout::FieldKind::Range, name, label)
3652 }
3653 }
3654
3655 /// One question with two ends, taking the name each end submits under.
3656 ///
3657 /// The names are arguments for [`layout::Field::interval`]'s reason: an
3658 /// interval built without the second one has an upper end with nowhere to be
3659 /// submitted, and nothing downstream can invent a name for it.
3660 ///
3661 /// The extent stays optional, unlike [`range`](Self::range)'s. An interval's
3662 /// bounds are a rule each end is checked against rather than the control, so
3663 /// a missing one is an open end rather than a control with nothing to slide
3664 /// across.
3665 pub fn interval(
3666 name: impl Into<String>,
3667 upper_name: impl Into<String>,
3668 label: impl Into<String>,
3669 ) -> Self {
3670 Self {
3671 upper_name: Some(upper_name.into()),
3672 ..Self::new(layout::FieldKind::Interval, name, label)
3673 }
3674 }
3675
3676 /// The name the upper end submits under, for an interval built by hand.
3677 ///
3678 /// [`interval`](Self::interval) is still the entry point a hand-written
3679 /// caller should take, and its doc says why the names are arguments there.
3680 /// This exists because a description cannot reach a three-argument
3681 /// constructor: `quasi-declare`'s `field` production says a kind, a name
3682 /// and a label, and everything else about a field is a setting in its body.
3683 /// audiofiles' filter panel is the site -- six intervals, and without this
3684 /// the only described spelling was the whole `Field` written out as an
3685 /// aggregate, which is the expression the form exists to refuse.
3686 ///
3687 /// Pairs with [`upper_value`](Self::upper_value), which names the same end's
3688 /// answer.
3689 #[must_use]
3690 pub fn upper_name(mut self, name: impl Into<String>) -> Self {
3691 self.upper_name = Some(name.into());
3692 self
3693 }
3694
3695 /// The granularity the value moves in.
3696 ///
3697 /// A builder rather than a fourth argument to [`range`](Self::range): the
3698 /// host's own granularity is a real answer, and a stepped
3699 /// [`layout::FieldKind::Number`] wants this too.
3700 #[must_use]
3701 pub fn step(mut self, step: impl Into<String>) -> Self {
3702 let step = step.into();
3703 // Routed to wherever this kind keeps its granularity, rather than made
3704 // two builders. A range's lives on its curve as of makeover-layout
3705 // 0.32.0 and a typed value's stays here, and a call site saying "this
3706 // moves in steps of 0.001" means the same thing either way.
3707 if self.kind == layout::FieldKind::Range {
3708 self.curve = std::mem::take(&mut self.curve).with_step(Some(step));
3709 } else {
3710 self.step = Some(step);
3711 }
3712 self
3713 }
3714
3715 /// How this slider's position becomes its value.
3716 ///
3717 /// [`layout::FieldKind::Range`]'s, and it carries the granularity with it:
3718 /// passing a curve replaces whatever [`step`](Self::step) had set.
3719 #[must_use]
3720 pub fn curve(mut self, curve: Curve) -> Self {
3721 self.curve = curve;
3722 self
3723 }
3724
3725 /// What the number is measured in.
3726 ///
3727 /// The symbol alone -- `s`, not `(s)` and not ` s`. Where it is drawn and
3728 /// how it is spaced is the renderer's, which is why the description carries
3729 /// neither.
3730 #[must_use]
3731 pub fn unit(mut self, unit: impl Into<String>) -> Self {
3732 self.unit = Some(unit.into());
3733 self
3734 }
3735
3736 /// The form refuses to submit without it.
3737 #[must_use]
3738 pub fn required(mut self) -> Self {
3739 self.required = true;
3740 self
3741 }
3742
3743 /// The wall-clock value is submitted as the moment it names.
3744 ///
3745 /// See [`as_instant`](Self::as_instant) for what it asks and who answers
3746 /// it. A [`layout::FieldKind::DateTime`]'s; sayable and ignored elsewhere.
3747 #[must_use]
3748 pub fn submits_instant(mut self) -> Self {
3749 self.as_instant = true;
3750 self
3751 }
3752
3753 /// Standing help, shown whether or not anything is wrong.
3754 #[must_use]
3755 pub fn hint(mut self, hint: impl Into<String>) -> Self {
3756 self.hint = Some(hint.into());
3757 self
3758 }
3759
3760 /// What is wrong with the value now.
3761 #[must_use]
3762 pub fn error(mut self, error: impl Into<String>) -> Self {
3763 self.error = Some(error.into());
3764 self
3765 }
3766
3767 /// Ghost text shown while the field is empty.
3768 ///
3769 /// Was reachable only by assigning the field, which is
3770 /// The missing-builder defect met again and on the largest field in the
3771 /// vocabulary: 54 sites across the three shape trees fall out of a builder
3772 /// chain to write it, which is more than any other member here.
3773 ///
3774 /// Not [`hint`](Self::hint), and the two are worth keeping apart. A hint is
3775 /// standing help that survives the reader typing; a placeholder is a sample
3776 /// answer that disappears the moment they do, so it cannot carry anything
3777 /// they will need later. A renderer that had only one of them would have to
3778 /// pick which behaviour to give it.
3779 ///
3780 #[must_use]
3781 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
3782 self.placeholder = Some(placeholder.into());
3783 self
3784 }
3785
3786 /// The longest the value may be, in characters.
3787 ///
3788 /// The description carries the rule and the renderer emits its host's
3789 /// idiom; deciding that a value is wrong stays with whoever validated it,
3790 /// which is [`max_length`](Self::max_length)'s own contract.
3791 #[must_use]
3792 pub const fn limited_to(mut self, characters: u32) -> Self {
3793 self.max_length = Some(characters);
3794 self
3795 }
3796
3797 /// The extent a value is accepted within, both ends.
3798 ///
3799 /// One builder for the pair rather than two, on
3800 /// [`Row::ticking`]'s grounds: a bound is only meaningful against the
3801 /// other one, and two setters can disagree. [`Field::range`] already takes
3802 /// both together for exactly this reason, and this is how every other kind
3803 /// reaches what a range gets from its constructor.
3804 ///
3805 /// Written the way the host writes a value, which is what
3806 /// [`min`](Self::min) says: these are strings because a date bound and a
3807 /// number bound are the same fact about a field and only one of them is a
3808 /// number.
3809 ///
3810 #[must_use]
3811 pub fn within(mut self, min: impl Into<String>, max: impl Into<String>) -> Self {
3812 self.min = Some(min.into());
3813 self.max = Some(max.into());
3814 self
3815 }
3816
3817 /// The lowest value accepted, leaving the upper end open.
3818 ///
3819 /// [`within`](Self::within) is the ordinary spelling and sets both. This
3820 /// exists because an open end is a real answer rather than a missing one --
3821 /// [`Field::interval`] says so in as many words -- and a field with a floor
3822 /// and no ceiling cannot be written by a builder that insists on the pair.
3823 #[must_use]
3824 pub fn at_least(mut self, min: impl Into<String>) -> Self {
3825 self.min = Some(min.into());
3826 self
3827 }
3828
3829 /// The highest value accepted, leaving the lower end open.
3830 ///
3831 /// See [`at_least`](Self::at_least).
3832 #[must_use]
3833 pub fn at_most(mut self, max: impl Into<String>) -> Self {
3834 self.max = Some(max.into());
3835 self
3836 }
3837
3838 /// Put the field behind a "more options" disclosure.
3839 ///
3840 /// Presence is the whole of it, which is why it takes no argument: a
3841 /// description says a field is secondary or says nothing, and
3842 /// `extended(false)` would be a way of writing the default twice.
3843 #[must_use]
3844 pub const fn extended(mut self) -> Self {
3845 self.extended = true;
3846 self
3847 }
3848
3849 /// What the answer the user has given costs, in the tone it deserves.
3850 ///
3851 /// Not a validation failure: the field stays valid and submittable. See
3852 /// [`note`](Self::note).
3853 #[must_use]
3854 pub fn note(mut self, tone: layout::Tone, note: impl Into<String>) -> Self {
3855 self.note = Some((tone, note.into()));
3856 self
3857 }
3858
3859 /// Whether the field is currently reporting a problem.
3860 #[must_use]
3861 pub fn invalid(&self) -> bool {
3862 self.error.is_some()
3863 }
3864
3865 /// Put this back in the box when the form is offered again.
3866 ///
3867 /// A [`layout::FieldKind::Secret`] keeps `None` whatever it is handed. A
3868 /// password that comes back down the wire is a password in a page, in a
3869 /// proxy log and in a browser cache, and the field kind exists to say so.
3870 /// Silently rather than by a `Result`, because there is no answer a caller
3871 /// could give that would make echoing it right.
3872 #[must_use]
3873 pub fn value(mut self, value: impl Into<String>) -> Self {
3874 if self.kind != layout::FieldKind::Secret {
3875 self.value = Some(value.into());
3876 }
3877 self
3878 }
3879
3880 /// Put this back in the upper box of an interval when the form is offered
3881 /// again.
3882 ///
3883 /// [`value`](Self::value)'s counterpart and it refuses a
3884 /// [`layout::FieldKind::Secret`] on the same terms, though no secret is an
3885 /// interval: the guarantee is written where the value is set rather than
3886 /// where the kinds happen not to overlap today.
3887 #[must_use]
3888 pub fn upper_value(mut self, value: impl Into<String>) -> Self {
3889 if self.kind != layout::FieldKind::Secret {
3890 self.upper_value = Some(value.into());
3891 }
3892 self
3893 }
3894
3895 /// Re-offer whatever was submitted under this field's name.
3896 ///
3897 /// What a refused write calls, with the [`Params`](crate::Params) it was
3898 /// refusing. A name with nothing under it stays empty, which is what an
3899 /// unticked checkbox and an untouched box both are.
3900 #[must_use]
3901 pub fn refilled(self, params: &crate::Params) -> Self {
3902 let filled = match params.get(&self.name) {
3903 Some(value) => {
3904 let value = value.to_owned();
3905 self.value(value)
3906 }
3907 None => self,
3908 };
3909 // An interval was submitted under two names, so re-offering it reads
3910 // both. Either end staying empty is a real answer rather than a
3911 // half-filled form: "over 120 BPM" has no upper end.
3912 let Some(upper_name) = filled.upper_name.clone() else {
3913 return filled;
3914 };
3915 match params.get(&upper_name) {
3916 Some(value) => {
3917 let value = value.to_owned();
3918 filled.upper_value(value)
3919 }
3920 None => filled,
3921 }
3922 }
3923
3924 /// Read this field as the description layer's own type.
3925 ///
3926 /// A callback rather than a return, because [`layout::Field`] holds its
3927 /// options as a slice and ours holds them as owned values, so the borrowed
3928 /// slice has to live somewhere for the duration of the read. Building it
3929 /// here means one allocation at the renderer's boundary instead of the
3930 /// borrow leaking into every caller's signature.
3931 pub fn with_layout<R>(&self, f: impl FnOnce(layout::Field<'_>) -> R) -> R {
3932 let options: Vec<layout::Choice<'_>> = self.options.iter().map(Choice::as_layout).collect();
3933 let themes: Vec<layout::ThemeChoice<'_>> =
3934 self.themes.iter().map(ThemeChoice::as_layout).collect();
3935 let accept: Vec<layout::Accepted<'_>> =
3936 self.accept.iter().map(Accepted::as_layout).collect();
3937 f(layout::Field {
3938 kind: self.kind,
3939 name: &self.name,
3940 upper_name: self.upper_name.as_deref(),
3941 label: &self.label,
3942 hint: self.hint.as_deref(),
3943 error: self.error.as_deref(),
3944 note: self.note.as_ref().map(|(t, n)| (*t, n.as_str())),
3945 placeholder: self.placeholder.as_deref(),
3946 options: &options,
3947 themes: &themes,
3948 follows: self.follows.as_ref().map(Choice::as_layout),
3949 accept: &accept,
3950 multiple: self.multiple,
3951 required: self.required,
3952 max_length: self.max_length,
3953 min: self.min.as_deref(),
3954 max: self.max.as_deref(),
3955 step: self.step.as_deref(),
3956 curve: self.curve.as_layout(),
3957 unit: self.unit.as_deref(),
3958 extended: self.extended,
3959 as_instant: self.as_instant,
3960 })
3961 }
3962 }
3963
3964 /// One run of source code, and what it is.
3965 ///
3966 /// The unit [`Node::Code`] is a list of, and the whole reason that node carries
3967 /// runs rather than a string: the app classified the source, and this is the
3968 /// classification crossing the seam. Decision `19d7602d`, 2026-09-02.
3969 ///
3970 /// Owned, for the reason stated at the top of this file: a router's answer
3971 /// outlives its handler, and the text here is usually built from state rather
3972 /// than found in it. [`layout::Syntax`] is `Copy`, so only the text allocates.
3973 ///
3974 /// # Why the text is carried and not an index
3975 ///
3976 /// A `(range, syntax)` pair over one source string would allocate less, and it
3977 /// was rejected: the ranges are byte offsets into a string the renderer must
3978 /// then re-slice, so every renderer would have to handle a range that does not
3979 /// land on a character boundary, and two of the three would get it wrong on the
3980 /// first file with a multi-byte character in it. The run carrying its own text
3981 /// cannot express that bug.
3982 #[derive(Debug, Clone, PartialEq, Eq, Default)]
3983 pub struct Lexeme {
3984 /// The characters, exactly as they appear in the source.
3985 ///
3986 /// Including whitespace. A classifier that strips it hands the renderer a
3987 /// file it cannot lay out, and indentation is most of what makes source
3988 /// readable.
3989 pub text: String,
3990 /// What the run is.
3991 pub syntax: layout::Syntax,
3992 }
3993
3994 impl Lexeme {
3995 /// A run of ordinary code.
3996 ///
3997 /// [`layout::Syntax::Plain`], which is what a classifier that ran and found
3998 /// nothing says, and what a host with no classifier at all says about a
3999 /// whole file.
4000 #[must_use]
4001 pub fn plain(text: impl Into<String>) -> Self {
4002 Self {
4003 text: text.into(),
4004 syntax: layout::Syntax::Plain,
4005 }
4006 }
4007
4008 /// A run of a stated class.
4009 #[must_use]
4010 pub fn new(text: impl Into<String>, syntax: layout::Syntax) -> Self {
4011 Self {
4012 text: text.into(),
4013 syntax,
4014 }
4015 }
4016 }
4017
4018 /// One column of a table, owned.
4019 ///
4020 /// The borrowed original is [`layout::Column`]. The `name` is both the heading
4021 /// and the address a cell is found by, which is what replaces addressing
4022 /// columns by position.
4023 /// No `Hash`, for the reason [`Tag`] and [`Field`] have none: it can hold an
4024 /// [`Action`], which holds [`Params`], which is a `Vec`.
4025 #[derive(Debug, Clone, PartialEq, Eq)]
4026 pub struct Column {
4027 /// The heading, and the name the cell is addressed by.
4028 pub name: String,
4029 /// How much room it asks for.
4030 pub width: layout::Width,
4031 /// What it is worth when room runs out.
4032 pub priority: layout::Priority,
4033 /// Which way the table is ordered by this column, if it is.
4034 pub sorted: Option<layout::Sort>,
4035 /// What pressing this heading calls.
4036 ///
4037 /// `makeover-layout` carries `Column::sortable`, a bare bool, because it
4038 /// cannot name an address; here the address *is* the sortability, so the
4039 /// two collapse into one field and cannot disagree.
4040 /// [`as_layout`](Self::as_layout) sets the bool from whether this is here.
4041 ///
4042 /// Reordering a table is a control that writes with no surrounding submit,
4043 /// which is `14612ed8`'s shape, and the renderer treats it the same way.
4044 pub reorder: Option<Action>,
4045 }
4046
4047 impl Column {
4048 /// A column that absorbs slack and drops after the optional ones.
4049 pub fn new(name: impl Into<String>) -> Self {
4050 Self {
4051 name: name.into(),
4052 width: layout::Width::Fill,
4053 priority: layout::Priority::Secondary,
4054 sorted: None,
4055 reorder: None,
4056 }
4057 }
4058
4059 /// Pressing this heading reorders the table.
4060 #[must_use]
4061 pub fn reorder(mut self, action: Action) -> Self {
4062 self.reorder = Some(action);
4063 self
4064 }
4065
4066 /// The table is currently ordered by this column, this way.
4067 #[must_use]
4068 pub const fn sorted(mut self, sort: layout::Sort) -> Self {
4069 self.sorted = Some(sort);
4070 self
4071 }
4072
4073 /// Set how much room it asks for.
4074 #[must_use]
4075 pub fn width(mut self, width: layout::Width) -> Self {
4076 self.width = width;
4077 self
4078 }
4079
4080 /// Set what it is worth when room runs out.
4081 #[must_use]
4082 pub fn priority(mut self, priority: layout::Priority) -> Self {
4083 self.priority = priority;
4084 self
4085 }
4086
4087 /// Borrow as the description layer's own type.
4088 #[must_use]
4089 pub fn as_layout(&self) -> layout::Column<'_> {
4090 layout::Column {
4091 name: &self.name,
4092 width: self.width,
4093 priority: self.priority,
4094 sortable: self.reorder.is_some(),
4095 sorted: self.sorted,
4096 }
4097 }
4098 }
4099
4100 /// Which region this is, owned.
4101 ///
4102 /// The borrowed original is [`layout::Region`], and only one member borrows:
4103 /// [`layout::Region::Handover`] carries a name the app owns and this crate never
4104 /// interprets.
4105 ///
4106 /// # Two members are gone, and a row says what they said
4107 ///
4108 /// `Split` was two panes side by side and `Columns` was a row of peers. Both
4109 /// were arrangement spelled as containment: they said nothing about scroll,
4110 /// depth or what may be inside, only how the room is divided. A [`Run`] whose
4111 /// members carry a [`layout::Width`] says the same thing and says more of it,
4112 /// because the row also carries a [`layout::Fallback`] and so can state what
4113 /// happens when the room runs out, which neither variant could. Split is a run
4114 /// of [`Content`](layout::Width::Content) then [`Fill`](layout::Width::Fill);
4115 /// Columns is a run of fills, which divide equally by that type's own rule.
4116 /// Ruled by Max 2026-09-07 on quasicoherent `b1d4c5d7`, built as `cf981aaa`.
4117 ///
4118 /// [`Sidebar`](Self::Sidebar) was measured with them and stays, because it is
4119 /// not the same kind of fact: it marks *which* region is the side of a
4120 /// [`layout::Arrangement::SidebarContent`] screen, which `quasi-tui` and
4121 /// `quasi-immediate` both match on to place it, and no width on a row member
4122 /// can say that. `Band`, `Pane`, `Group`, `TabGroup` and `Modal` are untouched
4123 /// for their own reasons: the first is a separate judgement, and the rest are
4124 /// containment, visibility and z-order rather than arrangement.
4125 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
4126 pub enum RegionKind {
4127 /// A full-width strip with a title slot and an actions cluster.
4128 Band,
4129 /// A persistent column beside the content, holding navigation.
4130 Sidebar,
4131 /// A region of content with its own scroll.
4132 Pane,
4133 /// Things that belong together, and nothing else.
4134 ///
4135 /// The block a [`Heading::Section`](layout::Heading::Section) names, which
4136 /// had no container until makeover-layout 0.27.0: a section heading is a
4137 /// leaf beside the things it names, so a description could say a section had
4138 /// started and never that one had ended. See [`layout::Region::Group`] for
4139 /// the count behind it.
4140 ///
4141 /// Claims no scroll and no depth of its own, which is what separates it from
4142 /// [`Pane`](Self::Pane) -- the member apps reached for instead, and the
4143 /// reason 28 of the 45 regions in the described screens were panes. A group
4144 /// in a pane is in a well; a group on the page is on the page.
4145 ///
4146 /// The heading is an ordinary node in the body rather than a field here, so
4147 /// a group of related toggles with no heading stays legal.
4148 Group,
4149 /// A set of panes, one visible at a time, and a strip that chooses
4150 /// between them.
4151 ///
4152 /// Says nothing about where the strip sits. A row over the panes, a column
4153 /// beside them, a wrapped run of links under them: all three are the same
4154 /// member drawn by a renderer that knows its host, the way the strip's
4155 /// overflow is. MNW's settings sub-nav is a column on a wide viewport and a
4156 /// wrapped row below the breakpoint, one component and one CSS breakpoint,
4157 /// and it is a tab group on every count this member makes.
4158 TabGroup,
4159 /// Content over a scrim, taking input until dismissed.
4160 ///
4161 /// A modal this screen *contains*, which is how a confirmation is drawn: it
4162 /// arrives with the screen and goes when the screen goes. The app-level one
4163 /// is [`Outcome::Over`](crate::Outcome::Over), which draws a whole screen
4164 /// over whatever is under it and is reachable from screens that know
4165 /// nothing about it.
4166 Modal,
4167 /// A place, and nothing else. The app fills it per host.
4168 ///
4169 /// Decision 4: the renderer hands the space over and the app puts a JS
4170 /// component, an egui closure or a TUI widget in it. The rejected
4171 /// alternative was giving the placeholder its own route and fetching a
4172 /// fragment for it, which is uniform on paper and wrong in currency: a byte
4173 /// payload is not what egui or a terminal wants.
4174 ///
4175 /// All three renderers offer the fill in their own currency:
4176 /// `Webview::with_fill` takes markup, `Tui::with_fill` takes a drawing that
4177 /// answers a height and paints into a rect, and `Immediate::with_fill`
4178 /// takes a closure over a `Ui`. Each is keyed by [`Slot::id`] rather than
4179 /// by the name here, since a screen of N rows each carrying a fill shares
4180 /// one name and has N ids.
4181 ///
4182 /// # A handover now, a widget eventually
4183 ///
4184 /// A behaviour with no described form takes this member rather than
4185 /// growing the vocabulary with members two of the three renderers could
4186 /// only degrade -- scrub, rate and chapters were the rejected shape -- and
4187 /// the surface is not ceded to the host permanently either. Once
4188 /// [`Widget`](Self::Widget) has grown, a behaviour already implemented
4189 /// once and tested behind a fill is a candidate to become one, and the
4190 /// media player is the first of them.
4191 ///
4192 /// The revisit trigger is the widget work rather than a count of consumers.
4193 ///
4194 /// # What separates it from [`Ceded`](Self::Ceded)
4195 ///
4196 /// A fill is owed here in every host's currency. See
4197 /// [`layout::Region::Handover`]; the short form is that a renderer handed
4198 /// one of these and given no fill is looking at a hole the app meant to
4199 /// fill, and should say so rather than draw an empty box.
4200 Handover {
4201 /// What the app calls it. Never interpreted here.
4202 name: String,
4203 },
4204 /// A place the app fills, that no host is owed a fill for.
4205 ///
4206 /// The other half of what was one opaque member. A chart, a waveform, a
4207 /// rendered picture of domain data: the app has ruled the space is not the
4208 /// description's to fill and is not going to become so, so a renderer with
4209 /// nothing to put here draws nothing and is right to.
4210 ///
4211 /// See [`layout::Region::Ceded`]. The distinction is worth two members
4212 /// because the two want opposite behaviour from a renderer that cannot
4213 /// fill them.
4214 Ceded {
4215 /// What the app calls it. Never interpreted here.
4216 name: String,
4217 },
4218 /// A named assembly of things the description already says.
4219 ///
4220 /// The third tier, and the one member that is a name *and* contents. See
4221 /// [`layout::Region::Widget`] for what separates it from the two either
4222 /// side of it; the short form is that a primitive has to be drawable by
4223 /// every host from scratch and a handover carries nothing under it, and a
4224 /// carousel is neither.
4225 ///
4226 /// The body is the assembly and it is ordinary description: a renderer that
4227 /// does not recognise the name walks it and draws primitives, which is why
4228 /// naming one costs no renderer release. Contrast
4229 /// [`Handover`](Self::Handover), whose body a renderer can draw but whose
4230 /// *fill* only the host has.
4231 Widget {
4232 /// What the assembly is called. Never interpreted here, and a renderer
4233 /// is free not to know it.
4234 name: String,
4235 },
4236 }
4237
4238 impl RegionKind {
4239 /// A place the app fills itself, and owes every host a fill for.
4240 ///
4241 /// Beside [`Slot::handover`], which builds the region and its id in one
4242 /// call. This is the kind on its own, which is what a caller holding the id
4243 /// separately needs: the three kinds that carry a name are the three a bare
4244 /// variant cannot spell.
4245 #[must_use]
4246 pub fn handover(name: impl Into<String>) -> Self {
4247 Self::Handover { name: name.into() }
4248 }
4249
4250 /// A place the app fills itself, that no host is owed a fill for.
4251 ///
4252 /// [`Slot::ceded`]'s kind on its own, for the same reason [`handover`] has
4253 /// one.
4254 ///
4255 /// [`handover`]: Self::handover
4256 #[must_use]
4257 pub fn ceded(name: impl Into<String>) -> Self {
4258 Self::Ceded { name: name.into() }
4259 }
4260
4261 /// Borrow as the description layer's own type.
4262 #[must_use]
4263 pub fn as_layout(&self) -> layout::Region<'_> {
4264 match self {
4265 Self::Band => layout::Region::Band,
4266 Self::Sidebar => layout::Region::Sidebar,
4267 Self::Pane => layout::Region::Pane,
4268 Self::Group => layout::Region::Group,
4269 Self::TabGroup => layout::Region::TabGroup,
4270 Self::Modal => layout::Region::Modal,
4271 Self::Handover { name } => layout::Region::Handover { name },
4272 Self::Ceded { name } => layout::Region::Ceded { name },
4273 Self::Widget { name } => layout::Region::Widget { name },
4274 }
4275 }
4276
4277 /// Whether the description can say anything about the contents.
4278 #[must_use]
4279 pub fn described(&self) -> bool {
4280 self.as_layout().described()
4281 }
4282
4283 /// How the region sits on what is behind it.
4284 #[must_use]
4285 pub fn depth(&self) -> layout::Depth {
4286 self.as_layout().depth()
4287 }
4288 }
4289
4290 /// A node, and what it is worth when there is not room for everything.
4291 ///
4292 /// The other half of "Any width, one answer" (`makeover-layout` 0.27.4). A
4293 /// renderer narrows by raising a cutoff over a declared total order, never by
4294 /// counting what fits, and the only placement that can declare its rank
4295 /// otherwise is a [`Column`]. So anything that is not a table hand-rolls its
4296 /// responsiveness, and hand-rolling means measuring, and measuring means
4297 /// keeping the measurement, which is how a layout becomes a function of the
4298 /// width you came from. audiofiles had four of those and one genuine
4299 /// measure-and-correct loop before its screens were described.
4300 ///
4301 /// # Why the rank sits here and not on the node
4302 ///
4303 /// [`Column`] already settled it: the priority is on the column and not on the
4304 /// cell's contents. The same node at [`layout::Priority::Essential`] in one
4305 /// band and [`layout::Priority::Optional`] in another is an ordinary thing to
4306 /// want, and a rank welded to the node could not say it.
4307 ///
4308 /// # Why not a wrapping [`Node`] member
4309 ///
4310 /// A `Node::Optional { priority, node }` would have been additive and needed
4311 /// no type change here, and it was refused. A renderer that has not learned a
4312 /// member draws it as one muted stand-in line, which is the right failure for
4313 /// an unknown leaf and exactly the wrong one for a member whose whole job is
4314 /// deciding what disappears: content wrapped for narrowing would go invisible
4315 /// in any renderer that had not caught up. Changing the type instead makes
4316 /// every renderer fail to compile, which is the failure that gets fixed.
4317 #[derive(Debug, Clone, PartialEq, Eq)]
4318 pub struct Ranked {
4319 /// The thing itself.
4320 pub node: Node,
4321 /// What it is worth when room runs out.
4322 ///
4323 /// [`layout::Priority::Essential`] by default, which never drops. That is
4324 /// what makes the rank additive at a call site rather than a re-reading of
4325 /// every screen in the tree.
4326 pub priority: layout::Priority,
4327 /// How much of the row it asks for.
4328 ///
4329 /// The other half of what a [`Column`] has always been able to say. A rank
4330 /// answers "what goes first when there is not room for everything"; this
4331 /// answers "how is the room divided while there is". A row of two panes
4332 /// where the left takes what it needs and the right absorbs the rest is
4333 /// the shape [`RegionKind`] used to spell as a variant, and the variants
4334 /// are gone because this says it (quasicoherent `cf981aaa`, Max's ruling
4335 /// on `b1d4c5d7`).
4336 ///
4337 /// **[`layout::Width::Content`] by default, because that is what a member
4338 /// of a run already drew.** A run is a flex row whose members have
4339 /// `min-width: min-content` and no grow, a terminal gives each what it
4340 /// asks for, and egui allocates what a galley needs. So every member
4341 /// written before this existed keeps its drawing, and a screen opts into
4342 /// dividing the room by saying so.
4343 ///
4344 /// **Read on a run, ignored in a body.** A [`Run`] is a row and its
4345 /// members divide a width between them; a [`Slot::body`] is a stack and
4346 /// its members each get the whole of it, so there is nothing for a width
4347 /// to divide. The field is on `Ranked` rather than on the run's own list
4348 /// because the rank is, and splitting one of the two into a second type
4349 /// would be two spellings of a member.
4350 pub width: layout::Width,
4351 }
4352
4353 impl Ranked {
4354 /// A node that never drops and takes what it needs.
4355 #[must_use]
4356 pub const fn new(node: Node) -> Self {
4357 Self {
4358 node,
4359 priority: layout::Priority::Essential,
4360 width: layout::Width::Content,
4361 }
4362 }
4363
4364 /// A node, and what it is worth.
4365 #[must_use]
4366 pub const fn worth(node: Node, priority: layout::Priority) -> Self {
4367 Self {
4368 node,
4369 priority,
4370 width: layout::Width::Content,
4371 }
4372 }
4373
4374 /// A node, what it is worth, and how much of the row it asks for.
4375 ///
4376 /// Beside [`Self::worth`] rather than replacing it: a width is the thing
4377 /// most members have no opinion about, and a constructor that demanded one
4378 /// would make every strip and every band say `Content` to mean "as before".
4379 #[must_use]
4380 pub const fn sized(node: Node, priority: layout::Priority, width: layout::Width) -> Self {
4381 Self {
4382 node,
4383 priority,
4384 width,
4385 }
4386 }
4387
4388 /// Whether a region narrowed to this cutoff still shows it.
4389 ///
4390 /// [`layout::Column::kept_at`] said for a region member, and it has to be
4391 /// the same comparison or a screen's table and the band above it would
4392 /// disappear at different points. A renderer raises the cutoff and asks
4393 /// this; nothing counts, so inserting a member changes what is emitted
4394 /// rather than changing which member vanishes.
4395 #[must_use]
4396 pub const fn kept_at(&self, cutoff: layout::Priority) -> bool {
4397 (self.priority as u8) >= (cutoff as u8)
4398 }
4399 }
4400
4401 /// The cutoffs a region narrows through, weakest first.
4402 ///
4403 /// The same sequence `makeover-tui`'s table keeps, and it is here rather than
4404 /// in each renderer so the three cannot drift into disagreeing about the order
4405 /// things go. [`layout::Priority`] is `#[non_exhaustive]`, so a tier added
4406 /// upstream has to be added here in its place in the sequence: the cost of
4407 /// missing one is a member that drops later than it should, which is visible,
4408 /// rather than a build that stops.
4409 ///
4410 /// *When* to raise the cutoff is the renderer's, and deliberately. A browser
4411 /// answers it with `@media` against CSS pixels, a terminal against cells, egui
4412 /// against points; those are three different units for one question and the
4413 /// description holds none of them.
4414 pub const CUTOFFS: [layout::Priority; 3] = [
4415 layout::Priority::Optional,
4416 layout::Priority::Secondary,
4417 layout::Priority::Essential,
4418 ];
4419
4420 impl From<Node> for Ranked {
4421 fn from(node: Node) -> Self {
4422 Self::new(node)
4423 }
4424 }
4425
4426 /// The members that share one row, and what the row does without enough of it.
4427 ///
4428 /// The gap it closes is goingson's, and it is worth stating exactly because
4429 /// the fix looks like a stylesheet bug and is not. `styles.css:702` pinned
4430 /// every `.page-header` over the pill strip with `position: absolute`, which
4431 /// is the one construction the description layer cannot audit: the toolbar
4432 /// left the flow, contributed no width to the row it shared, and so nothing
4433 /// could collide with it and nothing prevented the collision. At 913 CSS
4434 /// pixels the help control clipped off the edge; at 700 the search field sat
4435 /// on top of the "Contacts" pill; at 560 the new-contact button left the
4436 /// viewport and could not be reached at all.
4437 ///
4438 /// The rule that construction broke is the first of the four: every described
4439 /// member is in flow. What was missing was any way to say the thing the
4440 /// stylesheet was asserting -- that a [`RegionKind::Band`] and a
4441 /// [`RegionKind::TabGroup`]'s strip occupy one row. This says it.
4442 ///
4443 /// # What it does not carry
4444 ///
4445 /// **A size.** Not a minimum, not a breakpoint, not a count. The description
4446 /// says what the row holds and each renderer derives the minimum in its own
4447 /// units -- a webview from `min-content` under a container query, a terminal
4448 /// from cell widths, egui from the galley -- and composes them by the flow.
4449 /// A derived minimum cannot rot and an authored one always does. The single
4450 /// hardcoded number in the whole mechanism is makeover-geometry's 44px contact
4451 /// patch, which is a density fact and already lives there.
4452 ///
4453 /// **[`layout::Room`].** Whether the row is tight is measured, per render, by
4454 /// whoever is rendering. Nothing here authors it.
4455 #[derive(Debug, Clone, PartialEq, Eq)]
4456 pub struct Run {
4457 /// What the row does when it is [`layout::Room::Tight`].
4458 ///
4459 /// No `Default`, here or upstream, and that is the point rather than an
4460 /// omission: a row cannot be described without saying what it does when it
4461 /// runs out of room. A default would be this crate guessing, and the guess
4462 /// would be silently wrong on exactly the screens that made the guess
4463 /// necessary.
4464 pub fallback: layout::Fallback,
4465 /// What shares the row, after whatever the region puts in it itself.
4466 ///
4467 /// A [`RegionKind::TabGroup`] puts its strip there, generated from its
4468 /// children's [`labels`](Slot::labels), so a tab group's run is the strip
4469 /// and then these. Every other region puts nothing there, so its run is
4470 /// exactly these members in order.
4471 ///
4472 /// That asymmetry is what lets the tab-group case be described without a
4473 /// new [`layout::Region`] member and without the strip becoming a node.
4474 /// Describing the strip in its own right is a separate question, filed as
4475 /// makeover-layout `978d24f8`; it is not needed to say what shares a row
4476 /// with one.
4477 ///
4478 /// [`Ranked`] rather than [`Node`], because [`layout::Fallback::Shed`] and
4479 /// [`layout::Fallback::Menu`] both read a [`layout::Priority`] per member
4480 /// and the type already carries one. `Wrap` and `Stack` keep every member,
4481 /// so they ignore it, which is why the rank is not conditional on the
4482 /// fallback.
4483 pub members: Vec<Ranked>,
4484 }
4485
4486 impl Run {
4487 /// A row that holds nothing yet, and knows what it does when it is tight.
4488 #[must_use]
4489 pub const fn new(fallback: layout::Fallback) -> Self {
4490 Self {
4491 fallback,
4492 members: Vec::new(),
4493 }
4494 }
4495
4496 /// Put a node in the row, saying what it is worth when the row is tight.
4497 ///
4498 /// The member goes on the row and not on the region, and that is the whole
4499 /// of how rule 2 is held now. There is no receiver here that could be
4500 /// missing a fallback: reaching this method at all means holding a `Run`,
4501 /// and the only way to hold one is to have said what it does when it runs
4502 /// out of room. The rule is carried by the type rather than by a runtime
4503 /// check, which is what lets a row be handed to a function that knows
4504 /// nothing about where it came from.
4505 ///
4506 /// That handing-off is not hypothetical. Of the 70 places in the tree that
4507 /// put something in a row, 25 sit in a function that did not declare the
4508 /// row: audiofiles' toolbar declares it once in `body` and then threads it
4509 /// through `here`, `holding`, `leaving`, `looking` and `frames`, each of
4510 /// which adds members and none of which has any business restating what the
4511 /// bar does when it is tight. Under the old shape those 25 were correct
4512 /// only by convention, and a helper called on a region with no row was a
4513 /// panic no compiler could see coming. A `Run` parameter makes the same
4514 /// five functions say in their signatures what they were already relying
4515 /// on.
4516 ///
4517 /// The node goes in the row rather than in a region's
4518 /// [`body`](Slot::body), and the two are different places: the body stacks
4519 /// down the region, the run lies across its leading row. goingson's toolbar
4520 /// belongs here, beside the tab strip it was overlapping.
4521 #[must_use]
4522 pub fn beside(mut self, node: Node, priority: layout::Priority) -> Self {
4523 self.members.push(Ranked::worth(node, priority));
4524 self
4525 }
4526
4527 /// Put a node in the row, saying what it is worth and how much it asks for.
4528 ///
4529 /// [`Self::beside`] with the second half of what a [`Column`] says. Two
4530 /// methods rather than one with a width argument, for [`Ranked::sized`]'s
4531 /// reason: most members have no opinion about the division, and the ones
4532 /// that do are the two-pane rows the retired [`RegionKind`] variants used
4533 /// to spell.
4534 #[must_use]
4535 pub fn spread(mut self, node: Node, priority: layout::Priority, width: layout::Width) -> Self {
4536 self.members.push(Ranked::sized(node, priority, width));
4537 self
4538 }
4539
4540 /// Whether this row keeps every member it was given.
4541 ///
4542 /// True for [`Wrap`](layout::Fallback::Wrap) and
4543 /// [`Stack`](layout::Fallback::Stack), which rearrange; false for
4544 /// [`Shed`](layout::Fallback::Shed) and [`Menu`](layout::Fallback::Menu),
4545 /// which read [`Ranked::priority`] and take members out of the row. A
4546 /// renderer asks this before it bothers computing a cutoff.
4547 ///
4548 /// An unrecognised fallback reads as keeping everything. `Fallback` is
4549 /// `#[non_exhaustive]`, and the safe reading of a member this crate has not
4550 /// been taught is the one that draws too much rather than the one that
4551 /// silently removes something.
4552 #[must_use]
4553 pub const fn keeps_every_member(&self) -> bool {
4554 !matches!(
4555 self.fallback,
4556 layout::Fallback::Shed | layout::Fallback::Menu
4557 )
4558 }
4559
4560 /// The members a row narrowed to this cutoff still shows, in order.
4561 #[must_use]
4562 pub fn kept_at(&self, cutoff: layout::Priority) -> Vec<&Ranked> {
4563 if self.keeps_every_member() {
4564 return self.members.iter().collect();
4565 }
4566 self.members.iter().filter(|m| m.kept_at(cutoff)).collect()
4567 }
4568 }
4569
4570 /// A fallback on its own is the row that has said what it does and holds
4571 /// nothing yet.
4572 ///
4573 /// Here so that [`Slot::across`] reads as one thing at both of its call shapes.
4574 /// A tab strip that only needs the fallback writes `across(Fallback::Menu)` and
4575 /// a band that has members writes `across(Run::new(..).beside(..))`, and
4576 /// neither has to know that the other exists. Note which direction this goes:
4577 /// a fallback becomes a row, and a row is never anything but a row. There is
4578 /// no conversion that hands back a `Run` without one being named, because that
4579 /// is the description this vocabulary exists to make unwritable.
4580 impl From<layout::Fallback> for Run {
4581 fn from(fallback: layout::Fallback) -> Self {
4582 Self::new(fallback)
4583 }
4584 }
4585
4586 /// What a control has to be holding for the region that names it to be out.
4587 ///
4588 /// The four shapes the eight measured sites need, and no more: a ticked box,
4589 /// an unticked one, one value, or one of several. A predicate would cover all
4590 /// four and could not be read by a renderer that has to draw "not applicable
4591 /// right now" as words, which is the reading the shape was ruled on.
4592 ///
4593 /// The value a control holds is read the way a form reads it: a checkbox is
4594 /// there by presence, per [`Field::value`], so an unticked box holds nothing
4595 /// and an empty box holds nothing either. That is one convention across the
4596 /// three renderers rather than three readings of "empty".
4597 #[derive(Debug, Clone, PartialEq, Eq)]
4598 pub enum Held {
4599 /// Anything at all: a ticked box, a filled-in answer.
4600 Anything,
4601 /// Nothing: an unticked box, a box the reader has cleared, a select
4602 /// resting on an empty option.
4603 Nothing,
4604 /// Exactly this value.
4605 Value(String),
4606 /// Any one of these values.
4607 ///
4608 /// goingson's zone picker is the site: it is out on three of the four
4609 /// `TzKind` values, and saying that as three regions with one condition
4610 /// each would put the same body on the screen three times.
4611 OneOf(Vec<String>),
4612 }
4613
4614 /// What brings a region out, said by the region.
4615 ///
4616 /// A region names the control it is watching and the value that brings it out,
4617 /// and a renderer evaluates that without knowing what pressed what.
4618 ///
4619 /// # Why it is here rather than on the control
4620 ///
4621 /// The host with no cursor decided it. On a terminal a hidden region is not
4622 /// hidden the way a browser hides one: the honest reading is "this region is
4623 /// not applicable right now", which is a property of the region, and a
4624 /// renderer is free to dim it, omit it or explain it. Put the condition on the
4625 /// control and a terminal asking whether a section applies has to search
4626 /// outward across every control on the screen to find out. A region gated by
4627 /// two controls has one home for the condition under this shape and none under
4628 /// the other.
4629 ///
4630 /// Both alternatives were considered and rejected in the same ruling: the
4631 /// condition on the control, and both ends joined by a back-pointer, which
4632 /// saves every renderer a search and costs two members to keep in agreement
4633 /// plus a rule for what happens when they disagree.
4634 ///
4635 /// # What it is not
4636 ///
4637 /// It is not [`Field::writes`] plus [`Action::replaces`]. That expresses the
4638 /// same reveal and buys a round trip for it, and on a form the reader is
4639 /// midway through it re-renders a region holding uncommitted values.
4640 ///
4641 /// It is not a press either. A toggle that flips on every press is
4642 /// unconditional and does not track a value, which is why all six MNW sites
4643 /// wrap one in a function that reads `.checked` or `.value` first.
4644 #[derive(Debug, Clone, PartialEq, Eq)]
4645 pub struct Reveal {
4646 /// The control being watched, by [`Field::name`].
4647 ///
4648 /// By name rather than by id, for [`Prefill::field`]'s reason: the name is
4649 /// what the description carries and what a submit sends the value under,
4650 /// while an id is scoped per form instance by whoever is emitting.
4651 ///
4652 /// A name no control on the screen carries leaves the region holding a
4653 /// condition nothing can satisfy. Every renderer draws that the same way,
4654 /// as a region that is not applicable, which is a description bug and
4655 /// reports itself as one.
4656 pub control: String,
4657 /// What that control has to hold.
4658 pub when: Held,
4659 }
4660
4661 impl Reveal {
4662 /// Out while the box is ticked.
4663 #[must_use]
4664 pub fn ticked(control: impl Into<String>) -> Self {
4665 Self {
4666 control: control.into(),
4667 when: Held::Anything,
4668 }
4669 }
4670
4671 /// Out while the box is not ticked.
4672 #[must_use]
4673 pub fn unticked(control: impl Into<String>) -> Self {
4674 Self {
4675 control: control.into(),
4676 when: Held::Nothing,
4677 }
4678 }
4679
4680 /// Out while the control holds this value.
4681 #[must_use]
4682 pub fn holding(control: impl Into<String>, value: impl Into<String>) -> Self {
4683 Self {
4684 control: control.into(),
4685 when: Held::Value(value.into()),
4686 }
4687 }
4688
4689 /// Out while the control holds any one of these values.
4690 #[must_use]
4691 pub fn holding_one_of<V: Into<String>>(
4692 control: impl Into<String>,
4693 values: impl IntoIterator<Item = V>,
4694 ) -> Self {
4695 Self {
4696 control: control.into(),
4697 when: Held::OneOf(values.into_iter().map(Into::into).collect()),
4698 }
4699 }
4700
4701 /// Whether a control holding this is holding what the region asked for.
4702 ///
4703 /// Here rather than in each renderer, so the three cannot come apart over
4704 /// what an empty value means. [`None`] and `Some("")` are one answer:
4705 /// nothing is held. A terminal's unticked checkbox is an empty buffer, a
4706 /// browser's is an element with no value to read, and both are the same
4707 /// fact about the form.
4708 #[must_use]
4709 pub fn satisfied_by(&self, held: Option<&str>) -> bool {
4710 let held = held.filter(|value| !value.is_empty());
4711 match &self.when {
4712 Held::Anything => held.is_some(),
4713 Held::Nothing => held.is_none(),
4714 Held::Value(wanted) => held == Some(wanted.as_str()),
4715 Held::OneOf(wanted) => held.is_some_and(|value| wanted.iter().any(|one| one == value)),
4716 }
4717 }
4718 }
4719
4720 /// One member of a region that shows its members one at a time.
4721 ///
4722 /// # Why the label is here and not on the region
4723 ///
4724 /// A label is the name of the control that reveals a frame: the tab's text, the
4725 /// disclosure's summary. Only a region that discloses or steps through its
4726 /// members draws one, so only such a region can hold one, and that is what this
4727 /// type is for.
4728 ///
4729 /// It used to sit on [`Slot`], where any region could carry it and only some
4730 /// would ever render it. That is not a lint waiting to be written. A member is
4731 /// built before it is placed, so nothing about the member can say whether its
4732 /// label will be drawn, and the parent that decides may be in another
4733 /// `declare!` entirely -- which is exactly how MNW's `/use-cases` lost the
4734 /// titles of nine cards for weeks with a green suite (quasicoherent `2cdc6761`,
4735 /// wiki `quasi-declare-form` section 22). Max ruled it should not be
4736 /// representable, so the label moved to the only place that draws it.
4737 #[derive(Clone, Debug, PartialEq, Eq)]
4738 pub struct Frame {
4739 /// What the control that reveals this frame is called.
4740 ///
4741 /// Optional because a carousel's frames are [`Node::Image`] and have
4742 /// nowhere to put one, which is correct rather than a gap: a photograph has
4743 /// a caption, not a tab name. A renderer draws a strip when every frame is
4744 /// named and a prev/next row when they are not, which is
4745 /// [`Slot::labels`]'s all-or-nothing rule.
4746 pub label: Option<String>,
4747 /// The member itself, ranked as any other member is.
4748 pub member: Ranked,
4749 }
4750
4751 impl Frame {
4752 /// A frame with a name, which is what a tab or a disclosure is.
4753 #[must_use]
4754 pub fn named(label: impl Into<String>, node: Node) -> Self {
4755 Self {
4756 label: Some(label.into()),
4757 member: Ranked::new(node),
4758 }
4759 }
4760
4761 /// A frame with no name, which is what a carousel's are.
4762 #[must_use]
4763 pub fn unnamed(node: Node) -> Self {
4764 Self {
4765 label: None,
4766 member: Ranked::new(node),
4767 }
4768 }
4769 }
4770
4771 /// Which of a selective region's members are up at once.
4772 ///
4773 /// [`layout::Showing`]'s two selective members, and only those. The third,
4774 /// `All`, is the other arm of [`Body`], so a selective body cannot say it: this
4775 /// exists so `Selective { showing: Showing::All, .. }` is unspellable rather
4776 /// than merely wrong.
4777 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
4778 pub enum Picks {
4779 /// Exactly one. A carousel, a tab group.
4780 One,
4781 /// One, or none. A disclosure, which is closed until it is opened.
4782 AtMostOne,
4783 }
4784
4785 impl Picks {
4786 /// The vocabulary's word for this, for a renderer that asks in those terms.
4787 #[must_use]
4788 pub const fn showing(self) -> layout::Showing {
4789 match self {
4790 Self::One => layout::Showing::One,
4791 Self::AtMostOne => layout::Showing::AtMostOne,
4792 }
4793 }
4794 }
4795
4796 /// A region's members, and how many of them are up.
4797 ///
4798 /// Two arms, because there are two shapes of region and they do not hold the
4799 /// same thing. A region showing everything holds ranked members. A region
4800 /// showing one at a time holds frames, each with the name of the control that
4801 /// reveals it, plus which one is up.
4802 ///
4803 /// Collapsing `body`, `showing` and `shown` into one field is what makes the
4804 /// combinations that never meant anything unspellable: a label on a member
4805 /// nothing reveals, and a `shown` index on a region that shows everything. Both
4806 /// were fields that some regions read and others silently ignored.
4807 #[derive(Clone, Debug, PartialEq, Eq)]
4808 pub enum Body {
4809 /// Every member, in order. What every region did before `Showing` existed.
4810 All(Vec<Ranked>),
4811 /// One member at a time, each named for the control that reveals it.
4812 Selective {
4813 /// Whether showing nothing is a legal resting place.
4814 picks: Picks,
4815 /// Which frame the region opens on. `None` under [`Picks::AtMostOne`]
4816 /// is the closed state; read it through [`Slot::current`], which is
4817 /// where an index past the end is dealt with.
4818 shown: Option<usize>,
4819 /// The frames, in order.
4820 frames: Vec<Frame>,
4821 },
4822 }
4823
4824 impl Body {
4825 /// How many members there are, whichever shape this is.
4826 #[must_use]
4827 pub fn len(&self) -> usize {
4828 match self {
4829 Self::All(members) => members.len(),
4830 Self::Selective { frames, .. } => frames.len(),
4831 }
4832 }
4833
4834 /// Whether the region draws nothing at all.
4835 #[must_use]
4836 pub fn is_empty(&self) -> bool {
4837 self.len() == 0
4838 }
4839
4840 /// Every member in order, with its frame taken off if it had one.
4841 ///
4842 /// What a renderer walks when it is drawing content rather than chrome. The
4843 /// chrome is the caller's other question, answered by [`Slot::labels`] and
4844 /// [`Slot::current`].
4845 pub fn members(&self) -> impl Iterator<Item = &Ranked> + '_ {
4846 // Two iterator types, so they are boxed into one. A region's member
4847 // count is small and this is not on any hot path: the residual seam
4848 // exists so that a served screen walks no `Node` tree at all.
4849 let iter: Box<dyn Iterator<Item = &Ranked>> = match self {
4850 Self::All(members) => Box::new(members.iter()),
4851 Self::Selective { frames, .. } => Box::new(frames.iter().map(|frame| &frame.member)),
4852 };
4853 iter
4854 }
4855
4856 /// The first member, whichever shape the body is.
4857 #[must_use]
4858 pub fn first(&self) -> Option<&Ranked> {
4859 self.get(0)
4860 }
4861
4862 /// One member by position, whichever shape the body is.
4863 #[must_use]
4864 pub fn get(&self, at: usize) -> Option<&Ranked> {
4865 match self {
4866 Self::All(members) => members.get(at),
4867 Self::Selective { frames, .. } => frames.get(at).map(|frame| &frame.member),
4868 }
4869 }
4870
4871 /// Every member in order. The same as [`members`](Self::members), under the
4872 /// name a caller reaches for when it is walking a list.
4873 pub fn iter(&self) -> impl Iterator<Item = &Ranked> + '_ {
4874 self.members()
4875 }
4876
4877 /// Add one member, keeping whichever shape the body already has.
4878 pub fn push(&mut self, member: Ranked) {
4879 match self {
4880 Self::All(members) => members.push(member),
4881 Self::Selective { frames, .. } => frames.push(Frame {
4882 label: None,
4883 member,
4884 }),
4885 }
4886 }
4887
4888 /// Add several, in order.
4889 pub fn extend(&mut self, members: impl IntoIterator<Item = Ranked>) {
4890 for member in members {
4891 self.push(member);
4892 }
4893 }
4894
4895 /// Every member, mutably, under the name a caller walking a list reaches
4896 /// for.
4897 pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut Ranked> + '_ {
4898 self.members_mut()
4899 }
4900
4901 /// Drop every member, keeping the shape.
4902 ///
4903 /// A selective body that is emptied stays selective: what it shows one at a
4904 /// time is a fact about the region, not about how many members it happens to
4905 /// hold right now.
4906 pub fn clear(&mut self) {
4907 match self {
4908 Self::All(members) => members.clear(),
4909 Self::Selective { frames, .. } => frames.clear(),
4910 }
4911 }
4912
4913 /// Every member, mutably.
4914 pub fn members_mut(&mut self) -> impl Iterator<Item = &mut Ranked> + '_ {
4915 let iter: Box<dyn Iterator<Item = &mut Ranked>> = match self {
4916 Self::All(members) => Box::new(members.iter_mut()),
4917 Self::Selective { frames, .. } => {
4918 Box::new(frames.iter_mut().map(|frame| &mut frame.member))
4919 }
4920 };
4921 iter
4922 }
4923
4924 /// The vocabulary's word for how much of this is up at once.
4925 #[must_use]
4926 pub const fn showing(&self) -> layout::Showing {
4927 match self {
4928 Self::All(_) => layout::Showing::All,
4929 Self::Selective { picks, .. } => picks.showing(),
4930 }
4931 }
4932
4933 /// Which member the region opens on, before clamping.
4934 #[must_use]
4935 pub const fn shown(&self) -> Option<usize> {
4936 match self {
4937 Self::All(_) => None,
4938 Self::Selective { shown, .. } => *shown,
4939 }
4940 }
4941
4942 /// Turn a body that shows everything into one that shows one at a time.
4943 ///
4944 /// Members it already holds become unnamed frames, which is a carousel: a
4945 /// name arrives with [`Slot::frame`] and nowhere else, so nothing here can
4946 /// invent one or drop one.
4947 fn select(&mut self, picks: Picks, shown: Option<usize>) {
4948 match self {
4949 Self::All(members) => {
4950 *self = Self::Selective {
4951 picks,
4952 shown,
4953 frames: std::mem::take(members)
4954 .into_iter()
4955 .map(|member| Frame {
4956 label: None,
4957 member,
4958 })
4959 .collect(),
4960 };
4961 }
4962 Self::Selective {
4963 picks: held,
4964 shown: at,
4965 ..
4966 } => {
4967 *held = picks;
4968 *at = shown;
4969 }
4970 }
4971 }
4972 }
4973
4974 impl Default for Body {
4975 fn default() -> Self {
4976 Self::All(Vec::new())
4977 }
4978 }
4979
4980 /// A named region, and the thing a fragment is aimed at.
4981 ///
4982 /// The name is what decision 7 needs and [`layout::Region`] deliberately does
4983 /// not have: two panes in a split are both `Pane`, so the kind cannot be an
4984 /// address. A webview maps the id onto `hx-target`; egui and the terminal
4985 /// ignore it and redraw, which costs them nothing because they were redrawing
4986 /// anyway.
4987 #[derive(Debug, Clone, PartialEq, Eq)]
4988 pub struct Slot {
4989 /// The address. Unique within a screen, and stable across responses, or a
4990 /// fragment lands nowhere.
4991 pub id: String,
4992 /// Which region it is.
4993 pub kind: RegionKind,
4994 /// Whether the region's own content is here or on its way.
4995 ///
4996 /// The loading axis, and only that. Emptiness is *not* said here, which
4997 /// looks like the obvious place for it and is not: a column with a heading
4998 /// and no rows is a region that has content — the heading — and a list that
4999 /// has none. Marking the region empty would hide the heading with it. See
5000 /// [`Node::StandIn`].
5001 pub readiness: layout::Readiness,
5002 /// What is in it.
5003 ///
5004 /// Blocks, regions included, which is the nesting that was always accepted:
5005 /// a region inside a region is a nested rect on every host. Leaves are
5006 /// admitted too, and deliberately -- a fact under a heading is a
5007 /// [`Node::Text`] straight in a pane, and it is the commonest thing in the
5008 /// tree.
5009 ///
5010 /// # Why there is no bound here
5011 ///
5012 /// [`Cell::part`] and [`Row::part`] assert that what they are handed is a
5013 /// leaf, and this does not, which looks like an oversight and is the model
5014 /// working. The ladder forbids reaching *up*: a run may not hold a block,
5015 /// because a run has to be drawable on one wrapped line. A block holding a
5016 /// leaf is going down, and going down is what containment is for. There is
5017 /// no upward violation for [`with`](Self::with) to catch, so an assertion
5018 /// here would be a runtime check that can never fire.
5019 ///
5020 /// A region whose whole content is one badge is the case that made this
5021 /// look like a question. It is describable, and it should be: a status pane
5022 /// is a real screen. Whether it is a *good* screen is a judgement about
5023 /// that screen rather than a property of the vocabulary, and the bound is
5024 /// not the place to hold opinions about taste.
5025 ///
5026 /// # Why the members are [`Ranked`] and not bare nodes
5027 ///
5028 /// Each one carries what it is worth when the region runs out of room, so
5029 /// a renderer narrows a region the way it already narrows a table: raise a
5030 /// cutoff over a declared order. `Ranked`'s own docs carry the argument.
5031 pub body: Body,
5032 /// How many of [`body`](Self::body) are visible at once.
5033 ///
5034 /// [`layout::Showing::All`] by default, so a description written against
5035 /// the previous version says the same thing.
5036 ///
5037 /// This is the kind. The two fields below are the current answer and the
5038 /// per-child name, and they are here rather than in `makeover-layout` for
5039 /// the reason a [`Field`]'s value is: a layer that defers every address
5040 /// does not hold what is picked either.
5041 ///
5042 /// # A description never names client-only state
5043 ///
5044 /// A description says what a region *is*: these frames are peers, show
5045 /// one. State that lives only in the client and never reaches a handler is
5046 /// not part of that. Each renderer decides it, exactly as it already
5047 /// decides scroll position, focus and the duration of an undo window.
5048 ///
5049 /// The consequence is worth stating plainly rather than discovering later:
5050 /// **a screen has no way to say a preference should persist.** That is a
5051 /// deliberate limit. A description that wanted to say "remember which tab
5052 /// the reader was on across visits" would be naming storage, and storage is
5053 /// the host's.
5054 ///
5055 /// Three behaviours settle as renderer policy under this rule, recorded
5056 /// here because a reader meets the question at this field:
5057 ///
5058 /// - **Section tabs switched without a round trip.** This field already
5059 /// answers it: the description says the frames are peers and one is up,
5060 /// and the renderer toggles and rewrites the URL. MNW defines
5061 /// `switchSectionTab` verbatim in three bundles (`static/page-item-2.js`,
5062 /// `static/page-library-downloads.js`, `static/page-project.js`) and
5063 /// reaches a fourth site through the delegated handler in
5064 /// `static/actions-pages.js`. Three definitions, four call sites, and none
5065 /// of them is something a description should have been carrying.
5066 /// - **A persisted view preference.** The renderer decides whether it
5067 /// persists and where; nothing in the description mentions storage. The
5068 /// persist-across-swap behaviour has **one** implementation in MNW,
5069 /// `static/page-discover.js:176-178`, not the two an earlier count
5070 /// claimed. The count is recorded because it is what made a vocabulary
5071 /// addition look worth buying, and at one site it is not.
5072 /// - **Loading a region's contents on first reveal.** [`Readiness`] says
5073 /// what a region shows while it waits; what makes it *start* is the
5074 /// renderer's. MNW does it twice, license text on a `<details>` toggle
5075 /// and a video `src` on first `play`, each guarding with a `loaded` flag.
5076 ///
5077 /// One fragility worth knowing before a conversion, since a converter will
5078 /// arrive here first: MNW's `static/page-project-2.js` binds its
5079 /// `.view-btn` handlers directly at script load rather than by delegation.
5080 /// The container it binds into is never an htmx target today, so that is
5081 /// correct as written. If it ever becomes one, the handlers and the view
5082 /// state both break silently.
5083 ///
5084 /// [`Readiness`]: layout::Readiness
5085 /// What this region is called in its own right.
5086 ///
5087 /// Not [`label`](Self::label), which is the name a *parent* reads off this
5088 /// region when it is showing one child at a time -- the tab's name,
5089 /// gathered by [`labels`](Self::labels). Setting that on a tab strip
5090 /// itself is read by nothing, which is how MNW's library strip lost its
5091 /// `aria-label="Library sections"` when it converted and announced as an
5092 /// unnamed tab list.
5093 ///
5094 /// Counted before it was added, across MNW, goingson and Balanced
5095 /// Breakfast: 199 `aria-label` sites, and the ones on containers --
5096 /// `table`, `nav`, `ul`, `section`, `aside` -- are this. "Tag breadcrumbs",
5097 /// "Selected tags", "Waitlist entries", "Sales per month". The ones on
5098 /// controls are a different thing and need nothing new: an icon-only button
5099 /// already has [`Act::label`], and a renderer drawing it as a glyph is the
5100 /// renderer's choice about how to spell a name it was given.
5101 ///
5102 /// Every renderer has somewhere to put it, which is what separates it from
5103 /// [`Act::hint`]: a webview writes `aria-label`, a terminal a rule caption
5104 /// or a heading line, egui a frame's title. None of them has to invent
5105 /// copy, and none of them may draw it *instead of* a heading the body
5106 /// already carries -- a region with a `Heading` in it is named twice on
5107 /// purpose, once for the eye and once for the reader that cannot see it.
5108 ///
5109 /// `None` is a region with no name of its own, which is most of them and is
5110 /// what every region did before this field existed.
5111 pub name: Option<String>,
5112 /// The call that fills this region, when the region's content is not here
5113 /// yet.
5114 ///
5115 /// The region half of [`layout::Awaiting`]: a screen that is mostly local
5116 /// reads plus one slow part says so here instead of being hand-split into
5117 /// a second route, which is what MNW's user dashboard does with its payout
5118 /// summary because that one tab calls a payment provider and the rest of
5119 /// it reads the database.
5120 ///
5121 /// [`readiness`](Self::readiness) is [`Pending`](layout::Readiness::Pending)
5122 /// while this is set, and [`Screen::replace`] clears it when the answer
5123 /// lands, so a retained-screen host cannot ask twice for one region.
5124 ///
5125 /// The wait's size, if it has one, is on the action rather than here. A
5126 /// region is fed by a call and the call is what knows.
5127 ///
5128 /// Boxed, which is the one place in this file that is: [`Node::Region`]
5129 /// holds a [`Slot`] by value, so every node in every tree would carry an
5130 /// [`Action`]'s width for a field almost no region sets. Reach for
5131 /// [`fed_by`](Self::fed_by) and [`awaiting`](Self::awaiting) rather than the
5132 /// field, and the box is not something a caller has to think about.
5133 pub fed_by: Option<Box<Action>>,
5134 /// Whether this region's contents change without the user.
5135 ///
5136 /// The other word beside [`layout::Awaiting`], and the two divide by
5137 /// whether the waiting ends: awaiting resolves once, in finite time, and
5138 /// this never resolves. A sync panel whose state moves when an OAuth
5139 /// callback lands in another process is the case, and no measure of
5140 /// progress can carry it — the transition that prompted the question is
5141 /// `Authenticating -> NeedsEncryption`, which is not an amount of
5142 /// anything.
5143 ///
5144 /// A bool rather than a rate. The description says the contents move; how
5145 /// often to look is the renderer's, picked once per host rather than per
5146 /// screen, which is the whole of what this buys. MNW hand-writes
5147 /// `hx-trigger="every 10s"` in two templates and audiofiles re-reads every
5148 /// frame, and nothing makes those two comparable today.
5149 ///
5150 /// Orthogonal to [`readiness`](Self::readiness). A live region can be
5151 /// [`Ready`](layout::Readiness::Ready), [`Pending`](layout::Readiness::Pending)
5152 /// or [`Failed`](layout::Readiness::Failed); those four stay mutually
5153 /// exclusive states about whether there is content yet, and liveness is a
5154 /// fact about the content after it arrives.
5155 ///
5156 /// What a host does with it depends on where the content comes from. A
5157 /// region that also names a [`fed_by`](Self::fed_by) is re-asked on the
5158 /// renderer's cadence — see [`Screen::refreshes`], which is the walk that
5159 /// finds them and the reason [`Screen::replace`] leaves a live region's
5160 /// feed in place instead of clearing it. A live region with no call reads
5161 /// something the host already holds, and the cadence is a repaint.
5162 pub live: bool,
5163 /// What brings this region out, when it is not simply out.
5164 ///
5165 /// The region names a control and the value that reveals it, and every
5166 /// renderer answers it from what it already holds: no request, no
5167 /// fragment, no re-render of a form the reader is midway through. See
5168 /// [`Reveal`] for why the condition lives here and not on the control.
5169 ///
5170 /// `None` is a region that is always out, so a description written against
5171 /// the previous version says the same thing.
5172 ///
5173 /// Boxed, for [`fed_by`](Self::fed_by)'s reason and measured the same way:
5174 /// [`Node::Region`] holds a [`Slot`] by value, so an unboxed condition puts
5175 /// a `String` and a `Vec` on every node in every tree for a field a handful
5176 /// of regions in a screen set.
5177 pub revealed_by: Option<Box<Reveal>>,
5178 /// Whether this region leads with a row that things share, and what that
5179 /// row does when it is [`layout::Room::Tight`].
5180 ///
5181 /// `None` says it does not and is why the field is additive at a call site
5182 /// rather than a re-reading of every screen in the tree. It is not a
5183 /// defaulted fallback and must not be read as one: a region with no run
5184 /// has no row to fall back, so there is nothing for it to have failed to
5185 /// say.
5186 ///
5187 /// Set it with [`across`](Self::across), which takes the row itself. The
5188 /// only way to make a [`Run`] is [`Run::new`], and that takes the
5189 /// fallback, so there is no path to a row here that failed to say what it
5190 /// does when it is tight.
5191 ///
5192 /// Boxed, for [`fed_by`](Self::fed_by)'s reason and measured the same way:
5193 /// [`Node::Region`] holds a [`Slot`] by value, so an inline `Run` puts a
5194 /// `Vec`'s width on every node in every tree for a field one region in a
5195 /// screen sets.
5196 pub run: Option<Box<Run>>,
5197 /// What this region asks when the questions inside it move.
5198 ///
5199 /// The region names the route, the wait and the floor; the values it sends
5200 /// are the ones its own
5201 /// [`questions`](Self::questions) are holding; and the answer lands where
5202 /// [`Action::replaces`] points, which is usually a region inside this one.
5203 /// MNW's fee calculator is the site: five dials and a results panel that
5204 /// recomputes when any of them moves.
5205 ///
5206 /// # Why the region and not one of the dials
5207 ///
5208 /// The cheaper shape was a [`Consult::sends`] on one field naming its
5209 /// peers, and it was rejected for what it says rather than for what it
5210 /// costs: it nominates one of five equals as the owner of the recompute,
5211 /// and whichever is picked reads as arbitrary to the next person to open
5212 /// the file. It is also asymmetric on a host with focus, where the caret
5213 /// would have to be in the owning box for anything to happen.
5214 ///
5215 /// # What is gathered
5216 ///
5217 /// Every question this region contains, at any depth, nested regions
5218 /// included — [`questions`](Self::questions) is the walk, so the three
5219 /// renderers cannot disagree about what "inside" means. Beside that,
5220 /// whatever [`Consult::sends`] names, which is how a dial outside the panel
5221 /// joins in.
5222 ///
5223 /// Several, for [`Field::consults`]' reason: two frames recomputing from
5224 /// one set of dials at two rates are two questions, and one is not a
5225 /// special case of the other. Empty for nearly every region, which is every
5226 /// region written before this field existed.
5227 pub consults: Vec<Consult>,
5228 /// The children are answers to one question, and the reader may add and
5229 /// take them away.
5230 ///
5231 /// A region member, not a rework of [`Repeat`]. See [`Repeating`] for what
5232 /// separates the two -- briefly, `Repeat` is one *field* answered N times
5233 /// and this is one
5234 /// *group* answered N times, which is the shape nothing could say.
5235 ///
5236 /// Boxed for [`fed_by`](Self::fed_by)'s reason: `Node::Region` holds a
5237 /// `Slot` by value, so an unboxed member is paid for by every region in
5238 /// every tree for a field almost none of them set.
5239 pub repeating: Option<Box<Repeating>>,
5240 /// What taking *this* slot away calls, when this region is one.
5241 ///
5242 /// On the child rather than on the parent, which is the one place this
5243 /// diverges from the sketch in the task. A parent-level remove would have
5244 /// to reach a particular slot, and the two ways to do that are both worse:
5245 /// a renderer building `.../{at}/remove` is route construction in a
5246 /// renderer, and one route plus an index under an agreed name is a third
5247 /// word beside [`Node::SELECTED`] and [`Node::TICKED`] for one screen's
5248 /// benefit. A child carrying the action that concerns it is
5249 /// [`fed_by`](Self::fed_by)'s shape, already here and already understood.
5250 ///
5251 /// What the parent's [`Repeating::least`] adds is the *enforcement*: a
5252 /// renderer disables this once the floor is reached, so "at least one
5253 /// condition" stops being an app disabling its own button.
5254 ///
5255 /// Boxed with `repeating` and for its reason.
5256 pub removes: Option<Box<Act>>,
5257 }
5258
5259 /// What a region says when its children are answers to one question.
5260 ///
5261 /// [`Repeat`] describes a repeating *field*: an [`Instance`] is one value and
5262 /// one error, so a slot is one answer to one question. Nothing described a
5263 /// repeating *group*, and audiofiles' rule editor has two of them on one
5264 /// screen -- a condition is three questions that only mean anything together,
5265 /// and an action is two.
5266 ///
5267 /// # Why this and not a wider `Repeat`
5268 ///
5269 /// The regions already exist: 23 [`RegionKind::Group`] sites in the tree when
5270 /// this was measured, so a region member attaches to something with consumers,
5271 /// where widening `Repeat` would have been a redesign of vocabulary that has
5272 /// none.
5273 ///
5274 /// # What it deliberately does not carry
5275 ///
5276 /// A per-slot error, which is [`Instance::error`]'s job for a repeating field.
5277 /// A group's slots can be wrong in ways a string on the group cannot say, and
5278 /// no screen had needed one: **that was the reopening condition, and it
5279 /// fired.** MNW's version-upload queue wanted a status and an error against one
5280 /// slot, so [`Repeat`] grew [`Instance::parts`] and [`Progress`].
5281 ///
5282 /// This member still stands, and the line between the two did not move. A
5283 /// repeating *field* holds its slots in the renderer's own view until one
5284 /// submit carries all of them, which is why they need a wire naming and a
5285 /// per-slot status at all. These regions each carry their own fields and their
5286 /// own writes, so a slot is already addressed by the routes inside it and there
5287 /// is nothing to take apart on the way back. The queue was the first shape and
5288 /// it is a field, not a group.
5289 ///
5290 /// # The wire
5291 ///
5292 /// Nothing here. A repeating field names its answers `name[0]`, `name[1]`,
5293 /// because one submit carries all of them; these regions each carry their own
5294 /// fields with their own writes, so every slot is already addressed by the
5295 /// routes inside it. There is no set to take apart on the way back and so no
5296 /// naming convention to agree.
5297 #[derive(Debug, Clone, PartialEq, Eq)]
5298 pub struct Repeating {
5299 /// What one of them is called, singular. "Condition", "Action".
5300 ///
5301 /// The renderer numbers them from it -- "Condition 1", "Condition 2" --
5302 /// which is why this is the singular noun and not a heading. A description
5303 /// that wrote the numbers itself would be numbering for a screen it cannot
5304 /// see, and would go stale the moment a slot was removed from the middle.
5305 pub one: String,
5306 /// The fewest slots the reader may leave standing.
5307 ///
5308 /// [`Repeat::least`]'s meaning, one level up. `1` is audiofiles' rule
5309 /// editor, whose conditions cannot go to zero; `0` is the ordinary answer.
5310 ///
5311 /// **This is the enforcement the member exists for.** The editor said it by
5312 /// disabling the last Remove itself, which is a rule living in an app where
5313 /// every renderer needs it.
5314 pub least: usize,
5315 /// The most the reader may add, if there is a ceiling. `None` is none.
5316 pub most: Option<usize>,
5317 /// The control that adds a slot.
5318 ///
5319 /// A whole [`Act`] rather than a label, because adding a slot here is a
5320 /// route: the group's state is the app's, unlike a repeating field's, whose
5321 /// slots live in the renderer's own view until they are submitted.
5322 pub add: Act,
5323 }
5324
5325 impl Repeating {
5326 /// A repeating group with no floor and no ceiling.
5327 #[must_use]
5328 pub fn new(one: impl Into<String>, add: Act) -> Self {
5329 Self {
5330 one: one.into(),
5331 least: 0,
5332 most: None,
5333 add,
5334 }
5335 }
5336
5337 /// The same, with a floor on how many may be left standing.
5338 #[must_use]
5339 pub const fn least(mut self, least: usize) -> Self {
5340 self.least = least;
5341 self
5342 }
5343
5344 /// The same, with a ceiling on how many may be added.
5345 #[must_use]
5346 pub const fn most(mut self, most: usize) -> Self {
5347 self.most = Some(most);
5348 self
5349 }
5350
5351 /// Whether a group holding this many may lose one.
5352 ///
5353 /// Asked by every renderer before it draws a slot's remove control, so the
5354 /// three cannot disagree about what the floor means.
5355 #[must_use]
5356 pub const fn may_remove(&self, standing: usize) -> bool {
5357 standing > self.least
5358 }
5359
5360 /// Whether a group holding this many may gain one.
5361 #[must_use]
5362 pub const fn may_add(&self, standing: usize) -> bool {
5363 match self.most {
5364 Some(most) => standing < most,
5365 None => true,
5366 }
5367 }
5368 }
5369
5370 impl Slot {
5371 /// An empty region under this address.
5372 pub fn new(id: impl Into<String>, kind: RegionKind) -> Self {
5373 Self {
5374 id: id.into(),
5375 kind,
5376 readiness: layout::Readiness::Ready,
5377 body: Body::All(Vec::new()),
5378 name: None,
5379 fed_by: None,
5380 live: false,
5381 revealed_by: None,
5382 run: None,
5383 consults: Vec::new(),
5384 repeating: None,
5385 removes: None,
5386 }
5387 }
5388
5389 /// A place the app fills itself, and owes every host a fill for.
5390 ///
5391 /// See [`RegionKind::Handover`]. Use [`ceded`](Self::ceded) instead when
5392 /// no host is owed one.
5393 pub fn handover(id: impl Into<String>, name: impl Into<String>) -> Self {
5394 Self::new(id, RegionKind::Handover { name: name.into() })
5395 }
5396
5397 /// A place the app fills itself, that no host is owed a fill for.
5398 ///
5399 /// See [`RegionKind::Ceded`]. A chart, a waveform: a renderer with nothing
5400 /// to put here draws nothing and is right to.
5401 pub fn ceded(id: impl Into<String>, name: impl Into<String>) -> Self {
5402 Self::new(id, RegionKind::Ceded { name: name.into() })
5403 }
5404
5405 /// Things that belong together.
5406 ///
5407 /// Named rather than left to [`new`](Self::new) because this is the member
5408 /// a run of siblings under a heading should reach for, and the one it
5409 /// reached for instead -- [`RegionKind::Pane`] -- is what `Slot::new` makes
5410 /// easy. The heading goes in the body, not here.
5411 pub fn group(id: impl Into<String>) -> Self {
5412 Self::new(id, RegionKind::Group)
5413 }
5414
5415 /// A named assembly, whose body says what it is made of.
5416 ///
5417 /// The body is not optional in spirit, though nothing here enforces it: a
5418 /// widget with an empty body is a [`handover`](Self::handover) that has
5419 /// mislaid its host fill, and a renderer that does not know the name will
5420 /// draw nothing at all. Assemble it out of members the description already
5421 /// has, the way [`layout::Region::Widget`] describes.
5422 pub fn widget(id: impl Into<String>, name: impl Into<String>) -> Self {
5423 Self::new(id, RegionKind::Widget { name: name.into() })
5424 }
5425
5426 /// Say what brings this region out.
5427 ///
5428 /// The condition is the region's own, so a form with several conditional
5429 /// sections reads as a list of regions each stating its precondition
5430 /// rather than as controls reaching across the form.
5431 ///
5432 /// Calling it twice replaces the condition, which is the reading every
5433 /// builder here has: the second call is a correction.
5434 ///
5435 /// ```
5436 /// use quasi_router::{RegionKind, Reveal, Slot};
5437 ///
5438 /// let settings = Slot::group("pwyw-settings").revealed_by(Reveal::ticked("pwyw"));
5439 /// let custom = Slot::new("dash-custom-license", RegionKind::Pane)
5440 /// .revealed_by(Reveal::holding("license", "custom"));
5441 ///
5442 /// assert!(settings.revealed(Some("on")));
5443 /// assert!(!settings.revealed(None));
5444 /// assert!(custom.revealed(Some("custom")));
5445 /// assert!(!custom.revealed(Some("all-rights-reserved")));
5446 /// ```
5447 #[must_use]
5448 pub fn revealed_by(mut self, reveal: Reveal) -> Self {
5449 self.revealed_by = Some(Box::new(reveal));
5450 self
5451 }
5452
5453 /// Whether this region is out, given what its control is holding.
5454 ///
5455 /// `true` for a region that named no condition, which is nearly all of
5456 /// them: a region says when it is *not* applicable, and saying nothing is
5457 /// saying it always is.
5458 ///
5459 /// What a renderer does with `false` is the renderer's. A browser hides the
5460 /// element, and a host with no cursor may dim the region, leave it out, or
5461 /// say in words that it does not apply right now — the reading the shape
5462 /// was ruled for.
5463 #[must_use]
5464 pub fn revealed(&self, held: Option<&str>) -> bool {
5465 self.revealed_by
5466 .as_ref()
5467 .is_none_or(|reveal| reveal.satisfied_by(held))
5468 }
5469
5470 /// The name of the control this region is watching, if it watches one.
5471 #[must_use]
5472 pub fn watches(&self) -> Option<&str> {
5473 self.revealed_by
5474 .as_ref()
5475 .map(|reveal| reveal.control.as_str())
5476 }
5477
5478 /// Add a node, chaining.
5479 ///
5480 /// At [`layout::Priority::Essential`], so it never drops. The signature is
5481 /// unchanged from before [`Ranked`] existed and so is what it means, which
5482 /// is why the whole tree kept building.
5483 #[must_use]
5484 pub fn with(mut self, node: Node) -> Self {
5485 match &mut self.body {
5486 Body::All(members) => members.push(Ranked::new(node)),
5487 // A member added to a selective region is a frame with no name,
5488 // which is a carousel's. A name arrives through [`Self::frame`] and
5489 // nowhere else.
5490 Body::Selective { frames, .. } => frames.push(Frame::unnamed(node)),
5491 }
5492 self
5493 }
5494
5495 /// Add every node a shape answered with, chaining.
5496 ///
5497 /// [`with`](Self::with)'s plural, and the whole of what `include each`
5498 /// emits. A shape answering `Vec<Node>` has no region of its own -- what it
5499 /// returns is a run of members and not one node -- so before this a caller
5500 /// spread it by hand, with a loop whose body was `include node;`.
5501 ///
5502 /// That loop is why this exists. It reads as a loop over data and is a
5503 /// splice, and a staged shape cannot make sense of it: the `include` names
5504 /// a binding rather than a shape, so there is no twin to retarget to and no
5505 /// filler to call. Said as one `include each`, the caller splices a
5506 /// reference the way every other `include` does.
5507 #[must_use]
5508 pub fn with_all(mut self, nodes: impl IntoIterator<Item = Node>) -> Self {
5509 for node in nodes {
5510 self = self.with(node);
5511 }
5512 self
5513 }
5514
5515 /// A named frame: a tab, or a disclosure's one panel.
5516 ///
5517 /// The only way to write a label, and it places the member at the same
5518 /// time. That is the whole of the fix in quasicoherent `2cdc6761`: a name
5519 /// cannot be written on something that will not draw it, because the name
5520 /// and the thing that draws it are one call.
5521 ///
5522 /// Naming a frame on a region that shows everything makes it show one at a
5523 /// time, since a named frame is a claim that something reveals it. Say
5524 /// [`showing_one`](Self::showing_one) or
5525 /// [`showing_at_most_one`](Self::showing_at_most_one) first to choose
5526 /// which; unstated, a disclosure is the safer default because it has a
5527 /// resting state that shows nothing.
5528 #[must_use]
5529 pub fn frame(mut self, label: impl Into<String>, node: Node) -> Self {
5530 if matches!(self.body, Body::All(_)) {
5531 self.body.select(Picks::AtMostOne, None);
5532 }
5533 match &mut self.body {
5534 Body::Selective { frames, .. } => frames.push(Frame::named(label, node)),
5535 Body::All(_) => unreachable!("just selected"),
5536 }
5537 self
5538 }
5539
5540 /// Lead with a row that things share.
5541 ///
5542 /// Takes the row itself, so the fallback arrives with it: a
5543 /// [`layout::Fallback`] converts, which is the empty row this reads as, and
5544 /// a [`Run`] built up with [`Run::beside`] is the row that already holds
5545 /// something. Either way the thing handed over has said what it does when
5546 /// it is tight, because [`Run::new`] is the only way to make one and it
5547 /// takes the answer. That is rule 2, and it is now held by the type of the
5548 /// argument rather than by a check inside a method that could only fire
5549 /// after the description was already written.
5550 ///
5551 /// The empty form is not a degenerate case. Five tab strips in MNW declare
5552 /// a fallback and no members at all: a [`RegionKind::TabGroup`] puts its
5553 /// own strip in the row, generated from its children's
5554 /// [`labels`](Self::label), so the row is full without anything being put
5555 /// in it and the fallback is the only thing left to say.
5556 ///
5557 /// Calling it twice replaces the row, members and all. The row is one
5558 /// value now, so a second call is a second row rather than a correction to
5559 /// the first, and correcting a fallback means correcting it on the `Run`
5560 /// before the region is ever told about it.
5561 #[must_use]
5562 pub fn across(mut self, row: impl Into<Run>) -> Self {
5563 self.run = Some(Box::new(row.into()));
5564 self
5565 }
5566
5567 /// Add a node, saying what it is worth when room runs out.
5568 ///
5569 /// The narrowing member. Spelled as a second constructor rather than as a
5570 /// builder on the placement, the way [`Column::priority`] is, because a
5571 /// `Ranked` is built at the point it is inserted and there is nothing to
5572 /// hold between building it and pushing it.
5573 #[must_use]
5574 pub fn with_ranked(mut self, node: Node, priority: layout::Priority) -> Self {
5575 self.body.push(Ranked::worth(node, priority));
5576 self
5577 }
5578
5579 /// Add several nodes, chaining.
5580 ///
5581 /// Takes anything that becomes a [`Ranked`], so a run of bare nodes still
5582 /// works and reads as before, and a run of ranked ones needs no second
5583 /// method.
5584 #[must_use]
5585 pub fn extend(mut self, nodes: impl IntoIterator<Item = impl Into<Ranked>>) -> Self {
5586 self.body.extend(nodes.into_iter().map(Into::into));
5587 self
5588 }
5589
5590 /// The content is on its way rather than here.
5591 #[must_use]
5592 pub fn pending(mut self) -> Self {
5593 self.readiness = layout::Readiness::Pending;
5594 self
5595 }
5596
5597 /// The content arrives from this call rather than with the screen.
5598 ///
5599 /// Sets [`readiness`](Self::readiness) to
5600 /// [`Pending`](layout::Readiness::Pending) in the same breath, because a
5601 /// region that says where its content is coming from is by construction a
5602 /// region that does not have it yet, and the two disagreeing is a state no
5603 /// renderer could draw honestly.
5604 ///
5605 /// Mark the action [`awaiting`](Action::awaiting) unless there is a reason
5606 /// not to. Without the mark this is still a deferred load and every renderer
5607 /// still fetches; what is lost is the size of the wait, so the stand-in has
5608 /// no proportion to draw.
5609 #[must_use]
5610 pub fn fed_by(mut self, action: Action) -> Self {
5611 self.readiness = layout::Readiness::Pending;
5612 self.fed_by = Some(Box::new(action));
5613 self
5614 }
5615
5616 /// The contents change without the user, so a renderer keeps looking.
5617 ///
5618 /// See [`live`](Self::live). Says nothing about how often: the cadence is
5619 /// the renderer's, and a description that named one would be a description
5620 /// the webview and the terminal disagreed about.
5621 ///
5622 /// Combines with [`fed_by`](Self::fed_by) rather than replacing it. Called
5623 /// after it, the region is asked for once and then re-asked; called on a
5624 /// region with no call, the host re-reads whatever it is drawing from.
5625 #[must_use]
5626 pub const fn live(mut self) -> Self {
5627 self.live = true;
5628 self
5629 }
5630
5631 /// Ask this route when the questions inside this region move.
5632 ///
5633 /// See [`consults`](Self::consults). Appends rather than replaces, which is
5634 /// the reading [`Field::consulting`] has for the same reason: two questions
5635 /// about one set of dials are two questions, and a second call adding a
5636 /// second one is what a builder reads as.
5637 ///
5638 /// ```
5639 /// use quasi_router::{Action, Consult, Slot};
5640 ///
5641 /// let calculator = Slot::group("pricing-calculator").consulting(
5642 /// Consult::new(Action::get("/pricing/compare").replacing("results-panel"))
5643 /// .after(std::time::Duration::from_millis(300)),
5644 /// );
5645 ///
5646 /// assert_eq!(calculator.consults.len(), 1);
5647 /// ```
5648 #[must_use]
5649 pub fn consulting(mut self, consult: Consult) -> Self {
5650 self.consults.push(consult);
5651 self
5652 }
5653
5654 /// What this region is waiting on, when it is waiting on something.
5655 ///
5656 /// Asked once here rather than reached through the action in each renderer.
5657 #[must_use]
5658 pub fn awaiting(&self) -> Option<layout::Awaiting> {
5659 self.fed_by.as_ref().and_then(|action| action.awaiting)
5660 }
5661
5662 /// Show one child at a time, starting at this one.
5663 ///
5664 /// The carousel and the tab group, which are one thing said twice: whether
5665 /// a host draws a strip of names or a prev/next row falls out of whether
5666 /// the children carry a [`label`](Self::label), never out of the widget's
5667 /// name.
5668 ///
5669 /// `shown` is where the region starts, not where it stays. Whether a later
5670 /// visit comes back to the same child is the renderer's, and there is no
5671 /// way to say otherwise here. See [`showing`](Self::showing) for the rule
5672 /// and what it settles.
5673 #[must_use]
5674 pub fn showing_one(mut self, shown: usize) -> Self {
5675 self.body.select(Picks::One, Some(shown));
5676 self
5677 }
5678
5679 /// Show one child or none, starting closed unless a child is named.
5680 ///
5681 /// Disclosure. `None` is the closed state and is a legal resting place,
5682 /// which is the whole of what separates this from
5683 /// [`showing_one`](Self::showing_one).
5684 ///
5685 /// A closed disclosure that fetches its contents when it first opens is
5686 /// describable as `None` here plus a [`Readiness`](layout::Readiness) for
5687 /// the wait. What makes the fetch start is the renderer's, under the rule
5688 /// on [`showing`](Self::showing).
5689 #[must_use]
5690 pub fn showing_at_most_one(mut self, shown: Option<usize>) -> Self {
5691 self.body.select(Picks::AtMostOne, shown);
5692 self
5693 }
5694
5695 /// Say that this region's children are answers to one question.
5696 ///
5697 /// See [`Repeating`], and put [`removes`](Self::removes) on each child: the
5698 /// two are halves of one description and a group with neither is a group
5699 /// nobody can shrink.
5700 #[must_use]
5701 pub fn repeating(mut self, repeating: Repeating) -> Self {
5702 self.repeating = Some(Box::new(repeating));
5703 self
5704 }
5705
5706 /// Say what taking this slot away calls.
5707 ///
5708 /// Only meaningful on a child of a [`repeating`](Self::repeating) region.
5709 /// Ignored elsewhere rather than refused, for the reason every other
5710 /// builder here is a no-op off its own member: a description that says
5711 /// something no renderer reads is a description bug, and a panic in a
5712 /// builder chain is a worse way to find one than a control that is not
5713 /// drawn.
5714 #[must_use]
5715 pub fn removes(mut self, act: Act) -> Self {
5716 self.removes = Some(Box::new(act));
5717 self
5718 }
5719
5720 /// Name this region in its own right. See [`name`](Self::name) for what
5721 /// separates it from [`label`](Self::label).
5722 #[must_use]
5723 pub fn named(mut self, name: impl Into<String>) -> Self {
5724 self.name = Some(name.into());
5725 self
5726 }
5727
5728 /// Which child to draw, once [`shown`](Self::shown) is read against the body.
5729 ///
5730 /// `None` means draw them all, which is both [`layout::Showing::All`] and a
5731 /// closed disclosure — the two cases differ in what chrome sits around them
5732 /// and not in what a renderer does with the body, so they answer the same
5733 /// here.
5734 ///
5735 /// An index past the end is clamped rather than refused. A description
5736 /// pointing at a frame that is not there is a bug in the app, and a renderer
5737 /// that answers it by drawing nothing reports it as a region that vanished,
5738 /// which is the hardest kind of bug to find from what is on the screen.
5739 /// [`layout::Share::percent`] clamps for the same reason.
5740 ///
5741 /// The clamp is [`layout::Window`]'s, not this method's. A frame is a window
5742 /// of one over children that are all present, which is the same arithmetic a
5743 /// paged list runs over rows that are not — see [`Rest`]. Sharing it is what
5744 /// stops the two disagreeing about which frame is last.
5745 #[must_use]
5746 pub fn current(&self) -> Option<usize> {
5747 let Body::Selective {
5748 picks,
5749 shown,
5750 frames,
5751 } = &self.body
5752 else {
5753 return None;
5754 };
5755 if frames.is_empty() {
5756 return None;
5757 }
5758 let frame = |at: usize| layout::Window::frame(at, frames.len()).clamped().from;
5759 match picks {
5760 Picks::One => Some(frame(shown.unwrap_or(0))),
5761 Picks::AtMostOne => shown.map(frame),
5762 }
5763 }
5764
5765 /// How much of this region is up at once, in the vocabulary's words.
5766 #[must_use]
5767 pub const fn showing(&self) -> layout::Showing {
5768 self.body.showing()
5769 }
5770
5771 /// Which member the region opens on, before clamping.
5772 ///
5773 /// Read [`current`](Self::current) instead when drawing, which is where an
5774 /// index past the end of the body is dealt with.
5775 #[must_use]
5776 pub const fn shown(&self) -> Option<usize> {
5777 self.body.shown()
5778 }
5779
5780 /// Every member in order, whichever shape the body is.
5781 pub fn members(&self) -> impl Iterator<Item = &Ranked> + '_ {
5782 self.body.members()
5783 }
5784
5785 /// The children's names, when they have them.
5786 ///
5787 /// Empty unless *every* child is a named region, which is the test a
5788 /// renderer applies before drawing a strip: a strip with a hole in it is
5789 /// worse than the prev/next row it would otherwise have drawn, and a
5790 /// half-labelled body is an app bug rather than a third idiom.
5791 #[must_use]
5792 pub fn labels(&self) -> Vec<&str> {
5793 let Body::Selective { frames, .. } = &self.body else {
5794 // A region that shows everything reveals nothing, so it has no
5795 // controls to name. Since `2cdc6761` it cannot hold a name either.
5796 return Vec::new();
5797 };
5798
5799 let named: Vec<&str> = frames
5800 .iter()
5801 .filter_map(|frame| frame.label.as_deref())
5802 .collect();
5803
5804 if named.len() == frames.len() {
5805 named
5806 } else {
5807 Vec::new()
5808 }
5809 }
5810
5811 /// What this region and the regions inside it offer under a control name.
5812 ///
5813 /// The described half of a [`Reveal`]: what the box was handed to the
5814 /// reader holding, before anything was typed into it. A renderer that
5815 /// holds edits reads those first and falls back to this, which is the same
5816 /// order a submit reads a form in.
5817 #[must_use]
5818 pub fn holds(&self, name: &str) -> Option<&str> {
5819 self.run
5820 .iter()
5821 .flat_map(|run| run.members.iter())
5822 .chain(self.body.iter())
5823 .find_map(|placed| placed.node.holds(name))
5824 }
5825
5826 /// Every question inside this region, at any depth, in draw order.
5827 ///
5828 /// What a [`consults`](Self::consults) gathers, and it lives here rather
5829 /// than in each renderer for [`holds`](Self::holds)' reason: three walks
5830 /// over this crate's own tree are three chances to disagree about what
5831 /// "inside this region" means, and the browser's answer — every control
5832 /// the element contains — is not one a terminal can copy without being
5833 /// told the same shape.
5834 ///
5835 /// The run as well as the body: a filter bar that moved into the region's
5836 /// leading row is still a dial of that region, and a walk that skipped the
5837 /// run would recompute without it.
5838 ///
5839 /// A repeating question ([`Field::repeats`]) is one entry, under the name
5840 /// the description gave it. How many boxes that is standing right now is
5841 /// the renderer's, since it is the renderer that holds the count.
5842 #[must_use]
5843 pub fn questions(&self) -> Vec<&Field> {
5844 let mut found = Vec::new();
5845 for placed in self
5846 .run
5847 .iter()
5848 .flat_map(|run| run.members.iter())
5849 .chain(self.body.iter())
5850 {
5851 placed.node.questions(&mut found);
5852 }
5853 found
5854 }
5855
5856 /// Whether a control anywhere in this region carries this
5857 /// [`Act::id`](Act::id).
5858 ///
5859 /// The run as well as the body, for [`find`](Self::find)'s reason: a control
5860 /// in a toolbar is as reachable as one in a pane, and a walk that skipped
5861 /// the run would answer `false` for half the screens in the tree.
5862 #[must_use]
5863 pub fn names(&self, id: &str) -> bool {
5864 self.body
5865 .iter()
5866 .chain(self.run.iter().flat_map(|run| run.members.iter()))
5867 .any(|placed| placed.node.names(id))
5868 }
5869
5870 /// This slot, or the first slot under this address anywhere inside it.
5871 #[must_use]
5872 pub fn find(&self, id: &str) -> Option<&Self> {
5873 if self.id == id {
5874 return Some(self);
5875 }
5876 // The run is walked with the body, and it has to be: goingson's toolbar
5877 // is a region that moved out of the pane and into the tab strip's row,
5878 // and a region a fragment cannot be aimed at is a region that stops
5879 // updating. Body first, because that is draw order for everything that
5880 // is not a tab group and the one that is has no body a fragment names.
5881 self.body
5882 .iter()
5883 .chain(self.run.iter().flat_map(|run| run.members.iter()))
5884 .find_map(|placed| match &placed.node {
5885 Node::Region(slot) => slot.find(id),
5886 _ => None,
5887 })
5888 }
5889
5890 /// Whether this region, or one inside it, holds a row of a live selection.
5891 ///
5892 /// [`find`](Self::find)'s walk asking a different question. See
5893 /// [`Screen::chooses`], which is the only caller and carries the reasoning.
5894 #[must_use]
5895 pub fn chooses(&self) -> bool {
5896 self.body
5897 .iter()
5898 .chain(self.run.iter().flat_map(|run| run.members.iter()))
5899 .any(|placed| match &placed.node {
5900 Node::Region(slot) => slot.chooses(),
5901 Node::Table { rows, .. } => rows.iter().any(|row| row.chosen.is_some()),
5902 _ => false,
5903 })
5904 }
5905
5906 /// The mutable half of [`find`](Self::find).
5907 ///
5908 /// Same walk, and it has to be a second function rather than the same one
5909 /// generic over mutability: a `&mut` borrow of `self` cannot be handed to
5910 /// the recursive call and kept, which is what `find_map` does on the shared
5911 /// side.
5912 /// Every call this region and the regions inside it are waiting on, in draw
5913 /// order.
5914 ///
5915 /// Here rather than in each renderer because it is a walk over this crate's
5916 /// own tree, and two hosts writing it separately is two answers to "which
5917 /// regions have not arrived". A webview needs none of it: the markup carries
5918 /// a trigger per region and the browser does the walk. Every host that
5919 /// retains the description rather than the markup does need it.
5920 /// # A tab's panel is not waiting, it is unasked
5921 ///
5922 /// A labelled region that shows one child at a time is a tab strip, and a
5923 /// child of one carrying [`fed_by`](Self::fed_by) is naming the tab's
5924 /// address rather than saying its content is on its way. Those are not
5925 /// returned here: a host that asked for all of them would fetch every
5926 /// panel the moment the screen went up, which is what the reader pressing
5927 /// the tabs exists to avoid — measured on MNW's library page as five
5928 /// database reads per view where there had been one.
5929 ///
5930 /// The walk still descends, because a panel that has arrived may hold a
5931 /// region of its own that genuinely is waiting, and that one is nobody's
5932 /// tab.
5933 fn feeds_into<'a>(&'a self, out: &mut Vec<&'a Action>) {
5934 // A live region's call is a cadence rather than an arrival, so it is
5935 // [`Screen::refreshes`]'s and not this walk's. Returning it here would
5936 // have a retained host ask again the moment the answer landed, which is
5937 // a poll at whatever speed the event loop happens to run at.
5938 if let Some(action) = &self.fed_by
5939 && !self.live
5940 {
5941 out.push(action);
5942 }
5943 let tabbed = self.showing().selective() && !self.labels().is_empty();
5944 for placed in self
5945 .body
5946 .iter()
5947 .chain(self.run.iter().flat_map(|run| run.members.iter()))
5948 {
5949 if let Node::Region(slot) = &placed.node {
5950 if tabbed {
5951 slot.panel_feeds_into(out);
5952 } else {
5953 slot.feeds_into(out);
5954 }
5955 }
5956 }
5957 }
5958
5959 /// Every call this region and the regions inside it re-ask on a cadence.
5960 ///
5961 /// [`feeds_into`](Self::feeds_into)'s counterpart, split by
5962 /// [`live`](Self::live) so that the two answers never overlap: a call is
5963 /// one or the other and no host has to work out which.
5964 ///
5965 /// A tab's unopened panel is skipped here for the reason it is skipped
5966 /// there. A panel nobody has asked for is not being kept up to date either.
5967 fn refreshes_into<'a>(&'a self, out: &mut Vec<&'a Action>) {
5968 if let Some(action) = &self.fed_by
5969 && self.live
5970 {
5971 out.push(action);
5972 }
5973 let tabbed = self.showing().selective() && !self.labels().is_empty();
5974 for placed in self
5975 .body
5976 .iter()
5977 .chain(self.run.iter().flat_map(|run| run.members.iter()))
5978 {
5979 if let Node::Region(slot) = &placed.node {
5980 if tabbed {
5981 slot.panel_refreshes_into(out);
5982 } else {
5983 slot.refreshes_into(out);
5984 }
5985 }
5986 }
5987 }
5988
5989 /// This region and the regions inside it that ask a question of their own.
5990 ///
5991 /// [`refreshes_into`](Self::refreshes_into)'s shape for
5992 /// [`consults`](Self::consults), and here for its reason: what counts as a
5993 /// region of this screen is this crate's answer, and a renderer walking the
5994 /// tree itself is a second answer waiting to disagree.
5995 ///
5996 /// A tab's unopened panel is **not** skipped, unlike the two walks above.
5997 /// Those find calls a host would make on its own; this finds the questions
5998 /// a control the reader touched sets off, and a control inside a panel
5999 /// nobody has opened is a control nobody has touched. Skipping it would
6000 /// cost a walk and rule out nothing.
6001 fn consulting_into<'a>(&'a self, out: &mut Vec<&'a Self>) {
6002 if !self.consults.is_empty() {
6003 out.push(self);
6004 }
6005 for placed in self
6006 .body
6007 .iter()
6008 .chain(self.run.iter().flat_map(|run| run.members.iter()))
6009 {
6010 if let Node::Region(slot) = &placed.node {
6011 slot.consulting_into(out);
6012 }
6013 }
6014 }
6015
6016 /// Whether this region or anything inside it changes without the user.
6017 ///
6018 /// Wider than [`refreshes_into`](Self::refreshes_into), which finds only the
6019 /// live regions that name a call. A host asks this to decide whether to keep
6020 /// redrawing at all, and a live region reading state the host already holds
6021 /// is exactly the case that has no call to find.
6022 fn live_within(&self) -> bool {
6023 self.live
6024 || self
6025 .body
6026 .iter()
6027 .chain(self.run.iter().flat_map(|run| run.members.iter()))
6028 .any(|placed| match &placed.node {
6029 Node::Region(slot) => slot.live_within(),
6030 _ => false,
6031 })
6032 }
6033
6034 /// [`refreshes_into`](Self::refreshes_into) for a tab's panel: the panel's
6035 /// own call is its address, not a cadence.
6036 fn panel_refreshes_into<'a>(&'a self, out: &mut Vec<&'a Action>) {
6037 for placed in self
6038 .body
6039 .iter()
6040 .chain(self.run.iter().flat_map(|run| run.members.iter()))
6041 {
6042 if let Node::Region(slot) = &placed.node {
6043 slot.refreshes_into(out);
6044 }
6045 }
6046 }
6047
6048 /// [`feeds_into`](Self::feeds_into) for a tab's panel: whatever is inside it
6049 /// is waiting, and the panel itself is not.
6050 fn panel_feeds_into<'a>(&'a self, out: &mut Vec<&'a Action>) {
6051 for placed in self
6052 .body
6053 .iter()
6054 .chain(self.run.iter().flat_map(|run| run.members.iter()))
6055 {
6056 if let Node::Region(slot) = &placed.node {
6057 slot.feeds_into(out);
6058 }
6059 }
6060 }
6061
6062 /// The call that fills this region when something above it asks on its
6063 /// behalf, which is a tab strip pressing one of its own tabs.
6064 ///
6065 /// The address a panel carries but does not act on, so the one party that
6066 /// does act on it does not have to reach into the field and decide for
6067 /// itself what the field means here.
6068 ///
6069 /// # Only while it is empty, and only a retained-screen host can tell
6070 ///
6071 /// A panel that has been read is not asked for again, which is what going
6072 /// back to a tab means. That is the answer for a host holding the
6073 /// description, and it is not the answer a webview gives: the markup keeps
6074 /// the address on the button after [`Screen::replace`] has dropped it from
6075 /// the tree, so a browser re-reads the panel on every press. The divergence
6076 /// is `replace` clearing [`fed_by`](Self::fed_by), which predates this and
6077 /// is the rule that stops a retained screen asking twice on one paint.
6078 ///
6079 /// Neither is wrong and the difference is visible only as freshness, so it
6080 /// is recorded here rather than papered over. What both agree on is the one
6081 /// thing that mattered: pressing a tab is what fetches it, and four unpressed
6082 /// frames fetch nothing.
6083 #[must_use]
6084 pub fn asked_for(&self) -> Option<&Action> {
6085 if self.body.is_empty() {
6086 self.fed_by.as_deref()
6087 } else {
6088 None
6089 }
6090 }
6091
6092 fn find_mut(&mut self, id: &str) -> Option<&mut Self> {
6093 if self.id == id {
6094 return Some(self);
6095 }
6096 self.body
6097 .iter_mut()
6098 .chain(self.run.iter_mut().flat_map(|run| run.members.iter_mut()))
6099 .find_map(|placed| match &mut placed.node {
6100 Node::Region(slot) => slot.find_mut(id),
6101 _ => None,
6102 })
6103 }
6104 }
6105
6106 /// A value an act puts into a field on the same screen.
6107 ///
6108 /// An act names a destination field, and the renderer decides where in it the
6109 /// value lands. Measured on MNW's media picker, where the whole point of the
6110 /// button is to put an image reference into the box the reader is already
6111 /// typing in, and where the three surfaces it appears on lose between 30
6112 /// seconds and the entire unsaved draft if the server does the appending
6113 /// instead.
6114 ///
6115 /// # Stated as a destination and never as a caret
6116 ///
6117 /// The description says "into `body`". It never says "at the caret", because
6118 /// where inside a field a value lands is the renderer's, the same class of fact
6119 /// as how a menu overflows or when a toast clears. A webview inserts at the
6120 /// selection, a terminal and an immediate-mode host append, and neither is
6121 /// wrong. This is what keeps `d52884b0` — "a field's described state is its
6122 /// value, and the caret is the renderer's" — standing rather than reopened:
6123 /// nothing here carries a caret in either direction.
6124 ///
6125 /// # Both halves, because an act has no value of its own
6126 ///
6127 /// [`field`](Self::field) is addressed by [`Field::name`], which is already how
6128 /// [`Field::writes`] and a submit name a field, so no id relationship is
6129 /// invented. [`value`](Self::value) is here because an [`Act`] carries a label
6130 /// and an address and nothing else a renderer could put anywhere: the picker's
6131 /// card reads as a file name and deposits `![](media/kick.png)`, and the two
6132 /// are not the same string.
6133 ///
6134 /// # It does not replace the act's action
6135 ///
6136 /// A renderer fills first and then dispatches [`Act::action`] as it always
6137 /// would. An act that only fills says so with [`Destination::Local`], which is
6138 /// the vocabulary's existing way to say that no request goes out, and is what
6139 /// every measured site wants.
6140 #[derive(Debug, Clone, PartialEq, Eq)]
6141 pub struct Prefill {
6142 /// The [`Field::name`] that receives it.
6143 pub field: String,
6144 /// What lands there.
6145 pub value: String,
6146 }
6147
6148 /// A control that calls a route.
6149 ///
6150 /// A button, a link and a menu item are the same thing to a description: a
6151 /// label, an address, and how loudly it is saying it. Which of the three a
6152 /// renderer draws is a renderer decision.
6153 #[derive(Debug, Clone, PartialEq, Eq)]
6154 pub struct Act {
6155 /// What it is called.
6156 pub label: String,
6157 /// What it calls.
6158 pub action: Action,
6159 /// What it is saying. [`layout::Tone::Danger`] is what marks the button
6160 /// that destroys something.
6161 pub tone: layout::Tone,
6162 /// Focused, disabled, or neither.
6163 pub state: Option<layout::State>,
6164 /// What to ask before doing it, if it should be asked.
6165 ///
6166 /// The prompt only. The word on the agreeing button is
6167 /// [`label`](Self::label), because it already is — goingson's `confirmDelete`
6168 /// passes `confirmText: 'Delete'` for an act labelled "Delete" — and a
6169 /// second string would be the same word twice with a chance to disagree.
6170 /// [`tone`](Self::tone) already says whether the dialog is a dangerous one.
6171 ///
6172 /// `Region::Modal` names the box a confirmation appears in and does not name
6173 /// the pattern. This is the pattern: a webview raises a dialog, a touch host
6174 /// an action sheet, a terminal a y/n line, and none of them is a route to a
6175 /// modal screen and back, which is a different interaction.
6176 pub confirm: Option<String>,
6177 /// The key that reaches it, written the way a user would say it.
6178 ///
6179 /// An `Act` had a label and a destination and nothing said which key gets
6180 /// there, so goingson's 279-line `keyboard.js` holds the table beside the
6181 /// description, and the help overlay that lists the shortcuts is a second
6182 /// hand-written copy that can drift from it.
6183 ///
6184 /// A terminal makes the case sharper than a webview does: there the key *is*
6185 /// the affordance, so a description that cannot name one cannot describe the
6186 /// screen's primary interaction at all.
6187 ///
6188 /// Text rather than a modelled chord — "n", "ctrl+k", "?" — because the
6189 /// vocabulary of keys is the host's and a description that modelled it would
6190 /// be naming one host's keyboard. A renderer that does not know a name
6191 /// ignores it, which is what a webview does with a key a terminal wants.
6192 ///
6193 /// Screen-scoped, because a screen is what this describes. An app-wide
6194 /// shortcut belongs to the app and is not a fact about any one screen:
6195 /// that is [`Chrome::bindings`](crate::Chrome::bindings), held beside the
6196 /// router rather than inside any answer. A renderer matches those first, so
6197 /// a screen cannot capture the key that opens the palette.
6198 pub key: Option<String>,
6199 /// The [`Screen::selection`] this acts on, if it acts on one.
6200 ///
6201 /// Every ticked [`Row::value`] is sent under [`Node::TICKED`], repeated
6202 /// once per member. Repeated rather than joined, because a name appearing
6203 /// many times is what [`Params::get_all`] is for and a delimiter would have
6204 /// to be one no value can contain.
6205 ///
6206 /// # A handler still answers for an empty set
6207 ///
6208 /// Every renderer draws a control over an empty selection as disabled and
6209 /// refuses the press, so the ordinary way to reach a handler with no ticks
6210 /// is gone. It is not the only way: a hand-typed request has none, and a
6211 /// webview host that does not serve
6212 /// [`SELECTION_JS`](https://makenot.work/git/max/quasi) leaves the control
6213 /// live. A bulk write over nothing should still answer the screen rather
6214 /// than erroring — a renderer's refusal is an affordance, not a guarantee
6215 /// about what arrives.
6216 ///
6217 /// # The name does not select between sets yet, and cannot
6218 ///
6219 /// A screen holds one selection ([`Screen::selection`]), so being set at
6220 /// all is what makes a control a commit control, and the name is what makes
6221 /// it *readable* — "Archive" over `chosen` is a different sentence from
6222 /// "Archive" on a row.
6223 ///
6224 /// Matching it against the screen's name was the first shape and it does
6225 /// not work, because a renderer does not always have the screen: an
6226 /// [`Outcome::Fragment`] replaces a region and carries no screen at all, so
6227 /// a webview rendering one would have had to guess and a terminal, which
6228 /// keeps the screen beside it, would not. The two hosts would then disagree
6229 /// about a typo, which is exactly the drift this vocabulary exists to stop.
6230 /// So both read it the same way, and the name starts choosing between sets
6231 /// on the day [`Screen::selection`] becomes a map.
6232 ///
6233 /// [`Params::get_all`]: crate::Params::get_all
6234 /// [`Outcome::Fragment`]: crate::Outcome::Fragment
6235 pub over: Option<String>,
6236 /// What the press asks for before the call goes out, if it asks for
6237 /// anything.
6238 ///
6239 /// [`confirm`](Self::confirm) is the yes/no shape of this moment, a
6240 /// question raised after the press and before the call, and this is the
6241 /// shape that comes back with a value. Measured on MNW's content table,
6242 /// where five verbs sit over the selection and two of them, "Set Price"
6243 /// and "Add Tag", reveal a small form first: a label, one box, an Apply
6244 /// and a hint, hidden until the verb is pressed.
6245 ///
6246 /// Every field is sent under its own [`Field::name`], the way a submit
6247 /// sends it, and the ticks ride along under [`Node::TICKED`] when
6248 /// [`over`](Self::over) names a selection. A handler reads a verb that
6249 /// asked for a value the same way it reads one that did not.
6250 ///
6251 /// Empty is the ordinary control, which is nearly all of them.
6252 ///
6253 /// # Why it is not a form
6254 ///
6255 /// Said as a [`Node::Form`] the verb is lost: the description holds a
6256 /// route, a submit label and a box, and nothing says the box belongs to
6257 /// "Set Price" rather than to the screen. Said as a route that answers a
6258 /// form fragment it is a round trip to ask a question whose shape the
6259 /// screen already knows, and a fragment is markup a terminal or an egui
6260 /// host has nowhere to put.
6261 ///
6262 /// A field here carries no [`Field::writes`]. It is answered by the control
6263 /// that asked for it, so a renderer that honoured a write on it as well
6264 /// would fire twice for one value.
6265 ///
6266 /// # The disclosure is the renderer's
6267 ///
6268 /// quasi-webview puts the fields in a `<details>` under the control, which
6269 /// is the shape MNW's button-then-form already has. quasi-tui and
6270 /// quasi-immediate draw them beside the control instead: a terminal has no
6271 /// popover, and "first paint is final paint" is worth more there than
6272 /// hiding two boxes. Both send the same values, which is the part the
6273 /// description states.
6274 pub asks: Vec<Field>,
6275 /// The field on this screen that receives the act's value, if it has one.
6276 ///
6277 /// See [`Prefill`] for the ruling and for why both halves are there. Absent
6278 /// on nearly every control, which is what makes this additive: a renderer
6279 /// that finds nothing here draws exactly what it drew before the member
6280 /// existed.
6281 ///
6282 /// Not a second address. [`over`](Self::over) names a selection the act
6283 /// reads and this names a field the act writes, and an act may carry both:
6284 /// the picker's card names neither, and a verb that gathers ticks and
6285 /// deposits a summary somewhere would name both without either meaning the
6286 /// other.
6287 pub fills: Option<Prefill>,
6288 /// Standing help about the control, when the label does not carry it.
6289 ///
6290 /// The same member as [`Field::hint`] and it means the same thing: a
6291 /// sentence that is always true of this control, shown rather than hunted
6292 /// for. [`confirm`](Self::confirm) is a question asked at the press and
6293 /// [`asks`](Self::asks) is a value collected at it; both are about the
6294 /// moment, and this is about the control.
6295 ///
6296 /// Counted before it was added: 78 `title` attributes in MNW, 12 in
6297 /// Balanced Breakfast and 14 `on_hover_text` calls in audiofiles, and where
6298 /// they sit is the finding -- `button` and `a` outnumber every other
6299 /// element carrying one. audiofiles' storage section is the clearest single
6300 /// site, where six acts wanted one and three said something the label could
6301 /// not: "Local-only: other synced devices keep their own copies.",
6302 /// "Runs in the background: keep working", "the result appears in the
6303 /// status line."
6304 ///
6305 /// # Not a tooltip
6306 ///
6307 /// Half the hosts have no pointer. The shipped apps spelled this as a hover
6308 /// because egui and a browser both had one, and the hover is the spelling
6309 /// rather than the thing. A terminal puts it on a help line, egui may keep
6310 /// its hover, a webview writes `title` *and* stays free to draw it: what
6311 /// the description says is that the sentence is true, not that it is
6312 /// hidden.
6313 ///
6314 /// A renderer that draws it must not also drop it from the accessible tree,
6315 /// which is the failure `title` alone has on a browser.
6316 ///
6317 /// # It lives one layer down as of makeover-layout 0.40.0
6318 ///
6319 /// `makeover_layout::Act::hint` is where the member is now, and this one
6320 /// mirrors it across [`as_layout`](Self::as_layout).
6321 ///
6322 /// # Why not prose beside the act
6323 ///
6324 /// A [`Node::Text`] next to a control reads well and says nothing about
6325 /// which control it belongs to, so a renderer laying the region out its own
6326 /// way separates them. That is the same loss [`Rest`] has beside a table,
6327 /// and it is why three sites were enough to file this and 104 are enough to
6328 /// build it.
6329 ///
6330 /// `None` is a control whose label is the whole of it, which is nearly all
6331 /// of them.
6332 pub hint: Option<String>,
6333 /// The value the press puts on the clipboard, if that is what it does.
6334 ///
6335 /// Measured on the MNW server: seven of the 67 `window.<name>` globals are
6336 /// this, across 14 sites -- `copyElementText` (4), `copyEmbedBtn` (4),
6337 /// `copyText` (2 files), `copyFeedUrl`, `copyItemLink`, `copyKeyCode`,
6338 /// `onCopyItemId`.
6339 ///
6340 /// # Why it is a member here and not a [`Destination`]
6341 ///
6342 /// A copy asks no route, so its action is [`Action::local`]. That variant's
6343 /// own rule is the reason this member exists: *what happens locally is
6344 /// named by the member carrying the action, never by the variant*. A bare
6345 /// act with a local destination says only "the renderer's own affordance
6346 /// happens", so without a member saying what, a described copy button is
6347 /// `data-action="copyThing"` in a new hat -- the exact thing
6348 /// [`Destination::Local`] was written to refuse.
6349 ///
6350 /// # The value, not its source
6351 ///
6352 /// All 14 measured sites copy something the server already rendered, and
6353 /// six of the seven scrape it back off the DOM at press time. So this
6354 /// carries the string. Naming a source element instead would make two
6355 /// elements point at each other by id for a value the description is
6356 /// holding anyway, which is what [`Field::suggests`] declined.
6357 ///
6358 /// The cost, stated: a value the *reader* has since edited cannot be
6359 /// copied this way. No measured site is one. A control that wants the live
6360 /// contents of a field is a different member and should be filed when a
6361 /// second site asks for it.
6362 ///
6363 /// # The acknowledgement is not here
6364 ///
6365 /// Every one of the seven relabels itself to "Copied!" and reverts, six at
6366 /// 1500ms and `copyFeedUrl` at 2000ms. That is a temporary label, which is
6367 /// mnw-server `033c722f`'s class and belongs to `makeover-timing` rather
6368 /// than to this member. Deliberately separable: a host with no notion of a
6369 /// reverting label still needs to be told the act copies something.
6370 pub copies: Option<String>,
6371 /// The name an [`Outcome::Anchored`] reaches this control by.
6372 ///
6373 /// [`Anchor::Control`] names a control and an [`Act`] had no name to be
6374 /// named by: a label is what it says and can be the same word twice on one
6375 /// screen, and an action is where it goes, which two controls may share.
6376 ///
6377 /// `None` on nearly every control, which is what makes this additive: a
6378 /// control nothing anchors to needs no name, and a renderer that finds none
6379 /// draws exactly what it drew before the member existed.
6380 ///
6381 /// Screen-scoped and stable, the same contract [`Slot::id`] carries and for
6382 /// the same reason — an answer aimed at it lands nowhere if it moves.
6383 ///
6384 /// [`Outcome::Anchored`]: crate::Outcome::Anchored
6385 /// [`Anchor::Control`]: crate::Anchor::Control
6386 /// [`Slot::id`]: Slot::id
6387 pub id: Option<String>,
6388 /// The picture this control shows, when it shows one.
6389 ///
6390 /// [`label`](Self::label) stays what the control *says* and this is what
6391 /// it *shows*; a renderer draws both, because a control drawing only a
6392 /// picture and hiding its name is a control with no accessible text.
6393 ///
6394 /// # What the containment model already covers
6395 ///
6396 /// [`Row::part`] and [`Cell::part`] take any leaf, [`Node::Image`] is one,
6397 /// and a webview puts an activated row's whole run inside the anchor, so a
6398 /// picture in an activated row is already pressable with no member here.
6399 ///
6400 /// What that leaves is the case where the *control itself* is the picture,
6401 /// which a row cannot be:
6402 ///
6403 /// - `MNW/server/src/quasi/media_picker.rs`, `fn card`. A tile is a region
6404 /// holding a [`Node::Image`] and an [`Act`] as siblings, so only the file
6405 /// name answers a press and a reader aiming at the thumbnail hits
6406 /// nothing. It cannot be an activated row instead, because the press
6407 /// deposits a value and [`fills`](Self::fills) lives here rather than on
6408 /// [`Row`].
6409 /// - `MNW/server/templates/pages/project.html`, the storefront item card:
6410 /// `<a class="item-thumbnail"><img></a>`, with a second link on the title
6411 /// going to the same place. One act showing the cover and labelled with
6412 /// the title is one control where the markup has two.
6413 ///
6414 /// # What each renderer does
6415 ///
6416 /// A webview draws the picture inside the control. quasi-immediate draws it
6417 /// above the button, outside `makeover_immediate::widget::act`, because
6418 /// `makeover_layout::Act` does not carry a picture -- which was
6419 /// [`hint`](Self::hint)'s reason too until makeover-layout 0.40.0 moved
6420 /// that one down. quasi-tui ignores it and draws the label, which is
6421 /// already the honest terminal answer -- a picture's alt text is what a
6422 /// terminal has, and the label is saying it.
6423 pub shows: Option<Image>,
6424 }
6425
6426 impl Act {
6427 /// A neutral control calling this route.
6428 pub fn new(label: impl Into<String>, action: Action) -> Self {
6429 Self {
6430 label: label.into(),
6431 action,
6432 tone: layout::Tone::Neutral,
6433 state: None,
6434 confirm: None,
6435 key: None,
6436 over: None,
6437 asks: Vec::new(),
6438 fills: None,
6439 hint: None,
6440 copies: None,
6441 shows: None,
6442 id: None,
6443 }
6444 }
6445
6446 /// Standing help about this control. See [`hint`](Self::hint).
6447 #[must_use]
6448 pub fn hint(mut self, hint: impl Into<String>) -> Self {
6449 self.hint = Some(hint.into());
6450 self
6451 }
6452
6453 /// Pressing this puts that value on the clipboard.
6454 ///
6455 /// Sets the destination to [`Action::local`] as well, because a copy asks
6456 /// no route and the two facts are one sentence. See
6457 /// [`copies`](Self::copies).
6458 #[must_use]
6459 pub fn copying(mut self, value: impl Into<String>) -> Self {
6460 self.action = Action::local();
6461 self.copies = Some(value.into());
6462 self
6463 }
6464
6465 /// The picture this control shows. See [`shows`](Self::shows).
6466 ///
6467 /// The label is untouched and stays the control's name, which is what a
6468 /// renderer that draws no pictures reads and what a screen reader
6469 /// announces.
6470 #[must_use]
6471 pub fn showing(mut self, picture: Image) -> Self {
6472 self.shows = Some(picture);
6473 self
6474 }
6475
6476 /// The name an anchored screen reaches this control by.
6477 ///
6478 /// See [`id`](Self::id). Nothing else reads it: it is not a class, not a
6479 /// test hook and not a second address, and a control that is never anchored
6480 /// to should not carry one.
6481 #[must_use]
6482 pub fn id(mut self, id: impl Into<String>) -> Self {
6483 self.id = Some(id.into());
6484 self
6485 }
6486
6487 /// This acts on the screen's selection, by name.
6488 ///
6489 /// The commit half of a staged tick. See [`over`](Self::over) for what
6490 /// reaches the handler, and [`Screen::selection`] for why a tick stages
6491 /// rather than writes.
6492 #[must_use]
6493 pub fn over(mut self, selection: impl Into<String>) -> Self {
6494 self.over = Some(selection.into());
6495 self
6496 }
6497
6498 /// Ask for this value before doing it, chaining.
6499 ///
6500 /// Adds rather than replaces, for [`Field::consults`]' reason: MNW's two
6501 /// verbs ask for one value each, and a builder that took the last call
6502 /// would make a verb wanting two unwritable. See [`asks`](Self::asks).
6503 #[must_use]
6504 pub fn asking(mut self, field: Field) -> Self {
6505 self.asks.push(field);
6506 self
6507 }
6508
6509 /// Put this value into that field when it is pressed.
6510 ///
6511 /// Replaces rather than appends, unlike [`asking`](Self::asking): an act
6512 /// deposits one value, and a control writing into two boxes at once is a
6513 /// description doing two things under one press. See [`Prefill`].
6514 #[must_use]
6515 pub fn filling(mut self, field: impl Into<String>, value: impl Into<String>) -> Self {
6516 self.fills = Some(Prefill {
6517 field: field.into(),
6518 value: value.into(),
6519 });
6520 self
6521 }
6522
6523 /// Ask this before doing it.
6524 #[must_use]
6525 pub fn confirm(mut self, prompt: impl Into<String>) -> Self {
6526 self.confirm = Some(prompt.into());
6527 self
6528 }
6529
6530 /// The key that reaches it.
6531 #[must_use]
6532 pub fn key(mut self, key: impl Into<String>) -> Self {
6533 self.key = Some(key.into());
6534 self
6535 }
6536
6537 /// Set what it is saying.
6538 #[must_use]
6539 pub fn tone(mut self, tone: layout::Tone) -> Self {
6540 self.tone = tone;
6541 self
6542 }
6543
6544 /// Present, visible, and not answering.
6545 #[must_use]
6546 pub fn disabled(mut self) -> Self {
6547 self.state = Some(layout::State::Disabled);
6548 self
6549 }
6550
6551 /// Whether the control currently answers input.
6552 #[must_use]
6553 pub fn interactive(&self) -> bool {
6554 !self
6555 .state
6556 .is_some_and(layout::State::suppresses_interaction)
6557 }
6558
6559 /// Borrow as the description layer's own type.
6560 ///
6561 /// [`action`](Self::action) and [`confirm`](Self::confirm) do not survive
6562 /// the crossing, and that is what the two layers disagree about rather than
6563 /// an oversight. An address is quasi's — every host follows one differently
6564 /// — and a confirmation is a question asked after the press, so it belongs
6565 /// to whoever is holding the interaction. What is left is what a renderer
6566 /// needs to *draw* the control, which is all `layout::Act` claims to be.
6567 #[must_use]
6568 pub fn as_layout(&self) -> layout::Act<'_> {
6569 layout::Act {
6570 label: &self.label,
6571 key: self.key.as_deref(),
6572 tone: self.tone,
6573 state: self.state,
6574 hint: self.hint.as_deref(),
6575 }
6576 }
6577 }
6578
6579 // `Part` was merged into `Cell` on 2026-09-05.
6580 //
6581 // A part was a cell that carried its own role because a list had no columns to
6582 // carry it. Now a list declares columns like a table does, so the role is
6583 // `CellKey::Role` and there is one type. `Part::worth` is `Cell::priority`
6584 // falling back to its key.
6585
6586 /// One row of a list.
6587 ///
6588 /// # The run
6589 ///
6590 /// A row's content is an inline run of [`Part`]s, in the order the description
6591 /// says them, the same way a [`Cell`]'s is.
6592 /// -- each of which arrived as a counted-sites argument, a member here, a
6593 /// [`layout::RowPart`] variant and a release: `RowPart::Tokens` at
6594 /// makeover-layout 0.9.0 for a badge in a row, `RowPart::Proportion` at 0.11.0
6595 /// for a bar in one. A link in a row was simply not sayable, and a figure in
6596 /// one was not either. Under the run both are already sayable and cost nothing.
6597 ///
6598 /// The bound is that every part is a leaf, so a row is drawable on one wrapped
6599 /// line without a renderer knowing what is in it. [`Row::part`] is where that
6600 /// bites at a call site.
6601 ///
6602 /// Order is the description's: a part draws where it was put, so a tag between
6603 /// two facts stays between them.
6604 ///
6605 /// # What stayed a field
6606 ///
6607 /// [`activate`](Self::activate), [`current`](Self::current),
6608 /// [`selected`](Self::selected), [`menu`](Self::menu) and
6609 /// [`toggle`](Self::toggle) are facts *about* the row rather than content in
6610 /// it. A run of things on a line is not where "this row is the one the detail
6611 /// pane is showing" belongs.
6612 ///
6613 /// # The cost
6614 ///
6615 /// [`primary()`](Self::primary) is no longer guaranteed to be one string, which
6616 /// is what let a constrained renderer right-align a row cheaply. It answers the
6617 /// text of the primary parts joined, and a row built the ordinary way still has
6618 /// exactly one.
6619 /// A row and when it happens.
6620 ///
6621 /// The pairing [`Node::Timeline`] is made of. Deliberately a pair rather than
6622 /// members on [`Row`]: a row does not become a different kind of thing by
6623 /// being placed, and every list, table and detail pane in the tree would
6624 /// otherwise carry two integers it has no use for.
6625 #[derive(Debug, Clone, PartialEq, Eq)]
6626 pub struct Placed {
6627 /// Where it sits on the axis, and for how long.
6628 pub placement: layout::Placement,
6629 /// The thing itself, said the ordinary way.
6630 pub row: Row,
6631 }
6632
6633 impl Placed {
6634 /// A row at a start and a duration, both in minutes.
6635 #[must_use]
6636 pub const fn new(at: u16, minutes: u16, row: Row) -> Self {
6637 Self {
6638 placement: layout::Placement::new(at, minutes),
6639 row,
6640 }
6641 }
6642
6643 /// Whether this and another cover any of the same time.
6644 ///
6645 /// Forwarded so a renderer laying out collisions does not reach through to
6646 /// the placement and, in doing so, decide for itself what overlapping
6647 /// means.
6648 #[must_use]
6649 pub const fn overlaps(&self, other: &Self) -> bool {
6650 self.placement.overlaps(other.placement)
6651 }
6652 }
6653
6654 #[derive(Debug, Clone, PartialEq, Eq, Default)]
6655 pub struct Row {
6656 /// What is in the row, in order, one entry per column it says anything in.
6657 ///
6658 /// Built by [`Row::new`], [`secondary`](Row::secondary),
6659 /// [`meta`](Row::meta), [`token`](Row::token), [`act`](Row::act),
6660 /// [`meter`](Row::meter) for the default column set, and by
6661 /// [`cells`](Row::cells), [`at`](Row::at) and [`cell`](Row::cell) for a
6662 /// declared one.
6663 ///
6664 /// A cell says which column it answers to through [`Cell::key`], so a row
6665 /// is no longer two collections with a private staging vector between
6666 /// them. A cell keyed [`CellKey::Named`] is unresolved until
6667 /// [`Table::row`] sees it.
6668 pub cells: Vec<Cell>,
6669 /// The route that selects this row, if selecting it does anything.
6670 pub activate: Option<Action>,
6671 /// Whether this is the row the detail side is currently showing.
6672 ///
6673 /// This one is the app's own pointer into a set: what a list-detail
6674 /// arrangement highlights because its pane is showing it, and what a
6675 /// webview says with `aria-current`. The user's tick is
6676 /// [`selected`](Self::selected), and conflating them meant a screen with
6677 /// bulk actions could not describe its checkboxes at all.
6678 pub current: bool,
6679 /// Whether this row is part of a selection that is already in force.
6680 ///
6681 /// Three states for [`selected`]'s reason, the app owns the set, the
6682 /// renderer contributes the gesture through [`Choosing`], and it draws as a
6683 /// highlighted row rather than as a checkbox.
6684 ///
6685 /// [`selected`]: Self::selected
6686 pub chosen: Option<bool>,
6687 /// Whether the user has ticked this row, and whether they can.
6688 ///
6689 /// Three states in one field, which is why it is not a `bool`. `None` means
6690 /// the row is not selectable and no affordance should be drawn; `Some(false)`
6691 /// means it can be ticked and is not; `Some(true)` means it is. A plain bool
6692 /// cannot tell "not ticked" from "not tickable", so every renderer would
6693 /// have had to be told selectability some other way, and each would have
6694 /// picked a different way.
6695 ///
6696 /// This is the user's selection, as distinct from
6697 /// [`current`](Self::current). goingson's contacts and tasks screens both
6698 /// drive bulk actions from it.
6699 pub selected: Option<bool>,
6700 /// Everything else that can be done to this row.
6701 ///
6702 /// The [`Actions`](layout::RowPart::Actions) parts of the run are what the
6703 /// row shows; this is what it *offers*, reached by right-click on a
6704 /// pointer host, long-press on a touch one, and a key in a terminal. That
6705 /// split is the whole reason it belongs in the description rather than in
6706 /// a renderer: one description has to become a context menu, an action
6707 /// sheet and a key-driven menu, and no single renderer can be the place
6708 /// where it is said.
6709 ///
6710 /// goingson opens one at 14 sites and Balanced Breakfast at 9, on top of
6711 /// 680 lines of generic menu machinery between `components.js` and
6712 /// `context-menus.js`.
6713 ///
6714 /// A field rather than a role in the run, because a menu is not on the
6715 /// line. The run is what the row draws; this is what it holds back until
6716 /// the host asks, and no renderer draws it in sequence with the primary.
6717 pub menu: Vec<Act>,
6718 /// What ticking this row calls, if ticking it is the write.
6719 ///
6720 /// Part of it. [`selected`](Self::selected) says whether the row is ticked
6721 /// and whether it can be, and that was the whole story for a bulk
6722 /// checkbox, whose tick is client state feeding a later action. A
6723 /// checklist is the other case: the tick *is* the write, and it is the
6724 /// only affordance the screen offers for it. Described without this, the
6725 /// port drew the tick inert and put the toggle on a button beside it,
6726 /// which is a user clicking a button next to a checkbox that ignores
6727 /// clicks.
6728 ///
6729 /// Two fields rather than a `Selection` struct, matching how
6730 /// [`activate`](Self::activate) sits beside [`current`](Self::current):
6731 /// state and behaviour are separate facts about the row. They do have to
6732 /// agree — a `toggle` with no [`selected`](Self::selected) is a route on a
6733 /// control nothing draws — and [`Row::toggling`] is the constructor that
6734 /// makes them agree.
6735 pub toggle: Option<Action>,
6736 /// What this row's tick contributes to the screen's selection.
6737 ///
6738 /// [`selected`](Self::selected) says the row can be ticked; this says what
6739 /// ticking it *means*, which is the half that was missing. A set of ticks
6740 /// with nothing in them is not a selection, so a renderer holding
6741 /// [`Screen::selection`] holds these.
6742 ///
6743 /// `value` rather than `id`, matching [`Choice::value`]: throughout this
6744 /// vocabulary it is the word for what a control contributes when it is
6745 /// chosen, and a row's tick is the same kind of fact.
6746 ///
6747 /// A selectable row without one is the dead affordance this member exists
6748 /// to end, and [`Row::ticking`] is the constructor that cannot produce it.
6749 /// It is not enforced here, for [`toggle`](Self::toggle)'s reason: a
6750 /// description layer that refused to hold a half-built row would refuse it
6751 /// at the moment the app is still building it.
6752 ///
6753 /// [`Choice::value`]: Choice::value
6754 /// [`Screen::selection`]: Screen::selection
6755 pub value: Option<String>,
6756 /// The address a link reaches this row by.
6757 ///
6758 /// Not [`value`](Self::value): a value is unique within its list and an
6759 /// address is unique in the document, because an address is what a reader
6760 /// copies and sends on.
6761 ///
6762 /// `None` on nearly every row, and a renderer that finds none draws what it
6763 /// drew before.
6764 ///
6765 pub address: Option<String>,
6766 /// How far into a hierarchy this row sits. 0 is top level.
6767 ///
6768 /// A hierarchy of rows is a **flat list of rows each saying how deep it
6769 /// is**, not rows holding rows. audiofiles' tags are dotted --
6770 /// `drums.kick`, `genre.house` -- and the shipped sidebar builds a real
6771 /// tree with a recursive draw over it; described the other way, every tag
6772 /// was one row at its full dotted path and a vault with two hundred tags
6773 /// drew a wall where the shipped one drew an outline.
6774 ///
6775 /// Flat rather than nested for the reason the decision turned on: a
6776 /// renderer that has never heard of this member still draws the list it
6777 /// drew before, in order, with nothing missing. `Row::children` would have
6778 /// made every renderer's list walk recursive on pain of dropping rows
6779 /// silently, and a description that a renderer can only half-implement by
6780 /// losing content is not one this vocabulary should be able to say.
6781 ///
6782 /// A row deeper than the row above it is that row's child. Nothing checks
6783 /// that, and nothing should: a list whose first row is at depth 3 is a
6784 /// branch shown on its own, which is a screen somebody will write.
6785 ///
6786 /// Not [`Node::Heading`]'s level, which says how far down the *document* a
6787 /// title sits. That is depth in prose; this is containment in a set.
6788 pub depth: layout::Nesting,
6789 /// Whether this row has a disclosure, and whether it is currently open.
6790 ///
6791 /// `None` means no disclosure at all and no affordance drawn -- the state
6792 /// of every row written before this member existed, and the right answer
6793 /// for a leaf. `Some(false)` is a closed branch and `Some(true)` an open
6794 /// one. Three states in one field for [`selected`](Self::selected)'s
6795 /// reason: a bool cannot tell a leaf from a branch that happens to be shut.
6796 ///
6797 /// # What a renderer does with it, and what it does not
6798 ///
6799 /// It draws a disclosure, **as a separate hit target from the label**. The
6800 /// shipped egui sidebar already separates them deliberately, and it is the
6801 /// behaviour being described rather than an improvement on it: pressing a
6802 /// tag filters by it, pressing its chevron does not.
6803 ///
6804 /// A closed row's descendants -- the rows after it at a greater
6805 /// [`depth`](Self::depth), up to the next row at its own depth or less --
6806 /// are not drawn. That is computable from the flat list, which is what
6807 /// makes the flat list enough.
6808 ///
6809 /// # The gesture is the renderer's
6810 ///
6811 /// This says where the outline starts, not where it stays.
6812 /// [`Region::showing_at_most_one`] states the same rule for a disclosure
6813 /// around a region and every word of it applies here: what a second press
6814 /// does, and whether a later visit comes back to the same shape, is the
6815 /// renderer's. A description that had to be re-asked for to fold a branch
6816 /// would put a round trip on a gesture that changes nothing anyone else can
6817 /// observe.
6818 ///
6819 /// So there is no route beside this the way [`toggle`](Self::toggle) sits
6820 /// beside [`selected`](Self::selected). A tick is a write and has to reach
6821 /// the app; folding a branch is the reader tidying their own view. If a
6822 /// screen turns up whose open branches are app state worth persisting, that
6823 /// is the consumer that earns the third member, and it has not turned up.
6824 ///
6825 /// [`Region::showing_at_most_one`]: Region::showing_at_most_one
6826 pub open: Option<bool>,
6827 /// Which side of a change this row is on, when the table is a diff.
6828 ///
6829 /// Decision `19d7602d` (2026-09-02, option d). A diff is a table of lines,
6830 /// and the only thing the vocabulary was missing was a way for a line to
6831 /// say whether it was added, removed or unchanged. So it is a member here
6832 /// rather than a `Node::Diff` carrying git's data model into a vocabulary
6833 /// shared by a task manager and a sample browser.
6834 ///
6835 /// `None` is not [`Change::Context`]. `None` means this table is not a diff
6836 /// and no renderer should tint it; `Some(Context)` means it is a diff and
6837 /// this line did not change. Two facts, and a table of ordinary rows must
6838 /// not read as a diff whose every line is context.
6839 ///
6840 /// What a renderer does with it is [`Change`]'s `Intent` impl and its own
6841 /// palette. A terminal with two colours to spend gets the same three
6842 /// answers a browser does.
6843 pub change: Option<layout::Change>,
6844 }
6845
6846 impl Row {
6847 /// A row with only its primary text.
6848 ///
6849 /// An empty string is an empty run rather than a run holding an empty
6850 /// string, so `Row::new("")` and [`Row::default`] are the same value. Same
6851 /// rule as [`Cell::new`], and for the same reason.
6852 pub fn new(primary: impl Into<String>) -> Self {
6853 let primary = primary.into();
6854 Self {
6855 cells: if primary.is_empty() {
6856 Vec::new()
6857 } else {
6858 vec![Cell {
6859 key: CellKey::Role(layout::RowPart::Primary),
6860 content: vec![Node::text(primary)],
6861 flow: None,
6862 priority: None,
6863 span: 1,
6864 }]
6865 },
6866 ..Self::default()
6867 }
6868 }
6869
6870 /// Put this row at an indent level, 0 being top level.
6871 ///
6872 /// [`depth`](Self::depth) is the member and this is how a call site says
6873 /// it. audiofiles' tag sidebar counts the dots in `drums.kick` and hands
6874 /// a Nesting built from it here.
6875 #[must_use]
6876 pub const fn depth(mut self, depth: layout::Nesting) -> Self {
6877 self.depth = depth;
6878 self
6879 }
6880
6881 /// Give this row a disclosure, and say whether it is open.
6882 ///
6883 /// A branch. Without it the row is a leaf and draws no chevron, which is
6884 /// what every row written before [`open`](Self::open) existed is. Which
6885 /// rows it folds away is [`open`](Self::open)'s to say and the renderer's
6886 /// to do.
6887 #[must_use]
6888 pub const fn disclosing(mut self, open: bool) -> Self {
6889 self.open = Some(open);
6890 self
6891 }
6892
6893 /// Something else that can be done to this row, not shown inline.
6894 #[must_use]
6895 pub fn offers(mut self, act: Act) -> Self {
6896 self.menu.push(act);
6897 self
6898 }
6899
6900 /// Offer these acts on the row itself, rather than inside a part.
6901 ///
6902 /// The plural of [`offers`](Self::offers). A table row got this pair on
6903 /// 2026-09-02 and a list row did not, though both already carried the same
6904 /// facts under the same names, so a screen mixing a list and a table had to
6905 /// write one of them in the chain and the other by assignment. That gap is
6906 /// what the 2026-09-05 collapse closed by making them one type.
6907 ///
6908 /// The whole menu at once, because the measured sites hand over a `Vec`
6909 /// they already have: audiofiles' file list builds one conditionally on
6910 /// how many rows are chosen, and pushing it act by act would take the
6911 /// condition apart.
6912 ///
6913 #[must_use]
6914 pub fn menu(mut self, acts: impl IntoIterator<Item = Act>) -> Self {
6915 self.menu.extend(acts);
6916 self
6917 }
6918
6919 /// This is the row being shown elsewhere.
6920 ///
6921 /// Was reachable only by assigning the field, which is why a row built by
6922 /// a chain had to fall out of the chain to say it. A table row carried the
6923 /// same fact under the same name and got its builder first; this is the
6924 /// other half of that fix.
6925 ///
6926 /// The app's own pointer into a set, as distinct from
6927 /// [`selected`](Self::selected), which is the user's tick. Conflating them
6928 /// is what stopped a screen with bulk actions describing its checkboxes at
6929 /// all, and that argument is on the field.
6930 #[must_use]
6931 pub const fn current(mut self, current: bool) -> Self {
6932 self.current = current;
6933 self
6934 }
6935
6936 /// How much of this row's set is done.
6937 #[must_use]
6938 pub fn meter(mut self, meter: Meter) -> Self {
6939 self.set(layout::RowPart::Proportion, Node::Meter(meter));
6940 self
6941 }
6942
6943 /// A tick that is the write, in the state it is currently in.
6944 ///
6945 /// Sets [`selected`](Self::selected) and [`toggle`](Self::toggle) together,
6946 /// because a route on a tick nothing draws is the one way the two fields can
6947 /// disagree. A checklist item is what this is for; a bulk checkbox sets
6948 /// `selected` alone and keeps its meaning as client state.
6949 #[must_use]
6950 pub fn toggling(mut self, ticked: bool, action: Action) -> Self {
6951 self.selected = Some(ticked);
6952 self.toggle = Some(action);
6953 self
6954 }
6955
6956 /// Supporting text under the primary.
6957 #[must_use]
6958 pub fn secondary(mut self, text: impl Into<Prose>) -> Self {
6959 let node = match text.into() {
6960 Prose::Text(text) => Node::text(text),
6961 Prose::Rich(source) => Node::rich(source),
6962 };
6963 self.set(layout::RowPart::Secondary, node);
6964 self
6965 }
6966
6967 /// A short trailing fact.
6968 #[must_use]
6969 pub fn meta(mut self, text: impl Into<String>) -> Self {
6970 self.set(layout::RowPart::Meta, Node::text(text));
6971 self
6972 }
6973
6974 /// Add a token, chaining.
6975 #[must_use]
6976 pub fn token(mut self, tag: Tag) -> Self {
6977 self.cells.push(Cell {
6978 key: CellKey::Role(layout::RowPart::Tokens),
6979 content: vec![Node::Token(tag)],
6980 flow: None,
6981 priority: None,
6982 span: 1,
6983 });
6984 self
6985 }
6986
6987 /// Make the row tickable, and say whether it is ticked.
6988 ///
6989 /// A row is not selectable until something says so, which is what keeps a
6990 /// checkbox off every list in the app.
6991 ///
6992 /// Says nothing about what the tick contributes, so on a screen with a
6993 /// [`selection`](Screen::selection) it draws a box that joins no set. Reach
6994 /// for [`ticking`](Self::ticking) instead; this stays for the screens whose
6995 /// tick is the write, beside [`toggling`](Self::toggling).
6996 #[must_use]
6997 pub const fn selectable(mut self, ticked: bool) -> Self {
6998 self.selected = Some(ticked);
6999 self
7000 }
7001
7002 /// Make the row part of a live selection under this value, and say whether
7003 /// it is chosen.
7004 ///
7005 /// The same pairing [`ticking`](Self::ticking) makes one member along: a
7006 /// row that can be chosen and names nothing is a dead affordance, because
7007 /// the value is what the app reads back to know which row the press was
7008 /// about.
7009 #[must_use]
7010 pub fn choosing(mut self, value: impl Into<String>, chosen: bool) -> Self {
7011 self.value = Some(value.into());
7012 self.chosen = Some(chosen);
7013 self
7014 }
7015
7016 /// Make the row tickable under this value, and say whether it is ticked.
7017 ///
7018 /// Sets [`selected`](Self::selected) and [`value`](Self::value) together,
7019 /// which is the pair a screen's [`selection`](Screen::selection) needs.
7020 /// The two halves exist separately for [`toggling`](Self::toggling)'s
7021 /// reason — state and identity are different facts about the row — and
7022 /// this is the constructor that stops them being written apart.
7023 #[must_use]
7024 pub fn ticking(mut self, value: impl Into<String>, ticked: bool) -> Self {
7025 self.selected = Some(ticked);
7026 self.value = Some(value.into());
7027 self
7028 }
7029
7030 /// Name the row without making it tickable.
7031 ///
7032 /// Identity and tickability are different facts about a row, and
7033 /// [`ticking`](Self::ticking) writes both because a screen's selection needs
7034 /// both; a row that is only ever pointed at needs the first alone.
7035 #[must_use]
7036 pub fn identified(mut self, value: impl Into<String>) -> Self {
7037 self.value = Some(value.into());
7038 self
7039 }
7040
7041 /// Give the row a document address a link can reach it by.
7042 ///
7043 /// See [`address`](Self::address) for why it is not
7044 /// [`identified`](Self::identified), and write the name without the `#`.
7045 ///
7046 #[must_use]
7047 pub fn addressed(mut self, address: impl Into<String>) -> Self {
7048 self.address = Some(address.into());
7049 self
7050 }
7051
7052 /// The route selecting this row.
7053 #[must_use]
7054 pub fn activate(mut self, action: Action) -> Self {
7055 self.activate = Some(action);
7056 self
7057 }
7058
7059 /// A control acting on this row.
7060 #[must_use]
7061 pub fn act(mut self, act: Act) -> Self {
7062 self.cells.push(Cell {
7063 key: CellKey::Role(layout::RowPart::Actions),
7064 content: vec![Node::Act(act)],
7065 flow: None,
7066 priority: None,
7067 span: 1,
7068 });
7069 self
7070 }
7071
7072 /// Anything in this row, under the role it takes.
7073 ///
7074 /// The general form the constructors above are shorthands for, and the
7075 /// point of the model: a link in a row and a figure in a row became
7076 /// sayable at once, where each was previously a
7077 /// [`layout::RowPart`] variant, a member here, a renderer arm and a
7078 /// release.
7079 ///
7080 /// Appends rather than replacing, so a row can hold two of a role. The
7081 /// named constructors keep the single-valued roles single-valued, which is
7082 /// what their call sites already meant.
7083 ///
7084 /// # Panics
7085 ///
7086 /// If the node is not a leaf. A row is an inline run, so what goes in it
7087 /// has to be drawable on one wrapped line without the renderer knowing what
7088 /// it is -- the constrained-consumer bound, biting at a call site rather
7089 /// than in a doc comment. Same assertion as [`Cell::part`].
7090 #[must_use]
7091 pub fn part(mut self, role: layout::RowPart, node: Node) -> Self {
7092 assert!(
7093 node.containment() == Containment::Text,
7094 "a row is an inline run and holds leaves; {node:?} holds {:?}",
7095 node.containment()
7096 );
7097 self.cells.push(Cell {
7098 key: CellKey::Role(role),
7099 content: vec![node],
7100 flow: None,
7101 priority: None,
7102 span: 1,
7103 });
7104 self
7105 }
7106
7107 /// Let the part just added take two lines instead of one.
7108 ///
7109 /// Applies to the last part in the run, which is the one the call before it
7110 /// pushed: `Row::new(title).relaxed()` relaxes the title, and
7111 /// `.secondary(body).relaxed()` relaxes the body. Chaining is what makes
7112 /// "the last one" unambiguous at a call site, and it is why this is a
7113 /// method here rather than an argument on every constructor.
7114 ///
7115 /// A no-op on an empty run rather than a panic. `Row::new("")` is
7116 /// deliberately an empty run, so a caller that relaxes a title it turned
7117 /// out not to have is asking for nothing and gets nothing.
7118 ///
7119 /// What two lines means is [`layout::Flow::Relaxed`]'s to say, and what it
7120 /// costs each renderer is in that type's docs.
7121 #[must_use]
7122 pub fn relaxed(mut self) -> Self {
7123 if let Some(part) = self.cells.last_mut() {
7124 part.flow = Some(layout::Flow::Relaxed);
7125 }
7126 self
7127 }
7128
7129 /// Say what the part just added is worth when the run does not fit.
7130 ///
7131 /// Applies to the last part in the run, the same way
7132 /// [`relaxed`](Self::relaxed) does. Without it the part is worth whatever
7133 /// its role is worth.
7134 ///
7135 /// A no-op on an empty run rather than a panic, for `relaxed`'s reason.
7136 #[must_use]
7137 pub fn worth(mut self, priority: layout::Priority) -> Self {
7138 if let Some(part) = self.cells.last_mut() {
7139 part.priority = Some(priority);
7140 }
7141 self
7142 }
7143
7144 /// Set the one part taking a role, replacing it if it is already there.
7145 ///
7146 /// For the roles that are single-valued at every call site that has ever
7147 /// existed: the primary, the supporting line, the trailing fact, the bar.
7148 /// Building a row that calls `.meta` twice meant the second one won when
7149 /// `meta` was an `Option`, and it still does.
7150 fn set(&mut self, role: layout::RowPart, node: Node) {
7151 match self
7152 .cells
7153 .iter_mut()
7154 .find(|cell| cell.key == CellKey::Role(role))
7155 {
7156 Some(cell) => cell.content = vec![node],
7157 None => self.cells.push(Cell {
7158 key: CellKey::Role(role),
7159 content: vec![node],
7160 flow: None,
7161 priority: None,
7162 span: 1,
7163 }),
7164 }
7165 }
7166
7167 /// A row of a declared table, its cells in column order.
7168 ///
7169 /// Was the table row's own `new` before the 2026-09-05 collapse. It is a separate
7170 /// constructor from [`new`](Self::new) rather than an overload of it
7171 /// because the two say different things: `Row::new` names the primary
7172 /// column of the default set, and this answers a column list positionally.
7173 #[must_use]
7174 pub fn cells(values: impl IntoIterator<Item = impl Into<Cell>>) -> Self {
7175 Self {
7176 cells: values
7177 .into_iter()
7178 .enumerate()
7179 .map(|(at, cell)| {
7180 let mut cell = cell.into();
7181 cell.key = CellKey::Column(at);
7182 cell
7183 })
7184 .collect(),
7185 ..Self::default()
7186 }
7187 }
7188
7189 /// A cell naming the column it sits in, chaining.
7190 ///
7191 /// Safer than counting to a column in every way but one, which is why
7192 /// [`Table::row`] carries a debug assertion: a name no column has is
7193 /// dropped, so a typo renders an empty column rather than failing to
7194 /// compile. The row and the column list are usually written in different
7195 /// functions, so nothing above `Table::row` has both in hand to check.
7196 #[must_use]
7197 pub fn at(mut self, column: impl Into<String>, cell: impl Into<Cell>) -> Self {
7198 let mut cell = cell.into();
7199 cell.key = CellKey::Named(column.into());
7200 self.cells.push(cell);
7201 self
7202 }
7203
7204 /// A cell in the next column along, chaining.
7205 #[must_use]
7206 pub fn cell(mut self, cell: impl Into<Cell>) -> Self {
7207 let at = self.cells.len();
7208 let mut cell = cell.into();
7209 cell.key = CellKey::Column(at);
7210 self.cells.push(cell);
7211 self
7212 }
7213
7214 /// Which side of a change this row is on, when the table is a diff.
7215 #[must_use]
7216 pub const fn changed(mut self, change: layout::Change) -> Self {
7217 self.change = Some(change);
7218 self
7219 }
7220
7221 /// The parts taking one role, in order.
7222 pub fn role(&self, role: layout::RowPart) -> impl Iterator<Item = &Node> {
7223 self.cells
7224 .iter()
7225 .filter(move |cell| cell.key == CellKey::Role(role))
7226 .flat_map(|cell| cell.content.iter())
7227 }
7228
7229 /// The row's primary text.
7230 ///
7231 /// A row built the ordinary way has one primary part and answers its
7232 /// string; one that was given two answers both, joined, in order.
7233 #[must_use]
7234 pub fn primary(&self) -> String {
7235 self.role(layout::RowPart::Primary)
7236 .filter_map(|node| match node {
7237 Node::Text { text, .. } | Node::Link { text, .. } => Some(text.as_str()),
7238 _ => None,
7239 })
7240 .collect::<Vec<_>>()
7241 .join(" ")
7242 }
7243
7244 /// The controls the row shows.
7245 ///
7246 /// The same service [`primary`](Self::primary) does, for the member the run
7247 /// replaced. `actions` was a `Vec<Act>` before the run, and every consumer
7248 /// that read it now writes the same three lines: filter the run by role,
7249 /// match the one node kind that can be there, and collect. goingson wrote
7250 /// them twice in one file the day the member went away.
7251 ///
7252 /// Not what the row *offers*: that is [`menu`](Self::menu), which is held
7253 /// back until the host asks for it and is not on the line.
7254 /// Whether a control in this row's run or menu carries this
7255 /// [`Act::id`](Act::id).
7256 #[must_use]
7257 pub fn names(&self, id: &str) -> bool {
7258 self.menu.iter().any(|act| act.id.as_deref() == Some(id))
7259 || self
7260 .cells
7261 .iter()
7262 .any(|cell| cell.content.iter().any(|node| node.names(id)))
7263 }
7264
7265 pub fn acts(&self) -> impl Iterator<Item = &Act> {
7266 self.role(layout::RowPart::Actions)
7267 .filter_map(|node| match node {
7268 Node::Act(act) => Some(act),
7269 _ => None,
7270 })
7271 }
7272
7273 /// Every kind of time-derived readout in this row's run, added to `found`.
7274 fn clocks_into(&self, found: &mut BTreeSet<Clock>) {
7275 for cell in &self.cells {
7276 for node in &cell.content {
7277 node.clocks_into(found);
7278 }
7279 }
7280 }
7281
7282 /// The tags the row shows.
7283 ///
7284 /// [`acts`](Self::acts)' counterpart, for the same reason.
7285 pub fn tokens(&self) -> impl Iterator<Item = &Tag> {
7286 self.role(layout::RowPart::Tokens)
7287 .filter_map(|node| match node {
7288 Node::Token(tag) => Some(tag),
7289 _ => None,
7290 })
7291 }
7292 }
7293
7294 /// One cell of a table row.
7295 ///
7296 /// A cell was a `String` until then, so a table whose rows carry a control
7297 /// could not be described at all and had to become a column-less table,
7298 /// losing its headers -- which is what the MNW server's
7299 /// SSH-keys tab did, and why it read worse than the Askama original it replaced.
7300 ///
7301 /// # Why the acts sit on the cell and not on the row
7302 ///
7303 /// Counted across MNW's templates, 30 table rows carry a control. 25 put it
7304 /// alone in the last cell, which a row-level `actions` list would have covered.
7305 /// The other five put it *beside a value*: `project_content`'s position cell is
7306 /// the number plus two reorder arrows, `project_synckit`'s slug cell is the slug
7307 /// plus "Set slug", `promo_codes_list`'s use count is a number that is itself
7308 /// the button opening the redemptions. A row-level list renders as an appended
7309 /// cell and cannot say any of those, and neither can an actions *column*, since
7310 /// a column is a column. The control belongs where it actually is.
7311 ///
7312 /// An empty [`value`](Self::value) with acts is the common case, and
7313 /// [`Cell::acts`] is the constructor for it. That is the trailing actions cell
7314 /// the markup already writes an empty `<th>` for.
7315 ///
7316 /// A `Vec<Act>` and not a node: the ruling that a row holds no nodes holds
7317 /// here for the same reason. Acts carry their own tone, state and
7318 /// confirmation, and that is the whole of what these cells hold.
7319 /// Which column a cell answers to.
7320 ///
7321 /// The 2026-09-05 collapse: a list's parts were addressed by role and a table's
7322 /// values by column, and those were the same operation under two spellings, a
7323 /// lookup into a declared key set. The only difference was who declares the
7324 /// keys, and now that a list declares its own the difference is gone.
7325 #[derive(Debug, Clone, PartialEq, Eq)]
7326 pub enum CellKey {
7327 /// A column of the default set, which is what a container that declares no
7328 /// columns gets.
7329 ///
7330 /// [`layout::RowPart`]'s six variants are that set. A container spelled as
7331 /// a list is a table over them, which is why `list { row "x" { secondary
7332 /// "y" } }` still says what it always said.
7333 Role(layout::RowPart),
7334 /// A declared column, by position, already resolved.
7335 Column(usize),
7336 /// A declared column, by name, pending resolution by [`Table::row`].
7337 ///
7338 /// This replaces the private staging vector a table row used to carry. A named
7339 /// cell now sits in the row with the others and says it is unresolved,
7340 /// rather than living in a second collection that had to be drained.
7341 Named(String),
7342 }
7343
7344 impl Default for CellKey {
7345 fn default() -> Self {
7346 Self::Role(layout::RowPart::Primary)
7347 }
7348 }
7349
7350 /// One cell: a run of leaf nodes, addressed by a key.
7351 ///
7352 /// **The 2026-09-05 collapse merged `Part` into this type.** A part was a cell
7353 /// that carried its own role because a list had no columns to carry it; a cell
7354 /// was a part whose column carried the role instead. One type now, with
7355 /// [`key`](Self::key) saying which column it answers to.
7356 ///
7357 /// A cell was a `String` before that, so a table whose rows carry a control
7358 /// could not be described at all and had to become a column-less table, losing
7359 /// its headers, which is what the MNW server's SSH-keys tab did and why it
7360 /// read worse than the Askama original it replaced.
7361 ///
7362 /// # Why the acts sit on the cell and not on the row
7363 ///
7364 /// Counted across MNW's templates, 30 table rows carry a control. 25 put it
7365 /// alone in the last cell, which a row-level `actions` list would have covered.
7366 /// The other five put it *beside a value*: `project_content`'s position cell is
7367 /// the number plus two reorder arrows, `project_synckit`'s slug cell is the slug
7368 /// plus "Set slug", `promo_codes_list`'s use count is a number that is itself
7369 /// the button opening the redemptions. A row-level list renders as an appended
7370 /// cell and cannot say any of those, and neither can an actions *column*, since
7371 /// a column is a column. The control belongs where it actually is.
7372 #[derive(Debug, Clone, PartialEq, Eq)]
7373 pub struct Cell {
7374 /// What is in it, in order.
7375 ///
7376 /// An inline run: every entry is a leaf, so the whole cell is drawable on
7377 /// one wrapped line without a renderer knowing what is in it. That is the
7378 /// bound, and [`Cell::part`] is where it is enforced. It is also what
7379 /// [`CellKey::Role`] leans on: several tokens in one row are one cell
7380 /// holding several [`Node::Token`]s, not several cells fighting for a key.
7381 ///
7382 /// The run is what makes a meter in a cell or a figure in a cell sayable
7383 /// without a member each.
7384 pub content: Vec<Node>,
7385 /// Which column this cell answers to.
7386 pub key: CellKey,
7387 /// How much vertical room it may take, or `None` for its column's.
7388 ///
7389 /// An override rather than a duplicate: the column states the default and a
7390 /// cell may narrow it. BB clamps a feed row's title while its excerpt wraps
7391 /// freely underneath, which is two columns rather than one override, but a
7392 /// long value in one row of an otherwise tight column is the case that
7393 /// keeps this here.
7394 pub flow: Option<layout::Flow>,
7395 /// What it is worth when the row does not fit, or `None` for its column's.
7396 ///
7397 /// `None` means the description did not say, and the key answers instead:
7398 /// [`layout::RowPart::priority`] for a role, [`Column::priority`] for a
7399 /// declared column. There is no global default worth having, which is why
7400 /// this is an `Option` rather than a defaulted value.
7401 pub priority: Option<layout::Priority>,
7402 /// How many columns this cell covers. 1 is one column.
7403 ///
7404 /// **Defined and unread until the 2D work (`b1d4c5d7`).** It is here so
7405 /// spanning does not cost a second breaking change; nothing honours it yet,
7406 /// and a renderer meeting a value above 1 today should draw it as 1.
7407 pub span: u16,
7408 }
7409
7410 impl Default for Cell {
7411 /// An empty cell in the primary column, covering one column.
7412 ///
7413 /// `span` is 1 rather than 0 here, which is why this is written out: a
7414 /// derived `Default` would produce a cell covering no columns, and nothing
7415 /// downstream would say so.
7416 fn default() -> Self {
7417 Self {
7418 content: Vec::new(),
7419 key: CellKey::default(),
7420 flow: None,
7421 priority: None,
7422 span: 1,
7423 }
7424 }
7425 }
7426
7427 impl Cell {
7428 /// A cell holding text.
7429 ///
7430 /// An empty string is an empty run rather than a run holding an empty
7431 /// string, so an actions-only cell built through [`acts`](Self::acts) and
7432 /// one built as `Cell::new("").act(..)` are the same value.
7433 pub fn new(value: impl Into<String>) -> Self {
7434 let value = value.into();
7435 Self {
7436 key: CellKey::default(),
7437 flow: None,
7438 priority: None,
7439 span: 1,
7440 content: if value.is_empty() {
7441 Vec::new()
7442 } else {
7443 vec![Node::text(value)]
7444 },
7445 }
7446 }
7447
7448 /// A cell holding one tag and no text.
7449 ///
7450 /// What a status column is: the cell is the badge. `Cell::new("")` with a
7451 /// token would say the same thing and reads as an oversight.
7452 pub fn tag(tag: Tag) -> Self {
7453 Self {
7454 key: CellKey::default(),
7455 flow: None,
7456 priority: None,
7457 span: 1,
7458 content: vec![Node::Token(tag)],
7459 }
7460 }
7461
7462 /// A tag in this cell, chaining.
7463 #[must_use]
7464 pub fn token(mut self, tag: Tag) -> Self {
7465 self.content.push(Node::Token(tag));
7466 self
7467 }
7468
7469 /// A cell holding controls and no text.
7470 pub fn acts(actions: impl IntoIterator<Item = Act>) -> Self {
7471 Self {
7472 key: CellKey::default(),
7473 flow: None,
7474 priority: None,
7475 span: 1,
7476 content: actions.into_iter().map(Node::Act).collect(),
7477 }
7478 }
7479
7480 /// A control in this cell, chaining.
7481 #[must_use]
7482 pub fn act(mut self, act: Act) -> Self {
7483 self.content.push(Node::Act(act));
7484 self
7485 }
7486
7487 /// How much of a set this cell's row is through, chaining.
7488 ///
7489 /// [`Row::meter`]'s counterpart on the other container, and it is a setting
7490 /// for that one's reason: a proportion is a fact about the thing the cell is
7491 /// in rather than a leaf beside its text. `Node::Meter` has no member of its
7492 /// own and cannot get one -- `meter` is a setting here and on `Row`, so the
7493 /// name is taken -- which is what made a table's progress column reach for a
7494 /// supplier before this existed. goingson's task list is the site.
7495 #[must_use]
7496 pub fn meter(mut self, meter: Meter) -> Self {
7497 self.content.push(Node::Meter(meter));
7498 self
7499 }
7500
7501 /// Where this cell's value goes.
7502 ///
7503 /// The value becomes the link. A cell with no value and an `activate` is a
7504 /// link with nothing to press, so give it text.
7505 ///
7506 /// Under the run this rewrites the leading text into a [`Node::Link`]
7507 /// rather than setting a member beside it, which is the same fact said once
7508 /// instead of as a pair of fields that could disagree. A cell with no text
7509 /// to link gains nothing, because a link with no label is a control nothing
7510 /// draws.
7511 #[must_use]
7512 pub fn activate(mut self, action: Action) -> Self {
7513 if let Some(first) = self
7514 .content
7515 .iter_mut()
7516 .find(|part| matches!(part, Node::Text { .. }))
7517 && let Node::Text { text, .. } = first
7518 {
7519 *first = Node::Link {
7520 text: std::mem::take(text),
7521 action,
7522 };
7523 }
7524 self
7525 }
7526
7527 /// Anything in this cell, chaining.
7528 ///
7529 /// The general form the five constructors above are shorthands for, and the
7530 /// whole point of the model: a meter in a cell, a figure in a cell and a
7531 /// second linked value in a cell all became sayable at once, where each was
7532 /// previously a member, three renderer arms and a release.
7533 ///
7534 /// # Panics
7535 ///
7536 /// If the node is not a leaf. A cell is an inline run, so what goes in it
7537 /// has to be drawable on one wrapped line without the renderer knowing what
7538 /// it is -- that is the constrained-consumer bound, and this is where it
7539 /// bites at a call site rather than in a doc comment.
7540 #[must_use]
7541 pub fn part(mut self, node: Node) -> Self {
7542 assert!(
7543 node.containment() == Containment::Text,
7544 "a cell is an inline run and holds leaves; {node:?} holds \
7545 {:?}",
7546 node.containment()
7547 );
7548 self.content.push(node);
7549 self
7550 }
7551
7552 /// What this cell is worth when the row does not fit.
7553 ///
7554 /// [`priority`](Self::priority) if the description said, and otherwise the
7555 /// key's: [`layout::RowPart::priority`] for a role.
7556 ///
7557 /// **A cell keyed to a declared column cannot answer alone**, and reports
7558 /// [`layout::Priority::Essential`] rather than guessing. Its column holds
7559 /// the real answer, and a renderer drawing a declared table should read
7560 /// [`Column::priority`] instead of calling this. Essential is the safe end
7561 /// of the scale on purpose: a caller that forgets to consult the column
7562 /// draws a cell it could have dropped, rather than dropping one it should
7563 /// have drawn. That asymmetry is the collapse being honest -- a table row
7564 /// was always meaningless without its columns, and now it says so.
7565 #[must_use]
7566 pub fn worth(&self) -> layout::Priority {
7567 self.priority.unwrap_or_else(|| match &self.key {
7568 CellKey::Role(role) => role.priority(),
7569 CellKey::Column(_) | CellKey::Named(_) => layout::Priority::Essential,
7570 })
7571 }
7572
7573 /// How much vertical room this cell may take.
7574 ///
7575 /// [`flow`](Self::flow) if the description said, and otherwise
7576 /// [`layout::Flow::Tight`], which is one line and is what every part did
7577 /// before the field existed.
7578 #[must_use]
7579 pub fn room(&self) -> layout::Flow {
7580 self.flow.unwrap_or_default()
7581 }
7582
7583 /// The cell's text, with the parts that are not text left out.
7584 ///
7585 /// A cell that is one string answers that string; one that mixes answers
7586 /// the text between its tags and controls, in order.
7587 #[must_use]
7588 pub fn text(&self) -> String {
7589 self.content
7590 .iter()
7591 .filter_map(|part| match part {
7592 Node::Text { text, .. } | Node::Link { text, .. } => Some(text.as_str()),
7593 _ => None,
7594 })
7595 .collect::<Vec<_>>()
7596 .join(" ")
7597 }
7598
7599 /// Whether anything in this cell answers a click.
7600 ///
7601 /// A badge is not one: it says something and answers nothing, which is why
7602 /// this asks the tag rather than counting tags.
7603 #[must_use]
7604 pub fn carries_control(&self) -> bool {
7605 self.content.iter().any(|part| match part {
7606 Node::Act(_) | Node::Link { .. } => true,
7607 Node::Token(tag) => tag.kind.interactive() && tag.action.is_some(),
7608 _ => false,
7609 })
7610 }
7611 }
7612
7613 impl From<String> for Cell {
7614 fn from(value: String) -> Self {
7615 Self::new(value)
7616 }
7617 }
7618
7619 impl From<&str> for Cell {
7620 fn from(value: &str) -> Self {
7621 Self::new(value)
7622 }
7623 }
7624
7625 /// What a press on a row of a live selection meant.
7626 ///
7627 /// The renderer's half of [`Row::chosen`]: the description says which rows
7628 /// are chosen and the app owns the set, but *how a press was meant* is the one
7629 /// part only the renderer can know, because it is the host's idiom and a
7630 /// different idiom on each.
7631 ///
7632 /// Three members, which is what every file manager on every desktop has offered
7633 /// for thirty years and what audiofiles lost when its list was described. A host
7634 /// maps its own gesture onto them:
7635 ///
7636 /// | | pointer | touch | terminal |
7637 /// |---|---|---|---|
7638 /// | [`Only`](Self::Only) | click | tap | Enter |
7639 /// | [`Also`](Self::Also) | ctrl-click, cmd-click on macOS | long-press | Space |
7640 /// | [`Through`](Self::Through) | shift-click | drag over a run | none yet |
7641 ///
7642 /// The terminal's gap is stated rather than invented around: `quasi_tui::Key`
7643 /// carries no modifiers, so shift-Enter is not expressible without widening the
7644 /// key type every host driving that renderer maps onto.
7645 ///
7646 /// # What a reader holding both keys means
7647 ///
7648 /// Ruled here rather than left to each renderer, which is the whole point of
7649 /// the type: **shift wins**. Finder and Explorer both read ctrl-shift-click as
7650 /// "extend the run and keep what was already chosen", which is a fourth member
7651 /// and has no consumer asking for one. Of the three that exist, taking the run
7652 /// is nearer to what the reader asked for than toggling the single row they
7653 /// happened to land on -- and the one thing holding two keys cannot mean is the
7654 /// plain press.
7655 ///
7656 /// # Why this is in the vocabulary at all
7657 ///
7658 /// It looks like input state, which every other ruling here has refused to
7659 /// carry: a description says what is on the screen and never where, never how
7660 /// wide, never which key. The difference is that this is not the *gesture*, it
7661 /// is what the gesture **meant**, and the meaning is the same on every host
7662 /// while the gesture is not. A description that carried "ctrl was held" would
7663 /// be naming a keyboard; this names an intention a touch host can honour with
7664 /// no keyboard at all.
7665 ///
7666 /// The test it passes and a modifier would not: a terminal can implement it.
7667 ///
7668 /// # It travels in the payload, not in the address
7669 ///
7670 /// Under [`Node::CHOOSING`], beside whatever else the activation carries.
7671 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7672 pub enum Choosing {
7673 /// This row and nothing else. The plain press, and the default.
7674 #[default]
7675 Only,
7676 /// This row as well as whatever was already chosen, or out of it if it was
7677 /// already in.
7678 ///
7679 /// A toggle rather than an add, which is what every file manager does with
7680 /// ctrl-click and is the only reading that lets a reader correct a
7681 /// mis-click without starting over.
7682 Also,
7683 /// Every row from the one the app is pointing at to this one.
7684 ///
7685 /// The range's other end is the app's own -- `Row::current`, or whatever
7686 /// the app calls its focus -- and is deliberately not carried here. A
7687 /// renderer that named it would be answering with the row it *drew* as
7688 /// current, which is the description's answer from one frame ago; the app
7689 /// holds the live one.
7690 Through,
7691 }
7692
7693 impl Choosing {
7694 /// The spelling that travels in a payload.
7695 #[must_use]
7696 pub const fn as_str(self) -> &'static str {
7697 match self {
7698 Self::Only => "only",
7699 Self::Also => "also",
7700 Self::Through => "through",
7701 }
7702 }
7703
7704 /// What a handler reads back, defaulting to [`Only`](Self::Only).
7705 ///
7706 /// **An unknown spelling is [`Only`](Self::Only) rather than an error**, and
7707 /// that is the same bargain every other read of a submitted value strikes
7708 /// here: a press that arrives saying something this version does not know is
7709 /// still a press on a row, and refusing it would break the ordinary act to
7710 /// protect the extraordinary one. The plain reading is the safe one -- it
7711 /// chooses the row that was pressed and nothing else.
7712 #[must_use]
7713 pub fn read(value: Option<&str>) -> Self {
7714 match value {
7715 Some("also") => Self::Also,
7716 Some("through") => Self::Through,
7717 _ => Self::Only,
7718 }
7719 }
7720 }
7721
7722 /// A table: the columns, and the rows that answer to them.
7723 ///
7724 /// The columns arrive with the table, so a row is never built against a column
7725 /// list that does not exist yet. That is the rule [`Run::new`] holds for a
7726 /// shared row's fallback, here for the same reason: a table's invariant is that
7727 /// cells line up with columns, and the point a row is put in is the only place
7728 /// with enough context to keep that true.
7729 ///
7730 /// # Why a cell is addressed by name
7731 ///
7732 /// [`Column`] documented its `name` as "the heading, and the name the cell is
7733 /// addressed by ... what replaces addressing columns by position" while the row
7734 /// stayed positional, so the file held both answers at once. Position is the one
7735 /// that loses. A cell that appears on some rows and not others shifts every cell
7736 /// after it, so a conditional cell had to be written as a matched pair and
7737 /// nothing but care kept the pair matched. Under [`Row::at`] an absent cell is an
7738 /// empty cell in its own column, and a row whose arity depends on a runtime flag
7739 /// is an ordinary row.
7740 ///
7741 /// A row built by [`Row::cells`] is still positional and still works. The two
7742 /// are not mixed in one row: if a row names any column, the names are the row.
7743 #[derive(Debug, Clone, PartialEq, Eq, Default)]
7744 pub struct Table {
7745 columns: Vec<Column>,
7746 rows: Vec<Row>,
7747 more: Option<Rest>,
7748 }
7749
7750 impl Table {
7751 /// A table with these columns and no rows yet.
7752 pub fn new(columns: impl IntoIterator<Item = Column>) -> Self {
7753 Self {
7754 columns: columns.into_iter().collect(),
7755 rows: Vec::new(),
7756 more: None,
7757 }
7758 }
7759
7760 /// Add one column, for a caller building the table a piece at a time.
7761 ///
7762 /// Beside [`new`](Self::new) rather than instead of it: a table whose
7763 /// columns are a literal list still says so in one expression, and a table
7764 /// assembled by something that accretes (the declared form, which has no
7765 /// expression to hold a list in) says the same thing one column at a time.
7766 ///
7767 /// **Columns before rows.** [`row`](Self::row) resolves a named cell
7768 /// against the columns the table has when the row arrives, so a column
7769 /// added afterwards is invisible to every row already pushed.
7770 #[must_use]
7771 pub fn column(mut self, column: Column) -> Self {
7772 debug_assert!(
7773 self.rows.is_empty(),
7774 "a column added after a row cannot be seen by that row's named cells"
7775 );
7776 self.columns.push(column);
7777 self
7778 }
7779
7780 /// Add a row, resolving any cell that named its column.
7781 ///
7782 /// A named cell whose column this table does not have is dropped: the
7783 /// column list is the table's statement of what a row may say, and a row
7784 /// saying more than that is answered by the columns rather than by
7785 /// widening them. A column no cell named is empty in this row.
7786 #[must_use]
7787 pub fn row(mut self, mut row: Row) -> Self {
7788 if row
7789 .cells
7790 .iter()
7791 .any(|cell| matches!(cell.key, CellKey::Named(_)))
7792 {
7793 let placed: Vec<(String, Cell)> = std::mem::take(&mut row.cells)
7794 .into_iter()
7795 .filter_map(|cell| match &cell.key {
7796 CellKey::Named(name) => Some((name.clone(), cell.clone())),
7797 _ => None,
7798 })
7799 .collect();
7800 debug_assert!(
7801 placed
7802 .iter()
7803 .all(|(name, _)| self.columns.iter().any(|column| column.name == *name)),
7804 "a cell named a column this table does not have; the name is dropped and the \
7805 cell is silently lost. Named: {:?}. Columns: {:?}",
7806 placed.iter().map(|(name, _)| name).collect::<Vec<_>>(),
7807 self.columns
7808 .iter()
7809 .map(|column| &column.name)
7810 .collect::<Vec<_>>(),
7811 );
7812 debug_assert!(
7813 {
7814 let mut names = self
7815 .columns
7816 .iter()
7817 .map(|column| column.name.as_str())
7818 .collect::<Vec<_>>();
7819 names.sort_unstable();
7820 let before = names.len();
7821 names.dedup();
7822 names.len() == before
7823 },
7824 "two columns share a name, so a named cell cannot say which it meant and both \
7825 take the first one's value. Columns: {:?}",
7826 self.columns
7827 .iter()
7828 .map(|column| &column.name)
7829 .collect::<Vec<_>>(),
7830 );
7831 row.cells = self
7832 .columns
7833 .iter()
7834 .enumerate()
7835 .map(|(at, column)| {
7836 let mut cell = placed
7837 .iter()
7838 .find(|(name, _)| *name == column.name)
7839 .map_or_else(|| Cell::new(String::new()), |(_, cell)| cell.clone());
7840 cell.key = CellKey::Column(at);
7841 cell
7842 })
7843 .collect();
7844 }
7845 self.rows.push(row);
7846 self
7847 }
7848
7849 /// Add several rows, chaining.
7850 #[must_use]
7851 pub fn rows(mut self, rows: impl IntoIterator<Item = Row>) -> Self {
7852 for row in rows {
7853 self = self.row(row);
7854 }
7855 self
7856 }
7857
7858 /// Say what is not shown, and how to ask for it.
7859 #[must_use]
7860 pub fn more(mut self, rest: Rest) -> Self {
7861 self.more = Some(rest);
7862 self
7863 }
7864
7865 /// The columns, in order.
7866 #[must_use]
7867 pub fn columns(&self) -> &[Column] {
7868 &self.columns
7869 }
7870 }
7871
7872 impl From<Slot> for Node {
7873 /// A region inside a region, without the enclosing one naming the variant.
7874 /// `From<Act>` below is the same courtesy for the same reason.
7875 fn from(slot: Slot) -> Self {
7876 Self::Region(slot)
7877 }
7878 }
7879
7880 impl From<Act> for Node {
7881 /// The wrapping every caller of a control-returning function writes by
7882 /// hand. `From<Table>` below is the same courtesy for the same reason, and
7883 /// a shape that returns a control has to be placeable in a body without
7884 /// the caller naming the variant.
7885 fn from(act: Act) -> Self {
7886 Self::Act(act)
7887 }
7888 }
7889
7890 impl From<Field> for Node {
7891 /// A question inside a body, without the enclosing container naming the
7892 /// variant. The third of these, after `From<Slot>` and `From<Act>` above,
7893 /// and for their reason: a shape that returns a `Field` has to be placeable
7894 /// where a node goes. MNW's repository bar is the site -- its ref chooser
7895 /// is a shape of its own and the bar holds it beside the tab strip.
7896 fn from(field: Field) -> Self {
7897 Self::Field(::std::boxed::Box::new(field))
7898 }
7899 }
7900
7901 impl From<Table> for Node {
7902 fn from(table: Table) -> Self {
7903 Self::Table {
7904 columns: table.columns,
7905 rows: table.rows,
7906 more: table.more,
7907 }
7908 }
7909 }
7910
7911 /// A row that says where it sits in a hierarchy.
7912 ///
7913 /// [`Row`] carries [`depth`](Row::depth) and [`open`](Row::open), and
7914 /// [`folded`] is the one reading of them the three
7915 /// renderers must agree on. A trait rather than the function written twice: two
7916 /// copies of "which rows does a shut branch hide" is two answers waiting to
7917 /// disagree, and a reader who folds a branch in one host and finds a different
7918 /// list in another is being shown the disagreement.
7919 pub trait Outline {
7920 /// How far in the row sits. [`Row::depth`].
7921 fn depth(&self) -> layout::Nesting;
7922 /// Its disclosure, if it has one. [`Row::open`].
7923 fn open(&self) -> Option<bool>;
7924 /// What a renderer holding the reader's own folds keys this branch by.
7925 ///
7926 /// [`value`](Row::value) when the row names itself and its leading text
7927 /// otherwise, which is the same fallback `quasi_immediate::row_at` makes
7928 /// for the same reason: a row that names itself is identified by the app
7929 /// and one that does not is identified by what it says.
7930 ///
7931 /// Here rather than in each renderer, because a reader who folds a branch
7932 /// in a terminal and opens the same screen in a window is entitled to find
7933 /// the same branch. Two spellings of "which row is this" is two answers.
7934 fn key(&self) -> String;
7935 }
7936
7937 impl Outline for Row {
7938 fn depth(&self) -> layout::Nesting {
7939 self.depth
7940 }
7941
7942 fn open(&self) -> Option<bool> {
7943 self.open
7944 }
7945
7946 fn key(&self) -> String {
7947 self.value.clone().unwrap_or_else(|| self.primary())
7948 }
7949 }
7950
7951 /// Which rows a closed branch folds away, one answer per row in order.
7952 ///
7953 /// A row is folded when a row above it is a closed branch shallower than it,
7954 /// with nothing at that branch's own depth or shallower in between. That is the
7955 /// whole of what makes a flat list an outline, and it is computed from the list
7956 /// rather than described, which is why `Row::children` was not needed to say a
7957 /// hierarchy.
7958 ///
7959 /// A row folded by one branch cannot un-fold under another inside it: a shut
7960 /// branch takes its whole subtree, including the open branches in it, and those
7961 /// come back in the state they were left when it opens.
7962 ///
7963 /// A renderer that does not call this draws every row flat, which is today's
7964 /// list and is the graceful degradation the flat model was chosen for.
7965 #[must_use]
7966 pub fn folded<T: Outline>(rows: &[T]) -> Vec<bool> {
7967 folded_by(rows.iter().map(|row| (row.depth(), row.open())))
7968 }
7969
7970 /// [`folded`], for a renderer holding the reader's own answer about a branch.
7971 ///
7972 /// A terminal and an immediate-mode window keep the folds the reader made, the
7973 /// way they keep a scroll offset and a tick, and the description's `open` is
7974 /// where that starts rather than where it stays ([`Row::open`]). So they read
7975 /// each row's state off the reader first and hand the pairs here, and the
7976 /// walk itself stays in one place: two copies of "what does a shut branch
7977 /// cover" is two answers waiting to disagree across hosts.
7978 #[must_use]
7979 pub fn folded_by(levels: impl IntoIterator<Item = (layout::Nesting, Option<bool>)>) -> Vec<bool> {
7980 let levels = levels.into_iter();
7981 let mut folded = Vec::with_capacity(levels.size_hint().0);
7982 // The depth of the shallowest closed branch still folding, if any. One
7983 // variable and not a stack: a branch inside a folded subtree can never be
7984 // the reason a row is hidden, because it is hidden itself.
7985 let mut shut: Option<layout::Nesting> = None;
7986 for (depth, open) in levels {
7987 if shut.is_some_and(|shallowest| depth <= shallowest) {
7988 shut = None;
7989 }
7990 let hidden = shut.is_some();
7991 folded.push(hidden);
7992 if !hidden && open == Some(false) {
7993 shut = Some(depth);
7994 }
7995 }
7996 folded
7997 }
7998
7999 /// Which way a readout is reckoned against the current time.
8000 ///
8001 /// The three time-derived members of [`Node`] say the same thing about
8002 /// themselves in three spellings, and this is how a renderer asks which one it
8003 /// is holding without matching all three everywhere it cares. Two things read
8004 /// it: the formatting, which differs per kind, and the cadence, which follows
8005 /// the granularity the format chose.
8006 ///
8007 /// Ordered so a renderer taking the minimum of a screen's kinds gets the finest
8008 /// one first, which is what [`Screen::clocks`] is usually asked for.
8009 #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8010 pub enum Clock {
8011 /// Counting up from an instant. [`Node::Since`].
8012 Since,
8013 /// Counting down to one. [`Node::Until`].
8014 Until,
8015 /// How long ago one was. [`Node::Age`].
8016 Age,
8017 }
8018
8019 /// A thing on a screen.
8020 ///
8021 /// Every member composes something `makeover-layout` already names, and that is
8022 /// the admission test for a new one. A node with no counterpart there means the
8023 /// vocabulary is missing a word, and the fix is to add the word rather than to
8024 /// add a widget here.
8025 ///
8026 /// There is a second test, and a member passes both or neither: **a renderer
8027 /// must be able to size it before its content arrives.** A member that can only
8028 /// be laid out once it is filled makes the screen move under the reader, which
8029 /// is what "First paint is final paint" in `makeover-layout`'s header forbids.
8030 /// If a member cannot be sized in advance as written, it is missing the fact
8031 /// that would make it sizeable, and adding that fact is the fix — the same
8032 /// shape as the first test, one layer along.
8033 ///
8034 /// The failure this catches is an optional measurement standing in for a
8035 /// measurement that has not been taken. An `Option` on a count here means the
8036 /// host cannot count, for the life of the screen; it never means the count is
8037 /// still coming. A number that shows up after the first paint widens whatever
8038 /// prints it.
8039 ///
8040 /// `#[non_exhaustive]`, the pairing [`Region`](layout::Region) made when it grew
8041 /// [`Region::Widget`](layout::Region::Widget), and for the same reason: the
8042 /// member after this one should not be a lockstep event across three renderers.
8043 /// This is the enum that reason applies to most, since a renderer spells every
8044 /// member of it and nothing else.
8045 ///
8046 /// What the attribute buys is that a renderer *may* lag, not that it should. A
8047 /// wildcard arm here is a promise about one member, so write it the way the rest
8048 /// of the suite does: the arm says what the honest degradation is and why it is
8049 /// the safe read, rather than falling through to nothing without comment. The
8050 /// matches inside this crate stay exhaustive -- the attribute does not apply
8051 /// within it, and a wildcard here would only hide a member added without a
8052 /// spelling.
8053 #[derive(Debug, Clone, PartialEq, Eq)]
8054 #[non_exhaustive]
8055 pub enum Node {
8056 /// A title, at one of three depths in the heading tree.
8057 Heading {
8058 /// How far down the tree it sits.
8059 level: layout::Heading,
8060 /// The text.
8061 text: String,
8062 },
8063 /// Prose, with a tone.
8064 Text {
8065 /// The text.
8066 text: String,
8067 /// What it is saying. [`layout::Tone::Neutral`] is ordinary content.
8068 tone: layout::Tone,
8069 },
8070 /// Prose the author wrote in markdown.
8071 ///
8072 /// What is carried is the **source**, never markup, which is the property
8073 /// that lets this exist at all. Every renderer has an honest answer
8074 /// because each renders the source its own way: a webview through a
8075 /// markdown-to-HTML pass, a terminal through markdown-to-ANSI, egui
8076 /// through its own. A `Node::Html` would have handed every one of them a
8077 /// string it could not honour, and would have broken
8078 /// [`Node::Text`]'s escaping guarantee for every consumer rather than the
8079 /// one that asked. That refusal stands; this is not it.
8080 ///
8081 /// Sanitising is the renderer's, at the point markup is produced, for the
8082 /// reason escaping already is: this holds text a user typed, and a
8083 /// description that sanitised would be deciding what a host can draw.
8084 ///
8085 /// Not available inside a [`Row`]: a row part holds no node, by the
8086 /// ruling. What a row can hold is [`Prose`], which carries the same
8087 /// markdown source under the same reasoning without being a node, so the
8088 /// projects card no longer keeps its raw markdown in `secondary`.
8089 Rich {
8090 /// The markdown, as written.
8091 source: String,
8092 /// How much of the format this source may use.
8093 ///
8094 /// Separate from [`trust`](Self::Rich::trust) on purpose, and the two
8095 /// correlate without being the same question: a creator's forum post is
8096 /// rich and untrusted, a button's own hint is plain and trusted.
8097 richness: Richness,
8098 /// How far the source is trusted.
8099 trust: Trust,
8100 },
8101 /// Source code, already classified by whoever holds a lexer.
8102 ///
8103 /// Decision `19d7602d`, 2026-09-02, option (d). The node carries
8104 /// [`Lexeme`]s and never a bare string, which is the whole ruling: an app
8105 /// that browses source already has a lexer, a renderer does not and should
8106 /// not grow one, and three renderers each growing their own would disagree
8107 /// about the same file.
8108 ///
8109 /// # Why this is not [`Rich`](Self::Rich)
8110 ///
8111 /// `Rich` carries markdown and each renderer renders it its own way, which
8112 /// is right for prose and wrong here: markdown's fenced code block says a
8113 /// run is code and says nothing about what is in it, so a renderer would
8114 /// still be choosing whether to lex. This says what the runs are and leaves
8115 /// only the colouring.
8116 ///
8117 /// # Inline against block, and why one member rather than two
8118 ///
8119 /// A clone URL in a sentence and a file in a source browser are the same
8120 /// claim about the same text; what differs is whether it sits in the line or
8121 /// owns one. That is a containment fact, so it is a flag here and
8122 /// `Node::containment` reads it: `inline` is a leaf and may sit in a [`Row`]
8123 /// or a [`Cell`], and a block is not and may not. Two members would have
8124 /// made one distinction twice, once in the type and once in the bound.
8125 ///
8126 /// # The language is carried even though the runs are classified
8127 ///
8128 /// Not redundant. The classification is fixed at what [`layout::Syntax`]
8129 /// can say, and a renderer with an opinion of its own -- a terminal that
8130 /// already ships a highlighter, a host that wants a language badge over the
8131 /// block -- has nothing else to read. A hint, and no renderer is obliged to
8132 /// use it.
8133 Code {
8134 /// The source, run by run, in order. Concatenating every `text` gives
8135 /// the file back exactly, whitespace included.
8136 runs: Vec<Lexeme>,
8137 /// What language it is, when the app knows. A hint; see above.
8138 language: Option<String>,
8139 /// Whether it sits in a line rather than owning one.
8140 inline: bool,
8141 },
8142 /// A control that calls a route.
8143 Act(Act),
8144 /// Text that goes somewhere.
8145 ///
8146 /// The containment model predicted this before anything asked for it: the
8147 /// composability matrix had a `Link` row whose "as a `Node`" cell was a
8148 /// dash, and the only way to say it was [`Cell::activate`], a member on one
8149 /// container. Once a cell is a run of leaves, the run needs a leaf that
8150 /// means "this text is a link" or the thing stops being sayable at all.
8151 ///
8152 /// Distinct from [`Act`](Self::Act), and the difference is what the reader
8153 /// sees rather than what the route does. An act is a control drawn as one,
8154 /// which is right for `Edit` and wrong for a title: making every linked
8155 /// value a button would put a row of bevels down the first column of half a
8156 /// dashboard. Both call a route; only one of them looks like a button.
8157 Link {
8158 /// What it says.
8159 text: String,
8160 /// Where it goes.
8161 action: Action,
8162 },
8163 /// One figure, on a line.
8164 ///
8165 /// The second thing the containment model found, and it found it by
8166 /// refusing: a cell is a run of leaves, [`Stats`](Self::Stats) is a
8167 /// collection, so putting a figure in a cell failed the bound rather than
8168 /// quietly working. That is the check doing its job -- the matrix had
8169 /// "figure in a cell" as a dash nobody had attempted, and the dash turns
8170 /// out to have been hiding a missing member rather than a missing renderer
8171 /// arm.
8172 ///
8173 /// Not a duplicate of a one-element [`Stats`](Self::Stats), and the
8174 /// difference is the claim being made. A strip says "this is a row of
8175 /// tiles", which is why the set is the node there: a renderer handed one
8176 /// tile at a time cannot tell it is looking at a set. This says "this
8177 /// number sits on this line", where the run is already the grouping and
8178 /// there is nothing for a set to add. A dashboard strip of one is still a
8179 /// strip; a revenue column is not.
8180 Figure(Figure),
8181 /// Time elapsed since an instant, counting up.
8182 ///
8183 /// The three time-derived members are this, [`Until`](Self::Until) and
8184 /// [`Age`](Self::Age), and the ruling they implement is one sentence: **the
8185 /// renderer owns the clock.** What is carried here is an instant and which
8186 /// way the readout runs against now; the cadence and the spelling are the
8187 /// renderer's, and the description names neither.
8188 ///
8189 /// A stopwatch, an uptime, a running timer. goingson's time-tracking widget
8190 /// is the measured case: 564 lines of JS whose whole job is to subtract a
8191 /// start time from now every second, in two places, for a readout the
8192 /// description could not say at all.
8193 ///
8194 /// # Why a kind and not a format string
8195 ///
8196 /// A format string is presentation living in a description, and it would
8197 /// bind three renderers with different width budgets to one spelling. A
8198 /// bare instant is worse in the other direction: a stopwatch and a
8199 /// last-modified stamp become indistinguishable, so a renderer cannot pick
8200 /// a cadence and would tick a static date once a second.
8201 ///
8202 /// The kind answers many-readouts-one-tick for free. Every `Since` on a
8203 /// screen refreshes together because they share a kind, which is what
8204 /// `updateRowElapsed` walking `.task-timer-elapsed[data-started]` does by
8205 /// hand today, and [`Screen::clocks`] is how a renderer asks which kinds it
8206 /// is holding.
8207 ///
8208 /// # Why a duration is not carried
8209 ///
8210 /// [`Consult::after`] carries one, and the line between them is stated
8211 /// there: a duration is the description's when it is a property of the
8212 /// subject that renderers would otherwise disagree about, and the
8213 /// renderer's when it is presentation policy. A debounce is the route's own
8214 /// expense and no renderer could know it. A tick cadence follows the
8215 /// displayed granularity, so two renderers showing the same readout do not
8216 /// meaningfully disagree.
8217 ///
8218 /// A leaf, so it may sit in a row's run or a table cell. That is the half
8219 /// the goingson row readout needs: the elapsed time is one part of a task
8220 /// row beside its title, not a block of its own.
8221 Since {
8222 /// The instant it counts from.
8223 at: std::time::SystemTime,
8224 },
8225 /// Time remaining until an instant, counting down.
8226 ///
8227 /// [`Since`](Self::Since) run the other way, under the same ruling and with
8228 /// the same reasoning. A deadline, an expiry, a lock that lifts.
8229 ///
8230 /// What a renderer does once the instant is past is the renderer's, for the
8231 /// reason the spelling is. Nothing here says whether that reads as a
8232 /// negative countdown or as zero, because a description that said would be
8233 /// picking one of those for a terminal it has never seen.
8234 Until {
8235 /// The instant it counts down to.
8236 at: std::time::SystemTime,
8237 },
8238 /// How long ago an instant was.
8239 ///
8240 /// [`Since`](Self::Since)'s coarse sibling, and the reason it is a third
8241 /// member rather than the same one: "3h ago" and a running `h:mm:ss` are
8242 /// the same subtraction shown at granularities two orders apart, and the
8243 /// granularity is what decides the cadence. A stamp drawn as a stopwatch
8244 /// ticks a number that has not changed once a second for a day.
8245 ///
8246 /// A last-modified, a posted-at, a synced-at.
8247 Age {
8248 /// The instant being aged.
8249 at: std::time::SystemTime,
8250 },
8251 /// A picture, at a source this crate holds and the description does not.
8252 ///
8253 /// The [`Act`](Self::Act) split, and `layout::Image`'s own docs carry the
8254 /// argument: an address is not the description's to hold, so the shape and
8255 /// the alt text live there and the URL lives here.
8256 ///
8257 /// A leaf, so it may sit in a run the way [`Link`](Self::Link) does. What
8258 /// it may *not* do is stand in for a region: a picture is one thing on the
8259 /// page, and a gallery of them is a widget assembled out of several.
8260 Image(Image),
8261 /// A small labelled thing sitting inside something else.
8262 Token(Tag),
8263 /// Something the app is telling the user, unprompted.
8264 Notice {
8265 /// Transient and stacked, or persistent and in flow.
8266 kind: layout::Notice,
8267 /// What it is saying.
8268 tone: layout::Tone,
8269 /// The message.
8270 text: String,
8271 /// One thing to do about it, if there is one.
8272 ///
8273 /// [`Message::undo`] says a response can offer a way back, and a host
8274 /// that draws a `Message` by converting it into one of these had
8275 /// nowhere to put it: quasi-tui's `announce` said so in a comment and
8276 /// dropped it.
8277 ///
8278 /// Grown here rather than answered by a holder beside the screen, which
8279 /// was the alternative. Every retained-screen host would have written
8280 /// the same holder, and both shapes are the same thing said from two
8281 /// ends -- which is what [`Message`]'s own docs already claim.
8282 ///
8283 /// [`StandIn`](Self::StandIn)'s member under the same name and for the
8284 /// same reason: a sentence about a situation, and the one thing to do
8285 /// about it, are one thing on the screen.
8286 ///
8287 /// [`Message`]: crate::Message
8288 /// [`Message::undo`]: crate::Message::undo
8289 act: Option<Act>,
8290 },
8291 /// What stands where content would be, when there is none.
8292 ///
8293 /// goingson draws one at 27 sites across 12 files and Balanced Breakfast
8294 /// at 9, and the class families had already drifted into `empty-state--
8295 /// error` against `error-state` for the same fact. Every one of those
8296 /// sites substitutes markup where a list would go, which is what makes
8297 /// this a node.
8298 ///
8299 /// # Why not on the region
8300 ///
8301 /// It was on [`Slot`] first, and a real screen killed it: the project
8302 /// dashboard's columns are a heading and a list, and a column with no rows
8303 /// is a region that has content and a list that has none. Marking the
8304 /// region empty took the heading down with the rows. The emptiness belongs
8305 /// to the thing that is empty.
8306 ///
8307 /// [`Slot::readiness`] keeps the loading axis and only that, which is what
8308 /// `aria-busy` is about.
8309 ///
8310 /// # The state is the vocabulary's and the sentence is not
8311 ///
8312 /// `makeover-layout` names the four states because "nothing here yet" and
8313 /// "this broke" mean the same thing in every app that will have them. "No
8314 /// projects yet" is content, and so is the button under it, so both are
8315 /// here. A [`layout::Readiness::Ready`] renders nothing at all: the state
8316 /// that shows content has no stand-in to draw.
8317 StandIn {
8318 /// Which of the states this is standing in for.
8319 state: layout::Readiness,
8320 /// The sentence. "No projects yet", "Failed to load events".
8321 message: String,
8322 /// The way out, if there is one. "Add your first project", "Try again".
8323 ///
8324 /// 2 of goingson's 27 have one and 25 say a sentence and stop, which is
8325 /// why it is optional rather than a second required string.
8326 act: Option<Act>,
8327 },
8328 /// One control, standing on its own.
8329 ///
8330 /// A [`Form`](Self::Form) is a set of questions asked together and
8331 /// answered at once. A settings screen is not that: goingson's is sections
8332 /// with headings between them, each holding one control that writes as
8333 /// soon as it changes, and wrapping those in a form would describe markup
8334 /// that is not there and a submit that does not exist.
8335 ///
8336 /// Almost always carries a [`Field::writes`], because a control with no
8337 /// form around it and no route on it collects a value nothing reads.
8338 ///
8339 /// Boxed because it is the only member holding a whole struct by value, and
8340 /// [`Field`] is the largest one here — every other member holds a `Vec`, a
8341 /// `String` or a small enum. Unboxed it decides the size of every [`Node`]
8342 /// in every list, and of the [`Response`](crate::Response) that carries one.
8343 Field(Box<Field>),
8344 /// Fields, and the route that submits them.
8345 Form {
8346 /// Where the answers go. Almost always a [`Method::Post`].
8347 action: Action,
8348 /// What the submit control is called.
8349 submit: String,
8350 /// The questions, in order.
8351 fields: Vec<Field>,
8352 },
8353 /// Rows with named columns.
8354 ///
8355 /// # Which containers carry a [`Rest`], and which do not
8356 ///
8357 /// A container whose contents came from a query that can be partial carries
8358 /// one. That is this one -- in both its arrangements, since a list is this
8359 /// node with no columns -- and nothing else in this enum:
8360 /// [`Timeline`](Self::Timeline) is bounded by its `Track`, so more of
8361 /// it is a different window and that is navigation rather than paging;
8362 /// [`Stats`](Self::Stats) is a fixed set of figures; [`Region`](Self::Region)
8363 /// holds nodes rather than rows. Written down so the next container to
8364 /// arrive answers the question rather than inheriting an answer.
8365 ///
8366 /// This one went without for four minors. goingson's task list, the first
8367 /// described table anywhere, had to hang its paging off a separate
8368 /// [`Act`](Self::Act) under the table, and recorded the cost: the renderer
8369 /// could not tell the control belonged to the table above it.
8370 Table {
8371 /// The columns, in order. A cell keyed by position answers these in order.
8372 ///
8373 /// **Empty is a list.** That is the whole of what a list is since the
8374 /// 2026-09-06 collapse, and it is a fact the rows already carried rather
8375 /// than a flag added beside them: a row whose cells are keyed by
8376 /// [`CellKey::Role`] answers the default column set, and a table that
8377 /// declared no columns of its own is a table using that set. So there is
8378 /// no `look` member and no second variant to keep in step.
8379 ///
8380 /// A renderer reads it to choose an arrangement, not to choose a
8381 /// meaning. `columns.is_empty()` draws the flowed, one-per-line form a
8382 /// list has always drawn -- `<ul>` in a webview -- and a declared column
8383 /// list draws a grid. Both are the same node holding the same rows.
8384 columns: Vec<Column>,
8385 /// The rows, in order.
8386 rows: Vec<Row>,
8387 /// What is not shown, if anything is.
8388 ///
8389 /// A described list of the first 50 of 400 tasks was indistinguishable
8390 /// from a described list of 50 tasks, so each app grew its own answer:
8391 /// goingson a 159-line pagination manager with two consumers that had
8392 /// each written it separately first, Balanced Breakfast four
8393 /// `loadMore` sites. Two idioms for one fact, and the fact is what
8394 /// belongs here -- how much more there is and how to ask for it.
8395 /// Whether that becomes numbered pages, a load-more button or an
8396 /// infinite scroll is the renderer's.
8397 more: Option<Rest>,
8398 },
8399 /// Rows placed by when they happen, rather than in order.
8400 ///
8401 /// The second of the two ways this vocabulary says "several of the same
8402 /// kind of thing", and the last one to arrive.
8403 /// [`Table`](Self::Table) puts them in order and, when it declares columns,
8404 /// lines their parts up under headings; this one puts them on a clock.
8405 ///
8406 /// It was the third of three until the 2026-09-06 collapse, when a list
8407 /// stopped being a node of its own. A timeline stayed: what it adds is
8408 /// [`layout::Placement`] on every entry, which is a fact a `Row` does not
8409 /// carry and a column cannot supply.
8410 ///
8411 /// A row here is an ordinary [`Row`] and gets no new members: the item
8412 /// bodies on goingson's day view are a title, a time, a tag and a tone,
8413 /// which the vocabulary already said. What it could not say is *where the
8414 /// row sits*, and that is [`layout::Placement`] — a start and a duration,
8415 /// two integers, which is the whole of what the timeline refusal was
8416 /// pricing as a component library. See `makeover-layout` 0.24.0.
8417 ///
8418 /// # What a renderer owes it
8419 ///
8420 /// Draw the span, put each row at its placement, and lay overlapping rows
8421 /// so both can be read. That last part is presentation and deliberately
8422 /// unspecified: a webview puts them in columns, a terminal may stack them
8423 /// with a marker, and neither is wrong.
8424 /// [`layout::Placement::overlaps`] is how a renderer finds the pairs
8425 /// without the description declaring them.
8426 ///
8427 /// # What it is not
8428 ///
8429 /// Not a calendar and not a kanban board. Both were refused alongside the
8430 /// timeline and neither has been measured; whoever needs one counts the
8431 /// members it is missing rather than reaching for this.
8432 Timeline {
8433 /// The axis: its window, its granularity, how often it labels itself.
8434 track: layout::Track,
8435 /// What sits on it, each with where it sits.
8436 ///
8437 /// Not sorted here, and a renderer must not assume it is. Sorting by
8438 /// start is presentation for anything that draws top to bottom, and
8439 /// meaningless for anything that does not.
8440 entries: Vec<Placed>,
8441 /// A moment worth bringing into view, if any.
8442 ///
8443 /// "Show me 09:00" rather than a scroll offset in pixels. goingson's JS
8444 /// hardcodes `targetHour = 9` inside the renderer, which is the shape
8445 /// this replaces: the app knows the interesting hour, the renderer
8446 /// knows how to get there.
8447 ///
8448 /// `None` means the renderer chooses, which is usually the span's
8449 /// start.
8450 focus: Option<u16>,
8451 },
8452 /// How much of a set is done.
8453 Meter(Meter),
8454 /// A value with a caption, several of them as one strip.
8455 ///
8456 /// Against `makeover-layout`'s [`layout::Figure`], which arrived at 0.11.0
8457 /// for this. The dashboard shape: a large value over a small caption,
8458 /// several in a row. goingson had five of them across five screens with
8459 /// five class vocabularies for the one shape, and the port had been making
8460 /// each out of a [`Row`] with the caption as `primary` and the figure as
8461 /// `meta`, which reads backwards — a row's primary slot means the thing
8462 /// itself, and here the thing is the number.
8463 ///
8464 /// # Why the set is the node and not each figure
8465 ///
8466 /// Four tiles in a strip and four tiles down a column are different things,
8467 /// and a renderer handed one at a time cannot tell it is looking at a set.
8468 /// The objection to that is real and is answered by what is already here: a
8469 /// node whose value is its grouping sounds like a layout instruction, and
8470 /// [`Table`](Self::Table) has been exactly that since the beginning without
8471 /// anyone calling it one.
8472 ///
8473 /// # Why the action is here and not on the figure
8474 ///
8475 /// One of goingson's five is a control — sync's "Not Applied: 3" opens the
8476 /// list. `makeover-layout` cannot name an action at all, so the figure it
8477 /// describes carries none, and this pairs the description with the address
8478 /// the same way [`Row`] pairs its parts with [`Row::activate`].
8479 Stats {
8480 /// The figures, in order, and what each one calls if it calls anything.
8481 figures: Vec<(Figure, Option<Action>)>,
8482 },
8483 /// A region inside a region.
8484 Region(Slot),
8485 /// Markup this vocabulary did not write, and the scope that isolates it.
8486 ///
8487 /// The measured consumer is MNW's custom pages: a creator writes HTML and
8488 /// CSS, the server sanitises both and re-scopes every selector under a
8489 /// canvas element, and what comes out is a document whose middle is
8490 /// opaque. Nothing in a vocabulary of rows, cards and fields can say that,
8491 /// and no amount of growing it will -- the whole point of the feature is
8492 /// that the platform does not know what the creator drew.
8493 ///
8494 /// # This is the one member a description does not describe
8495 ///
8496 /// Everywhere else, a description says what a thing *is* and a renderer
8497 /// decides what it looks like. Here the app hands over markup and the
8498 /// renderer writes it out. That is the concession, it is deliberate, and
8499 /// the way to keep it from spreading is to remember what earned it: markup
8500 /// that is *data*, authored by somebody who is not the app and stored
8501 /// rather than written. A screen reaching for this to avoid describing its
8502 /// own layout is a screen that has not been described.
8503 ///
8504 /// # Sanitised by whoever produced it
8505 ///
8506 /// This crate does not parse the markup, does not sanitise it, and has no
8507 /// opinion about what is in it. It cannot: what is safe depends on the
8508 /// document's own headers, and MNW's answer is an allowlist pass plus a
8509 /// `default-src 'none'` CSP on a cookieless host. An app putting reader
8510 /// input in here is putting reader input in a browser.
8511 ///
8512 /// # Every host but a markup one draws nothing
8513 ///
8514 /// A terminal has no use for a string of HTML and will not grow one, so it
8515 /// draws nothing here rather than drawing the tags. That is the same read
8516 /// [`Binding::key`] gets from a host that has never heard of the key, and
8517 /// it is honest for this member in a way it would not be for most: these
8518 /// are public web pages and no terminal is going to serve one.
8519 ///
8520 /// [`Binding::key`]: crate::chrome::Binding::key
8521 Canvas(Box<Canvas>),
8522 }
8523
8524 /// Markup an app stored rather than wrote, and how it is kept to itself.
8525 ///
8526 /// The body half of what a custom page needs; the stylesheet half is
8527 /// [`Document::style`], because a sheet is true of the document rather than of
8528 /// a place in it.
8529 ///
8530 /// # The scope is markup-shaped on purpose
8531 ///
8532 /// [`class`](Self::class) and [`id`](Self::id) are the two hooks a stylesheet
8533 /// can be confined to, and they are named here in the markup's own words for
8534 /// [`Document`]'s reason: this is where a host keeps what its own taxonomy
8535 /// says, rather than where the vocabulary grows a word for it. MNW's sanitiser
8536 /// rewrites every creator selector to sit under `.user-canvas#uc-{owner}`, so
8537 /// the element the renderer writes has to carry that class and that id or the
8538 /// sheet it was scoped for matches nothing. Which strings those are is the
8539 /// app's to know; that they are a class and an id is the sanitiser's contract,
8540 /// not this crate's invention.
8541 ///
8542 /// Both optional, and both empty is a canvas that isolates nothing -- markup
8543 /// dropped into the page with no sheet aimed at it, which is a real shape for
8544 /// an app whose stylesheet is its own.
8545 ///
8546 /// # An empty [`markup`](Self::markup) is the other half of the same idea
8547 ///
8548 /// The scope is here because a class or an id can be a *contract with a
8549 /// stylesheet the app did not write*, and creator markup is only one way to
8550 /// arrive at one. The other is a block the app draws itself and has published
8551 /// a name for: MNW's guide tells creators that the buy block is `.mnw-buy`,
8552 /// the file list `.mnw-files` and the item block `.mnw-item`, with worked CSS
8553 /// against them. Those names are as load-bearing as the canvas id and for the
8554 /// same reason -- somebody outside the app has already written selectors
8555 /// against them -- and a renderer that prefixed or renamed them would silently
8556 /// stop every creator page that styles one.
8557 ///
8558 /// So a canvas holding no markup and only [`within`](Self::within) is an
8559 /// ordinary shape: described content, under a name the app has promised. What
8560 /// the two uses share is the whole of what this member is, which is why they
8561 /// are one member and not two. What they do not share is risk: markup is
8562 /// opaque and dangerous, and a name is neither.
8563 #[derive(Debug, Clone, Default, PartialEq, Eq)]
8564 pub struct Canvas {
8565 /// The markup, as whoever produced it left it.
8566 pub markup: String,
8567 /// The classes on the element that scopes it, space-separated.
8568 pub class: Option<String>,
8569 /// The id on the element that scopes it.
8570 pub id: Option<String>,
8571 /// Nodes drawn inside the scope, after the markup.
8572 ///
8573 /// Inside rather than beside, because that is what the measured consumer
8574 /// does and the difference is visible: MNW's project pages put the buy
8575 /// block and the file list in the scoping element after the creator's
8576 /// markup, which is what lets a creator style the platform's own blocks to
8577 /// match the page they wrote. Nodes placed outside would be a different
8578 /// page, quietly.
8579 ///
8580 /// Ordinary nodes. Nothing is injected into the middle of the markup and
8581 /// nothing can be: a canvas is opaque, so the only place the app's own
8582 /// nodes can go is after it.
8583 pub within: Vec<Node>,
8584 }
8585
8586 impl Canvas {
8587 /// Markup with no scope on it yet.
8588 #[must_use]
8589 pub fn new(markup: impl Into<String>) -> Self {
8590 Self {
8591 markup: markup.into(),
8592 ..Self::default()
8593 }
8594 }
8595
8596 /// Put this class on the element that scopes the markup.
8597 #[must_use]
8598 pub fn classed(mut self, class: impl Into<String>) -> Self {
8599 self.class = Some(class.into());
8600 self
8601 }
8602
8603 /// Put this id on the element that scopes the markup.
8604 #[must_use]
8605 pub fn identified(mut self, id: impl Into<String>) -> Self {
8606 self.id = Some(id.into());
8607 self
8608 }
8609
8610 /// Draw this node inside the scope, after the markup.
8611 #[must_use]
8612 pub fn with(mut self, node: Node) -> Self {
8613 self.within.push(node);
8614 self
8615 }
8616 }
8617
8618 impl Node {
8619 /// What this node offers under a control name, if anything below it does.
8620 ///
8621 /// The described half of a [`Reveal`]: a region names a control by
8622 /// [`Field::name`], and this is how a renderer finds what that control was
8623 /// handed to the reader holding.
8624 ///
8625 /// The match is exhaustive rather than a wildcard over the containers,
8626 /// which is what keeps a member that grows a body from quietly hiding
8627 /// fields from every conditional region on the screen. `Node` is
8628 /// `#[non_exhaustive]` outside this crate and not inside it, so the
8629 /// compiler is the reviewer here.
8630 ///
8631 /// Every describable choice is a [`Field`] and therefore has a name, which
8632 /// is what makes a watched choice reachable at all. There is one way to
8633 /// describe a choice.
8634 #[must_use]
8635 pub fn holds(&self, name: &str) -> Option<&str> {
8636 /// What one field offers, if it is the field being asked about.
8637 fn offered<'a>(field: &'a Field, name: &str) -> Option<&'a str> {
8638 (field.name == name)
8639 .then_some(field.value.as_deref())
8640 .flatten()
8641 }
8642
8643 match self {
8644 Self::Field(field) => offered(field, name),
8645 Self::Form { fields, .. } => fields.iter().find_map(|field| offered(field, name)),
8646 Self::Region(slot) => slot.holds(name),
8647 // The markup is opaque and holds nothing this crate can find; what
8648 // is walked is the app's own nodes inside the scope.
8649 Self::Canvas(canvas) => canvas.within.iter().find_map(|node| node.holds(name)),
8650 Self::Table { rows, .. } => rows.iter().find_map(|row| {
8651 row.cells
8652 .iter()
8653 .find_map(|cell| cell.content.iter().find_map(|node| node.holds(name)))
8654 }),
8655 Self::Timeline { entries, .. } => entries.iter().find_map(|placed| {
8656 placed
8657 .row
8658 .cells
8659 .iter()
8660 .find_map(|cell| cell.content.iter().find_map(|n| n.holds(name)))
8661 }),
8662 // Everything with no field under it. Written out rather than left
8663 // to a wildcard, for the reason above.
8664 Self::Heading { .. }
8665 | Self::Text { .. }
8666 | Self::Rich { .. }
8667 | Self::Act(_)
8668 | Self::Link { .. }
8669 | Self::Figure(_)
8670 | Self::Since { .. }
8671 | Self::Until { .. }
8672 | Self::Age { .. }
8673 | Self::Image(_)
8674 | Self::Token(_)
8675 | Self::Notice { .. }
8676 | Self::StandIn { .. }
8677 | Self::Meter(_)
8678 // Classified text and nothing else: no field, no act, no clock.
8679 | Self::Code { .. }
8680 | Self::Stats { .. } => None,
8681 }
8682 }
8683
8684 /// Every question under this node, in draw order, appended to `found`.
8685 ///
8686 /// [`holds`](Self::holds)' walk asking for the boxes themselves rather
8687 /// than for one of their values, which is what a
8688 /// [`Slot::consults`] gathers. Exhaustive over the containers for
8689 /// `holds`' reason, and it appends rather than returning so a region's walk
8690 /// over a body of nodes allocates once.
8691 pub fn questions<'a>(&'a self, found: &mut Vec<&'a Field>) {
8692 match self {
8693 Self::Field(field) => found.push(field),
8694 Self::Canvas(canvas) => {
8695 for node in &canvas.within {
8696 node.questions(found);
8697 }
8698 }
8699 Self::Form { fields, .. } => found.extend(fields),
8700 Self::Region(slot) => found.extend(slot.questions()),
8701 Self::Table { rows, .. } => {
8702 for row in rows {
8703 for cell in &row.cells {
8704 for node in &cell.content {
8705 node.questions(found);
8706 }
8707 }
8708 }
8709 }
8710 Self::Timeline { entries, .. } => {
8711 for placed in entries {
8712 for cell in &placed.row.cells {
8713 for node in &cell.content {
8714 node.questions(found);
8715 }
8716 }
8717 }
8718 }
8719 // Everything with no question under it. Written out rather than
8720 // left to a wildcard, so a member that grows a body has to answer
8721 // here instead of quietly contributing nothing to every region
8722 // consult on the screen.
8723 Self::Heading { .. }
8724 | Self::Text { .. }
8725 | Self::Rich { .. }
8726 | Self::Act(_)
8727 | Self::Link { .. }
8728 | Self::Figure(_)
8729 | Self::Since { .. }
8730 | Self::Until { .. }
8731 | Self::Age { .. }
8732 | Self::Image(_)
8733 | Self::Token(_)
8734 | Self::Notice { .. }
8735 | Self::StandIn { .. }
8736 | Self::Meter(_)
8737 | Self::Code { .. }
8738 | Self::Stats { .. } => {}
8739 }
8740 }
8741
8742 /// Whether an [`Act`] under this node carries this [`Act::id`].
8743 ///
8744 /// [`holds`](Self::holds)'s shape for a different question, and it walks the
8745 /// same containers for the same reason: a control is anywhere a run, a row,
8746 /// a cell or a nested region can put one.
8747 ///
8748 /// A row's [`menu`](Row::menu) is walked too. A menu act is a control the
8749 /// description carries and a renderer draws, so a screen that answers `true`
8750 /// for one is telling the truth; whether that host has drawn it yet is the
8751 /// host's business.
8752 #[must_use]
8753 pub fn names(&self, id: &str) -> bool {
8754 match self {
8755 Self::Act(act) => act.id.as_deref() == Some(id),
8756 Self::Region(slot) => slot.names(id),
8757 // A control inside the markup is the creator's and carries no id
8758 // this crate handed out, so only the app's own nodes are walked.
8759 Self::Canvas(canvas) => canvas.within.iter().any(|node| node.names(id)),
8760 Self::Table { rows, .. } => rows.iter().any(|row| row.names(id)),
8761 Self::Timeline { entries, .. } => entries.iter().any(|placed| placed.row.names(id)),
8762 // Everything with no control under it. Written out rather than left
8763 // to a wildcard, so a node kind that gains one stops compiling here.
8764 Self::Heading { .. }
8765 | Self::Text { .. }
8766 | Self::Rich { .. }
8767 | Self::Field(_)
8768 | Self::Form { .. }
8769 | Self::Link { .. }
8770 | Self::Figure(_)
8771 | Self::Since { .. }
8772 | Self::Until { .. }
8773 | Self::Age { .. }
8774 | Self::Image(_)
8775 | Self::Token(_)
8776 | Self::Notice { .. }
8777 | Self::StandIn { .. }
8778 | Self::Meter(_)
8779 | Self::Code { .. }
8780 | Self::Stats { .. } => false,
8781 }
8782 }
8783
8784 /// The value a control sends when what it sends is presence rather than a
8785 /// number: a ticked checkbox, and anything else spelling "this one".
8786 ///
8787 /// Named once here rather than agreed by convention between each renderer
8788 /// and each handler, which is how a value arrives under `tab` in one screen
8789 /// and `selected` in the next. It is HTML's own convention for a checkbox
8790 /// read back out, which is why the word is `value`.
8791 ///
8792 /// A [`Field`] carries its own name, so a choice needs no shared word to
8793 /// arrive under.
8794 pub const SELECTED: &'static str = "value";
8795
8796 /// The parameter name an [`Act::over`] sends each ticked value under.
8797 ///
8798 /// [`SELECTED`](Self::SELECTED)'s sibling, named here for the same reason:
8799 /// a convention agreed separately by each renderer and each handler is a
8800 /// convention that holds until one of them is written by someone else.
8801 ///
8802 /// Distinct from `SELECTED` rather than shared with it, because the two
8803 /// carry different counts. A control saying "this one" sends one value and
8804 /// a handler reads it with [`Params::get`]; a selection sends however many
8805 /// are ticked, including none, and a handler reads it with
8806 /// [`Params::get_all`]. One name for both would make "the one thing picked"
8807 /// and "the first of the things ticked" the same read.
8808 ///
8809 /// [`Params::get`]: crate::Params::get
8810 /// [`Params::get_all`]: crate::Params::get_all
8811 pub const TICKED: &'static str = "ticked";
8812
8813 /// The parameter name a row activation sends its [`Choosing`] under.
8814 ///
8815 /// [`SELECTED`](Self::SELECTED)'s and [`TICKED`](Self::TICKED)'s third
8816 /// sibling, named here for the reason both of those are: a convention
8817 /// agreed separately by each renderer and each handler holds until one of
8818 /// them is written by someone else.
8819 ///
8820 /// Sent with **every** activation of a row that can be chosen, including the
8821 /// ordinary one, so a handler reads one parameter rather than branching on
8822 /// whether a parameter arrived. A row that carries no
8823 /// [`Row::chosen`] fact is not part of a selection and
8824 /// sends nothing, so a handler that never asks is unaffected.
8825 pub const CHOOSING: &'static str = "choosing";
8826
8827 /// A page title.
8828 pub fn page(text: impl Into<String>) -> Self {
8829 Self::Heading {
8830 level: layout::Heading::Page,
8831 text: text.into(),
8832 }
8833 }
8834
8835 /// A section title.
8836 pub fn section(text: impl Into<String>) -> Self {
8837 Self::Heading {
8838 level: layout::Heading::Section,
8839 text: text.into(),
8840 }
8841 }
8842
8843 /// A subsection title.
8844 ///
8845 /// The third of the three heading levels, which had no constructor while
8846 /// the other two did. Every site that wanted one wrote the variant out
8847 /// with its `level` and its `text`, which is the struct rather than the
8848 /// vocabulary.
8849 pub fn subsection(text: impl Into<String>) -> Self {
8850 Self::Heading {
8851 level: layout::Heading::Subsection,
8852 text: text.into(),
8853 }
8854 }
8855
8856 /// Ordinary prose.
8857 pub fn text(text: impl Into<String>) -> Self {
8858 Self::Text {
8859 text: text.into(),
8860 tone: layout::Tone::Neutral,
8861 }
8862 }
8863
8864 /// The same prose, saying something about itself.
8865 ///
8866 /// [`text`](Self::text) is this at [`layout::Tone::Neutral`], which is
8867 /// ordinary content, and it was the only constructor `Text` had: a line
8868 /// that is a warning wrote the variant out by hand. **21 sites in the tree
8869 /// do**, nine of them in MNW's commit view alone, which makes this the most
8870 /// repeated instance of the gap `Node::literal` and `Node::token` closed
8871 /// before it.
8872 ///
8873 /// Not a notice. A notice is a thing that happened and carries a way out;
8874 /// this is a sentence in the reading that is warning-coloured, which is
8875 /// what a diff's deletion count and an unverifiable signature both are.
8876 pub fn toned(text: impl Into<String>, tone: layout::Tone) -> Self {
8877 Self::Text {
8878 text: text.into(),
8879 tone,
8880 }
8881 }
8882
8883 /// Machine text in a line of reading.
8884 ///
8885 /// One unclassified run, inline, no language: a ref path, a fingerprint, a
8886 /// clone URL, a line of a file. [`Code`](Self::Code) is the only node with
8887 /// no constructor of its own, and six sites in the tree write this exact
8888 /// literal out -- one of them under a private helper called `literal`,
8889 /// which is where the name comes from.
8890 ///
8891 /// Nothing lexed it and nothing should, so what this buys is the monospace
8892 /// and not a colour. A block, or runs a lexer classified, still writes the
8893 /// variant; the screen that asks for either earns the constructor for it.
8894 pub fn literal(text: impl Into<String>) -> Self {
8895 Self::Code {
8896 runs: ::std::vec![Lexeme::plain(text)],
8897 language: None,
8898 inline: true,
8899 }
8900 }
8901
8902 /// Machine text a lexer has been over, inline.
8903 ///
8904 /// The constructor [`literal`](Self::literal) reserved for the screen that
8905 /// asked, and MNW's source browser is it: a file is drawn one row per line,
8906 /// because `#L42` is the address of a line and a block that owned its own
8907 /// lines would have nothing to hang one on. So every line is classified
8908 /// runs plus the extension, and `literal`'s one-plain-run shape cannot say
8909 /// it.
8910 ///
8911 /// Still inline. A block is the other half and is still unasked for.
8912 #[must_use]
8913 pub fn code(runs: Vec<Lexeme>, language: Option<String>) -> Self {
8914 Self::Code {
8915 runs,
8916 language,
8917 inline: true,
8918 }
8919 }
8920
8921 /// Prose written in markdown, by somebody the app does not vouch for.
8922 ///
8923 /// [`Richness::Sentence`] and [`Trust::Untrusted`], which is what this member
8924 /// has always meant and what every call site written before the two axes
8925 /// existed still gets. Say otherwise with [`trust`](Self::trust) and
8926 /// [`richness`](Self::richness).
8927 pub fn rich(source: impl Into<String>) -> Self {
8928 Self::Rich {
8929 source: source.into(),
8930 richness: Richness::Sentence,
8931 trust: Trust::Untrusted,
8932 }
8933 }
8934
8935 /// Who wrote this markdown. See [`Trust`].
8936 ///
8937 /// Does nothing to a node that is not [`Rich`](Self::Rich), rather than
8938 /// refusing: the same rule `Screen::replace` follows for a region it cannot
8939 /// find, and for the same reason -- a miss is worth seeing and is not worth
8940 /// refusing to draw a screen over.
8941 #[must_use]
8942 pub fn trust(mut self, trust: Trust) -> Self {
8943 if let Self::Rich { trust: current, .. } = &mut self {
8944 *current = trust;
8945 }
8946 self
8947 }
8948
8949 /// How much of the format this markdown may use. See [`Richness`].
8950 ///
8951 /// [`trust`](Self::trust)'s note about a node that is not `Rich` applies
8952 /// here too.
8953 #[must_use]
8954 pub fn richness(mut self, richness: Richness) -> Self {
8955 if let Self::Rich {
8956 richness: current, ..
8957 } = &mut self
8958 {
8959 *current = richness;
8960 }
8961 self
8962 }
8963
8964 /// A control calling a route.
8965 pub fn act(label: impl Into<String>, action: Action) -> Self {
8966 Self::Act(Act::new(label, action))
8967 }
8968
8969 /// A persistent message, dismissed by fixing what caused it.
8970 pub fn banner(tone: layout::Tone, text: impl Into<String>) -> Self {
8971 Self::Notice {
8972 kind: layout::Notice::Banner,
8973 tone,
8974 text: text.into(),
8975 act: None,
8976 }
8977 }
8978
8979 /// A transient message that dismisses itself.
8980 pub fn toast(tone: layout::Tone, text: impl Into<String>) -> Self {
8981 Self::Notice {
8982 kind: layout::Notice::Toast,
8983 tone,
8984 text: text.into(),
8985 act: None,
8986 }
8987 }
8988
8989 /// The same notice, with one thing to do about it.
8990 ///
8991 /// [`offering`](Self::offering)'s shape, and a no-op on anything else for
8992 /// the same reason [`and_more`](Self::and_more) is one. Named apart from
8993 /// `offering` because a stand-in's act is a way *out* of an empty screen and
8994 /// a notice's is a way *back* from what just happened.
8995 #[must_use]
8996 pub fn about(mut self, offer: Act) -> Self {
8997 if let Self::Notice { act, .. } = &mut self {
8998 *act = Some(offer);
8999 }
9000 self
9001 }
9002
9003 /// A list of rows.
9004 ///
9005 /// A [`Table`](Self::Table) that declares no columns, which is what a list
9006 /// is since the 2026-09-06 collapse. The constructor stays because "a list
9007 /// of rows" is what the caller means and `Table { columns: vec![], .. }` is
9008 /// how the vocabulary spells it, not something every caller should have to
9009 /// spell.
9010 pub fn list(rows: impl IntoIterator<Item = Row>) -> Self {
9011 Self::Table {
9012 columns: Vec::new(),
9013 rows: rows.into_iter().collect(),
9014 more: None,
9015 }
9016 }
9017
9018 /// The same list, saying there is more of it.
9019 ///
9020 /// A no-op on anything that is not a [`Self::Table`], which is the one place
9021 /// this file allows that: the alternative is a constructor taking rows and a
9022 /// `Rest` together, and every call site that has no more rows then passes a
9023 /// `None` to say so.
9024 #[must_use]
9025 pub fn and_more(mut self, rest: Rest) -> Self {
9026 if let Self::Table { more, .. } = &mut self {
9027 *more = Some(rest);
9028 }
9029 self
9030 }
9031
9032 /// A proportion of a set, untoned and unlabelled.
9033 #[must_use]
9034 pub const fn meter(done: u32, total: u32) -> Self {
9035 Self::Meter(Meter::new(done, total))
9036 }
9037
9038 /// Nothing here yet.
9039 pub fn empty(message: impl Into<String>) -> Self {
9040 Self::StandIn {
9041 state: layout::Readiness::Empty,
9042 message: message.into(),
9043 act: None,
9044 }
9045 }
9046
9047 /// This did not load.
9048 pub fn failed(message: impl Into<String>) -> Self {
9049 Self::StandIn {
9050 state: layout::Readiness::Failed,
9051 message: message.into(),
9052 act: None,
9053 }
9054 }
9055
9056 /// This is on its way.
9057 ///
9058 /// The third of the three drawn states, and the one nothing built until
9059 /// [`Outcome::Started`](crate::Outcome::Started) needed it. A host that
9060 /// retains the description says a region is waiting on
9061 /// [`Slot::readiness`] and needs no node; a host that swaps markup has
9062 /// nowhere to put an attribute and needs one, and this is what it puts
9063 /// there.
9064 ///
9065 /// [`Slot::readiness`] stays the axis either way. This is the sentence
9066 /// beside it, not a second way of saying the same thing: a region can be
9067 /// [`Pending`](layout::Readiness::Pending) with no words at all, which is
9068 /// every deferred load on every screen.
9069 pub fn pending(message: impl Into<String>) -> Self {
9070 Self::StandIn {
9071 state: layout::Readiness::Pending,
9072 message: message.into(),
9073 act: None,
9074 }
9075 }
9076
9077 /// The same stand-in, with a way out of it.
9078 ///
9079 /// A no-op on anything else, for the reason [`Self::and_more`] is one.
9080 #[must_use]
9081 pub fn offering(mut self, way_out: Act) -> Self {
9082 if let Self::StandIn { act, .. } = &mut self {
9083 *act = Some(way_out);
9084 }
9085 self
9086 }
9087
9088 /// One control on its own, outside any form.
9089 pub fn field(field: Field) -> Self {
9090 Self::Field(Box::new(field))
9091 }
9092
9093 /// A strip of figures, none of which answers a click.
9094 pub fn stats(figures: impl IntoIterator<Item = Figure>) -> Self {
9095 Self::Stats {
9096 figures: figures.into_iter().map(|figure| (figure, None)).collect(),
9097 }
9098 }
9099
9100 /// One more figure, beside [`stats`](Self::stats).
9101 ///
9102 /// `stats` takes the whole list, and every other container in this
9103 /// vocabulary accretes: `Table::column`, `Row::cell` and `Field::options`
9104 /// were all added for that reason and this is the fourth. A caller building
9105 /// figures one at a time, or conditionally, has nowhere to hold the list.
9106 ///
9107 /// Does nothing to a node that is not [`Stats`](Self::Stats), on
9108 /// [`trust`](Self::trust)'s rule.
9109 #[must_use]
9110 pub fn figure(mut self, figure: Figure) -> Self {
9111 if let Self::Stats { figures } = &mut self {
9112 figures.push((figure, None));
9113 }
9114 self
9115 }
9116
9117 /// An axis with nothing on it yet.
9118 ///
9119 /// [`Timeline`](Self::Timeline) was the last variant with no constructor of
9120 /// its own, and its entries accrete for [`figure`](Self::figure)'s reason:
9121 /// a caller building them one at a time, or conditionally, has nowhere to
9122 /// hold the list. goingson's day view is the site.
9123 #[must_use]
9124 pub const fn timeline(track: layout::Track) -> Self {
9125 Self::Timeline {
9126 track,
9127 entries: Vec::new(),
9128 focus: None,
9129 }
9130 }
9131
9132 /// One more thing on the axis, beside [`timeline`](Self::timeline).
9133 ///
9134 /// Does nothing to a node that is not a [`Timeline`](Self::Timeline), on
9135 /// [`figure`](Self::figure)'s rule.
9136 #[must_use]
9137 pub fn placed(mut self, entry: Placed) -> Self {
9138 if let Self::Timeline { entries, .. } = &mut self {
9139 entries.push(entry);
9140 }
9141 self
9142 }
9143
9144 /// The moment the axis should bring into view, in minutes from its start.
9145 ///
9146 /// Does nothing to a node that is not a [`Timeline`](Self::Timeline), on
9147 /// [`figure`](Self::figure)'s rule.
9148 #[must_use]
9149 pub fn focus(mut self, at: u16) -> Self {
9150 if let Self::Timeline { focus, .. } = &mut self {
9151 *focus = Some(at);
9152 }
9153 self
9154 }
9155
9156 /// A tag standing on its own, rather than inside a row or a cell.
9157 ///
9158 /// [`Token`](Self::Token) was the last node variant with no constructor of
9159 /// its own once [`literal`](Self::literal) landed. A chip that is a control
9160 /// carries its action and its latched state on the tag.
9161 #[must_use]
9162 pub fn token(tag: Tag) -> Self {
9163 Self::Token(tag)
9164 }
9165
9166 /// Time counting up from an instant.
9167 #[must_use]
9168 pub const fn since(at: std::time::SystemTime) -> Self {
9169 Self::Since { at }
9170 }
9171
9172 /// Time counting down to an instant.
9173 #[must_use]
9174 pub const fn until(at: std::time::SystemTime) -> Self {
9175 Self::Until { at }
9176 }
9177
9178 /// How long ago an instant was.
9179 #[must_use]
9180 pub const fn age(at: std::time::SystemTime) -> Self {
9181 Self::Age { at }
9182 }
9183
9184 /// Which way this node runs against the current time, and from when.
9185 ///
9186 /// `None` for everything that is not a time-derived readout, which is most
9187 /// of the vocabulary. A renderer reads this to format one; a host reads
9188 /// [`Screen::clocks`] to decide how often to draw again.
9189 #[must_use]
9190 pub const fn clock(&self) -> Option<(Clock, std::time::SystemTime)> {
9191 match self {
9192 Self::Since { at } => Some((Clock::Since, *at)),
9193 Self::Until { at } => Some((Clock::Until, *at)),
9194 Self::Age { at } => Some((Clock::Age, *at)),
9195 _ => None,
9196 }
9197 }
9198
9199 /// Every kind of time-derived readout at or under this node, added to
9200 /// `found`.
9201 ///
9202 /// Walks into the containers rather than stopping at the top, because the
9203 /// measured case is a readout in a row: goingson puts an elapsed time on
9204 /// task rows, and a walk that only looked at the region's own blocks would
9205 /// answer that a screen full of running timers needs no clock.
9206 fn clocks_into(&self, found: &mut BTreeSet<Clock>) {
9207 if let Some((clock, _)) = self.clock() {
9208 found.insert(clock);
9209 return;
9210 }
9211 match self {
9212 Self::Timeline { entries, .. } => {
9213 for placed in entries {
9214 placed.row.clocks_into(found);
9215 }
9216 }
9217 Self::Table { rows, .. } => {
9218 for row in rows {
9219 row.clocks_into(found);
9220 }
9221 }
9222 Self::StandIn { .. } => {}
9223 Self::Region(slot) => {
9224 for ranked in slot.body.iter() {
9225 ranked.node.clocks_into(found);
9226 }
9227 }
9228 // Only the app's own nodes: a readout the creator wrote is text in
9229 // a string, and no script this renderer loads is going to tick it.
9230 Self::Canvas(canvas) => {
9231 for node in &canvas.within {
9232 node.clocks_into(found);
9233 }
9234 }
9235 // The leaves, and the containers that hold no node. A form holds
9236 // questions, a strip holds figures, a control holds choices, and
9237 // none of those is a place a readout can be.
9238 Self::Heading { .. }
9239 | Self::Text { .. }
9240 | Self::Rich { .. }
9241 | Self::Act(_)
9242 | Self::Link { .. }
9243 | Self::Figure(_)
9244 | Self::Image(_)
9245 | Self::Token(_)
9246 | Self::Notice { .. }
9247 | Self::Field(_)
9248 | Self::Form { .. }
9249 | Self::Meter(_)
9250 | Self::Code { .. }
9251 | Self::Stats { .. }
9252 | Self::Since { .. }
9253 | Self::Until { .. }
9254 | Self::Age { .. } => {}
9255 }
9256 }
9257 }
9258
9259 /// A whole screen.
9260 ///
9261 /// [`Arrangement`](layout::Arrangement) is `makeover-layout`'s, and there are
9262 /// two of them because our apps have two: goingson is list-detail, Balanced
9263 /// Breakfast is sidebar plus content. Naming a third before an app has one is
9264 /// how a description becomes a framework.
9265 #[derive(Debug, Clone, PartialEq, Eq)]
9266 pub struct Screen {
9267 /// What the screen is called. A window title, a tab title, a page heading.
9268 pub title: String,
9269 /// How the regions are laid out.
9270 pub arrangement: layout::Arrangement,
9271 /// The regions, in order.
9272 pub slots: Vec<Slot>,
9273 /// Messages raised by whatever produced this screen.
9274 ///
9275 /// Separate from the slots because a notice belongs to the screen rather
9276 /// than to a place in it: which region a toast stacks in is the renderer's
9277 /// question, and a handler answering it would be describing a webview.
9278 pub notices: Vec<Node>,
9279 /// How this screen is found, shared and indexed.
9280 ///
9281 /// Not an `Option`. The default is meaningful — a screen nobody said
9282 /// anything about is an indexable website — and an `Option` would make
9283 /// "nobody said" and "indexable" two spellings of one thing.
9284 pub discovery: Discovery,
9285 /// Which of the app's places this screen is, if it is one of them.
9286 ///
9287 /// A [`Chrome`](crate::Chrome) is built once and held beside the router,
9288 /// so a `current` flag on a [`Place`](crate::chrome::Place) would be
9289 /// frozen at build time and could never point at where the user is. The
9290 /// nav says what the places are; this says which one is showing, and the
9291 /// renderer marks the place whose [`key`](crate::chrome::Place::key)
9292 /// matches.
9293 ///
9294 /// The same move [`Row::current`] makes one level down, and for the same
9295 /// reason: it is the app's own pointer at what is showing, said by the
9296 /// thing that knows.
9297 ///
9298 /// `None` for a screen that is not a place in the nav. A confirmation
9299 /// drawn over one, a detail reached from a row, an app with no nav at all:
9300 /// nothing is marked, rather than the last place staying lit.
9301 ///
9302 /// A key no [`Place`](crate::chrome::Place) carries marks nothing. Not an
9303 /// error, because the nav is the app's and so is this, and a renderer is
9304 /// the wrong place to discover that an app disagrees with itself.
9305 pub place: Option<String>,
9306 /// The name of the set this screen's ticks go into, if it holds one.
9307 ///
9308 /// [`Row::selected`] said a row could be ticked and nothing said what the
9309 /// tick was *for*, so the tick had nowhere to go: a webview hid the hole
9310 /// because the browser owns a checkbox's checked state, and every app then
9311 /// wrote its own JS to gather the boxes back up. A terminal could not hide
9312 /// it. It drew the `[ ]`, bound the key, and the key did nothing, which is
9313 /// worse than not drawing the box.
9314 ///
9315 /// So the screen names the set, each [`Row::value`] is what that row's tick
9316 /// contributes, and [`Act::over`] is how a control says it acts on the
9317 /// whole of it. The renderer holds the set the way `quasi-tui` already
9318 /// holds an edit buffer and a scroll offset, and the commit control reads
9319 /// it by name.
9320 ///
9321 /// # Ticking never writes
9322 ///
9323 /// Wiki `explicit-commit-affordance`, the general rule: a change that
9324 /// happens with no obvious indication is confusing, so a tick stages and
9325 /// the commit control is what locks it in. [`Row::toggle`] describes the
9326 /// other thing — screens where the tick *is* the write — and is left alone
9327 /// here rather than removed, because stopping those screens is work in the
9328 /// apps that have them.
9329 ///
9330 /// # One set per screen
9331 ///
9332 /// A screen with two independent sets has not been measured. Naming one is
9333 /// the smallest thing that closes the hole, and the field grows to a map
9334 /// when an app turns up wanting two, on the same rule every other member
9335 /// here arrived under.
9336 ///
9337 /// [`Row::selected`]: Row::selected
9338 /// [`Row::value`]: Row::value
9339 /// [`Act::over`]: Act::over
9340 pub selection: Option<String>,
9341 /// Which of this screen's questions the caret starts in, by
9342 /// [`Field::name`].
9343 ///
9344 /// A statement about the screen rather than a flag on a field, and it is
9345 /// the same move [`place`](Self::place) and [`selection`](Self::selection)
9346 /// already make: the screen names the one thing that is true of it once,
9347 /// and a fact that cannot be said twice beats a field-local flag that can.
9348 /// Two fields each claiming the caret is a description arguing with itself,
9349 /// and there would be no honest way for a renderer to settle it.
9350 ///
9351 /// `None` for nearly every screen, which is every screen the reader arrives
9352 /// at to read. It is the sessionless form -- a login, a password reset --
9353 /// where the only thing to do is type, and the caret starting anywhere else
9354 /// is a keystroke the reader has to spend before they can begin.
9355 ///
9356 /// A name no [`Field`] on the screen carries marks nothing. Not an error,
9357 /// for [`place`](Self::place)'s reason: the screen is the app's and so is
9358 /// the name, and a renderer is the wrong place to discover that an app
9359 /// disagrees with itself.
9360 ///
9361 /// # This is the initial caret, not [`Choosing::Through`]'s refusal
9362 ///
9363 /// The two get conflated because both are about focus, and they are about
9364 /// different focus. [`Choosing::Through`] declines to carry the *live*
9365 /// focus -- where the app is pointing right now, as a range is dragged --
9366 /// on the grounds that a renderer naming it would be answering with the row
9367 /// it drew a frame ago while the app holds the moving one. This is the
9368 /// opposite fact: where the caret is before the reader has done anything,
9369 /// which nothing else knows and only the description can say.
9370 ///
9371 /// Nothing here moves the caret afterwards. A screen cannot pull focus back
9372 /// on a redraw, and a renderer that read this on every frame would take the
9373 /// caret away from wherever the reader had walked it to.
9374 ///
9375 /// # A renderer honours it once, on arrival
9376 ///
9377 /// `quasi-webview` emits `autofocus`, and only in a whole document: most of
9378 /// what a browser is answered with is a fragment, and moving the caret on a
9379 /// swap takes it out of whatever the reader was typing into. `quasi-tui`
9380 /// puts its caret on the matching stop when the screen arrives, and
9381 /// `quasi-immediate` asks egui for focus on the first frame of one.
9382 ///
9383 /// [`Field::name`]: Field::name
9384 /// [`Choosing::Through`]: Choosing::Through
9385 pub opens_at: Option<String>,
9386 /// How wide this screen's content runs.
9387 ///
9388 /// Measured in the MNW server, where 69 of 72 templates carry one of three
9389 /// mutually exclusive CSS classes for it and nothing described it, so the
9390 /// choice lived in the template rather than in the screen.
9391 ///
9392 /// Beside [`arrangement`](Self::arrangement) and answering the level above
9393 /// it: that one divides the screen's width between regions, this says how
9394 /// much of the window the screen takes in the first place. Both are the
9395 /// description's, which is what answering `e0fd485e` and `0eccff0d`
9396 /// together settled.
9397 ///
9398 /// Not an `Option`, for [`discovery`](Self::discovery)'s reason. The
9399 /// default is meaningful -- a screen nobody said anything about uses the
9400 /// window it was given -- and an `Option` would make "nobody said" and
9401 /// "the whole width" two spellings of one thing.
9402 pub measure: layout::Measure,
9403 /// What this screen is about the document it is drawn into.
9404 ///
9405 /// Not an `Option`, for [`discovery`](Self::discovery)'s reason: an empty
9406 /// [`Document`] is what nearly every screen says, and it is meaningful.
9407 pub document: Document,
9408 }
9409
9410 /// What a screen is about the document it is drawn into.
9411 ///
9412 /// Every
9413 /// [`Response`](crate::Response) variant changes what is inside `<body>`, and
9414 /// nothing changed anything outside it: a webview host builds its shell once
9415 /// and hands it to the renderer as an `Arc` before app state exists, so the
9416 /// head and the `<body>` tag were fixed for the process. A screen that needed
9417 /// its own could only be served by a route family of its own, which is how
9418 /// MNW's embeds ended up with a `document()` function outside the adapter.
9419 ///
9420 /// Chosen against `Response::Reload`, which was the other option. That is a
9421 /// host instruction rather than a description of a screen, and it is the line
9422 /// the vocabulary has held throughout; it also served only one of the two
9423 /// consumers.
9424 ///
9425 /// # What belongs here, and what does not
9426 ///
9427 /// The facts that are true of the whole document and that no region can carry.
9428 /// Both members are markup-shaped and that is deliberate: this is where a host
9429 /// keeps what its own taxonomy says, not where the vocabulary grows a word for
9430 /// it. A layout width is [`Screen::measure`] and belongs there; MNW's
9431 /// `admin-page` grouping and its 24 one-off screen-identity tokens are the
9432 /// app's own and belong here, which `16ba941e` settled by closing with no
9433 /// quasi crate gaining a field for them.
9434 ///
9435 /// # Every host but the webview ignores it
9436 ///
9437 /// A terminal has no document and neither does an egui frame, so both drop it
9438 /// whole -- the rule [`Binding::key`] already states for a key one host has
9439 /// never heard of. That is why this is here rather than in `makeover-layout`:
9440 /// it is not presentation, it is what the one host with a document is told
9441 /// about it.
9442 #[derive(Debug, Clone, Default, PartialEq, Eq)]
9443 pub struct Document {
9444 /// Classes for `<body>`, space-separated, beside whatever the host's own
9445 /// shell puts there.
9446 ///
9447 /// Beside and not instead: a host's shell carries the part that is true of
9448 /// every page and this carries the part that is true of this one, and a
9449 /// screen that replaced the global half would be a screen that had to know
9450 /// it.
9451 pub body_class: Option<String>,
9452 /// Attributes for the root element, by name.
9453 ///
9454 /// goingson's pinned theme is the measured consumer: once every theme ships
9455 /// in one sheet keyed by a root attribute, changing theme is setting an
9456 /// attribute rather than swapping a `<link>`, and a screen answering a
9457 /// preference change is the thing that knows the new value. Until this
9458 /// existed the honest answer was to reload the app.
9459 ///
9460 /// A name a renderer will not write is dropped rather than refused, for
9461 /// [`Screen::replace`]'s reason: a miss is worth being able to see and is
9462 /// not worth refusing to draw a screen over. What counts as writable is
9463 /// each renderer's, since it is the one that knows what its document can
9464 /// hold -- see [`writable_root_attr`].
9465 pub root: Vec<(String, String)>,
9466 /// A stylesheet this one document gets, on top of the host's own.
9467 ///
9468 /// For a document whose styling is data rather than build output: MNW's
9469 /// custom pages serve creator-authored CSS, sanitised and re-scoped per
9470 /// request, and a markup renderer's shell is built once before app state
9471 /// exists, so there was nowhere for it to go. The measured consumer is the
9472 /// whole of the reason this is here.
9473 ///
9474 /// # Opaque, and sanitised by whoever produced it
9475 ///
9476 /// This crate does not read the CSS, does not scope it, and does not
9477 /// sanitise it. It cannot: what counts as safe depends on the document's
9478 /// own headers, and MNW's answer is a `lightningcss` pass that rewrites
9479 /// every selector under a canvas id plus a `default-src 'none'` CSP. An
9480 /// app handing raw reader input here is handing raw reader input to a
9481 /// browser, and nothing below this line will save it.
9482 ///
9483 /// What a renderer does owe is that the string cannot end the element it is
9484 /// written into, which is a markup concern rather than a CSS one and is
9485 /// therefore the renderer's -- see `quasi-webview`'s `push_style`.
9486 ///
9487 /// # Last, so it wins
9488 ///
9489 /// A per-document sheet exists to override what every document shares, so a
9490 /// markup renderer writes it after the shell's own and outside the cascade
9491 /// layers the shell declares. Unlayered rules beat layered ones, which is
9492 /// what makes "on top of" true without this having to name a layer.
9493 ///
9494 /// # Every host but the webview ignores it
9495 ///
9496 /// A terminal has no stylesheet to add one to and draws the screen the way
9497 /// it draws every other, the same read [`Binding::key`] gets from a host
9498 /// that has never heard of the key.
9499 ///
9500 /// [`Binding::key`]: crate::chrome::Binding::key
9501 pub style: Option<String>,
9502 }
9503
9504 impl Document {
9505 /// Give this document a stylesheet of its own, on top of the host's.
9506 ///
9507 /// Replaces rather than adds, unlike [`rooted`](Self::rooted): a document
9508 /// has one sheet of its own, and two callers each setting one would be two
9509 /// answers to what this document looks like rather than two facts about it.
9510 #[must_use]
9511 pub fn styled(mut self, css: impl Into<String>) -> Self {
9512 self.style = Some(css.into());
9513 self
9514 }
9515
9516 /// Put these classes on `<body>`, beside the host shell's own.
9517 #[must_use]
9518 pub fn classed(mut self, class: impl Into<String>) -> Self {
9519 self.body_class = Some(class.into());
9520 self
9521 }
9522
9523 /// Set this attribute on the root element.
9524 ///
9525 /// Adds rather than replaces, for [`Field::consults`]' reason: a document
9526 /// saying two things about its root is saying two things, and a builder
9527 /// that took the last call would make the pair unwritable. A name given
9528 /// twice is a description arguing with itself, and the renderer takes the
9529 /// first.
9530 #[must_use]
9531 pub fn rooted(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
9532 self.root.push((name.into(), value.into()));
9533 self
9534 }
9535 }
9536
9537 /// Whether a renderer with a markup document will write this root attribute.
9538 ///
9539 /// A description names the attribute and a host writes it, so the name reaches
9540 /// markup as a name rather than as a value and cannot be escaped the way a
9541 /// value is. `data-theme` is the shape the measured consumer wants; `x
9542 /// onload=alert(1)` is the shape this refuses.
9543 ///
9544 /// ASCII letters, digits and `-`, starting with a letter. Deliberately narrower
9545 /// than what HTML permits: every attribute anybody has wanted here fits, and
9546 /// the cost of being wrong is a script tag rather than a missing class.
9547 ///
9548 /// Here rather than in the webview because the terminal and egui hosts answer
9549 /// the same question if they ever grow a document, and a second copy is a
9550 /// second answer.
9551 #[must_use]
9552 pub fn writable_root_attr(name: &str) -> bool {
9553 let mut characters = name.chars();
9554 characters.next().is_some_and(|c| c.is_ascii_alphabetic())
9555 && characters.all(|c| c.is_ascii_alphanumeric() || c == '-')
9556 }
9557
9558 /// How a screen is found, shared and indexed.
9559 ///
9560 /// Not presentation, which is why it is here and not in `makeover-layout`: a
9561 /// terminal ignores every field, the same way it ignores [`Slot::id`]. It is an
9562 /// address-and-identity fact, and that is the line that put [`Action`] in this
9563 /// crate rather than in the vocabulary.
9564 ///
9565 /// Measured before it was added. Every `og:*` value in the MNW server's 37
9566 /// templates is one of four things interpolated from the entity the screen is
9567 /// about: a title, a summary sentence, an image URL, or the screen's own
9568 /// address. None of them needed knowledge only a handler has, which is what
9569 /// made this the screen's to say rather than the host's.
9570 #[derive(Debug, Clone, PartialEq, Eq)]
9571 pub struct Discovery {
9572 /// Whether a crawler should index this screen.
9573 ///
9574 /// Defaults to indexable, because most screens are and a default that hides
9575 /// pages is a default that hides the bug. The six screens saying otherwise
9576 /// are purchased-content pages, and this field is why that is a fact the
9577 /// type carries rather than a line in a template that a conversion can drop
9578 /// in silence.
9579 pub indexable: bool,
9580 /// The sentence a link preview shows. [`Screen::title`] is the title.
9581 pub summary: Option<String>,
9582 /// The image a link preview shows, as an absolute URL.
9583 pub image: Option<String>,
9584 /// What kind of thing this screen is about.
9585 pub kind: SocialKind,
9586 /// The canonical address, when the screen answers at more than one.
9587 pub canonical: Option<String>,
9588 /// The syndication feed this screen offers, if it offers one.
9589 ///
9590 /// Singular, because no measured screen offers two: MNW's project blog, a
9591 /// user's page and a project page each publish exactly one. It widens to a
9592 /// `Vec` on the day a screen actually offers a second, which is the rule
9593 /// every other member here arrived under.
9594 ///
9595 /// Typed rather than a MIME string. `application/rss+xml` written out at
9596 /// each of the three sites is three chances to write `application/rss` and
9597 /// have a reader skip it, and the spelling is the same on every host, so it
9598 /// belongs to [`FeedKind::media_type`] rather than to whoever is describing
9599 /// the screen.
9600 ///
9601 /// # What a renderer with no autodiscovery does
9602 ///
9603 /// Ignores it, the way it already ignores [`image`](Self::image). A feed is
9604 /// a fact a browser acts on -- it is what `<link rel="alternate">` says --
9605 /// and a terminal has nothing to hand it to. Said here rather than left to
9606 /// each renderer's author to guess, because a guess is how two hosts end up
9607 /// disagreeing about what a description means.
9608 pub feed: Option<Feed>,
9609 }
9610
9611 /// A syndication feed a screen offers.
9612 ///
9613 /// Three members and no more: what kind of document it is, what it is called,
9614 /// and where it is. That is the whole of what a browser's autodiscovery reads,
9615 /// and anything else here would be describing the feed's contents rather than
9616 /// its existence.
9617 #[derive(Debug, Clone, PartialEq, Eq)]
9618 pub struct Feed {
9619 /// What kind of feed document it is.
9620 pub kind: FeedKind,
9621 /// What it is called, which is what a reader's subscribe list shows.
9622 ///
9623 /// Not [`Screen::title`], and not derived from it. A page titled "Max
9624 /// Johnson" offers a feed called "Max Johnson's posts", and a subscribe
9625 /// list holding a dozen entries called after their pages is a list nobody
9626 /// can read.
9627 pub title: String,
9628 /// Where the feed document is.
9629 pub href: String,
9630 }
9631
9632 impl Feed {
9633 /// A feed, by kind, name and address.
9634 pub fn new(kind: FeedKind, title: impl Into<String>, href: impl Into<String>) -> Self {
9635 Self {
9636 kind,
9637 title: title.into(),
9638 href: href.into(),
9639 }
9640 }
9641 }
9642
9643 /// What kind of syndication document a feed is.
9644 ///
9645 /// The three formats a browser and every reader understand. `#[non_exhaustive]`
9646 /// for [`SocialKind`]'s reason: a fourth arriving should not be a lockstep
9647 /// event across every renderer that spells one.
9648 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9649 #[non_exhaustive]
9650 pub enum FeedKind {
9651 /// RSS 2.0. The default, and what all three measured screens publish.
9652 #[default]
9653 Rss,
9654 /// Atom.
9655 Atom,
9656 /// JSON Feed.
9657 JsonFeed,
9658 }
9659
9660 impl FeedKind {
9661 /// What this is spelled as in a `type` attribute.
9662 ///
9663 /// Named here rather than agreed between each renderer and each host, for
9664 /// [`SocialKind::as_str`]'s reason: that is how one screen ends up
9665 /// `application/rss+xml` and the next `application/rss`.
9666 #[must_use]
9667 pub const fn media_type(self) -> &'static str {
9668 match self {
9669 Self::Rss => "application/rss+xml",
9670 Self::Atom => "application/atom+xml",
9671 Self::JsonFeed => "application/feed+json",
9672 }
9673 }
9674 }
9675
9676 impl Default for Discovery {
9677 /// Indexable, and nothing else claimed.
9678 ///
9679 /// Written out rather than derived, and the reason is the one field that
9680 /// matters: `bool::default()` is `false`, so a derived impl would deindex
9681 /// every screen that never mentioned the subject, silently, and the failure
9682 /// would show up as traffic rather than as a test.
9683 fn default() -> Self {
9684 Self {
9685 indexable: true,
9686 summary: None,
9687 image: None,
9688 kind: SocialKind::Website,
9689 canonical: None,
9690 feed: None,
9691 }
9692 }
9693 }
9694
9695 /// What kind of thing a screen is about.
9696 ///
9697 /// The six the server actually emits, and no more. Naming a seventh before a
9698 /// screen has one is how a description becomes a framework, which is the
9699 /// argument [`Arrangement`](layout::Arrangement) is held to two screens by.
9700 ///
9701 /// `#[non_exhaustive]`, because a seventh arriving should not be a lockstep
9702 /// event across every renderer that spells one. The match below stays
9703 /// exhaustive: within this crate the attribute does not apply, and a wildcard
9704 /// here would only hide a member added without a spelling.
9705 #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9706 #[non_exhaustive]
9707 pub enum SocialKind {
9708 /// A page. The default, and four of the server's screens.
9709 #[default]
9710 Website,
9711 /// Something written, with an author and a date.
9712 Article,
9713 /// A person or an account.
9714 Profile,
9715 /// Something for sale.
9716 Product,
9717 /// A video.
9718 Video,
9719 /// A piece of music.
9720 Song,
9721 }
9722
9723 impl SocialKind {
9724 /// What this is spelled as in `og:type`.
9725 ///
9726 /// Named here rather than agreed between each renderer and each host, which
9727 /// is how one screen ends up `video.other` and the next `video`.
9728 #[must_use]
9729 pub const fn as_str(self) -> &'static str {
9730 match self {
9731 Self::Website => "website",
9732 Self::Article => "article",
9733 Self::Profile => "profile",
9734 Self::Product => "product",
9735 Self::Video => "video.other",
9736 Self::Song => "music.song",
9737 }
9738 }
9739 }
9740
9741 impl Screen {
9742 /// An empty screen with the given arrangement.
9743 pub fn new(title: impl Into<String>, arrangement: layout::Arrangement) -> Self {
9744 Self {
9745 title: title.into(),
9746 arrangement,
9747 slots: Vec::new(),
9748 notices: Vec::new(),
9749 discovery: Discovery::default(),
9750 place: None,
9751 selection: None,
9752 opens_at: None,
9753 measure: layout::Measure::default(),
9754 document: Document::default(),
9755 }
9756 }
9757
9758 /// Say what this screen is about the document it is drawn into.
9759 ///
9760 /// See [`document`](Self::document). Replaces rather than adds, because a
9761 /// [`Document`] is one statement built with its own builders:
9762 /// `screen.documented(Document::default().classed("admin-page"))`.
9763 #[must_use]
9764 pub fn documented(mut self, document: Document) -> Self {
9765 self.document = document;
9766 self
9767 }
9768
9769 /// Say which of the app's places this screen is.
9770 ///
9771 /// See [`place`](Self::place). The key is a
9772 /// [`Place::key`](crate::chrome::Place::key), not a label and not an
9773 /// address.
9774 #[must_use]
9775 pub fn at_place(mut self, key: impl Into<String>) -> Self {
9776 self.place = Some(key.into());
9777 self
9778 }
9779
9780 /// How wide this screen's content runs, chaining.
9781 ///
9782 /// See [`measure`](Self::measure). [`Measure::Wide`](layout::Measure::Wide)
9783 /// is the default and does not need saying.
9784 #[must_use]
9785 pub const fn measured(mut self, measure: layout::Measure) -> Self {
9786 self.measure = measure;
9787 self
9788 }
9789
9790 /// This screen holds a set of ticks under this name, chaining.
9791 ///
9792 /// The rows that join it say so with [`Row::ticking`], and the control that
9793 /// acts on it with [`Act::over`]. See [`selection`](Self::selection).
9794 #[must_use]
9795 pub fn selecting(mut self, name: impl Into<String>) -> Self {
9796 self.selection = Some(name.into());
9797 self
9798 }
9799
9800 /// The caret starts in this question, chaining.
9801 ///
9802 /// The name is a [`Field::name`], which is what the value is submitted
9803 /// under, and not the label. See [`opens_at`](Self::opens_at).
9804 #[must_use]
9805 pub fn opening_at(mut self, name: impl Into<String>) -> Self {
9806 self.opens_at = Some(name.into());
9807 self
9808 }
9809
9810 /// Whether a crawler should index this screen, chaining.
9811 #[must_use]
9812 pub fn indexed(mut self, indexable: bool) -> Self {
9813 self.discovery.indexable = indexable;
9814 self
9815 }
9816
9817 /// The sentence a link preview shows, chaining.
9818 #[must_use]
9819 pub fn summarised(mut self, text: impl Into<String>) -> Self {
9820 self.discovery.summary = Some(text.into());
9821 self
9822 }
9823
9824 /// The image a link preview shows, chaining. An absolute URL.
9825 #[must_use]
9826 pub fn illustrated(mut self, url: impl Into<String>) -> Self {
9827 self.discovery.image = Some(url.into());
9828 self
9829 }
9830
9831 /// What kind of thing this screen is about, chaining.
9832 #[must_use]
9833 pub fn about(mut self, kind: SocialKind) -> Self {
9834 self.discovery.kind = kind;
9835 self
9836 }
9837
9838 /// The address this screen should be known by, chaining.
9839 #[must_use]
9840 pub fn canonical_at(mut self, url: impl Into<String>) -> Self {
9841 self.discovery.canonical = Some(url.into());
9842 self
9843 }
9844
9845 /// The feed this screen offers, chaining.
9846 ///
9847 /// See [`Discovery::feed`]. `screen.syndicating(Feed::new(FeedKind::Rss,
9848 /// "Project updates", "/p/thing/feed.xml"))`.
9849 #[must_use]
9850 pub fn syndicating(mut self, feed: Feed) -> Self {
9851 self.discovery.feed = Some(feed);
9852 self
9853 }
9854
9855 /// A list that chooses what the detail beside it shows.
9856 pub fn list_detail(title: impl Into<String>, tabbed: bool) -> Self {
9857 Self::new(title, layout::Arrangement::list_detail(tabbed))
9858 }
9859
9860 /// Navigation down the side, content filling the rest.
9861 pub fn sidebar_content(title: impl Into<String>) -> Self {
9862 Self::new(title, layout::Arrangement::sidebar_content())
9863 }
9864
9865 /// One region, filling the document.
9866 ///
9867 /// What a screen with nothing beside anything says.
9868 pub fn single(title: impl Into<String>) -> Self {
9869 Self::new(title, layout::Arrangement::Single)
9870 }
9871
9872 /// Add a region, chaining.
9873 #[must_use]
9874 pub fn with(mut self, slot: Slot) -> Self {
9875 self.slots.push(slot);
9876 self
9877 }
9878
9879 /// Raise a message on this screen, chaining.
9880 ///
9881 /// # Panics
9882 ///
9883 /// If the node is not a [`Node::Notice`]. The field is typed as a [`Node`]
9884 /// so a renderer walks one kind of thing, and this is the constructor that
9885 /// keeps that from meaning anything can go in it.
9886 #[must_use]
9887 pub fn saying(mut self, notice: Node) -> Self {
9888 assert!(
9889 matches!(notice, Node::Notice { .. }),
9890 "Screen::saying takes a Node::Notice"
9891 );
9892 self.notices.push(notice);
9893 self
9894 }
9895
9896 /// What this screen offers under a control name.
9897 ///
9898 /// The described half of a [`Reveal`], asked of the whole screen. See
9899 /// [`Slot::holds`], and [`Node::holds`] for what the walk reaches.
9900 #[must_use]
9901 pub fn holds(&self, name: &str) -> Option<&str> {
9902 self.slots.iter().find_map(|slot| slot.holds(name))
9903 }
9904
9905 /// The slot under this address, at any depth.
9906 #[must_use]
9907 pub fn slot(&self, id: &str) -> Option<&Slot> {
9908 self.slots.iter().find_map(|slot| slot.find(id))
9909 }
9910
9911 /// Whether any row on this screen is part of a live selection.
9912 ///
9913 /// [`Row::chosen`] is a fact on a row rather than on the screen --
9914 /// unlike [`selection`](Self::selection), which names the staged set -- so
9915 /// the answer is a walk. Every region, nested ones included, for
9916 /// [`slot`](Self::slot)'s reason: a table inside a pane inside the shell is
9917 /// where every real app puts its list.
9918 ///
9919 /// Walked rather than cached. A screen is built fresh per answer, and a
9920 /// cached fact about its rows is one more thing that can disagree with them.
9921 #[must_use]
9922 pub fn chooses(&self) -> bool {
9923 self.slots.iter().any(Slot::chooses)
9924 }
9925
9926 /// Whether this screen carries the thing an anchor names.
9927 ///
9928 /// One answer for all three renderers rather than three walks that can
9929 /// disagree, which is the same argument [`replace`](Self::replace) makes:
9930 /// deciding what a description means is this crate's, and a host writing
9931 /// it means every host picks its own answer.
9932 ///
9933 /// A renderer asks this to know whether it can draw an
9934 /// [`Outcome::Anchored`](crate::Outcome::Anchored) *at* something or has to
9935 /// fall back to drawing it over everything. `false` is a description bug --
9936 /// a route answered with an anchor naming what is not on the screen -- and
9937 /// every renderer degrades rather than refusing, the way a missing region
9938 /// does.
9939 #[must_use]
9940 pub fn anchors(&self, anchor: &crate::Anchor) -> bool {
9941 match anchor {
9942 crate::Anchor::Region(id) => self.slot(id).is_some(),
9943 // Set at all is the whole of it, matching how `Act::over` reads its
9944 // own name. See `Anchor::Selection`.
9945 //
9946 // Either kind of selection, since `1894e95d`: a screen naming a
9947 // staged set has one, and so does a screen whose rows carry a live
9948 // one. The second is what audiofiles' file list has, and reading
9949 // only the first is what made this answer `false` there while the
9950 // reader was looking at eleven chosen rows.
9951 crate::Anchor::Selection => self.selection.is_some() || self.chooses(),
9952 crate::Anchor::Control(id) => {
9953 self.notices.iter().any(|notice| notice.names(id))
9954 || self.slots.iter().any(|slot| slot.names(id))
9955 }
9956 }
9957 }
9958
9959 /// Every call this screen's regions are waiting on, in draw order.
9960 ///
9961 /// What a host performs after putting a screen up: each answer comes back as
9962 /// a [`Response::Fragment`] naming the region, and [`replace`](Self::replace)
9963 /// clears the feed as it lands, so asking again after applying one is
9964 /// answered with what is still outstanding rather than with the same list.
9965 ///
9966 /// Empty for every screen that has all of its content, which is nearly all
9967 /// of them.
9968 #[must_use]
9969 pub fn feeds(&self) -> Vec<&Action> {
9970 let mut out = Vec::new();
9971 for slot in &self.slots {
9972 slot.feeds_into(&mut out);
9973 }
9974 out
9975 }
9976
9977 /// Every region on this screen that asks a question of its own, at any
9978 /// depth, in draw order.
9979 ///
9980 /// What a renderer walks when a question moves: the regions holding that
9981 /// question are the ones whose [`Slot::consults`] the keystroke set off,
9982 /// and [`Slot::questions`] says which those are.
9983 ///
9984 /// Empty for nearly every screen, which is every screen written before
9985 /// [`Slot::consults`] existed, so the ordinary keystroke pays a walk and no
9986 /// more.
9987 #[must_use]
9988 pub fn consulting(&self) -> Vec<&Slot> {
9989 let mut out = Vec::new();
9990 for slot in &self.slots {
9991 slot.consulting_into(&mut out);
9992 }
9993 out
9994 }
9995
9996 /// Every call this screen's regions re-ask on a cadence, in draw order.
9997 ///
9998 /// [`feeds`](Self::feeds)'s counterpart, and the two never return the same
9999 /// call: a feed arrives once and a refresh never stops. What a host does
10000 /// with these is ask again on whatever interval its renderer picked, and
10001 /// keep doing it for as long as the screen is up.
10002 ///
10003 /// Empty for every screen with no [`Slot::live`] region, which is nearly
10004 /// all of them. A live region that names no call is not here either — there
10005 /// is nothing to ask — and the host re-reads it by redrawing.
10006 #[must_use]
10007 pub fn refreshes(&self) -> Vec<&Action> {
10008 let mut out = Vec::new();
10009 for slot in &self.slots {
10010 slot.refreshes_into(&mut out);
10011 }
10012 out
10013 }
10014
10015 /// Whether anything on this screen changes without the user.
10016 ///
10017 /// What a retained host asks to decide whether to keep drawing. True for a
10018 /// live region whether or not it names a call, which is the difference from
10019 /// [`refreshes`](Self::refreshes): the audiofiles sync panel reads state the
10020 /// host already holds, so its cadence is a repaint and there is no request
10021 /// to make.
10022 #[must_use]
10023 pub fn is_live(&self) -> bool {
10024 self.slots.iter().any(Slot::live_within)
10025 }
10026
10027 /// Every kind of time-derived readout on this screen, at any depth.
10028 ///
10029 /// What a renderer reads to pick a cadence: the description says a readout
10030 /// is derived from now and which way it runs, and how often to redraw
10031 /// follows the granularity the renderer chose to show it at. A screen of
10032 /// running stopwatches wants a second and a screen of last-modified stamps
10033 /// does not, and this is the difference said in the one place a host can
10034 /// act on it.
10035 ///
10036 /// A set rather than a count, because the cadence question is per kind:
10037 /// every [`Clock::Since`] on the screen redraws together, which is
10038 /// many-readouts-one-tick falling out of the vocabulary rather than being
10039 /// arranged by hand.
10040 ///
10041 /// Empty for nearly every screen, which is the cheap answer a host asks for
10042 /// on each draw. [`is_live`](Self::is_live) is the region-level fact beside
10043 /// it, and the two are independent: a still region can hold a stopwatch,
10044 /// and a live region usually holds none.
10045 #[must_use]
10046 pub fn clocks(&self) -> BTreeSet<Clock> {
10047 let mut found = BTreeSet::new();
10048 for node in self.notices.iter().chain(
10049 self.slots
10050 .iter()
10051 .flat_map(|slot| slot.body.iter().map(|ranked| &ranked.node)),
10052 ) {
10053 node.clocks_into(&mut found);
10054 }
10055 found
10056 }
10057
10058 /// Apply a fragment: put `node` in the region under `region`, replacing
10059 /// whatever was there. Returns whether the region was found.
10060 ///
10061 /// This is what a host holding a `Screen` does with
10062 /// [`Outcome::Fragment`](crate::Outcome::Fragment). A webview host needs
10063 /// none of it -- `quasi-http` turns the same outcome into an `hx-retarget`
10064 /// header and the browser performs the swap against a document it already
10065 /// has -- but a host that retains the description rather than the markup
10066 /// has nothing between the fragment and the tree.
10067 ///
10068 /// It lives here and not in a host because applying a fragment is surgery
10069 /// on this crate's own type. A host writing it means every retained-screen
10070 /// host writes it separately and each picks its own answer for the three
10071 /// decisions below, which is the thing this crate's no-host-imports rule
10072 /// exists to prevent.
10073 ///
10074 /// **A region that is not there answers `false`, not a panic.** The caller
10075 /// is the one that can act on it: a host can fall back to a redraw, and a
10076 /// test can assert it. What is worth avoiding is the silent no-op, because
10077 /// a miss means a route naming a slot that no longer exists, and that is a
10078 /// description bug rather than a rendering one.
10079 ///
10080 /// **It replaces rather than appends.** `Outcome::Fragment` is one region's
10081 /// new contents, which is the whole reason it can be smaller than a screen.
10082 ///
10083 /// **The region becomes [`Ready`](layout::Readiness::Ready).** A fragment
10084 /// arriving is the content arriving, so a slot marked
10085 /// [`Pending`](layout::Readiness::Pending) while it was in flight stops
10086 /// being pending here, and a [`Slot::fed_by`] naming the call that just
10087 /// answered is cleared with it. Emptiness is a different axis and rides on the node:
10088 /// a [`Node::StandIn`] carries its own state, and replacing with one is a
10089 /// region that is ready and has nothing to show.
10090 pub fn replace(&mut self, region: &str, node: Node) -> bool {
10091 let Some(slot) = self.slots.iter_mut().find_map(|slot| slot.find_mut(region)) else {
10092 return false;
10093 };
10094 slot.body.clear();
10095 slot.body.push(Ranked::new(node));
10096 slot.readiness = layout::Readiness::Ready;
10097 // The call that fed it has answered, so the region stops naming one. A
10098 // retained-screen host redraws from this tree, and a region still
10099 // pointing at its feed would ask again on the next paint.
10100 //
10101 // A live region keeps it. There the call is the cadence rather than an
10102 // arrival, so clearing it would make the region live exactly once and
10103 // then go still, and [`feeds`](Self::feeds) already refuses to return
10104 // it — the repeat is [`refreshes`](Self::refreshes)' and is paced.
10105 if !slot.live {
10106 slot.fed_by = None;
10107 }
10108 true
10109 }
10110
10111 /// Mark a region as waiting on work that has been handed off, saying so.
10112 ///
10113 /// [`replace`](Self::replace)'s opposite number, and what a host holding a
10114 /// `Screen` does with [`Outcome::Started`](crate::Outcome::Started). It
10115 /// lives here for the same reason: applying an outcome to a retained
10116 /// description is surgery on this crate's own type, and a host writing it
10117 /// means every retained-screen host writes it separately.
10118 ///
10119 /// **A region that is not there answers `false`.** Same contract, same
10120 /// reason: a route naming a slot that is gone is a description bug, and the
10121 /// caller is the party that can say so.
10122 ///
10123 /// **[`readiness`](Slot::readiness) becomes
10124 /// [`Pending`](layout::Readiness::Pending)**, which is what every renderer
10125 /// draws its wait from. The message goes in as a
10126 /// [`Node::pending`](Node::pending) stand-in, replacing what was there —
10127 /// so a host that draws the body under a pending region shows the sentence,
10128 /// and one that draws its own wait and returns early loses nothing it
10129 /// wanted.
10130 ///
10131 /// **[`fed_by`](Slot::fed_by) is left exactly as it is.** That call is how
10132 /// the finish gets reported, so clearing it here would be marking a region
10133 /// as waiting and removing the thing it waits on in the same breath. A
10134 /// region with no call keeps having none, and stays pending until something
10135 /// else tells it otherwise; see [`Outcome::Started`](crate::Outcome::Started)
10136 /// on why that is a description bug this cannot catch.
10137 pub fn started(&mut self, region: &str, message: impl Into<String>) -> bool {
10138 let Some(slot) = self.slots.iter_mut().find_map(|slot| slot.find_mut(region)) else {
10139 return false;
10140 };
10141 slot.body.clear();
10142 slot.body.push(Ranked::new(Node::pending(message)));
10143 slot.readiness = layout::Readiness::Pending;
10144 true
10145 }
10146 }
10147
10148 #[cfg(test)]
10149 mod tests {
10150 use super::{Cell, Column, Node, Row, Table};
10151
10152 /// The whole reason a cell names its column.
10153 ///
10154 /// `git_repos` builds a visibility cell only for an owner, by pushing onto
10155 /// a `Vec` whose length has to agree with a column list built under a
10156 /// second, separate conditional. Named, the two cannot disagree: the row
10157 /// answers the columns the table has.
10158 #[test]
10159 fn a_conditional_cell_does_not_shift_the_columns_after_it() {
10160 let columns = || {
10161 [
10162 Column::new("Name"),
10163 Column::new("Visibility"),
10164 Column::new("Description"),
10165 ]
10166 };
10167 let row = |is_owner: bool| {
10168 let mut cells = Row::default()
10169 .at("Name", Cell::new("quasi"))
10170 .at("Description", Cell::new("the app stack"));
10171 if is_owner {
10172 cells = cells.at("Visibility", Cell::new("public"));
10173 }
10174 cells
10175 };
10176
10177 let owner = Table::new(columns()).row(row(true));
10178 let stranger = Table::new(columns()).row(row(false));
10179
10180 let read = |table: &Table| {
10181 table.rows[0]
10182 .cells
10183 .iter()
10184 .map(Cell::text)
10185 .collect::<Vec<_>>()
10186 };
10187
10188 assert_eq!(read(&owner), ["quasi", "public", "the app stack"]);
10189 // The description stays in its own column rather than sliding left.
10190 assert_eq!(read(&stranger), ["quasi", "", "the app stack"]);
10191 }
10192
10193 /// A column nothing named is empty rather than absent.
10194 #[test]
10195 fn a_column_nothing_named_is_empty() {
10196 let table = Table::new([Column::new("Name"), Column::new("Size")])
10197 .row(Row::default().at("Name", Cell::new("kick.wav")));
10198
10199 assert_eq!(table.rows[0].cells.len(), 2);
10200 assert_eq!(table.rows[0].cells[0].text(), "kick.wav");
10201 assert_eq!(table.rows[0].cells[1].text(), "");
10202 }
10203
10204 /// Naming a column the table does not have is a bug, and it is loud.
10205 ///
10206 /// The one failure mode naming introduces: the cell is dropped, so a typo
10207 /// is an empty column rather than a compile error. Nothing above this can
10208 /// catch it, because the row and the column list are written apart, so the
10209 /// check lives where both are in hand.
10210 #[test]
10211 #[should_panic(expected = "a cell named a column this table does not have")]
10212 fn naming_a_column_the_table_does_not_have_is_caught() {
10213 let _ =
10214 Table::new([Column::new("Name")]).row(Row::default().at("Nmae", Cell::new("kick.wav")));
10215 }
10216
10217 /// Two columns with one name cannot be addressed apart.
10218 #[test]
10219 #[should_panic(expected = "two columns share a name")]
10220 fn two_columns_sharing_a_name_is_caught() {
10221 let _ = Table::new([Column::new("Actions"), Column::new("Actions")])
10222 .row(Row::default().at("Actions", Cell::new("edit")));
10223 }
10224
10225 /// A row built by position still works and is untouched by resolution.
10226 #[test]
10227 fn a_positional_row_is_left_alone() {
10228 let table = Table::new([Column::new("Name"), Column::new("Size")])
10229 .row(Row::cells(["kick.wav", "2.1 MB"]));
10230
10231 assert_eq!(table.rows[0].cells.len(), 2);
10232 assert_eq!(table.rows[0].cells[1].text(), "2.1 MB");
10233 }
10234
10235 /// The table is the node, so nothing reaches for the variant by hand.
10236 #[test]
10237 fn a_table_becomes_its_node() {
10238 let node = Node::from(Table::new([Column::new("Name")]).row(Row::cells(["kick.wav"])));
10239
10240 match node {
10241 Node::Table { columns, rows, .. } => {
10242 assert_eq!(columns.len(), 1);
10243 assert_eq!(rows.len(), 1);
10244 }
10245 other => panic!("expected a table, got {other:?}"),
10246 }
10247 }
10248
10249 use super::*;
10250
10251 fn frames(count: usize) -> Vec<Node> {
10252 (0..count)
10253 .map(|n| Node::Image(Image::new(format!("/frame-{n}.png"), format!("frame {n}"))))
10254 .collect()
10255 }
10256
10257 /// `drums` shut, `drums.kick` and `drums.snare` under it, `genre` after.
10258 fn outline() -> Vec<Row> {
10259 vec![
10260 Row::new("drums").disclosing(false),
10261 Row::new("drums.kick").depth(layout::Nesting::at(1)),
10262 Row::new("drums.snare").depth(layout::Nesting::at(1)),
10263 Row::new("genre").disclosing(true),
10264 Row::new("genre.house").depth(layout::Nesting::at(1)),
10265 ]
10266 }
10267
10268 #[test]
10269 fn a_row_is_flat_and_a_leaf_until_it_says_otherwise() {
10270 // Every row described before these members existed, unchanged.
10271 let row = Row::new("kick.wav");
10272 assert_eq!(row.depth, layout::Nesting::top());
10273 assert_eq!(row.open, None);
10274 assert_eq!(Row::cells(["kick.wav"]).depth, layout::Nesting::top());
10275 assert_eq!(Row::cells(["kick.wav"]).open, None);
10276 }
10277
10278 #[test]
10279 fn a_shut_branch_folds_what_is_under_it_and_nothing_else() {
10280 assert_eq!(
10281 folded(&outline()),
10282 [false, true, true, false, false],
10283 "the two under the shut `drums` go, the open `genre`'s child stays"
10284 );
10285 }
10286
10287 #[test]
10288 fn a_shut_branch_takes_the_open_branches_inside_it() {
10289 let rows = vec![
10290 Row::new("drums").disclosing(false),
10291 Row::new("drums.kick")
10292 .depth(layout::Nesting::at(1))
10293 .disclosing(true),
10294 Row::new("drums.kick.hard").depth(layout::Nesting::at(2)),
10295 Row::new("genre"),
10296 ];
10297 assert_eq!(folded(&rows), [false, true, true, false]);
10298 }
10299
10300 #[test]
10301 fn a_list_with_no_disclosure_folds_nothing() {
10302 // The graceful-degradation case, from the other side: depth alone is an
10303 // indent and never hides a row.
10304 let rows: Vec<Row> = (0..4u8)
10305 .map(|n| Row::new("t").depth(layout::Nesting::at(n)))
10306 .collect();
10307 assert_eq!(folded(&rows), [false; 4]);
10308 }
10309
10310 #[test]
10311 fn a_branch_ends_at_the_next_row_no_deeper_than_it() {
10312 // Including a sibling at its own depth, which is the boundary case a
10313 // greater-than would get wrong.
10314 let rows = vec![
10315 Row::new("a")
10316 .depth(layout::Nesting::at(1))
10317 .disclosing(false),
10318 Row::new("a.one").depth(layout::Nesting::at(2)),
10319 Row::new("b").depth(layout::Nesting::at(1)),
10320 Row::new("root").depth(layout::Nesting::at(0)),
10321 ];
10322 assert_eq!(folded(&rows), [false, true, false, false]);
10323 }
10324
10325 #[test]
10326 fn a_table_row_says_the_hierarchy_the_same_way_a_list_row_does() {
10327 let rows = vec![
10328 Row::cells(["drums"]).disclosing(false),
10329 Row::cells(["drums.kick"]).depth(layout::Nesting::at(1)),
10330 Row::cells(["genre"]),
10331 ];
10332 assert_eq!(folded(&rows), [false, true, false]);
10333 }
10334
10335 #[test]
10336 fn a_navigating_action_says_so_and_changes_nothing_else() {
10337 // `00ee7af5`. The mark is a member beside the others rather than a kind
10338 // of destination, so where the call goes is untouched by it.
10339 let action = Action::get("/p/slow-reader")
10340 .carrying("from", "discover")
10341 .navigating();
10342 assert!(action.navigates);
10343 assert_eq!(action.route(), Some("/p/slow-reader"));
10344 assert_eq!(action.method, Method::Get);
10345 assert_eq!(action.carried.get("from"), Some("discover"));
10346 assert!(action.replaces.is_none());
10347 assert!(!action.elsewhere);
10348 }
10349
10350 #[test]
10351 fn an_action_does_not_navigate_unless_it_says_so() {
10352 // Off by default in every constructor, so nothing described before the
10353 // member existed says anything new.
10354 assert!(!Action::get("/p/slow-reader").navigates);
10355 assert!(!Action::post("/p/slow-reader").navigates);
10356 assert!(!Action::local().navigates);
10357 assert!(!Action::external("https://example.com").navigates);
10358 assert!(!Action::default().navigates);
10359 }
10360
10361 #[test]
10362 fn an_interval_states_both_names_and_holds_both_values() {
10363 let f = Field::interval("bpm_min", "bpm_max", "BPM")
10364 .value("90")
10365 .upper_value("130");
10366 assert_eq!(f.kind, layout::FieldKind::Interval);
10367 assert_eq!(f.name, "bpm_min");
10368 assert_eq!(f.upper_name.as_deref(), Some("bpm_max"));
10369 assert_eq!(f.value.as_deref(), Some("90"));
10370 assert_eq!(f.upper_value.as_deref(), Some("130"));
10371 // The extent stays optional, unlike a range's: an interval's bounds are
10372 // a rule on each end rather than the control.
10373 assert_eq!(f.min, None);
10374 assert_eq!(f.max, None);
10375 }
10376
10377 #[test]
10378 fn a_refused_interval_is_re_offered_under_both_names() {
10379 // The half option (b) would not have paid. Without a second value a
10380 // refusal hands back the low end and silently drops the high one, so
10381 // the user retypes half of what they already answered.
10382 let mut params = crate::Params::new();
10383 params.insert("bpm_min".to_owned(), "90".to_owned());
10384 params.insert("bpm_max".to_owned(), "130".to_owned());
10385 let f = Field::interval("bpm_min", "bpm_max", "BPM").refilled(&params);
10386 assert_eq!(f.value.as_deref(), Some("90"));
10387 assert_eq!(f.upper_value.as_deref(), Some("130"));
10388 }
10389
10390 #[test]
10391 fn an_open_end_comes_back_open() {
10392 // "Over 120 BPM" is an answer rather than a half-filled form, so an
10393 // absent end stays absent instead of being filled with a bound.
10394 let mut params = crate::Params::new();
10395 params.insert("bpm_min".to_owned(), "120".to_owned());
10396 let f = Field::interval("bpm_min", "bpm_max", "BPM").refilled(&params);
10397 assert_eq!(f.value.as_deref(), Some("120"));
10398 assert_eq!(f.upper_value, None);
10399 }
10400
10401 #[test]
10402 fn a_field_with_one_name_reads_only_that_one() {
10403 // Every other kind is untouched: `refilled` returns after the lower
10404 // half when there is no second name to read.
10405 let mut params = crate::Params::new();
10406 params.insert("title".to_owned(), "Kick".to_owned());
10407 let f = Field::new(layout::FieldKind::Text, "title", "Title").refilled(&params);
10408 assert_eq!(f.value.as_deref(), Some("Kick"));
10409 assert_eq!(f.upper_value, None);
10410 }
10411
10412 #[test]
10413 fn a_consult_with_no_floor_asks_about_anything_including_nothing() {
10414 let consult = Consult::new(Action::get("/api/validate/username"));
10415 assert_eq!(consult.at_least, 0);
10416 assert!(consult.asks_about(""));
10417 assert!(consult.asks_about("m"));
10418 }
10419
10420 /// The pages a strip offers are the description's, and so is each one's
10421 /// address: a renderer cannot build page 5's out of prev and next without
10422 /// knowing the address grammar.
10423 #[test]
10424 fn a_pager_offers_the_pages_the_host_windowed_and_marks_the_one_being_read() {
10425 let rest = Rest::page(100, 50)
10426 .of(400)
10427 .back(Action::get("/feed?page=2"))
10428 .forward(Action::get("/feed?page=4"))
10429 .jumping(2, Action::get("/feed?page=2"))
10430 .jumping(3, Action::get("/feed?page=3"))
10431 .jumping(4, Action::get("/feed?page=4"));
10432
10433 // Five pages out of eight, which is the window MNW already computes.
10434 // Nothing here windows anything: a renderer choosing its own would give
10435 // a different answer per host for one list.
10436 assert_eq!(rest.jumps.len(), 3);
10437 assert_eq!(rest.as_layout().page(), Some(3));
10438 assert_eq!(rest.as_layout().pages_total(), Some(8));
10439
10440 // The page is carried rather than implied by position, because a window
10441 // around the reader does not start at one.
10442 assert!(!rest.is_here(&rest.jumps[0]));
10443 assert!(rest.is_here(&rest.jumps[1]));
10444 assert!(!rest.is_here(&rest.jumps[2]));
10445 }
10446
10447 /// Empty jumps is prev/next paging, which is every site that existed before
10448 /// the member did.
10449 #[test]
10450 fn a_pager_offers_no_pages_until_it_is_given_some() {
10451 assert!(Rest::page(0, 50).of(400).jumps.is_empty());
10452 assert!(Rest::more(50, Action::get("/more")).jumps.is_empty());
10453 }
10454
10455 /// A screen may say what is true of the document it is drawn into, and
10456 /// every host without one drops it whole.
10457 #[test]
10458 fn a_screen_says_nothing_about_its_document_until_it_does() {
10459 let plain = Screen::new("A", layout::Arrangement::Single);
10460 assert_eq!(plain.document, Document::default());
10461 assert!(plain.document.body_class.is_none());
10462 assert!(plain.document.root.is_empty());
10463
10464 let said = plain.documented(
10465 Document::default()
10466 .classed("admin-page")
10467 .rooted("data-theme", "slate")
10468 .rooted("dir", "rtl"),
10469 );
10470 assert_eq!(said.document.body_class.as_deref(), Some("admin-page"));
10471 // Adds rather than replaces: a document saying two things about its
10472 // root is saying two things.
10473 assert_eq!(said.document.root.len(), 2);
10474 }
10475
10476 /// The name reaches markup as a name rather than as a value, so a gate is
10477 /// what protects it and escaping is not.
10478 #[test]
10479 fn a_root_attribute_name_is_letters_digits_and_dashes_from_a_letter() {
10480 assert!(writable_root_attr("data-theme"));
10481 assert!(writable_root_attr("dir"));
10482 assert!(writable_root_attr("x1-2"));
10483
10484 assert!(!writable_root_attr(""));
10485 assert!(!writable_root_attr("1data"));
10486 assert!(!writable_root_attr("-theme"));
10487 // The shapes that are the reason for the gate.
10488 assert!(!writable_root_attr("x\" onload=alert(1) y"));
10489 assert!(!writable_root_attr("data theme"));
10490 assert!(!writable_root_attr("data_theme"));
10491 }
10492
10493 /// A control chosen in one gesture has nothing to wait out, and the wait
10494 /// stays the description's rather than becoming something a kind implies.
10495 #[test]
10496 fn a_question_about_a_value_chosen_in_one_gesture_waits_for_nothing() {
10497 let at_once = Consult::at_once(Action::get("/mail/list"));
10498 assert_eq!(at_once.after, std::time::Duration::ZERO);
10499 // Everything else is what `new` gives it: a floor of nothing, and the
10500 // value travelling under the field's own name.
10501 assert_eq!(at_once.at_least, 0);
10502 assert!(at_once.sends.is_empty());
10503 assert_eq!(
10504 Consult::new(Action::get("/mail/list")).after,
10505 Consult::SETTLES
10506 );
10507 }
10508
10509 /// The question a field owns and the questions it merely asks are separate
10510 /// members, because MNW's discover box has both: a list of its own, and a
10511 /// results route that lands in a region.
10512 #[test]
10513 fn a_field_owns_one_list_and_may_still_ask_other_questions() {
10514 let field = Field::new(layout::FieldKind::Text, "q", "Search")
10515 .suggesting(Consult::new(Action::get("/discover/suggestions")).at_least(2))
10516 .consulting(Consult::new(Action::get("/discover/results")).sending(["mode"]));
10517
10518 let owned = field.suggests.as_ref().expect("a list of its own");
10519 assert_eq!(owned.action.destination.as_str(), "/discover/suggestions");
10520 assert_eq!(owned.at_least, 2);
10521 assert_eq!(field.consults.len(), 1);
10522
10523 // One list, so a second call is a description changing its mind rather
10524 // than asking twice. `consults` adds, for the opposite reason.
10525 let field = field.suggests(Action::get("/other"));
10526 assert_eq!(
10527 field
10528 .suggests
10529 .expect("the later one")
10530 .action
10531 .destination
10532 .as_str(),
10533 "/other"
10534 );
10535 }
10536
10537 /// A list row and a table row say the same facts the same way.
10538 ///
10539 /// A table row gained `current` and the plural `menu` on 2026-09-02 and a
10540 /// list row did not, although both carried the same fact under the same
10541 /// name. So a screen holding both had to write one in the chain and the
10542 /// other by assignment. The 2026-09-05 collapse ended the drift by ending
10543 /// the second type; this test is what holds the parity it left behind.
10544 ///
10545 #[test]
10546 fn a_list_row_and_a_table_row_say_row_ness_alike() {
10547 let acts = || {
10548 [
10549 Act::new("Rename", Action::post("/rename")),
10550 Act::new("Delete", Action::post("/delete")),
10551 ]
10552 };
10553
10554 let row = Row::new("kick.wav").current(true).menu(acts());
10555 let cells = Row::cells(["kick.wav"]).current(true).menu(acts());
10556
10557 assert!(row.current);
10558 assert!(cells.current);
10559 assert_eq!(row.menu.len(), 2);
10560 assert_eq!(cells.menu.len(), 2);
10561
10562 // The plural extends rather than replacing, so it composes with the
10563 // singular.
10564 let row = row.offers(Act::new("Reveal", Action::local()));
10565 assert_eq!(row.menu.len(), 3);
10566
10567 // And neither is set until it is said.
10568 let plain = Row::new("kick.wav");
10569 assert!(!plain.current);
10570 assert!(plain.menu.is_empty());
10571 }
10572
10573 /// Every fact a field carries is reachable without leaving the chain.
10574 ///
10575 /// Five members had no builder and were set by assigning the public field:
10576 /// `placeholder`, `max_length`, `min`, `max` and `extended`. That is
10577 /// the missing-builder defect on the vocabulary's largest struct, and the
10578 /// measured cost was 66 sites across the three shape trees breaking out of
10579 /// a builder chain to write one of them. It also made those five unsayable
10580 /// in a declared description, which resolves an attribute to a builder's
10581 /// name and so cannot reach a member that has none.
10582 ///
10583 #[test]
10584 fn every_fact_a_field_carries_is_reachable_from_the_chain() {
10585 let field = Field::new(layout::FieldKind::Text, "promo", "Promo code")
10586 .placeholder("e.g. TRIAL14")
10587 .limited_to(8)
10588 .extended();
10589
10590 assert_eq!(field.placeholder.as_deref(), Some("e.g. TRIAL14"));
10591 assert_eq!(field.max_length, Some(8));
10592 assert!(field.extended);
10593 // A placeholder is not a hint: one disappears when the reader types and
10594 // the other does not, so writing one must not write the other.
10595 assert!(field.hint.is_none());
10596
10597 // The extent, both ends at once, which is the ordinary spelling.
10598 let dial = Field::new(layout::FieldKind::Number, "price", "Price").within("0", "9999");
10599 assert_eq!(dial.min.as_deref(), Some("0"));
10600 assert_eq!(dial.max.as_deref(), Some("9999"));
10601
10602 // And one end alone, because an open end is a real answer rather than a
10603 // missing one. `Field::interval`'s own doc says so.
10604 let floor = Field::new(layout::FieldKind::Number, "n", "How many").at_least("1");
10605 assert_eq!(floor.min.as_deref(), Some("1"));
10606 assert!(floor.max.is_none());
10607 let ceiling = Field::new(layout::FieldKind::Number, "n", "How many").at_most("10");
10608 assert!(ceiling.min.is_none());
10609 assert_eq!(ceiling.max.as_deref(), Some("10"));
10610
10611 // Nothing is set until it is said, so a field written the short way
10612 // still measures exactly as it did before these existed.
10613 let plain = Field::new(layout::FieldKind::Text, "title", "Title");
10614 assert!(plain.placeholder.is_none());
10615 assert!(plain.max_length.is_none());
10616 assert!(plain.min.is_none());
10617 assert!(plain.max.is_none());
10618 assert!(!plain.extended);
10619 }
10620
10621 /// A field asks nothing about its own value until it says so, and owns no
10622 /// list until it says so either.
10623 #[test]
10624 fn a_field_owns_no_list_until_it_says_it_does() {
10625 let field = Field::new(layout::FieldKind::Text, "title", "Title");
10626 assert!(field.suggests.is_none());
10627 assert!(field.consults.is_empty());
10628 }
10629
10630 #[test]
10631 fn a_control_asks_for_nothing_until_it_says_it_does() {
10632 let act = Act::new("Delete", Action::post("/items/delete"));
10633 assert!(act.asks.is_empty());
10634 }
10635
10636 #[test]
10637 fn a_control_deposits_nothing_until_it_names_a_field_and_a_value() {
10638 // The member has to be absent by default or every renderer starts
10639 // writing into a box on a press that never did before.
10640 let act = Act::new("Delete", Action::post("/items/delete"));
10641 assert!(act.fills.is_none());
10642 }
10643
10644 #[test]
10645 fn a_picker_card_names_the_box_it_writes_to_and_what_lands_there() {
10646 // MNW's media picker, measured 2026-08-19: the card reads as a file
10647 // name and deposits a markdown reference, and the two are different
10648 // strings, which is why a destination on its own is not enough.
10649 let act = Act::new("kick.png", Action::local()).filling("body", "![](media/kick.png)");
10650
10651 let fill = act.fills.as_ref().expect("a destination");
10652 assert_eq!(fill.field, "body");
10653 assert_eq!(fill.value, "![](media/kick.png)");
10654 // Nothing about a caret, in either direction. `d52884b0` stands.
10655 assert_eq!(act.label, "kick.png");
10656 }
10657
10658 #[test]
10659 fn a_deposit_replaces_rather_than_accumulating() {
10660 // Unlike `asking`. One press deposits one value, and a control writing
10661 // into two boxes is a description doing two things at once.
10662 let act = Act::new("Insert", Action::local())
10663 .filling("body", "first")
10664 .filling("body", "second");
10665 assert_eq!(act.fills.expect("a destination").value, "second");
10666 }
10667
10668 #[test]
10669 fn a_deposit_does_not_cross_into_the_description_layer() {
10670 // Where a value lands is quasi's, the same as an address and a
10671 // confirmation. `layout::Act` claims to be what a renderer needs to
10672 // *draw* the control and nothing more.
10673 let act = Act::new("Insert", Action::local()).filling("body", "![](x.png)");
10674 let drawn = act.as_layout();
10675 assert_eq!(drawn.label, "Insert");
10676 assert_eq!(drawn.tone, layout::Tone::Neutral);
10677 }
10678
10679 #[test]
10680 fn a_verb_that_needs_a_value_carries_the_question_and_the_set() {
10681 // MNW's bulk bar, measured 2026-08-18: "Set Price" and "Add Tag" each
10682 // reveal one box and apply it to whatever is ticked. Both halves are on
10683 // the control, so a renderer never has to pair a form with a verb by
10684 // where they sit on the screen.
10685 let act = Act::new("Set Price", Action::post("/items/price"))
10686 .over("chosen")
10687 .asking(
10688 Field::new(layout::FieldKind::Number, "price", "New price ($)")
10689 .hint("Enter 0 to make items free."),
10690 );
10691
10692 assert_eq!(act.over.as_deref(), Some("chosen"));
10693 assert_eq!(act.asks.len(), 1);
10694 assert_eq!(act.asks[0].name, "price");
10695 }
10696
10697 #[test]
10698 fn a_box_a_verb_asked_for_writes_nothing_of_its_own() {
10699 // The value is answered by the press. A write of the box's own would
10700 // send it twice.
10701 let asked = Field::new(layout::FieldKind::Text, "tag", "Tag slug")
10702 .writes(Action::post("/items/tag"))
10703 .consulting(Consult::new(Action::get("/tags/known")))
10704 .as_asked();
10705
10706 assert!(asked.writes.is_none());
10707 // A question about the value survives: asking whether a slug is taken
10708 // is not a write of it.
10709 assert_eq!(asked.consults.len(), 1);
10710 }
10711
10712 #[test]
10713 fn a_field_asks_nothing_until_it_says_it_does() {
10714 let field = Field::new(layout::FieldKind::Text, "q", "Search");
10715 assert!(field.consults.is_empty());
10716 }
10717
10718 #[test]
10719 fn a_box_can_ask_two_routes_at_two_rates() {
10720 // MNW's discover search, measured 2026-08-18: `#search-input` re-reads
10721 // the results after 150ms and a hand-written `fetch` asks for
10722 // suggestions on its own schedule. Two questions about one value, and
10723 // only one of them was sayable.
10724 let field = Field::new(layout::FieldKind::Text, "q", "Search")
10725 .consulting(
10726 Consult::new(Action::get("/discover/suggestions"))
10727 .after(std::time::Duration::from_millis(200))
10728 .at_least(2),
10729 )
10730 .consulting(
10731 Consult::new(Action::get("/discover/results"))
10732 .after(std::time::Duration::from_millis(150))
10733 .at_least(2)
10734 .sending(["mode", "sort", "tags"]),
10735 );
10736
10737 assert_eq!(field.consults.len(), 2);
10738 assert_eq!(
10739 field.consults[0].after,
10740 std::time::Duration::from_millis(200)
10741 );
10742 assert!(field.consults[0].sends.is_empty());
10743 assert_eq!(field.consults[1].sends, ["mode", "sort", "tags"]);
10744 }
10745
10746 #[test]
10747 fn consulting_adds_rather_than_replaces() {
10748 // The builder reading that makes two questions writable at all. A
10749 // builder taking the last call would make the shape unsayable rather
10750 // than merely awkward.
10751 let field = Field::new(layout::FieldKind::Text, "q", "Search")
10752 .consults(Action::get("/one"))
10753 .consults(Action::get("/two"));
10754
10755 assert_eq!(field.consults.len(), 2);
10756 }
10757
10758 #[test]
10759 fn a_question_carries_nothing_beside_its_own_value_by_default() {
10760 // The common field, and the reason `sends` is a list rather than a
10761 // required argument: a validate route asks about the box and nothing
10762 // else.
10763 let consult = Consult::new(Action::get("/api/validate/username"));
10764 assert!(consult.sends.is_empty());
10765 }
10766
10767 #[test]
10768 fn sending_replaces_the_set_rather_than_growing_it() {
10769 let consult = Consult::new(Action::get("/discover/results"))
10770 .sending(["mode"])
10771 .sending(["sort", "tags"]);
10772 assert_eq!(consult.sends, ["sort", "tags"]);
10773 }
10774
10775 #[test]
10776 fn a_floor_counts_characters_and_not_bytes() {
10777 // The difference the renderers are spared: "el" and "él" are the same
10778 // question, and a byte count refuses one of them.
10779 let consult = Consult::new(Action::get("/discover/tag-suggest")).at_least(2);
10780 assert!(!consult.asks_about("e"));
10781 assert!(consult.asks_about("el"));
10782 assert!(!consult.asks_about("é"));
10783 assert!(consult.asks_about("él"));
10784 }
10785
10786 #[test]
10787 fn a_region_is_still_until_it_says_it_is_live() {
10788 // The default has to be the old behaviour: a screen written before this
10789 // field existed says the same thing after it arrives.
10790 let slot = Slot::new("summary", RegionKind::Pane);
10791 assert!(!slot.live);
10792 assert!(slot.live().live);
10793 }
10794
10795 #[test]
10796 fn a_live_call_is_a_refresh_and_not_a_feed() {
10797 // The split that keeps a host from having to work out which kind of
10798 // call it is holding. One walk answers "ask now", the other "keep
10799 // asking", and no action is in both.
10800 let screen = Screen::sidebar_content("Admin")
10801 .with(Slot::new("payouts", RegionKind::Pane).fed_by(Action::get("/dashboard/payouts")))
10802 .with(
10803 Slot::new("queue", RegionKind::Pane)
10804 .fed_by(Action::get("/admin/queue"))
10805 .live(),
10806 );
10807
10808 let feeds: Vec<_> = screen
10809 .feeds()
10810 .iter()
10811 .map(|call| call.destination.as_str().to_owned())
10812 .collect();
10813 let refreshes: Vec<_> = screen
10814 .refreshes()
10815 .iter()
10816 .map(|call| call.destination.as_str().to_owned())
10817 .collect();
10818
10819 assert_eq!(feeds, ["/dashboard/payouts"]);
10820 assert_eq!(refreshes, ["/admin/queue"]);
10821 }
10822
10823 #[test]
10824 fn a_live_region_keeps_its_call_when_the_answer_lands() {
10825 // The half `replace` had to learn. A feed is cleared as it answers so a
10826 // retained host does not ask twice; a cadence cleared on its first
10827 // answer would tick once and stop.
10828 let mut screen = Screen::sidebar_content("Admin").with(
10829 Slot::new("queue", RegionKind::Pane)
10830 .fed_by(Action::get("/admin/queue"))
10831 .live(),
10832 );
10833
10834 assert!(screen.replace("queue", Node::text("4 waiting")));
10835
10836 assert!(screen.feeds().is_empty(), "a live call is never a feed");
10837 assert_eq!(
10838 screen.refreshes().len(),
10839 1,
10840 "the cadence survives an answer"
10841 );
10842 }
10843
10844 #[test]
10845 fn a_still_region_still_drops_its_call_when_the_answer_lands() {
10846 let mut screen = Screen::sidebar_content("Payments")
10847 .with(Slot::new("payouts", RegionKind::Pane).fed_by(Action::get("/dashboard/payouts")));
10848
10849 assert!(screen.replace("payouts", Node::text("$12.00")));
10850
10851 assert!(screen.feeds().is_empty());
10852 assert!(screen.refreshes().is_empty());
10853 }
10854
10855 #[test]
10856 fn a_screen_is_live_when_a_region_inside_it_is() {
10857 // The predicate a retained host reads to decide whether to keep
10858 // drawing. It has to see through nesting, because the live thing is
10859 // usually a panel of something rather than a top-level region.
10860 let still = Screen::sidebar_content("Sync").with(Slot::new("body", RegionKind::Pane));
10861 assert!(!still.is_live());
10862
10863 let nested = Screen::sidebar_content("Sync").with(
10864 Slot::new("body", RegionKind::Pane)
10865 .with(Node::Region(Slot::new("sync", RegionKind::Pane).live())),
10866 );
10867 assert!(nested.is_live());
10868 }
10869
10870 #[test]
10871 fn a_screen_carries_no_clock_until_a_readout_derives_itself_from_one() {
10872 let still = Screen::sidebar_content("Tasks")
10873 .with(Slot::new("body", RegionKind::Pane).with(Node::text("Write the brief")));
10874 assert!(still.clocks().is_empty());
10875 }
10876
10877 #[test]
10878 fn a_readout_in_a_row_is_found_as_readily_as_one_in_a_region() {
10879 // The walk that matters. goingson puts the elapsed time on a task row
10880 // beside its title, so a search that stopped at the region's own blocks
10881 // would answer that a screen full of running timers needs no clock.
10882 let started = std::time::SystemTime::UNIX_EPOCH;
10883 let screen = Screen::sidebar_content("Tasks").with(
10884 Slot::new("body", RegionKind::Pane).with(Node::list([
10885 Row::new("Write the brief").part(layout::RowPart::Meta, Node::since(started))
10886 ])),
10887 );
10888
10889 assert_eq!(screen.clocks(), BTreeSet::from([Clock::Since]));
10890 }
10891
10892 #[test]
10893 fn each_kind_is_reported_once_however_many_readouts_say_it() {
10894 // Many readouts, one tick: the renderer asks which kinds it is holding
10895 // and picks a cadence per kind, rather than a timer per readout.
10896 let at = std::time::SystemTime::UNIX_EPOCH;
10897 let screen = Screen::sidebar_content("Tasks").with(
10898 Slot::new("body", RegionKind::Pane)
10899 .with(Node::since(at))
10900 .with(Node::since(at))
10901 .with(Node::Region(
10902 Slot::new("footer", RegionKind::Pane).with(Node::age(at)),
10903 )),
10904 );
10905
10906 assert_eq!(screen.clocks(), BTreeSet::from([Clock::Since, Clock::Age]));
10907 // Finest first, which is what a renderer taking the minimum wants.
10908 assert_eq!(screen.clocks().into_iter().next(), Some(Clock::Since));
10909 }
10910
10911 #[test]
10912 fn a_time_derived_node_says_which_way_it_runs_and_from_when() {
10913 let at = std::time::SystemTime::UNIX_EPOCH;
10914 assert_eq!(Node::since(at).clock(), Some((Clock::Since, at)));
10915 assert_eq!(Node::until(at).clock(), Some((Clock::Until, at)));
10916 assert_eq!(Node::age(at).clock(), Some((Clock::Age, at)));
10917 assert_eq!(Node::text("12:04").clock(), None);
10918 }
10919
10920 #[test]
10921 fn a_live_region_with_no_call_asks_for_nothing() {
10922 // Liveness and a route are separate halves. The audiofiles sync panel
10923 // reads state the host already holds, so its cadence is a repaint and
10924 // there is nothing here for a host to perform.
10925 let screen = Screen::sidebar_content("Sync").with(
10926 Slot::new("sync", RegionKind::Pane)
10927 .live()
10928 .with(Node::text("Authenticating")),
10929 );
10930
10931 assert!(screen.refreshes().is_empty());
10932 assert!(screen.feeds().is_empty());
10933 }
10934
10935 #[test]
10936 fn a_region_shows_everything_until_it_says_otherwise() {
10937 // The default has to be the old behaviour, or every description written
10938 // before this field existed changes meaning when it arrives.
10939 let pane = Slot::new("content", RegionKind::Pane).extend(frames(3));
10940
10941 assert_eq!(pane.showing(), layout::Showing::All);
10942 assert_eq!(pane.current(), None);
10943 }
10944
10945 #[test]
10946 fn a_carousel_with_no_stated_frame_is_on_its_first() {
10947 // `Showing::One` says exactly one is up, so there is no honest reading
10948 // of a missing index other than the first. A renderer never has to
10949 // decide this for itself, which is the point of the method.
10950 let mut carousel = Slot::widget("shots", "carousel").extend(frames(3));
10951 carousel = carousel.showing_one(0);
10952
10953 assert_eq!(carousel.current(), Some(0));
10954 }
10955
10956 #[test]
10957 fn a_frame_past_the_end_clamps_rather_than_vanishing() {
10958 // An out-of-range index is an app bug either way. Clamping reports it as
10959 // a carousel stuck on its last frame, which is findable; drawing nothing
10960 // reports it as a region that disappeared, which is not.
10961 let carousel = Slot::widget("shots", "carousel")
10962 .extend(frames(3))
10963 .showing_one(9);
10964
10965 assert_eq!(carousel.current(), Some(2));
10966
10967 // And an empty body has no frame to clamp to.
10968 assert_eq!(
10969 Slot::widget("shots", "carousel").showing_one(0).current(),
10970 None
10971 );
10972 }
10973
10974 #[test]
10975 fn a_closed_disclosure_is_the_one_selective_region_showing_nothing() {
10976 let closed = Slot::widget("details", "disclosure")
10977 .extend(frames(1))
10978 .showing_at_most_one(None);
10979 let open = Slot::widget("details", "disclosure")
10980 .extend(frames(1))
10981 .showing_at_most_one(Some(0));
10982
10983 assert_eq!(closed.current(), None);
10984 assert_eq!(open.current(), Some(0));
10985
10986 // Closed and `Showing::All` answer the same here on purpose: they differ
10987 // in the chrome around the body, not in what a renderer does with it.
10988 assert!(closed.showing().selective());
10989 }
10990
10991 #[test]
10992 fn labels_are_all_or_nothing() {
10993 // A strip with a hole in it is worse than the prev/next row it would
10994 // have replaced, so a half-labelled body gets the row.
10995 let tabs = Slot::new("detail", RegionKind::TabGroup)
10996 .frame(
10997 "Overview",
10998 Node::Region(Slot::new("overview", RegionKind::Pane)),
10999 )
11000 .frame("Files", Node::Region(Slot::new("files", RegionKind::Pane)));
11001 assert_eq!(tabs.labels(), ["Overview", "Files"]);
11002
11003 let half = tabs
11004 .clone()
11005 .with(Node::Region(Slot::new("history", RegionKind::Pane)));
11006 assert!(half.labels().is_empty());
11007 }
11008
11009 #[test]
11010 fn a_carousels_frames_carry_no_label_and_that_is_the_switch() {
11011 // Which idiom a renderer draws falls out of this rather than out of the
11012 // widget's name. A frame has a caption; only a region has a tab name.
11013 let carousel = Slot::widget("shots", "carousel")
11014 .extend(frames(3))
11015 .showing_one(1);
11016
11017 assert!(carousel.labels().is_empty());
11018 assert_eq!(carousel.current(), Some(1));
11019 }
11020
11021 #[test]
11022 fn a_region_member_never_drops_unless_it_was_asked_to() {
11023 // The whole of what makes `Ranked` additive. Every screen in the tree
11024 // was written before it existed and every one of them still says the
11025 // same thing.
11026 let pane = Slot::new("main", RegionKind::Pane)
11027 .with(Node::text("kept"))
11028 .extend([Node::text("also kept")]);
11029
11030 for placed in pane.body.iter() {
11031 assert_eq!(placed.priority, layout::Priority::Essential);
11032 for cutoff in CUTOFFS {
11033 assert!(placed.kept_at(cutoff));
11034 }
11035 }
11036 }
11037
11038 #[test]
11039 fn a_tab_strip_and_the_band_beside_it_are_one_row() {
11040 // goingson's bug, said in the description instead of in a stylesheet.
11041 // The toolbar is not in the pane and is not out of flow; it is a member
11042 // of the tab strip's row, and it says what it is worth when that row
11043 // runs out of space.
11044 let view = Slot::new("work-view", RegionKind::TabGroup).across(
11045 Run::new(layout::Fallback::Menu).beside(
11046 Node::Region(Slot::new("work-toolbar", RegionKind::Band)),
11047 layout::Priority::Secondary,
11048 ),
11049 );
11050
11051 let run = view.run.as_ref().expect("the row was declared");
11052 assert_eq!(run.fallback, layout::Fallback::Menu);
11053 assert_eq!(run.members.len(), 1);
11054 // The strip itself is not a member. It is what the tab group already
11055 // puts in the row, generated from its children's labels, and a member
11056 // standing for it would be a second source for the same fact.
11057 assert!(view.body.is_empty());
11058 }
11059
11060 #[test]
11061 fn a_row_member_is_still_a_region_a_fragment_can_be_aimed_at() {
11062 // The property that makes the toolbar's move out of the pane free. A
11063 // region that stopped being findable would stop updating, which is a
11064 // worse bug than the overlap it was moved to fix.
11065 let view = Slot::new("work-view", RegionKind::TabGroup).across(
11066 Run::new(layout::Fallback::Menu).beside(
11067 Node::Region(
11068 Slot::new("work-toolbar", RegionKind::Band).fed_by(Action::get("/toolbar")),
11069 ),
11070 layout::Priority::Secondary,
11071 ),
11072 );
11073
11074 assert!(view.find("work-toolbar").is_some());
11075 let mut screen = Screen::sidebar_content("Work").with(view);
11076 assert_eq!(screen.feeds().len(), 1);
11077 assert!(screen.replace("work-toolbar", Node::text("filters")));
11078 }
11079
11080 #[test]
11081 fn what_a_tight_row_keeps_depends_on_what_it_said_it_would_do() {
11082 let members = |fallback| {
11083 Slot::new("head", RegionKind::TabGroup)
11084 .across(
11085 Run::new(fallback)
11086 .beside(Node::text("search"), layout::Priority::Secondary)
11087 .beside(Node::text("count"), layout::Priority::Optional),
11088 )
11089 .run
11090 .expect("declared")
11091 };
11092
11093 // Wrap and Stack rearrange, so every member survives however tight the
11094 // row is and the rank goes unread.
11095 for keeping in [layout::Fallback::Wrap, layout::Fallback::Stack] {
11096 let run = members(keeping);
11097 assert!(run.keeps_every_member());
11098 assert_eq!(run.kept_at(layout::Priority::Essential).len(), 2);
11099 }
11100
11101 // Shed and Menu take members out of the row, by rank and never by
11102 // position, which is the whole reason the rank is on the member.
11103 for shedding in [layout::Fallback::Shed, layout::Fallback::Menu] {
11104 let run = members(shedding);
11105 assert!(!run.keeps_every_member());
11106 assert_eq!(run.kept_at(layout::Priority::Optional).len(), 2);
11107 assert_eq!(run.kept_at(layout::Priority::Secondary).len(), 1);
11108 assert!(run.kept_at(layout::Priority::Essential).is_empty());
11109 }
11110 }
11111
11112 #[test]
11113 fn a_region_that_never_declared_a_row_has_no_row_to_put_anything_in() {
11114 // Rule 2, held by the type instead of by a check. This was a
11115 // `#[should_panic]` test while `beside` was a method on the region: it
11116 // had to look for a row at runtime and refuse when there was none,
11117 // which made a correct call and an incorrect one identical to a
11118 // compiler. There is nothing left to panic on. `beside` is on `Run`,
11119 // `Run::new` takes the fallback, and a region that never declared a
11120 // row offers no method that would put a member in one.
11121 //
11122 // What a running program can still observe is the half below: silence
11123 // stays silence, and both ways of declaring a row arrive with the
11124 // answer the panic used to stand in for.
11125 let quiet = Slot::new("head", RegionKind::Band);
11126 assert!(quiet.run.is_none());
11127
11128 let empty = Slot::new("head", RegionKind::Band).across(layout::Fallback::Shed);
11129 let run = empty.run.expect("declared");
11130 assert_eq!(run.fallback, layout::Fallback::Shed);
11131 assert!(run.members.is_empty());
11132
11133 let filled = Slot::new("head", RegionKind::Band).across(
11134 Run::new(layout::Fallback::Menu)
11135 .beside(Node::text("search"), layout::Priority::Secondary),
11136 );
11137 let run = filled.run.expect("declared");
11138 assert_eq!(run.fallback, layout::Fallback::Menu);
11139 assert_eq!(run.members.len(), 1);
11140 }
11141
11142 #[test]
11143 fn declaring_the_row_twice_is_a_second_row_rather_than_a_correction() {
11144 // The row is one value, so handing the region another one replaces it
11145 // whole. Correcting a fallback means correcting it on the `Run`, where
11146 // the members it belongs to are, rather than reaching past them.
11147 let head = Slot::new("head", RegionKind::TabGroup)
11148 .across(
11149 Run::new(layout::Fallback::Shed)
11150 .beside(Node::text("search"), layout::Priority::Secondary),
11151 )
11152 .across(layout::Fallback::Menu);
11153
11154 let run = head.run.expect("declared");
11155 assert_eq!(run.fallback, layout::Fallback::Menu);
11156 assert!(run.members.is_empty());
11157 }
11158
11159 #[test]
11160 fn a_member_inserted_above_the_cut_does_not_change_what_drops() {
11161 // `makeover-tui`'s table states this for columns and tests it; this is
11162 // the same property for a region's members, and it is the property the
11163 // whole rank exists to buy. Positional narrowing -- goingson's
11164 // `nth-child(n+5)` -- fails it, which is the bug `Priority` replaced.
11165 let before = Slot::new("bar", RegionKind::Band)
11166 .with(Node::text("title"))
11167 .with_ranked(Node::text("filter"), layout::Priority::Optional);
11168
11169 let after = Slot::new("bar", RegionKind::Band)
11170 .with(Node::text("title"))
11171 .with(Node::text("inserted"))
11172 .with_ranked(Node::text("filter"), layout::Priority::Optional);
11173
11174 let dropped = |slot: &Slot| -> Vec<String> {
11175 slot.body
11176 .iter()
11177 .filter(|placed| !placed.kept_at(layout::Priority::Secondary))
11178 .map(|placed| format!("{:?}", placed.node))
11179 .collect()
11180 };
11181
11182 assert_eq!(dropped(&before), dropped(&after));
11183 assert_eq!(dropped(&before).len(), 1);
11184 }
11185
11186 #[test]
11187 fn the_cutoffs_run_weakest_first() {
11188 // A renderer walks these in order and stops at the first that fits, so
11189 // the order is the whole meaning of the constant. A tier added
11190 // upstream and appended here rather than placed would narrow to it
11191 // last, whatever it said.
11192 assert_eq!(
11193 CUTOFFS,
11194 [
11195 layout::Priority::Optional,
11196 layout::Priority::Secondary,
11197 layout::Priority::Essential,
11198 ]
11199 );
11200 assert!(CUTOFFS.is_sorted());
11201 }
11202
11203 #[test]
11204 fn started_marks_a_region_waiting_and_leaves_the_call_that_reports_the_finish() {
11205 // `dc2f2b46`. `replace`'s opposite number, and the pair is the whole
11206 // cycle: the work is handed off, the region goes pending, and the live
11207 // call that was already declared is what eventually puts content back.
11208 let mut screen = Screen::list_detail("Import & Export", false).with(
11209 Slot::new("backups", RegionKind::Pane)
11210 .with(Node::text("3 backups"))
11211 .fed_by(Action::get("/backups").awaiting())
11212 .live(),
11213 );
11214 // A live region is asked again on a cadence rather than once, so it is
11215 // `refreshes` and not `feeds` that names its call.
11216 assert!(screen.feeds().is_empty());
11217 assert_eq!(screen.refreshes().len(), 1);
11218
11219 assert!(screen.started("backups", "Creating backup…"));
11220 assert_eq!(screen.slots[0].readiness, layout::Readiness::Pending);
11221 assert_eq!(
11222 screen.slots[0].body.iter().next().expect("one member").node,
11223 Node::pending("Creating backup…"),
11224 "the sentence stands where the content was"
11225 );
11226 // The call is untouched. Clearing it here would mark the region as
11227 // waiting and remove the thing it waits on in one breath.
11228 assert_eq!(screen.refreshes().len(), 1);
11229
11230 // And the finish is an ordinary fragment, which takes it back out.
11231 assert!(screen.replace("backups", Node::text("4 backups")));
11232 assert_eq!(screen.slots[0].readiness, layout::Readiness::Ready);
11233 assert_eq!(
11234 screen.refreshes().len(),
11235 1,
11236 "a live region keeps its call after an answer lands"
11237 );
11238
11239 // A region that is not there is the description bug a fragment naming
11240 // one is, and answers the same way rather than panicking.
11241 assert!(!screen.started("gone", "…"));
11242 }
11243
11244 #[test]
11245 fn a_region_fed_by_a_call_is_pending_until_the_answer_lands() {
11246 // `d8d6f380`. The two facts move together: a region that says where its
11247 // content is coming from does not have it, and a region that has been
11248 // filled is no longer asking.
11249 let mut screen = Screen::list_detail("Payments", false).with(
11250 Slot::new("payouts", RegionKind::Pane)
11251 .fed_by(Action::get("/dashboard/payouts").awaiting()),
11252 );
11253 assert_eq!(screen.feeds().len(), 1);
11254 assert_eq!(
11255 screen.slots[0].readiness,
11256 layout::Readiness::Pending,
11257 "a fed region has not arrived"
11258 );
11259
11260 assert!(screen.replace("payouts", Node::text("paid out")));
11261 assert_eq!(screen.slots[0].readiness, layout::Readiness::Ready);
11262 assert!(
11263 screen.feeds().is_empty(),
11264 "a filled region asks for itself again"
11265 );
11266 }
11267
11268 #[test]
11269 fn an_upload_carries_all_four_axes_and_only_two_of_them_are_the_layers() {
11270 // f7261a5a. What it takes and how many are the description's; where the
11271 // bytes go and how far along it is are the action's, and both already
11272 // existed. The test is that reading the field as the layer's own type
11273 // carries the first two across the owned/borrowed seam.
11274 let field = Field::upload(
11275 "media",
11276 "Media",
11277 [
11278 Accepted::family(layout::Family::Image),
11279 Accepted::media_type("text/csv"),
11280 Accepted::suffix(".tar.gz"),
11281 ],
11282 )
11283 .many()
11284 .writes(Action::post("/media").awaiting_amount(41_943_040));
11285
11286 assert!(field.multiple);
11287 // A suffix names no family and a csv is not media, so what earns the
11288 // preview here is the one entry that says so.
11289 assert!(field.accepts_media());
11290 assert!(!Field::upload("build", "Build", [Accepted::suffix(".zip")]).accepts_media());
11291 // The destination and the size ride on the action, unchanged by any of
11292 // this.
11293 let action = field.writes.clone().expect("the field writes on its own");
11294 assert_eq!(
11295 action.awaiting.and_then(|mark| mark.amount),
11296 Some(41_943_040)
11297 );
11298
11299 field.with_layout(|borrowed| {
11300 assert_eq!(borrowed.kind, layout::FieldKind::File);
11301 assert!(borrowed.multiple);
11302 assert_eq!(
11303 borrowed.accept,
11304 [
11305 layout::Accepted::Family(layout::Family::Image),
11306 layout::Accepted::Type("text/csv"),
11307 layout::Accepted::Suffix(".tar.gz"),
11308 ]
11309 );
11310 assert!(borrowed.accepts_media());
11311 });
11312 }
11313
11314 #[test]
11315 fn a_field_that_is_not_an_upload_takes_no_files_and_says_so() {
11316 let text = Field::new(layout::FieldKind::Text, "title", "Title");
11317 assert!(!text.kind.takes_files());
11318 assert!(text.accept.is_empty());
11319 assert!(!text.multiple);
11320 // An upload with an empty list is a field that takes any file, which is
11321 // a different sentence from a field that takes none.
11322 let any = Field::upload("file", "File", []);
11323 assert!(any.kind.takes_files());
11324 assert!(any.accept.is_empty());
11325 }
11326
11327 #[test]
11328 fn the_wait_is_measured_only_where_something_measured_it() {
11329 // The ruling on `5fa96a82`: an amount is a fact about the payload, and
11330 // a call with nothing countable about it says nothing rather than
11331 // guessing.
11332 let stripe = Action::post("/checkout").awaiting();
11333 let upload = Action::post("/media").awaiting_amount(41_943_040);
11334 assert!(stripe.awaits() && upload.awaits());
11335 assert_eq!(stripe.awaiting.and_then(|mark| mark.amount), None);
11336 assert_eq!(
11337 upload.awaiting.and_then(|mark| mark.amount),
11338 Some(41_943_040)
11339 );
11340 assert!(!Action::post("/save").awaits());
11341 }
11342
11343 #[test]
11344 fn a_nested_region_is_fed_too() {
11345 // The walk descends the way `find_mut` does, or a region inside a pane
11346 // never asks for itself and shows its stand-in forever.
11347 let inner = Slot::new("inner", RegionKind::Pane).fed_by(Action::get("/slow").awaiting());
11348 let screen = Screen::list_detail("Screen", false)
11349 .with(Slot::new("outer", RegionKind::Pane).with(Node::Region(inner)));
11350 let feeds = screen.feeds();
11351 assert_eq!(feeds.len(), 1);
11352 assert_eq!(feeds[0].route(), Some("/slow"));
11353 }
11354
11355 #[test]
11356 fn a_tab_strips_panels_are_not_fed_because_the_strip_asks() {
11357 // `dfbc88ce`. A labelled child's `fed_by` is the tab's address, so a
11358 // host asking for every one of them fetches five frames for a reader
11359 // looking at one. Measured on MNW's library page, which is the screen
11360 // that found this.
11361 let screen = Screen::list_detail("Library", false).with(
11362 Slot::new("tab-content", RegionKind::TabGroup)
11363 .frame(
11364 "Purchases",
11365 Node::Region(
11366 Slot::new("purchases", RegionKind::Pane)
11367 .fed_by(Action::get("/library/tabs/purchases")),
11368 ),
11369 )
11370 .frame(
11371 "Feed",
11372 Node::Region(
11373 Slot::new("feed", RegionKind::Pane)
11374 .fed_by(Action::get("/library/tabs/feed")),
11375 ),
11376 )
11377 .showing_one(0),
11378 );
11379
11380 assert!(screen.feeds().is_empty(), "{:?}", screen.feeds());
11381 }
11382
11383 #[test]
11384 fn a_carousels_frames_were_never_fed_and_still_are_not() {
11385 // The unlabelled half of the same region. Nothing here changed; the
11386 // assertion exists so the tab rule cannot be widened into one that
11387 // silences an ordinary nested feed.
11388 let screen = Screen::list_detail("Project", false).with(
11389 Slot::new("detail", RegionKind::TabGroup)
11390 .with(Node::Region(Slot::new("one", RegionKind::Pane)))
11391 .with(Node::Region(
11392 Slot::new("two", RegionKind::Pane).fed_by(Action::get("/slow")),
11393 ))
11394 .showing_one(0),
11395 );
11396
11397 let feeds = screen.feeds();
11398 assert_eq!(feeds.len(), 1, "an unlabelled child still asks for itself");
11399 assert_eq!(feeds[0].route(), Some("/slow"));
11400 }
11401
11402 #[test]
11403 fn a_region_inside_a_panel_that_arrived_still_asks_for_itself() {
11404 // The walk descends past the panel, because a panel that has arrived can
11405 // hold something genuinely slow and that thing is nobody's tab.
11406 let panel = Slot::new("purchases", RegionKind::Pane)
11407 .fed_by(Action::get("/library/tabs/purchases"))
11408 .with(Node::Region(
11409 Slot::new("payouts", RegionKind::Pane).fed_by(Action::get("/slow")),
11410 ));
11411 let screen = Screen::list_detail("Library", false).with(
11412 Slot::new("tab-content", RegionKind::TabGroup)
11413 .frame("Purchases", Node::Region(panel))
11414 .frame("Feed", Node::Region(Slot::new("feed", RegionKind::Pane)))
11415 .showing_one(0),
11416 );
11417
11418 let feeds = screen.feeds();
11419 assert_eq!(feeds.len(), 1);
11420 assert_eq!(feeds[0].route(), Some("/slow"));
11421 }
11422
11423 #[test]
11424 fn a_panel_names_what_asks_for_it_only_while_it_is_empty() {
11425 // `asked_for` is what a retained-screen host reads when a tab is
11426 // pressed. A panel already read is not asked for again, which is what
11427 // going back to a tab means.
11428 let empty = Slot::new("feed", RegionKind::Pane).fed_by(Action::get("/library/tabs/feed"));
11429 assert_eq!(
11430 empty.asked_for().and_then(Action::route),
11431 Some("/library/tabs/feed")
11432 );
11433
11434 let read = empty.clone().with(Node::text("what your creators posted"));
11435 assert!(read.asked_for().is_none());
11436 }
11437
11438 #[test]
11439 fn a_local_destination_is_no_route_and_no_address() {
11440 // The three answers a renderer asks for, and the reason the third one
11441 // is empty rather than absent: `as_str` is for rendering, and there is
11442 // nothing to render. A renderer reading it for a local action has
11443 // skipped a branch, which is what the empty string makes visible
11444 // instead of an address that half-works.
11445 let action = Action::local();
11446 assert_eq!(action.destination.route(), None);
11447 assert_eq!(action.destination.as_str(), "");
11448 assert!(action.destination.is_local());
11449 // Not external. The two are the whole of "not a route this app answers"
11450 // and they are opposite kinds of not: one leaves and is still a
11451 // request, the other asks nothing at all.
11452 assert!(!action.destination.is_external());
11453 assert!(!Destination::External("https://example.invalid".into()).is_local());
11454 assert!(!Destination::Route("/x".into()).is_local());
11455 }
11456
11457 #[test]
11458 fn a_local_action_still_carries_what_the_behaviour_acts_on() {
11459 // Which suggestion was picked is a value, not an address, so it rides
11460 // where every other value does. Losing it here would leave the mark
11461 // saying something happens and nothing saying to what.
11462 let action = Action::local().with("choice", "ada");
11463 assert_eq!(action.params.get("choice"), Some("ada"));
11464 assert_eq!(action.destination, Destination::Local);
11465 }
11466
11467 #[test]
11468 fn only_the_hybrid_renderer_is_obliged_to_read_the_mark() {
11469 use crate::Renderer;
11470
11471 assert!(Renderer::Hybrid.reads_locality());
11472 assert!(!Renderer::Client.reads_locality());
11473 }
11474
11475 /// The four shapes, and the one convention they share: a control holding
11476 /// an empty string is holding nothing, which is what an unticked box is on
11477 /// every host.
11478 #[test]
11479 fn a_condition_reads_an_empty_value_as_nothing_held() {
11480 let ticked = Reveal::ticked("pwyw");
11481 assert!(ticked.satisfied_by(Some("on")));
11482 assert!(!ticked.satisfied_by(Some("")));
11483 assert!(!ticked.satisfied_by(None));
11484
11485 let unticked = Reveal::unticked("pwyw");
11486 assert!(unticked.satisfied_by(None));
11487 assert!(unticked.satisfied_by(Some("")));
11488 assert!(!unticked.satisfied_by(Some("on")));
11489
11490 let custom = Reveal::holding("license", "custom");
11491 assert!(custom.satisfied_by(Some("custom")));
11492 assert!(!custom.satisfied_by(Some("cc-by")));
11493 assert!(!custom.satisfied_by(None));
11494
11495 let zoned = Reveal::holding_one_of("tz_kind", ["floating", "zoned", "utc"]);
11496 assert!(zoned.satisfied_by(Some("zoned")));
11497 assert!(!zoned.satisfied_by(Some("none")));
11498 assert!(!zoned.satisfied_by(None));
11499 }
11500
11501 /// A region that says nothing about what reveals it is always out, which is
11502 /// what every region did before the member existed.
11503 #[test]
11504 fn a_region_with_no_condition_is_out_whatever_is_held() {
11505 let plain = Slot::group("body");
11506 assert!(plain.revealed(None));
11507 assert!(plain.revealed(Some("anything")));
11508 assert_eq!(plain.watches(), None);
11509
11510 let conditional = Slot::group("pwyw-settings").revealed_by(Reveal::ticked("pwyw"));
11511 assert_eq!(conditional.watches(), Some("pwyw"));
11512 assert!(!conditional.revealed(None));
11513 }
11514
11515 /// The condition is on the region and names the control, so a renderer
11516 /// asking "does this section apply" reads one member. Nothing is added to
11517 /// the control, which is the half the ruling rejected.
11518 #[test]
11519 fn the_condition_is_the_regions_and_the_control_is_untouched() {
11520 let control = Field::new(layout::FieldKind::Checkbox, "pwyw", "Pay what you want");
11521 let form = Slot::group("pricing")
11522 .with(Node::Field(Box::new(control.clone())))
11523 .with(Node::Region(
11524 Slot::group("pwyw-settings")
11525 .revealed_by(Reveal::ticked("pwyw"))
11526 .with(Node::section("Suggested price")),
11527 ));
11528
11529 let section = form.find("pwyw-settings").expect("the section");
11530 assert_eq!(section.watches(), Some("pwyw"));
11531 // The field is what it was: nothing points back at the region it
11532 // reveals, which is the back-pointer the ruling turned down.
11533 assert_eq!(
11534 control,
11535 Field::new(layout::FieldKind::Checkbox, "pwyw", "Pay what you want")
11536 );
11537 }
11538
11539 /// The described half of the condition: what the box was handed to the
11540 /// reader holding, wherever on the screen it sits.
11541 #[test]
11542 fn a_screen_answers_what_a_named_control_is_offered_holding() {
11543 let screen = Screen::new("Settings", layout::Arrangement::sidebar_content())
11544 .with(Slot::group("licensing").with(Node::Form {
11545 action: Action::post("/settings"),
11546 submit: "Save".into(),
11547 fields: vec![
11548 Field::select("license", "Licence", vec![Choice::new("custom", "Custom")])
11549 .value("custom"),
11550 ],
11551 }))
11552 .with(
11553 Slot::group("recurrence").with(Node::Region(
11554 Slot::group("rule").with(Node::Field(Box::new(
11555 Field::new(layout::FieldKind::Text, "rrule", "Repeats")
11556 .value("FREQ=WEEKLY"),
11557 ))),
11558 )),
11559 );
11560
11561 assert_eq!(screen.holds("license"), Some("custom"));
11562 // Nested, because a form inside a region is where most fields are.
11563 assert_eq!(screen.holds("rrule"), Some("FREQ=WEEKLY"));
11564 // A control the screen does not carry holds nothing, which is not the
11565 // same as holding an empty value and is why this is an `Option`.
11566 assert_eq!(screen.holds("pwyw"), None);
11567 }
11568
11569 /// A field in a row of a list is still a control a region can watch. The
11570 /// walk is exhaustive over the containers rather than over the two nodes
11571 /// that hold a field directly.
11572 #[test]
11573 fn the_walk_reaches_a_field_inside_a_row() {
11574 let row = Row::new("Repeat").part(
11575 layout::RowPart::Meta,
11576 Node::Field(Box::new(
11577 Field::new(layout::FieldKind::Checkbox, "repeats", "Repeat").value("on"),
11578 )),
11579 );
11580 let screen = Screen::new("Task", layout::Arrangement::sidebar_content()).with(
11581 Slot::group("body").with(Node::Table {
11582 columns: Vec::new(),
11583 rows: vec![row],
11584 more: None,
11585 }),
11586 );
11587
11588 assert_eq!(screen.holds("repeats"), Some("on"));
11589 }
11590
11591 /// The eight sites the member was measured against, each said as one
11592 /// region with one condition. Six MNW toggles and two goingson ones, and
11593 /// every one of them is a `window.<name>` function today.
11594 #[test]
11595 fn the_eight_measured_sites_are_describable() {
11596 // MNW, static/actions-tabs.js and actions-partials.js.
11597 let pwyw = Slot::group("pwyw-settings").revealed_by(Reveal::ticked("pwyw"));
11598 let license = Slot::group("dash-custom-license")
11599 .revealed_by(Reveal::holding("license_kind", "custom"));
11600 let keys = Slot::group("license-keys-section").revealed_by(Reveal::ticked("license_keys"));
11601 let trial = Slot::group("trial-details").revealed_by(Reveal::ticked("trial"));
11602 let promo =
11603 Slot::group("promo-fields").revealed_by(Reveal::holding("promo_type", "percent"));
11604 let offset = Slot::group("offset-input").revealed_by(Reveal::holding_one_of(
11605 "placement_position",
11606 ["before", "after"],
11607 ));
11608
11609 assert!(pwyw.revealed(Some("on")) && !pwyw.revealed(None));
11610 assert!(license.revealed(Some("custom")) && !license.revealed(Some("cc-by")));
11611 assert!(keys.revealed(Some("on")) && !keys.revealed(None));
11612 assert!(trial.revealed(Some("on")) && !trial.revealed(None));
11613 assert!(promo.revealed(Some("percent")) && !promo.revealed(Some("fixed")));
11614 assert!(offset.revealed(Some("after")) && !offset.revealed(Some("inline")));
11615
11616 // goingson: the zone picker, out on three of the four kinds, and the
11617 // recurrence detail, out while the rule is anything at all.
11618 let zone = Slot::group("tz-config").revealed_by(Reveal::holding_one_of(
11619 "tz_kind",
11620 ["floating", "zoned", "utc"],
11621 ));
11622 // `edit_fields` offers four patterns and grows a second form under any
11623 // of them, which is the same shape as the zone picker: the detail is
11624 // out on the patterns and away on "none".
11625 let recurrence = Slot::group("recurrence-detail").revealed_by(Reveal::holding_one_of(
11626 "recurrence",
11627 ["daily", "weekly", "monthly", "yearly"],
11628 ));
11629
11630 assert!(zone.revealed(Some("zoned")));
11631 assert!(!zone.revealed(Some("none")));
11632 assert!(recurrence.revealed(Some("weekly")));
11633 assert!(!recurrence.revealed(Some("none")));
11634 assert!(!recurrence.revealed(None));
11635 }
11636
11637 /// Found by the consumer `079a011e` was ruled for: a form's questions are
11638 /// a flat list, so a single conditional question inside one has nowhere to
11639 /// put the fact a region would carry.
11640 #[test]
11641 fn one_question_inside_a_form_can_say_what_reveals_it() {
11642 // goingson's `initTzKindConfig`: the box is out on one of the kinds.
11643 let zone = Field::new(layout::FieldKind::Text, "timezone", "Anchored to")
11644 .revealed_by(Reveal::holding("tz_kind", "local"));
11645
11646 assert_eq!(zone.watches(), Some("tz_kind"));
11647 assert!(zone.revealed(Some("local")));
11648 assert!(!zone.revealed(Some("relative")));
11649 assert!(!zone.revealed(None));
11650
11651 // The ordinary question says nothing and is always asked, which is
11652 // every other field in the tree.
11653 let title = Field::new(layout::FieldKind::Text, "title", "Title");
11654 assert_eq!(title.watches(), None);
11655 assert!(title.revealed(None));
11656
11657 // A slot of a repeating question does not carry the condition: the
11658 // question does, and a renderer that has reached the slots has already
11659 // answered it once for the whole group.
11660 let repeating = Field::new(layout::FieldKind::Number, "reminder", "Reminder")
11661 .repeating(Repeat::answered(["300"]))
11662 .revealed_by(Reveal::ticked("remind"));
11663 assert_eq!(repeating.watches(), Some("remind"));
11664 assert_eq!(repeating.instance(0).watches(), None);
11665 }
11666
11667 /// The wire naming, picked once here so the three renderers and whatever
11668 /// reads the submission back cannot disagree.
11669 #[test]
11670 fn a_slot_submits_under_its_question_and_its_index() {
11671 assert_eq!(Repeat::at("reminder", 0), "reminder[0]");
11672 assert_eq!(Repeat::at("reminder", 11), "reminder[11]");
11673
11674 assert_eq!(
11675 Repeat::instance_of("reminder[2]"),
11676 Some(("reminder", 2usize))
11677 );
11678 // An ordinary field name is not a slot, which is what makes it safe to
11679 // ask this of any name a host is holding.
11680 assert_eq!(Repeat::instance_of("reminder"), None);
11681 // Nor is anything this did not write.
11682 assert_eq!(Repeat::instance_of("reminder[]"), None);
11683 assert_eq!(Repeat::instance_of("reminder[ 1]"), None);
11684 assert_eq!(Repeat::instance_of("reminder[+1]"), None);
11685 assert_eq!(Repeat::instance_of("reminder[2"), None);
11686 assert_eq!(Repeat::instance_of("reminder[two]"), None);
11687 }
11688
11689 /// One slot is an ordinary field, so every renderer draws it with what it
11690 /// already does to a field. The value and the error are the slot's; the
11691 /// question's own error is not any slot's.
11692 #[test]
11693 fn a_slot_is_the_question_under_its_own_name() {
11694 let mut field = Field::new(layout::FieldKind::Number, "reminder", "Reminder")
11695 .repeating(Repeat::answered(["300", "900"]).wrong(1, "Must be positive"));
11696 field.error = Some("At most eight".into());
11697 field.min = Some("0".into());
11698
11699 let second = field.instance(1);
11700 assert_eq!(second.name, "reminder[1]");
11701 assert_eq!(second.label, "Reminder 2");
11702 assert_eq!(second.value.as_deref(), Some("900"));
11703 assert_eq!(second.error.as_deref(), Some("Must be positive"));
11704 // Carried through: the question is what repeats, so everything it said
11705 // about itself holds for every slot.
11706 assert_eq!(second.kind, layout::FieldKind::Number);
11707 assert_eq!(second.min.as_deref(), Some("0"));
11708 // And a slot cannot recurse into slots of its own.
11709 assert!(second.repeats.is_none());
11710
11711 assert_eq!(field.instance(0).error, None);
11712 // A slot past the end is an empty box under the right name, which is
11713 // exactly what a slot the reader has just added is.
11714 let added = field.instance(2);
11715 assert_eq!(added.name, "reminder[2]");
11716 assert_eq!(added.value, None);
11717 assert_eq!(added.error, None);
11718 }
11719
11720 /// The floor, the ceiling, and how many slots stand before anyone touches
11721 /// anything. Every renderer asks these rather than doing the arithmetic, so
11722 /// the three of them offer the same controls.
11723 #[test]
11724 fn the_floor_and_the_ceiling_say_which_controls_are_offered() {
11725 let capped = Repeat::answered(["300", "900"]).most(2);
11726 assert_eq!(capped.standing(), 2);
11727 assert!(!capped.more(2));
11728 assert!(capped.more(1));
11729 assert!(capped.fewer(1));
11730
11731 let floored = Repeat::answered(["ana"]).least(1);
11732 assert!(!floored.fewer(1));
11733 assert!(floored.fewer(2));
11734 assert!(floored.more(9));
11735
11736 // Zero answers is a real state: the add control alone.
11737 let none = Repeat::new();
11738 assert_eq!(none.standing(), 0);
11739 assert!(none.more(0));
11740 assert!(!none.fewer(0));
11741
11742 // A question that must be answered twice opens with two boxes rather
11743 // than with none and a refusal on submit.
11744 assert_eq!(Repeat::new().least(2).standing(), 2);
11745 }
11746
11747 /// A refusal naming a slot the form did not offer is worth seeing on the
11748 /// screen rather than being dropped.
11749 #[test]
11750 fn an_error_reaches_a_slot_past_the_ones_described() {
11751 let repeat = Repeat::answered(["300"]).wrong(2, "Must be positive");
11752 assert_eq!(repeat.instances.len(), 3);
11753 assert_eq!(repeat.holds(0), Some("300"));
11754 assert_eq!(repeat.holds(1), None);
11755 assert_eq!(repeat.amiss(2), Some("Must be positive"));
11756 }
11757
11758 /// The consumer, said in the vocabulary: goingson `8fdb814c`'s event form,
11759 /// against `Event.reminder_offsets_seconds` and the cap
11760 /// `sanitize_reminder_offsets` enforces.
11761 #[test]
11762 fn the_reminders_question_is_describable() {
11763 let held: Vec<i64> = vec![300, 900, 3600];
11764 let field = Field::new(layout::FieldKind::Number, "reminder", "Reminder").repeating(
11765 Repeat::answered(held.iter().map(i64::to_string))
11766 .most(8)
11767 .adding("Add reminder"),
11768 );
11769
11770 assert_eq!(field.slots(), 3);
11771 let names: Vec<String> = (0..field.slots())
11772 .map(|at| field.instance(at).name)
11773 .collect();
11774 assert_eq!(names, ["reminder[0]", "reminder[1]", "reminder[2]"]);
11775 // Which is one submit carrying three values under one question, not
11776 // three submits and not one control taking a set.
11777 assert!(!field.multiple);
11778 }
11779
11780 /// An ordinary field answers the repeating questions the way it always did,
11781 /// so nothing that loops over slots has to branch first.
11782 #[test]
11783 fn a_field_that_repeats_nothing_stands_in_one_slot() {
11784 let field = Field::new(layout::FieldKind::Text, "title", "Title");
11785 assert!(field.repeats.is_none());
11786 assert_eq!(field.slots(), 1);
11787 }
11788
11789 #[test]
11790 fn a_part_of_a_slot_submits_under_the_slot_and_its_own_name() {
11791 assert_eq!(Repeat::part_at("file", 0, "size"), "file[0].size");
11792 assert_eq!(Repeat::part_at("file", 11, "name"), "file[11].name");
11793 }
11794
11795 #[test]
11796 fn a_parts_wire_name_comes_back_apart_and_a_bare_slots_does_not() {
11797 assert_eq!(Repeat::part_of("file[0].size"), Some(("file", 0, "size")));
11798 // The two readers never both answer, which is what makes it safe to
11799 // ask either of any name a host is holding.
11800 assert_eq!(Repeat::part_of("file[0]"), None);
11801 assert_eq!(Repeat::instance_of("file[0].size"), None);
11802 assert_eq!(Repeat::part_of("file"), None);
11803 // Refused rather than parsed loosely, for `instance_of`'s reason.
11804 assert_eq!(Repeat::part_of("file[0]."), None);
11805 assert_eq!(Repeat::part_of("file[0].a.b"), None);
11806 assert_eq!(Repeat::part_of("file[x].size"), None);
11807 }
11808
11809 #[test]
11810 fn a_grouped_slot_draws_one_field_per_question_and_an_ordinary_one_draws_itself() {
11811 let ordinary = Field::new(layout::FieldKind::Text, "reminder", "Reminder")
11812 .repeating(Repeat::answered(["300", "900"]));
11813 assert_eq!(ordinary.instance_fields(0).len(), 1);
11814 assert_eq!(ordinary.instance_fields(0)[0].name, "reminder[0]");
11815
11816 let grouped = Field::new(layout::FieldKind::Text, "file", "File").repeating(
11817 Repeat::new()
11818 .of([Question::new("name", "Name"), Question::new("size", "Size")])
11819 .instances_of([Instance::grouped([
11820 Answer::new("track.wav"),
11821 Answer::new("4.2 MB"),
11822 ])]),
11823 );
11824 let slots = grouped.instance_fields(0);
11825 assert_eq!(slots.len(), 2);
11826 assert_eq!(slots[0].name, "file[0].name");
11827 assert_eq!(slots[0].label, "Name");
11828 assert_eq!(slots[0].value.as_deref(), Some("track.wav"));
11829 assert_eq!(slots[1].name, "file[0].size");
11830 assert_eq!(slots[1].value.as_deref(), Some("4.2 MB"));
11831 }
11832
11833 #[test]
11834 fn a_slot_the_reader_just_added_is_the_questions_with_nothing_in_them() {
11835 // What the blank a renderer offers is built from: the questions live on
11836 // the repeat, so a slot past the end still knows what it is asking.
11837 let grouped = Field::new(layout::FieldKind::Text, "file", "File").repeating(
11838 Repeat::new().of([Question::new("name", "Name"), Question::new("size", "Size")]),
11839 );
11840 let slots = grouped.instance_fields(3);
11841 assert_eq!(slots.len(), 2);
11842 assert_eq!(slots[0].name, "file[3].name");
11843 assert_eq!(slots[0].label, "Name");
11844 assert_eq!(slots[0].value, None);
11845 assert_eq!(slots[0].error, None);
11846 }
11847
11848 #[test]
11849 fn a_slots_own_message_and_one_of_its_questions_are_different_facts() {
11850 let slot = Instance::grouped([Answer::new("track.wav").wrong("Already uploaded")])
11851 .getting(Progress::Failed);
11852 assert_eq!(slot.part(0).error.as_deref(), Some("Already uploaded"));
11853 // The slot's own error stays free for what is wrong with the slot
11854 // rather than with one of its questions.
11855 assert_eq!(slot.error, None);
11856 assert!(slot.progress.failed());
11857 assert!(!slot.progress.busy());
11858 }
11859
11860 #[test]
11861 fn a_slot_nothing_is_happening_to_is_the_default() {
11862 assert_eq!(Instance::blank().progress, Progress::Idle);
11863 assert_eq!(Instance::new("300").progress, Progress::Idle);
11864 assert!(!Progress::Idle.busy());
11865 assert!(Progress::Working(None).busy());
11866 assert!(Progress::Working(Some(Meter::new(1, 4))).busy());
11867 assert!(!Progress::Done.busy());
11868 }
11869
11870 #[test]
11871 fn a_question_whose_slots_come_from_elsewhere_offers_no_control_of_its_own() {
11872 let ordinary = Repeat::new();
11873 assert_eq!(ordinary.add.label(), Some("Add"));
11874 assert_eq!(ordinary.add.from(), None);
11875
11876 let named = Repeat::new().adding("Add reminder");
11877 assert_eq!(named.add.label(), Some("Add reminder"));
11878
11879 // The queue: the picker above the table makes the slots, so there is
11880 // no blank a reader could fill and every renderer draws no control.
11881 let queue = Repeat::new().added_by("version-files");
11882 assert_eq!(queue.add.label(), None);
11883 assert_eq!(queue.add.from(), Some("version-files"));
11884 }
11885
11886 #[test]
11887 fn a_slot_named_by_what_is_in_it_is_not_numbered() {
11888 let field = Field::new(layout::FieldKind::Text, "version-file", "File").repeating(
11889 Repeat::new().instances_of([
11890 Instance::new("macOS (arm)").called("track-arm.dmg"),
11891 Instance::new("Linux (x86_64)"),
11892 ]),
11893 );
11894
11895 // What is in it, for a queue whose slots differ by their file.
11896 assert_eq!(field.instance(0).label, "track-arm.dmg");
11897 // And the ordinal still, for a slot that differs only by position.
11898 assert_eq!(field.instance(1).label, "File 2");
11899 // A slot past the end is a blank the reader just made, and it has no
11900 // name of its own yet.
11901 assert_eq!(field.instance(2).label, "File 3");
11902 }
11903
11904 #[test]
11905 fn an_owned_option_carries_its_second_line_across_the_conversion() {
11906 // `5e21dcfc`. The conversion is what keeps the mirror from drifting, so
11907 // the member is asserted through it rather than on the struct: a field
11908 // added over there and forgotten here compiles until something reads
11909 // it, and this is the something.
11910 let tier = Choice::new("24", "Small Files")
11911 .detailing("$24/mo. Fits audio, plugins, binaries.")
11912 .unless("Sold out while the founder window is open.");
11913
11914 let borrowed = tier.as_layout();
11915 assert_eq!(borrowed.value, "24");
11916 assert_eq!(borrowed.label, "Small Files");
11917 assert_eq!(
11918 borrowed.detail,
11919 Some("$24/mo. Fits audio, plugins, binaries.")
11920 );
11921 assert_eq!(
11922 borrowed.unavailable,
11923 Some("Sold out while the founder window is open.")
11924 );
11925 assert!(!borrowed.available());
11926
11927 // An ordinary option says neither, which is nearly all of them.
11928 let free = Choice::plain("free");
11929 let plain = free.as_layout();
11930 assert_eq!(plain.detail, None);
11931 assert!(plain.available());
11932 }
11933
11934 #[test]
11935 fn a_regions_questions_are_the_ones_inside_it_at_any_depth() {
11936 // `cb62a9dc`. The one walk all three renderers read, so "the dials
11937 // inside this panel" cannot mean three things.
11938 let calculator = Slot::group("calculator")
11939 .across(Run::new(layout::Fallback::Shed).beside(
11940 Node::field(Field::new(layout::FieldKind::Text, "tier", "Tier")),
11941 layout::Priority::Essential,
11942 ))
11943 .with(Node::field(Field::new(
11944 layout::FieldKind::Number,
11945 "item_price",
11946 "Price",
11947 )))
11948 .with(Node::Form {
11949 action: Action::post("/save"),
11950 submit: "Save".into(),
11951 fields: vec![Field::new(layout::FieldKind::Number, "sales", "Sales")],
11952 })
11953 .with(Node::Region(Slot::group("other").with(Node::field(
11954 Field::new(layout::FieldKind::Number, "other_pct", "Their cut"),
11955 ))))
11956 .with(Node::text("You keep $9.00"));
11957
11958 let names: Vec<&str> = calculator
11959 .questions()
11960 .iter()
11961 .map(|field| field.name.as_str())
11962 .collect();
11963 // The run first, because that is draw order, then the body outside in.
11964 assert_eq!(names, ["tier", "item_price", "sales", "other_pct"]);
11965 }
11966
11967 #[test]
11968 fn only_the_regions_that_ask_are_walked_for() {
11969 let screen = Screen::sidebar_content("Pricing")
11970 .with(Slot::group("notes").with(Node::text("Nothing to ask")))
11971 .with(
11972 Slot::group("calculator")
11973 .with(Node::Region(Slot::group("panel").consulting(Consult::new(
11974 Action::get("/nested").replacing("x"),
11975 ))))
11976 .consulting(Consult::new(
11977 Action::get("/pricing/compare").replacing("results"),
11978 )),
11979 );
11980
11981 let asking: Vec<&str> = screen
11982 .consulting()
11983 .iter()
11984 .map(|slot| slot.id.as_str())
11985 .collect();
11986 // Outside in, and a region that asks nothing is not here at all.
11987 assert_eq!(asking, ["calculator", "panel"]);
11988 }
11989 }
11990