//! The screen tree: what a route answers with. //! //! Decision 3 on the wiki note puts this in quasi rather than in //! `makeover-layout`, for two reasons. The tree will churn while the router is //! being proven against a second host, and churning it here costs nothing where //! churning it there is a breaking release against three adopters. And a route //! is an address, which is the one thing `makeover-layout`'s deferral rule says //! it never names. //! //! # What this crate adds, and what it does not //! //! It adds three things: an [`Action`], which is an address; a [`Slot`], which //! is a region with a name a fragment can be aimed at; and ownership. //! //! Everything else is `makeover-layout`'s. Every member of [`Node`] composes a //! vocabulary that already exists there, and that is the admission test for a //! new one: if the thing being drawn has no name in the description layer, it //! does not get a node here, it gets a [`Region::Handover`] or it gets named //! there first. Without that rule this file becomes a widget library, which is //! the failure `makeover-layout` was extracted to prevent. //! //! # Why these are owned when the description layer is borrowed //! //! `makeover-layout`'s structs borrow, because a description is built, read //! once and dropped inside one frame. A router's answer outlives its handler by //! construction: it is returned from a function, and its text is usually built //! from state rather than found in it. So [`Field`], [`Choice`] and [`Column`] //! have owned mirrors here, each with a conversion back, and the conversion is //! what keeps them from drifting: adding a field over there stops the mirror //! compiling over here. use std::collections::BTreeSet; use makeover_layout as layout; use crate::containment::{Containment, Element}; use crate::request::{Method, Params}; /// Where an action goes. /// /// The port put the URL in the row's trailing text, which made it something to /// copy rather than something to follow. /// /// # Why this and not a separate link node /// /// Both were on the table. A destination keeps one concept where there would /// have been two, and the cost is that every renderer now branches: a webview /// emits an anchor rather than a button, and a terminal has to decide whether /// it can open a browser or should show the address. That branch is honest /// work, and it is work each renderer must do anyway once external addresses /// exist at all. /// /// What it must never become is a guess. The renderer branches on this enum and /// never on the shape of the string, because "starts with https" is how a route /// named `/https-setup` ends up opening a browser. /// /// # Locality /// /// [`Local`](Self::Local) is for what happens without a request: arrow keys /// moving a highlight, a toast dismissing itself, a price recomputing as a /// slider is dragged. /// /// The mark hangs here, per element, because an [`Action`] does, and because /// which interactions are local varies within one behaviour: typing asks the /// route for suggestions, arrowing through what came back does not. Both are /// actions on the same field and only one leaves. /// /// **What happens locally is named by the member carrying the action, never by /// this variant.** A suggestion source's pick action being local says picking /// sets the field; a notice's dismiss action being local says the notice goes /// away. A bare [`Act`] with a local destination says only "press this and the /// renderer's own affordance happens", which is a description bug everywhere /// except inside a [`Region::Handover`](layout::Region::Handover) or a /// [`Region::Ceded`](layout::Region::Ceded). This is the /// same rule that keeps the other two variants off string-shape guessing, and /// it is what stops this becoming `data-action="doTheThing"` in a new hat. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Destination { /// A path this app's router answers. Route(String), /// An address outside the app, offered as a reference. Nothing here will /// ever call it. /// /// The reader is expected to come back, so a windowing host puts it aside /// rather than in front: a webview opens a tab. See /// [`Leaving`](Self::Leaving) for the other half, and read the two /// together -- what separates them is the description's, and what a host /// does about it is the host's. External(String), /// An address outside the app that this document hands the reader on to. /// /// The reader is finished here, so a host navigates in place rather than /// opening anything: nothing is being kept. /// /// The measured consumer is MNW's custom-pages host. `u.makenot.work` /// serves a creator's page with a platform strip on it, and every link in /// that strip -- the brand, "View on makenot.work", the footer credit -- /// goes to the apex in the same tab, because going to the apex is the /// whole point of the strip. Said as [`External`](Self::External) all /// three would open tabs, which is a satellite document refusing to let go /// of a reader who asked to leave. /// /// # The split is intent, and the mechanism is still the renderer's /// /// A tab is window management, which is placement, which this vocabulary /// has held is the host's throughout -- the same call [`Frame`] placement /// and the clock a toast expires on both got. What a renderer cannot infer /// from an address alone is which of the two kinds of leaving it is, and /// that is what these two variants say. A terminal that has no tabs /// follows both the one way it can, and is not wrong. /// /// [`Frame`]: crate::chrome::Frame Leaving(String), /// Nothing leaves the machine. The renderer performs it from what it /// already holds, and no route is asked. /// /// Not "cheap" and not "fast", both of which are judgements. What it says /// is that there is no request, which is a fact the description knows and /// no renderer can infer. /// /// Who is obliged to read it is [`Renderer`](crate::Renderer)'s business: a /// client renderer redraws every frame from memory and gets this for free, /// a hybrid one has to be told. Read that type before implementing a /// renderer against this variant. Local, /// Wherever the reader came from. /// /// A route cannot name this, because which route it is depends on history /// the runtime holds and the description does not. That is what makes it a /// destination rather than a path some screen computes: the description /// says "back" and the host says where that is. /// /// # Whose history /// /// The runtime's, and this is the part worth stating. Each host runtime /// keeps what it has been asked for -- `Outcome::Screen` pushes through its /// own `remember` -- and answers this by popping it. A webview maps that /// onto the browser's history, not the other way round: the description /// cannot see a browser and must not be written against one. /// /// # Why not [`Local`](Self::Local) /// /// `Local` says no request is made at all. Going back usually makes one: /// the runtime pops an address and calls it. What this variant says is that /// the *address* is the host's to supply, which is a different fact, and /// [`is_local`](Self::is_local) stays false for it. /// /// One thing to say it with, so a key, a visible Close and a system gesture /// cannot disagree: /// /// ``` /// # use quasi_router::{Act, Action, Chrome}; /// Chrome::new().bind("escape", "Back", Action::back()); /// Act::new("Close", Action::back()); /// ``` Back, } /// An empty route rather than an empty external address, so a half-built /// [`Action`] is something this app would answer rather than somewhere it would /// send a user. Same reasoning as [`Method`]'s default being the safe verb. /// /// Written out because `#[default]` only applies to unit variants. impl Default for Destination { fn default() -> Self { Self::Route(String::new()) } } impl Destination { /// The route path, if it is one. /// /// `None` for an external address, which is the answer a host wants when it /// is deciding whether it can dispatch something. #[must_use] pub fn route(&self) -> Option<&str> { match self { Self::Route(path) => Some(path), // `Back` has no path *here*. The runtime supplies one when it pops // its history, and that is a route like any other by then. Self::External(_) | Self::Leaving(_) | Self::Local | Self::Back => None, } } /// The address as written, whichever kind it is. /// /// For rendering only. A host deciding whether to dispatch wants /// [`route`](Self::route), which cannot hand back something uncallable. /// /// Empty for [`Local`](Self::Local), which has no address to write: the /// absence of one is the whole of what that variant says. A renderer /// reaching here for a local action has skipped the branch it owes. #[must_use] pub fn as_str(&self) -> &str { match self { Self::Route(address) | Self::External(address) | Self::Leaving(address) => address, Self::Local | Self::Back => "", } } /// Whether it leaves the app. /// /// True for both ways of leaving it. What separates them is where the /// reader ends up, which is [`Leaving`](Self::Leaving)'s subject; a host /// asking this is asking whether it can dispatch the thing, and it cannot /// either way. #[must_use] pub const fn is_external(&self) -> bool { matches!(self, Self::External(_) | Self::Leaving(_)) } /// Whether performing it takes no request at all. /// /// The complement of neither of the others: an external address leaves the /// app and is still a request, and this is the case where nothing is asked. #[must_use] pub const fn is_local(&self) -> bool { matches!(self, Self::Local) } /// Whether it means "wherever I came from". /// /// The one question a host asks before dispatching, because it is the one /// destination whose address the host has and the description does not. #[must_use] pub const fn is_back(&self) -> bool { matches!(self, Self::Back) } } /// Where a call's answer lands, when the responder cannot say. /// /// The value of [`Action::replaces`], which carries the whole of when this is /// set at all: only for a route the description layer does not serve. Read that /// first. This type is about what may be named once you are already in that /// case. /// /// # Why three /// /// Measured on the MNW server's dashboard, across the two conversions that /// stopped on this. A region id covered neither. /// /// - Two acts target the repeated element they sit in and nothing else /// (`hx-target="closest .link-row"`, `closest .tag`). Both are shared /// partials called from several parents, so there is no id to name. /// - Fifteen acts target nothing at all: they fire and the surface they sit on /// is refetched, navigated away from, or reloaded whole (`data-after` in /// `frontend/src/core/dispatch.ts`). /// /// # Deliberately not `#[non_exhaustive]` /// /// [`Outcome`](crate::Outcome)'s reasoning, and for the same reason: every /// renderer has to decide what to draw for each of these, and a wildcard arm /// has nothing sensible to put in it. A member added here should break every /// host on purpose. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Replaces { /// The region with this [`Slot::id`]. /// /// What this was before it was three things, and still the common case. Region(String), /// The repeated element this control is drawn inside. /// /// A row of a list or of a table, without either side naming the other. /// The containment is already in the description — a [`Row`] holds its acts /// — so the alternative was making every repeated element author a unique /// id whose only purpose is to be pointed at from within itself. That is /// the reasoning [`Field::suggests`] used when it gave the field its own /// list rather than having two elements point at each other by id. /// /// Replaces the element rather than filling it: "remove this row" is what /// both measured sites mean, and a row that answered into itself would /// nest. Enclosing, /// Nothing on screen contains it, so what is showing is stale. /// /// The act fires and the surface it sits on is no longer trustworthy. A /// statement about staleness rather than a verb naming a mechanism, which /// is the same choice [`Invalidated`](crate::Invalidated) made and for the /// same reason: "reload the page" is a webview sentence, and two of the /// three renderers have no page to reload. /// /// What each renderer does with it: a webview asks the host to load the /// document again, a terminal redraws, an immediate-mode host does nothing /// because it was going to redraw anyway. /// /// This is the *control's* half. A described route says the same thing by /// answering [`Outcome::Screen`](crate::Outcome::Screen), which is the /// member for an effect no one region contains. Everything, } /// An address a control calls when it acts. /// /// Decision 2: an action is a route. The webview emits this as an `hx-get` or /// `hx-post`, the terminal binds a key to it, egui calls it directly. All three /// are calling the same path with the same verb. /// /// A route is not the only thing it can be: see /// [`Destination`]. Decision 2 still holds for everything the app answers, and /// an external address is the case it never covered. /// /// It does not carry a target. What a response replaces is the *response's* /// business, per decision 7, because the router is the only party that knows /// what it just changed. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Action { /// Asking or telling. /// /// Meaningless for a [`Destination::External`], which is nobody's route to /// answer. Left on the struct rather than moved inside `Destination` /// because a method that is ignored is simpler than two shapes of action. pub method: Method, /// Where it goes. pub destination: Destination, /// Values the control sends that are not in the path. /// /// A webview emits these as `hx-vals`; a terminal passes them straight /// through. Here so that no app hand-builds a query string, which is where /// escaping bugs live. /// /// Empty on a read. A read has nothing to send: its values are its address, /// so [`Action::with`] puts them in [`Self::carried`] instead. See /// [`Request`](crate::Request) for the whole of why the two are separate. pub params: Params, /// The view this control was offered under. /// /// A filtered list sends its filters on every control it draws, so that /// pressing one answers with the list you were looking at rather than with a /// default. Kept apart from [`Self::params`] because a screen that filters /// on `status` and also writes a `status` would otherwise have one name for /// two things, and the handler would read whichever landed first. /// /// Emitted as the query string, on the address itself, which is where a view /// belongs: the link is then the view, and a middle-click reaches the same /// place the control does. pub carried: Params, /// The region this call's answer replaces, when the responder cannot say. /// /// Normally nothing sets this and nothing should: a described route answers /// with a `Response::Fragment` naming the region it changed, `quasi-http` /// turns that into the transport's retarget header, and the router is the /// only party that knows what it just changed. That is decision 7 and it is /// unchanged. /// /// **Decision 7 assumes the responder is described, and a control may call /// a route that is not.** Every write on the MNW server's dashboard goes to /// a plain API route that quasi never sees and that answers with a status or /// a hand-rendered fragment. Those routes cannot name a region, so if the /// control does not either, nobody does: the answer lands wherever the /// transport's default puts it, which for htmx is inside the button that was /// pressed. That is not a second party deciding one thing. It is the only /// party that can decide, because the other one is outside the description /// layer. /// /// So: leave it unset when calling a described route, and set it when /// calling something else. A screen that sets it against a described route /// is overriding an answer that already knew better, and that is the misuse /// decision 7 was guarding against. /// /// What it may name is [`Replaces`]. A region is still the common case; /// the other two are the shapes the MNW server's undescribed routes were /// measured to want and a region id could not say. pub replaces: Option, /// The name to keep the answer under, when the answer is a file. /// /// `Some` means the response is not a view: nothing swaps, and the reader /// ends up holding a file called this. A webview makes that a browser /// download; a terminal writes it to disk; either way the screen said what /// it meant rather than a class name on a button implying it. /// /// Counted before adding it. Nine sites in the MNW server: five CSV export /// buttons across four dashboard templates, a sixth in the item-sales tab's /// own script, and three anchors carrying a `download` attribute. Six of the /// nine are writes, which is what makes this a property of the action rather /// than a kind of destination: a write cannot be a plain link, so the host /// has to be told, and until now it was told by /// `data-action="exportCsvButton"` plus two positional arguments. /// /// Independent of [`method`](Self::method). A read that saves is an anchor /// the browser downloads instead of navigating to; a write that saves has to /// be performed and then handed to the reader. Both are the same sentence /// here and differ only in the emitting. pub saves: Option, /// That this call waits on something which resolves once, in expected /// finite time. /// /// The mark itself is /// [`layout::Awaiting`] and its docs carry the whole of what is described /// and what is not. It rides on the action rather than on the control /// because the wait is a fact about the call, and because the same call is /// what a region is fed by: one mark, read twice. /// /// A control carrying one goes busy when it is pressed and refuses a second /// press until the answer lands, which is the double-submit guard the MNW /// server writes by hand twice against 57 spinners. A region carrying one /// through [`Slot::fed_by`] stands in and fills. /// /// Not remoteness, which [`Destination`] would already answer and which /// misses a heavy local query. Not slowness, which is a judgement. What it /// says is that something is outstanding and will finish. pub awaiting: Option, /// That the host makes this call, and the renderer does not. /// /// The same shape as /// [`saves`](Self::saves) and for the same reason it is stated rather than /// inferred: there are calls a renderer cannot make, and the alternative to /// saying so is a host reaching around the description to a control the /// description already owns. /// /// The case it was ruled on is an upload. One described destination, and /// three requests behind it: MNW asks its own server to sign a URL, PUTs the /// file to S3 with it, then tells the server the file landed. A renderer /// posting the field to the first of those would be wrong about the response /// shape and about where the bytes go, and the sequence is not describable — /// it is one address in the description because it is one thing to the /// reader, and three calls in the host because that is how the bytes get /// there. /// /// # What this costs, said out loud /// /// **A call marked this way is not portable.** Every other action in this /// vocabulary is a sentence any host can carry out; this one is a sentence /// only a host that already knows the chain can. A terminal meeting a /// described upload learns the accept list, the multiplicity and that it /// waits, and has nothing to perform. /// /// That is a real limit rather than a temporary one, and it is the price the /// ruling accepted for keeping the file going browser-to-storage instead of /// through the server. It is named here so the next host to meet one finds /// the reason rather than the gap. /// /// Independent of [`method`](Self::method) and of everything else on this /// type. The destination, the parameters and [`awaiting`](Self::awaiting) /// all still mean what they mean; what changes is who acts on them. pub by_host: bool, /// That this call's answer belongs in a mount of its own, not in this one. /// /// The same family as /// [`saves`](Self::saves) and [`replaces`](Self::replaces): all three say /// where the answer lands, and this one says it lands somewhere that is not /// here. /// /// The case it was ruled on is a compose window. goingson draws the same /// compose screen in the main window and in a window of its own, and /// [`Frame`](crate::Frame) already says what a mount puts around a screen. /// What was missing was the sentence that asks for the second mount at all, /// and without it the only ways left were a host script reaching around the /// description or a menu item the description cannot see. /// /// # What a mount of its own means, per host /// /// Deliberately not "a window". A mount is whatever the renderer puts a /// screen up in, and each host already has one: /// /// ```text /// webview a second document, which the host opens as a window /// immediate a viewport of its own /// terminal nothing; the call is performed where it stands /// ``` /// /// The terminal's answer is the interesting one and it is not a gap. A /// second mount in a terminal would be a split or a tab, and both are that /// renderer's furniture rather than the description's: a screen asking for /// one would be asking for a layout. So a terminal reads this and navigates, /// which is the honest degradation and is what /// [`Renderer`](crate::Renderer) exists to allow. /// /// # Why it is not a [`Destination`] /// /// A destination says where the call goes. This says where its answer is /// put, and the two are independent: the same address is the main window's /// compose screen and the compose window's, which is the whole point of /// [`Frame`]. One address, two mounts, and a screen that does not know /// which one it is in. /// /// Independent of [`method`](Self::method), and mutually exclusive with /// [`saves`](Self::saves) and [`replaces`](Self::replaces) by meaning /// rather than by type: an answer cannot land in a region here, be kept as /// a file, and be put up in a mount of its own. Nothing enforces that, /// because a type that made it impossible would have to be a fourth /// vocabulary for "where an answer goes". pub elsewhere: bool, /// That performing this replaces the whole document, rather than a region /// of it. /// /// The plain "go there" that every list of links on a public site is made /// of, and nothing said it until now: /// [`replaces`](Self::replaces) names a region, and the whole document is /// not one. /// /// On [`Action`] rather than as a fourth [`Destination`], knowingly against /// the precedent `f35aafee` set when it put the sibling fact on the /// destination. What differs between a navigation and a fragment swap is /// what happens to the document, and the document is the action's business; /// where it goes is the same place either way. A member would also foreclose /// stacking navigation with a later destination kind, since a value can only /// be one of them. /// /// # What each renderer does with it /// /// ```text /// webview the anchor and no verb, so the browser navigates /// terminal pushes a screen: anything open over it is put away first /// immediate swaps its view, the same way /// ``` /// /// The webview case is a narrowing rather than an addition. A read of a /// route already emits an `href` beside the verb, for middle-click, /// copy-link, crawlers and the page with JS off; a navigating act keeps that /// anchor and drops the htmx swap, which would otherwise put a whole screen /// inside the page it was meant to leave. /// /// Independent of [`method`](Self::method) on the type, and a read in /// practice: navigation is asking for a place. A renderer meeting it on a /// write performs the write as it otherwise would, since an anchor there /// would ask where the description said to tell. /// /// Mutually exclusive with [`replaces`](Self::replaces), /// [`saves`](Self::saves) and [`elsewhere`](Self::elsewhere) by meaning /// rather than by type, for the reason `elsewhere` records: an answer cannot /// land in a region here, be kept as a file, go up in a mount of its own, /// and be the whole document. pub navigates: bool, } impl Action { /// A read. pub fn get(path: impl Into) -> Self { Self { method: Method::Get, destination: Destination::Route(path.into()), params: Params::new(), carried: Params::new(), saves: None, replaces: None, awaiting: None, by_host: false, elsewhere: false, navigates: false, } } /// A write. pub fn post(path: impl Into) -> Self { Self { method: Method::Post, destination: Destination::Route(path.into()), params: Params::new(), carried: Params::new(), saves: None, replaces: None, awaiting: None, by_host: false, elsewhere: false, navigates: false, } } /// A write that removes what is at the address. /// /// Reach for it when the route the app already answers is a `DELETE`, not /// to editorialise about what a `POST` means: the verb here exists to /// address an interface, and a route that deletes over `POST` is still /// [`post`](Self::post). pub fn delete(path: impl Into) -> Self { Self { method: Method::Delete, destination: Destination::Route(path.into()), params: Params::new(), carried: Params::new(), saves: None, replaces: None, awaiting: None, by_host: false, elsewhere: false, navigates: false, } } /// A write that replaces what is at the address. pub fn put(path: impl Into) -> Self { Self { method: Method::Put, destination: Destination::Route(path.into()), params: Params::new(), carried: Params::new(), saves: None, replaces: None, awaiting: None, by_host: false, elsewhere: false, navigates: false, } } /// Somewhere outside the app. /// /// [`Method::Get`], because following a link asks and does not tell, and a /// host that ignores the method loses nothing by it. pub fn external(url: impl Into) -> Self { Self { method: Method::Get, destination: Destination::External(url.into()), params: Params::new(), carried: Params::new(), saves: None, replaces: None, awaiting: None, by_host: false, elsewhere: false, navigates: false, } } /// Somewhere outside the app, and the reader is going there. /// /// [`external`](Self::external)'s other half. See [`Destination::Leaving`] /// for the split: a reference is kept aside, and this replaces the page. /// /// [`Method::Get`] for `external`'s reason. pub fn leaving(url: impl Into) -> Self { Self { method: Method::Get, destination: Destination::Leaving(url.into()), params: Params::new(), carried: Params::new(), saves: None, replaces: None, awaiting: None, by_host: false, elsewhere: false, navigates: false, } } /// Wherever the reader came from. /// /// See [`Destination::Back`], which carries the whole argument: the address /// is the host's and the description does not have it. One thing to say it /// with, so a key binding, a visible Close and a system gesture cannot /// disagree about where back goes. /// /// [`Method::Get`], for [`external`](Self::external)'s reason: what the /// host performs is a read of somewhere it has been, and the safe verb is /// the honest default. #[must_use] pub fn back() -> Self { Self { method: Method::Get, destination: Destination::Back, params: Params::new(), carried: Params::new(), saves: None, replaces: None, awaiting: None, by_host: false, elsewhere: false, navigates: false, } } /// Something that happens without a request. /// /// See [`Destination::Local`], and read it before reaching for this: what /// happens is named by the member this action is set on, never by the /// action itself. A [`Node::Act`] built straight from this says only "press /// this and something local happens", which no renderer can perform. /// /// [`Method::Get`], for [`external`](Self::external)'s reason: nothing is /// asked, so nothing reads the verb, and the safe one is the honest /// default. [`params`](Self::params) still carries what the behaviour acts /// on — which suggestion was picked, which notice was dismissed — because /// that is a value and not an address. #[must_use] pub fn local() -> Self { Self { method: Method::Get, destination: Destination::Local, params: Params::new(), carried: Params::new(), saves: None, replaces: None, awaiting: None, by_host: false, elsewhere: false, navigates: false, } } /// Put this call's answer into the region with this id. /// /// For a route the description layer does not serve. See /// [`replaces`](Self::replaces) before reaching for it. #[must_use] pub fn replacing(mut self, region: impl Into) -> Self { self.replaces = Some(Replaces::Region(region.into())); self } /// Put this call's answer in place of the repeated element it sits in. /// /// [`Replaces::Enclosing`], and the same caveat as /// [`replacing`](Self::replacing): only for a route the description layer /// does not serve. #[must_use] pub fn replacing_enclosing(mut self) -> Self { self.replaces = Some(Replaces::Enclosing); self } /// Say this call's effect is contained by nothing on screen. /// /// [`Replaces::Everything`], and the same caveat as /// [`replacing`](Self::replacing): only for a route the description layer /// does not serve. A described route answers /// [`Outcome::Screen`](crate::Outcome::Screen) instead. #[must_use] pub fn invalidating(mut self) -> Self { self.replaces = Some(Replaces::Everything); self } /// Keep the answer as a file with this name, rather than showing it. #[must_use] pub fn saving(mut self, filename: impl Into) -> Self { self.saves = Some(filename.into()); self } /// Say that this call waits on something which resolves once, with nothing /// countable about the wait. /// /// The common case: a round trip to a payment provider, a report the server /// assembles, a heavy local query. The renderer draws it indeterminate, /// because manufacturing a figure for it is the prediction /// [`layout::Awaiting`] refuses. #[must_use] pub const fn awaiting(mut self) -> Self { self.awaiting = Some(layout::Awaiting::unmeasured()); self } /// Say that it waits, and how much there is to get through. /// /// Only with a measured figure. An upload knows its file length; nothing /// else here may guess one, since a renderer cannot tell a measurement from /// an estimate once it is written down. #[must_use] pub const fn awaiting_amount(mut self, amount: u64) -> Self { self.awaiting = Some(layout::Awaiting::of(amount)); self } /// Whether this call waits on something. #[must_use] pub const fn awaits(&self) -> bool { self.awaiting.is_some() } /// Put this call's answer up in a mount of its own. /// /// See [`elsewhere`](Self::elsewhere) for what a mount is on each host and /// for why a terminal is allowed to ignore it. #[must_use] pub const fn elsewhere(mut self) -> Self { self.elsewhere = true; self } /// Performing this replaces the whole document. /// /// See [`navigates`](Self::navigates) for what each renderer does with it, /// and reach for it where the act is a plain "go there": a search result /// standing for a page, a name standing for the profile behind it. #[must_use] pub const fn navigating(mut self) -> Self { self.navigates = true; self } /// The host makes this call, not the renderer. /// /// See [`by_host`](Self::by_host) for what it means and for the /// portability it costs. Reach for it only where a renderer genuinely /// cannot make the call, which today is one case: a destination the host /// reaches through a sequence of requests rather than one. #[must_use] pub const fn by_host(mut self) -> Self { self.by_host = true; self } /// The route this calls, if it calls one. #[must_use] pub fn route(&self) -> Option<&str> { self.destination.route() } /// Send a value along with the call. /// /// On a write this is the payload: what the control is telling the route. /// On a read it is the address, because a read sends nothing and its values /// are where it goes — so this lands in [`Self::carried`] rather than in /// [`Self::params`], and a read's two bags are never both populated. /// /// That is what keeps the rule one sentence at the reading end: a filter is /// in `carried` whichever verb offered it. /// /// A [`Destination::Local`] takes the write's side of that whatever its /// method says. The read rule rests on the values being the address, and a /// local action has no address for them to be: putting them in `carried` /// would file them as the view a call was made under, and nothing is called. /// So they are the payload — which suggestion was picked, which notice was /// dismissed — and that is the bag every renderer reads for one. #[must_use] pub fn with(mut self, name: impl Into, value: impl Into) -> Self { if self.method.mutates() || self.destination.is_local() { self.params.insert(name, value); } else { self.carried.insert(name, value); } self } /// Keep this control pointed at the view it was offered under. /// /// What a filtered screen puts on every control it draws. Distinct from /// [`Self::with`] on a write, and the same thing as it on a read. #[must_use] pub fn carrying(mut self, name: impl Into, value: impl Into) -> Self { self.carried.insert(name, value); self } } /// A small labelled thing: a badge, a chip, a tag. /// /// [`Node::Token`]. Extracted because a row can carry these now /// ([`Row::tokens`], against `makeover-layout`'s `RowPart::Tokens`), and the /// alternative was defining the same fields twice and watching them drift. /// /// The tone rides on the tag rather than on whatever holds it, which is what /// lets a strip of them say different things: a neutral type and an amber /// status, side by side in one row. /// /// No `Hash`, because it can hold an [`Action`], which holds [`Params`], which /// is a `Vec`. Same derive set as `Action` for that reason. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Tag { /// Whether it answers a click, and whether it can be removed. pub kind: layout::Token, /// What it says. pub label: String, /// What it is saying. pub tone: layout::Tone, /// Whether it is currently held down. Only meaningful for a chip. pub latched: bool, /// What clicking it calls, if it answers a click. pub action: Option, /// The detail behind the label, for a renderer that has somewhere to put /// it. /// /// A badge's label is short because a badge is small, and the shipped /// goingson board says the short thing and carries the long one: "Blocked" /// with the block depth behind it, "Unblocks 3" with the wording, "Cycle" /// with the repair instruction. A described card said the label and /// dropped the detail, so the description could not say what the shipped /// markup already did. /// /// Standing detail, not a message: it is true whenever the tag is on /// screen, which is what makes it a property of the tag rather than /// something a response says. **Every renderer may drop it**, and dropping /// is the graceful degradation this vocabulary keeps choosing rather than a /// gap; what each one does is stated in its own docs. Never put anything /// here that is the only place a fact appears. pub hint: Option, } impl Tag { /// A neutral badge: it says something and answers nothing. pub fn badge(label: impl Into) -> Self { Self { kind: layout::Token::Badge, label: label.into(), tone: layout::Tone::Neutral, latched: false, action: None, hint: None, } } /// A chip that calls a route when clicked. /// /// Not removable. A removable chip is a different control with a different /// affordance, so it says so rather than being inferred from carrying an /// action. pub fn chip(label: impl Into, action: Action) -> Self { Self { kind: layout::Token::Chip { removable: false }, label: label.into(), tone: layout::Tone::Neutral, latched: false, action: Some(action), hint: None, } } /// A chip the reader can take off again. /// /// The affordance a removable chip has and a plain one does not, said /// rather than inferred, which is the same reason [`chip`](Self::chip) is /// not removable by virtue of carrying an action. pub fn removable(label: impl Into, action: Action) -> Self { Self { kind: layout::Token::Chip { removable: true }, ..Self::chip(label, action) } } /// Set what it is saying. #[must_use] pub const fn tone(mut self, tone: layout::Tone) -> Self { self.tone = tone; self } /// Hold it down. Only meaningful for a chip. /// /// No argument, which is [`Choice::chosen`]'s shape and its reason. A /// description says a chip is held down by saying so, and a caller with a /// condition writes the condition as a guard -- `latched when is_shown(..)` /// -- rather than handing over a bool. Taking one meant the fact crossed as /// a value, and a `bool` has no stand-in, so a chip strip could be described /// and could not be compiled: `symbolic::PLACED` is where the guard is /// allowed and why. #[must_use] pub const fn latched(mut self) -> Self { self.latched = true; self } /// The detail behind the label; see [`hint`](Self::hint). /// /// A renderer with nowhere to put it drops it, so this must never be the /// only place a fact appears. #[must_use] pub fn hinted(mut self, hint: impl Into) -> Self { self.hint = Some(hint.into()); self } } /// One option offered by a field, owned. /// /// The borrowed original is `makeover-layout`'s [`layout::Choice`]. Two strings /// rather than one for the reason recorded there: the submitted value and the /// read label are different facts, and every renderer that collapsed them has /// had to un-collapse them later. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Choice { /// What is submitted. pub value: String, /// What is read. pub label: String, /// Why it cannot be picked right now, when it cannot. /// /// The borrowed original is [`layout::Choice::unavailable`], and everything /// it says applies. One member rather than a flag beside a reason, so an /// option greyed out with no explanation stays unsayable here too. pub unavailable: Option, /// The line under the label that says what picking this means. /// /// The borrowed original is [`layout::Choice::detail`], where the /// measurement and the per-host placement live. Here it matters for the /// reason `picks` does not exist on the layout type: an owned mirror is /// what a handler builds, and a tier list built from the database carries a /// price and a description that were never `&'static str`. pub detail: Option, /// Whether this is the option currently chosen. /// /// The borrowed original is [`layout::Choice::chosen`], where the reason it /// is per option rather than one value at the field lives. What it buys /// here specifically: a described option list is a loop, and a residual /// holds one compiled body per loop, so a difference decided by comparing /// the field's value against each option is a difference the body cannot /// carry. Said per option it is a branch inside the body, which is a shape /// a residual has. /// /// Never set alongside a value on the same field. quasi-declare refuses the /// pair where a screen is written; nothing here can, and a field that says /// both marks two options. pub chosen: bool, } impl Choice { /// An option whose submitted value is also its label. pub fn plain(value: impl Into) -> Self { let value = value.into(); Self { label: value.clone(), value, unavailable: None, detail: None, chosen: false, } } /// An option that reads differently from what it submits. pub fn new(value: impl Into, label: impl Into) -> Self { Self { value: value.into(), label: label.into(), unavailable: None, detail: None, chosen: false, } } /// The same option, not pickable yet, and why. #[must_use] pub fn unless(mut self, reason: impl Into) -> Self { self.unavailable = Some(reason.into()); self } /// The same option, with the line that says what picking it means. /// /// See [`detail`](Self::detail). Two different sentences from /// [`unless`](Self::unless), and an option carrying both has said two /// things: what the tier is, and that it is not available yet. #[must_use] pub fn detailing(mut self, detail: impl Into) -> Self { self.detail = Some(detail.into()); self } /// The same option, marked as the one currently chosen. /// /// See [`chosen`](Self::chosen). Takes no argument: a description says /// `chosen when ` and the guard decides whether the call is /// made, which is what puts the branch inside the option's own markup /// rather than inside a bool the markup does not vary on. #[must_use] pub const fn chosen(mut self) -> Self { self.chosen = true; self } /// Whether the option can be picked right now. #[must_use] pub const fn available(&self) -> bool { self.unavailable.is_none() } /// Borrow as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::Choice<'_> { let mut choice = layout::Choice::new(&self.value, &self.label); if self.chosen { choice = choice.chosen(); } if let Some(detail) = self.detail.as_deref() { choice = choice.detailing(detail); } match self.unavailable.as_deref() { Some(reason) => choice.unless(reason), None => choice, } } } /// One theme a picker offers, owned. /// /// The borrowed original is `makeover-layout`'s [`layout::ThemeChoice`], and /// the reason it is not [`Choice`] is recorded there: a theme is four facts and /// an option is two. The two extra ones — which group it sits in and how /// legible it measured — are resolved by the theme layer and neither survives /// being written into a label. /// /// Built from `makeover::ThemeOption` at each adopter. That conversion is the /// seam the layering costs: `quasi-router` takes only `makeover-layout`, which /// takes nothing at all, so the crate that reads theme files off disk is not in /// this graph and the app is what joins them. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct ThemeChoice { /// The id stored, and what the picker submits. pub id: String, /// What the picker reads. pub name: String, /// Which group it belongs to. pub variant: layout::ThemeVariant, /// How legible its muted text measured. pub contrast: layout::Contrast, } impl ThemeChoice { /// A theme, with everything a picker needs to place and mark it. /// /// Every fact is an argument, matching [`layout::ThemeChoice::new`] and for /// its reason: a theme with no variant has no group and a theme with no /// tier has no badge, so both are the control rather than trimmings on it. pub fn new( id: impl Into, name: impl Into, variant: layout::ThemeVariant, contrast: layout::Contrast, ) -> Self { Self { id: id.into(), name: name.into(), variant, contrast, } } /// Borrow as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::ThemeChoice<'_> { layout::ThemeChoice::new(&self.id, &self.name, self.variant, self.contrast) } } /// One entry in a field's suggestion list, owned. /// /// The borrowed original is `makeover-layout`'s [`layout::Candidate`], and the /// reason it is not [`Choice`] is recorded there: an option and a candidate are /// submitted the same way and **read differently**. An option is picked out of /// a set the user can see whole; a candidate is offered out of a set nobody can /// see, so it carries the line that tells it from its neighbours. /// /// # What this mirror carries that the borrowed type cannot /// /// [`picks`](Self::picks). What happens when a candidate is chosen is an /// [`Action`], and `Action` is not a word the description layer has — exactly /// as [`Field::suggests`] has no counterpart on [`layout::Field`]. So the /// member lives here, on the same terms and for the same reason. /// No `Hash`, unlike [`Choice`]: [`Action`] is not hashable and a candidate /// carrying one could not be. Nothing in the tree hashes a suggestion row. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Candidate { /// What is submitted, and what picking writes into the field by default. pub value: String, /// What is read. pub label: String, /// The second line: what orients this candidate among rows that read alike. /// /// The borrowed original is [`layout::Candidate::detail`]. [`None`] draws /// one line rather than an empty second one. pub detail: Option, /// What picking this candidate does instead of writing its value. /// /// **Picking is local by default, not by definition.** Absent an action, /// picking writes /// [`value`](Self::value) into the field that owns the list, which is what /// every site that exists today wants and what ownership buys. A candidate /// that carries one has that performed instead. /// /// The two measured sites are why. MNW's search box navigates — each /// candidate is a project, item or creator page, nothing is written into /// the box, and the typed value is discarded — so its candidates carry /// `Action::get(url)`. MNW's tag box adds a facet and re-reads, which is /// the same route its drill-down checkbox already calls, so the handover /// `choose()` in hand-written JS disappears rather than moving. Neither /// wants the one behaviour ownership gives for free. /// /// It is on this type rather than on [`Choice`] deliberately. `Choice` is /// the most-consumed struct in the vocabulary and a per-row action there /// would land on every option list in the tree the day it shipped. Confined /// here, the objection this raises against itself — that the route decides /// where a pick goes, per row, which is more than a description says about /// any other control — is confined with it. pub picks: Option, } impl Candidate { /// A candidate whose submitted value is also its label. pub fn plain(value: impl Into) -> Self { let value = value.into(); Self { label: value.clone(), value, detail: None, picks: None, } } /// A candidate that reads differently from what it submits. pub fn new(value: impl Into, label: impl Into) -> Self { Self { value: value.into(), label: label.into(), detail: None, picks: None, } } /// The same candidate, with the line that tells it from its neighbours. #[must_use] pub fn detailed(mut self, detail: impl Into) -> Self { self.detail = Some(detail.into()); self } /// The same candidate, picking it performs this instead of writing. #[must_use] pub fn picking(mut self, action: Action) -> Self { self.picks = Some(action); self } /// Borrow as the description layer's own type. /// /// [`picks`](Self::picks) does not survive the borrow, and cannot: the /// description layer has no [`Action`]. A renderer reading a candidate /// through this sees the row and not what picking it does, so a renderer /// that performs picks reads this type rather than the borrowed one. #[must_use] pub fn as_layout(&self) -> layout::Candidate<'_> { let candidate = layout::Candidate::new(&self.value, &self.label); match self.detail.as_deref() { Some(detail) => candidate.detailed(detail), None => candidate, } } } /// One entry in a file field's accept list, owned. /// /// The borrowed original is [`layout::Accepted`], and everything it says /// applies: three shapes because all three are in the measured sites, and a /// suffix names no family because a suffix-to-family table rots. This is the /// same split [`Choice`] makes, and for the same reason — a screen is built by /// a handler and outlives the strings it was built from. /// /// [`layout::Family`] is used directly rather than mirrored. It borrows nothing, /// so there is no owned counterpart to write and a second spelling would only be /// a second place to add the fourth family to. #[derive(Debug, Clone, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Accepted { /// Every file of a family: `image/*` and its siblings. Family(layout::Family), /// One media type: `image/jpeg`, `text/csv`. Type(String), /// One file-name suffix, with its leading dot: `.zip`, `.tar.gz`. Suffix(String), } impl Accepted { /// Every file of a family. #[must_use] pub const fn family(family: layout::Family) -> Self { Self::Family(family) } /// One media type. #[must_use] pub fn media_type(media_type: impl Into) -> Self { Self::Type(media_type.into()) } /// One file-name suffix, written with its leading dot. #[must_use] pub fn suffix(suffix: impl Into) -> Self { Self::Suffix(suffix.into()) } /// Borrow as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::Accepted<'_> { match self { Self::Family(family) => layout::Accepted::Family(*family), Self::Type(media_type) => layout::Accepted::Type(media_type), Self::Suffix(suffix) => layout::Accepted::Suffix(suffix), } } } /// A picture and where it is. /// /// One type, since makeover-layout 0.42.0. The borrowed twin this used to /// mirror carried the same five fields and a single method, had no consumer /// anywhere in the makeover suite, and was built in exactly one place: here. /// What survives of that split is the rule which produced it, and [`Act`] still /// carries it: makeover-layout defers every address, so [`src`](Self::src) is /// this crate's and a renderer wanting the bytes comes here for them. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Image { /// Where the picture is. A URL, or whatever the host resolves. /// /// Never interpreted here. A renderer escapes it for wherever it is putting /// it, the way it does every other app-supplied string. pub src: String, /// What the picture says, for anything not showing it. /// /// Empty means decorative. `layout::Image` carries the argument for why /// this is a `String` and not an `Option`. pub alt: String, /// A visible line under it, where the app wants one. pub caption: Option, /// How it sits in the box it is given. pub fit: layout::Fit, /// The picture's own dimensions, where the app knows them. /// /// `layout::Image::intrinsic` carries the argument. The short form: without /// it a renderer cannot hold the picture's place, so the picture takes no /// room until it arrives and then shoves the page down. pub intrinsic: Option, /// Whether the picture is needed with the screen, or can arrive later. pub loading: layout::Loading, } impl Image { /// A picture at a source, carrying its own proportions. pub fn new(src: impl Into, alt: impl Into) -> Self { Self { src: src.into(), alt: alt.into(), caption: None, fit: layout::Fit::Natural, intrinsic: None, loading: layout::Loading::Eager, } } /// The picture's own dimensions, so a renderer can hold its place. #[must_use] pub const fn intrinsic(mut self, width: u32, height: u32) -> Self { self.intrinsic = Some(layout::Extent::new(width, height)); self } /// This picture is not on screen yet; it can arrive when it is near. #[must_use] pub const fn lazy(mut self) -> Self { self.loading = layout::Loading::Lazy; self } /// A visible line under it. #[must_use] pub fn caption(mut self, caption: impl Into) -> Self { self.caption = Some(caption.into()); self } /// How it sits in its box. #[must_use] pub const fn fit(mut self, fit: layout::Fit) -> Self { self.fit = fit; self } /// Whether the alt text says anything. /// /// An empty `alt` is a claim that the picture adds nothing to the text /// beside it, so a renderer that cannot show the bytes draws nothing rather /// than standing in for it. Came off `layout::Image` when the two merged in /// makeover-layout 0.42.0. #[must_use] pub fn speaks(&self) -> bool { !self.alt.is_empty() } } /// One figure with a caption, owned. /// /// The borrowed original is [`layout::Figure`], and everything it says applies: /// the value is text because only the app knows whether the number is a /// percentage, a duration or a ratio, and the tone is carried because no /// renderer can work out that a streak of zero is worth colouring. /// /// What it calls, if it calls anything, is not here. That is an address, which /// `makeover-layout` never names, and it rides beside the figure in /// [`Node::Stats`] the way [`Row::activate`] rides beside a row's parts. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Figure { /// The number, formatted the way the app means it to read. pub value: String, /// What it counts. The caption under the value. pub caption: String, /// How the value has moved, if the app is tracking that. /// /// Mirrors `layout::Figure::change`, added there at 0.13.0. Text for the /// same reason [`value`](Self::value) is: only the app knows whether a move /// reads as `+12.5%`, `+3` or `2x`. /// /// This is what [`tone`](Self::tone) was for. Counted before adding it: the /// MNW server has four screens whose stat card is a label, a value and a /// delta, and on all four the delta is the toned part while the number /// itself is an ordinary fact. Without it the delta folds into the caption, /// which loses the tone and turns a second smaller line into a longer first /// one. pub change: Option, /// What the figure means. [`layout::Tone::Neutral`] is an ordinary fact. /// /// Applies to [`change`](Self::change) where there is one, and to the value /// where there is not. The renderer decides which element that lands on. pub tone: layout::Tone, } impl Figure { /// A figure that is an ordinary fact. pub fn new(value: impl Into, caption: impl Into) -> Self { Self { value: value.into(), caption: caption.into(), change: None, tone: layout::Tone::Neutral, } } /// How the value has moved. #[must_use] pub fn change(mut self, change: impl Into) -> Self { self.change = Some(change.into()); self } /// What the figure means. #[must_use] pub const fn tone(mut self, tone: layout::Tone) -> Self { self.tone = tone; self } /// Borrow as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::Figure<'_> { layout::Figure { value: &self.value, caption: &self.caption, change: self.change.as_deref(), tone: self.tone, } } } /// What a list has that it is not showing. /// /// Deliberately not virtual scrolling, which is the neighbouring thing and is /// not a description concern: goingson's `virtual-scroller.js` windows rows the /// app already holds, which is a renderer performance technique. This is a fact /// about the data — there are rows that were never fetched — and only the thing /// that fetched them knows it. /// # The position is the description layer's, the addresses are this one's /// /// [`layout::Paging`] holds where the reader is and how big the set is, and it /// is the same [`layout::Window`] a carousel instantiates. What it cannot hold /// is the way to ask for the next part, because `makeover-layout` names no /// actions at all. So this is the pairing, the way [`Row`] pairs its parts with /// [`Row::activate`] and [`Node::Stats`] pairs a figure with an address. /// /// That split is the reason the two can share an implementation. A carousel's /// frames are already in the description and moving between them asks nobody /// anything; a page's rows were never fetched and moving costs a round trip. /// Presence is the difference, and it shows up here as whether there are /// actions rather than as a second copy of the arithmetic. /// /// # Both directions, and why neither is required /// /// A host that pages forward only supplies [`forward`](Self::forward) alone and /// gets a load-more control. One that pages both ways supplies both and gets /// prev/next. A renderer offers what it was given and never invents the other, /// because an address this crate made up would not resolve. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Rest { /// Where the reader is, and how much there is. pub paging: layout::Paging, /// What asking for the next part calls. pub forward: Option, /// What asking for the previous part calls. pub back: Option, /// The pages a reader may go straight to, each with its own address. /// /// Empty is the common case and is prev/next paging. /// /// # An address per page, not a count /// /// A renderer cannot build page 5's address out of /// [`forward`](Self::forward) and [`back`](Self::back) without knowing the /// address grammar, and that grammar is the private vocabulary a conversion /// exists to retire. So the router names the addresses and the renderer /// draws them, which is the split [`Row::activate`] and [`Node::Stats`] /// already make. A number alone would put URL construction in three hosts. /// /// # Which pages, and why that is the description's call too /// /// A set of 400 pages is not a strip of 400 controls, so somebody windows /// it, and it is not this crate: MNW's `build_pagination_range` already /// windows to five around the reader, and a renderer inventing its own /// would give a different answer per host for one list. What arrives here /// is the pages being offered, in the order they should read. /// /// Which of them the reader is on is not stated twice: /// [`paging`](Self::paging) says it, and a renderer marks the jump whose /// [`Jump::page`] matches. pub jumps: Vec, /// What a request decides about runs of the controls this pager draws. /// /// Empty answering a request, filled by a staged twin. The index space is /// back, then the jumps, then forward, which is the order a pager is drawn /// in and not the order a declaration may write them in -- see /// [`Marking::placed`](crate::stage::Marking::placed), which says that the /// numbering is the render order for every container that draws from more /// than one list. pub marks: crate::stage::Marks, } impl crate::stage::Marking for Rest { /// Back, then the jumps, then forward: what a renderer draws, in order. fn placed(&self) -> usize { usize::from(self.back.is_some()) + self.jumps.len() + usize::from(self.forward.is_some()) } fn mark(&mut self, mark: crate::stage::Mark) { self.marks.add(mark); } } /// One page a reader can go straight to. /// /// The number is carried rather than implied by position, because what a strip /// offers is a window around the reader -- pages 8 through 12 of 20 -- and an /// index into that list is not the page it names. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Jump { /// Which page this reaches, counting from one, the way /// [`layout::Paging::page`] counts. pub page: usize, /// What going there calls. pub action: Action, /// Whether this is the page the reader is on. /// /// Said per jump rather than worked out by comparing each against the /// paging, which is what every renderer did before this existed. The reason /// is [`Choice::chosen`]'s, exactly: a strip is a loop, a residual holds one /// compiled body per loop, and "exactly one row differs" is not a property /// of the body when the difference is a comparison the body does not make. /// Said here it is a branch inside the body. /// /// A strip that marks none is a host that windowed its pages badly enough /// to offer a strip the reader is not in. It is worth being able to see and /// it is not worth refusing to draw a list over, which is what the renderers /// already said about the same case. pub here: bool, } impl Jump { /// Going to this page calls this route. #[must_use] pub const fn new(page: usize, action: Action) -> Self { Self { page, action, here: false, } } /// The same jump, marked as the page the reader is on. /// /// See [`here`](Self::here). #[must_use] pub const fn here(mut self) -> Self { self.here = true; self } } impl Rest { /// The first `shown` of something longer, and how to ask for more. /// /// The load-more shape. The window starts at the beginning and grows, so /// there is no page to number and no way back to offer. #[must_use] pub const fn more(shown: usize, action: Action) -> Self { Self { paging: layout::Paging::more(shown), forward: Some(action), back: None, jumps: Vec::new(), marks: crate::stage::Marks::none(), } } /// The first `shown` of something longer, with nothing to press. /// /// [`more`](Self::more) without the address, which is a real shape rather /// than a degenerate one: audiofiles' bulk-rename preview caps its table at /// fifty rows because fifty is all a modal can show, and the rename acts on /// every name whether or not it was drawn. There is nowhere to ask for the /// rest because the rest were never missing -- the cap is a rendering /// budget, and what the reader needs to know is that there are more than /// these. /// /// A constructor and not a struct literal, which is what those two sites /// were: a literal names every field, so it breaks on each one this type /// gains, and it broke on `jumps`. #[must_use] pub const fn showing(shown: usize) -> Self { Self { paging: layout::Paging::more(shown), forward: None, back: None, jumps: Vec::new(), marks: crate::stage::Marks::none(), } } /// One page of `per`, starting at `from`. /// /// Arrives with no addresses; [`forward`](Self::forward) and /// [`back`](Self::back) add whichever of them exists. A first page has no /// back and a last page has no forward, and saying so by leaving one off is /// how a renderer knows to draw the control disabled rather than absent. #[must_use] pub const fn page(from: usize, per: usize) -> Self { Self { paging: layout::Paging::pages(from, per), forward: None, back: None, jumps: Vec::new(), marks: crate::stage::Marks::none(), } } /// How many there are altogether. /// /// Left unsaid by a host that will not pay for the count, and then left /// unsaid for good: a total arriving on a later pass widens the text that /// prints it. See "First paint is final paint" in `makeover-layout`'s /// header, and "What a handler owes the first paint" in this crate's. #[must_use] pub const fn of(mut self, total: usize) -> Self { self.paging = self.paging.of(total); self } /// What asking for the next part calls. #[must_use] pub fn forward(mut self, action: Action) -> Self { self.forward = Some(action); self } /// What asking for the previous part calls. #[must_use] pub fn back(mut self, action: Action) -> Self { self.back = Some(action); self } /// Offer a jump straight to this page. /// /// See [`jumps`](Self::jumps). Adds rather than replaces, because a strip /// is built one page at a time out of whatever window the host chose, and /// the order of the calls is the order they read. #[must_use] pub fn jumping(mut self, jump: Jump) -> Self { self.jumps.push(jump); self } /// Borrow as the description layer's own type. #[must_use] pub const fn as_layout(&self) -> layout::Paging { self.paging } } /// Prose in a row part: what it says, and whether it is markdown. /// /// `secondary` has always been a `String`, and three call sites had markdown to /// put in it: the goingson projects card's description, the mail list's body /// preview, and a contact's note next. Each put the **source** in, so a row read /// `**Ships Q3.** See [the brief](https://...)` where the screen it stands in /// for reads the sentence. Flattening at the call site fixes what the user sees /// and loses the fact on the way: a renderer receiving the row cannot tell text /// an author typed from markdown somebody already flattened, so it cannot decide /// for itself, and the flattening is copied per site. /// /// This is [`Meter`]'s answer, not [`Node`]'s. The row still holds no node it /// holds a two-case value saying which of two things its string is. A webview /// renders the markdown inline, a terminal can emit bold, and a renderer that /// wants neither flattens it, each from the same description. /// /// [`Text`](Self::Text) is the default in every sense: `From<&str>` and /// `From` both produce it, so `.secondary("...")` means what it always /// meant and no existing call site changes. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Prose { /// Text as written. A renderer escapes it and draws it, and nothing in it /// is markup however it is punctuated. Text(String), /// Markdown source, carried as source for the reason [`Node::Rich`] does: /// every renderer has an honest answer because each renders it its own way, /// and nothing here is markup a renderer has to trust. Rich(String), } /// How much of the markdown format a source may use. /// /// One of the two axes [`Node::Rich`] carries, and the one about *shape*. /// [`Trust`] is the other, about *provenance*. They correlate -- the sources an /// app does not vouch for are usually the ones it also wants to keep plain -- /// and they are not the same question, which is why fusing them was a mistake /// worth undoing: a platform's own policy page is prose it wrote and wants /// followed, and a creator's long-form description is a document it did not /// write and still wants tables in. /// /// Nothing here says *phrase*. One line of markdown inside a row is decided by /// where the node sits rather than by what it declares: a row part holds no /// node, and the run a cell holds is rendered inline whatever this says. See /// the renderer's row handling. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Richness { /// Paragraphs, lists, emphasis, code and links. /// /// The default, and what ordinary prose needs. No tables, no footnotes, no /// task lists, no images -- a page's own sentence does not want them and a /// stranger's paragraph should not have them by default. #[default] Sentence, /// Everything the format has: tables, task lists, footnotes, strikethrough, /// smart punctuation and images. /// /// For a source that is a document rather than a sentence. A creator's /// long-form item description is the measured consumer. Document, } /// How far the app vouches for a source. /// /// [`Node::Rich`]'s other axis. See [`Richness`] for why they are two. /// /// **The default is [`Untrusted`](Self::Untrusted)**, written out rather than /// derived, for the reason `Discovery::indexable` is: the unsafe direction has /// to be the one somebody types. A description that said nothing about /// provenance and got the permissive treatment would be a hole nobody could see /// in a diff. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Trust { /// Somebody the app does not vouch for wrote it. /// /// A renderer hardens it: links get `nofollow`, raw markup in the source is /// dropped rather than passed through, and schemes a host would fetch are /// filtered. Forum posts, item descriptions, anything a reader typed. #[default] Untrusted, /// The app wrote it. /// /// The screen's own copy, in the same repository as the screen. A renderer /// takes it at its word: its links are the app's own and are followed, and /// its markup is the app's own. /// /// This is not a claim about the *reader*; it is a claim about the author. /// A string that reached the screen from a database is untrusted however /// well-behaved it has been. Trusted, } impl Prose { /// Markdown, to be rendered by whoever draws it. pub fn rich(source: impl Into) -> Self { Self::Rich(source.into()) } /// The string, whichever case this is. /// /// For a renderer that treats both the same, and for a test that does not /// care. A renderer that draws this without looking at the case is drawing /// markdown as text, which is the bug this type exists to make visible /// rather than impossible. #[must_use] pub fn source(&self) -> &str { match self { Self::Text(text) | Self::Rich(text) => text, } } /// Whether there is anything to draw. #[must_use] pub fn is_empty(&self) -> bool { self.source().is_empty() } } impl From for Prose { fn from(text: String) -> Self { Self::Text(text) } } impl From<&str> for Prose { fn from(text: &str) -> Self { Self::Text(text.to_owned()) } } impl From<&String> for Prose { fn from(text: &String) -> Self { Self::Text(text.clone()) } } /// How much of a set is done, owned. /// /// The borrowed original is [`layout::Meter`], which arrived at 0.10.0 for this. /// Before it, a screen with a progress bar concatenated the two numbers into its /// heading — "Subtasks 3/7" — which keeps both facts and loses the reading, the /// same way a toned status badge read as prose before [`Row::tokens`]. /// /// Its own struct as of 0.11.0, having been the inline payload of /// [`Node::Meter`]. Extracted for the reason [`Tag`] was: a row can carry one /// now ([`Row::meter`], against `makeover-layout`'s `RowPart::Proportion`), and /// the alternative was defining the same four fields twice and watching them /// drift. /// /// Carries the pair rather than a percentage for the reason [`layout::Meter`] /// gives: a bar that is full because it landed exactly and one that is full /// because it ran over are the same width and not the same fact. /// /// This carries a proportion, and one answer never carries motion. That is the /// whole of the restriction: **a meter does not tick**, so no renderer animates /// one between answers. /// /// The proportion need not be of a static set. Files written of files to write /// and subtasks done of subtasks are, and audiofiles' transport is not: it /// states a playback position against a duration, re-answered as the host /// redraws. That is the same member under the same rule, and the doc used to /// call the fact static, which was true of the first consumers and never of the /// restriction. /// /// Live progress is described by re-answering. Each answer states the /// proportion as it stood when the route was asked, and the host asks again /// through its runtime's `reload`, which is where the cadence belongs: how /// often a fact goes stale is a property of the app holding it. So an export /// reporting files written is a meter, re-answered, rather than something a /// description was refusing. /// /// What still has no proportion to state gets [`layout::Readiness::Pending`], /// and what has finished and wants saying gets a [`layout::Notice::Toast`]. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Meter { /// How much is done. May exceed [`total`](Self::total). pub done: u32, /// How much there is to do. pub total: u32, /// What the proportion means. No renderer can derive this. pub tone: layout::Tone, /// What is being counted: "subtasks", "tasks". The noun, not the ratio. pub label: Option, } impl Meter { /// A proportion, untoned and unlabelled. #[must_use] pub const fn new(done: u32, total: u32) -> Self { Self { done, total, tone: layout::Tone::Neutral, label: None, } } /// What the proportion means. #[must_use] pub const fn tone(mut self, tone: layout::Tone) -> Self { self.tone = tone; self } /// What is being counted. The noun, not the ratio. #[must_use] pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self } /// Borrow as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::Meter<'_> { layout::Meter { done: self.done, total: self.total, tone: self.tone, label: self.label.as_deref(), } } } /// A run of magnitudes read against one axis. /// /// The owned mirror of [`layout::Chart`], where the reasoning lives: an axis /// stated once rather than per bar, and both integers carried rather than the /// percentage an app computes from them. /// /// # Why this is a member and not a run of [`Meter`]s /// /// A meter carries its own `total`, so a chart said as a run of them states the /// axis once per bar and nothing holds the copies together. The shared maximum /// is what makes a set of bars one figure: it is the only reason two widths can /// be compared. It also puts the drawing in one place per renderer rather than /// leaving each host to invent how a run of troughs becomes a chart, which is /// the disagreement quasicoherent `19d7602d` ruled against. /// /// The two labels differ as well. [`Meter::label`] is the noun being counted /// and [`Bar::at`] is the place on the axis, which is a different fact in the /// same slot. /// /// # What a renderer may not work out for itself /// /// The axis. `most` is windowed by the app the way a pager's jumps are: which /// bars are shown is the description's call, so the maximum across them is too, /// and a renderer that took its own maximum would give a different answer per /// host for one series. `Rest::jumps` settles this for the pager and the same /// answer holds here. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Chart { /// The magnitude the axis runs to. Zero means there is no axis. pub most: usize, /// What the magnitudes are: "revenue". The noun, not the unit. pub label: Option, /// What the axis means, where it means anything. pub tone: layout::Tone, } impl Chart { /// An axis running to `most`, untoned and unlabelled. #[must_use] pub const fn new(most: usize) -> Self { Self { most, label: None, tone: layout::Tone::Neutral, } } /// What the magnitudes are. #[must_use] pub fn label(mut self, label: impl Into) -> Self { self.label = Some(label.into()); self } /// What the axis means. #[must_use] pub const fn tone(mut self, tone: layout::Tone) -> Self { self.tone = tone; self } /// Borrow as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::Chart<'_> { layout::Chart { most: self.most, label: self.label.as_deref(), tone: self.tone, } } } /// One magnitude in a [`Chart`], at its place on the axis. /// /// The owned mirror of [`layout::Bar`]. [`reading`](Self::reading) and /// [`note`](Self::note) arrive worded because the units are the app's and a /// count's noun inflects with the count; see the borrowed type. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Bar { /// Where on the axis this sits: "Mar 3". pub at: String, /// The magnitude, read against [`Chart::most`]. pub value: usize, /// The magnitude as the app words it: "$42.10". pub reading: Option, /// A second fact about the bar, already worded: "3 sales". pub note: Option, } impl Bar { /// A place on the axis, with no magnitude on it yet. /// /// The magnitude arrives through [`of`](Self::of), for the reason /// [`layout::Bar::at`] gives: a constructor's plain arguments are staged /// all one way, so a place and a magnitude in one call would have the place /// standing in as a number. #[must_use] pub fn at(at: impl Into) -> Self { Self { at: at.into(), value: 0, reading: None, note: None, } } /// How far up the axis this bar reaches. #[must_use] pub const fn of(mut self, value: usize) -> Self { self.value = value; self } /// How the app words this magnitude. #[must_use] pub fn reading(mut self, reading: impl Into) -> Self { self.reading = Some(reading.into()); self } /// A second fact about the bar, already worded. #[must_use] pub fn note(mut self, note: impl Into) -> Self { self.note = Some(note.into()); self } /// Borrow as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::Bar<'_> { layout::Bar { at: &self.at, value: self.value, reading: self.reading.as_deref(), note: self.note.as_deref(), } } } /// How a slider's position becomes its value, and how finely it moves. /// /// The borrowed original is [`layout::Curve`], and the reasoning lives there: /// the data of a slider is a fraction and a function taking numbers to numbers, /// so [`Field::min`] and [`Field::max`] are `f(0)` and `f(1)` rather than the /// control's extent. This is the owned mirror, holding its step as a `String` /// for the reason every other member here does. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum Curve { /// Constant slope. The default, and what every described range meant before /// the curve existed. Linear { /// The granularity, in the value's own units. step: Option, }, /// Constant ratio, for an extent spanning orders of magnitude. Logarithmic { /// The granularity, in the value's own units. step: Option, }, } impl Default for Curve { fn default() -> Self { Self::Linear { step: None } } } impl Curve { /// Read this curve as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::Curve<'_> { match self { Self::Linear { step } => layout::Curve::Linear { step: step.as_deref(), }, Self::Logarithmic { step } => layout::Curve::Logarithmic { step: step.as_deref(), }, } } /// The same curve with its granularity replaced. #[must_use] pub fn with_step(self, step: Option) -> Self { match self { Self::Linear { .. } => Self::Linear { step }, Self::Logarithmic { .. } => Self::Logarithmic { step }, } } } /// A question answered zero or more times, with the reader adding and removing /// the slots. /// /// **A repeating group enters the vocabulary, submitting once.** Every other /// member of [`layout::FieldKind`] is one field holding one value or one /// choice, and nothing said "this question is answered N times". A description /// that needs one says it here, and every renderer draws the slots, the /// control that adds one and the control that takes one away. /// /// # What it is not /// /// Not a list of forms, which is N submits. This is one submit /// carrying N values under one question, which is what makes the two hard parts /// hard: the names have to come back apart, and a refusal has to be able to say /// *which* answer is wrong. /// /// Not [`Field::multiple`], which was the near miss and was rejected in the /// same ruling. That is [`layout::FieldKind::File`]'s pick-several and a /// multi-select: one control taking a set, with one value, one error and no /// slots for the reader to add. Widening it would have put two shapes behind /// one word and left every renderer disambiguating them from the kind beside /// it. /// /// # The names on the wire /// /// `name[0]`, `name[1]`, and [`Repeat::at`] is the only place that is spelled. /// Picked once here rather than per renderer, because the three of them have to /// agree with each other and with whatever reads the submission back: /// [`Params::repeated`](crate::Params::repeated) is that reader. /// /// The index rather than N values under one bare name, which the wire already /// allows and [`Params::get_all`](crate::Params::get_all) already reads. An /// index survives a slot the reader emptied and a slot a host dropped: an error /// reported against the third answer means the third box on the way back /// whatever happened to the second, where a positional list renumbers itself /// silently and attaches the message to a different value. /// /// Holes are legitimate for the same reason and every reader here tolerates /// them: a browser removing the second of three slots may leave `0` and `2` /// standing rather than renumbering, and the answers are still the answers. /// /// # One consumer, which is enough /// /// goingson's event form, whose `Event.reminder_offsets_seconds` is a /// `Vec` capped at eight by `sanitize_reminder_offsets`. Nothing else in /// the described screens submits a variable-length set under one question. /// /// # A slot is one question, or several named ones /// /// One question is the ordinary slot and the one goingson's reminders use: /// [`Instance::value`] is the answer and [`Instance::error`] is what is wrong /// with it. /// /// Several is [`Instance::parts`], and it exists because a picked-file queue is /// a slot that is a name, a size and a failure of its own. The growth this /// type's header left open is the one that was taken: `name[0].size` beside /// `name[0]`, so a slot that is one question submits exactly as it always did. /// [`Answer`] is one of the several and [`Progress`] is the per-slot status, /// which is the half [`Repeating`] deliberately does not carry. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Repeat { /// The answers standing right now, in the order they are asked. /// /// Empty is a question nobody has answered yet, which is what "zero or /// more" means and what a renderer draws as the add control alone. pub instances: Vec, /// The fewest slots the reader may leave standing. /// /// `0` is the ordinary answer and is why removing the last slot is allowed. /// A question that must be answered at least once says `1`, which is /// [`Field::required`]'s reading for a repeating question: the flag is /// about one box holding a value, and this is about how many boxes there /// are. pub least: usize, /// The most slots the reader may add, if there is a ceiling. /// /// `None` is no ceiling. goingson's reminders carry `Some(8)`, which is the /// cap `sanitize_reminder_offsets` already enforces on the way in: stating /// it here is what stops the reader filling in a ninth slot that the write /// path silently drops. pub most: Option, /// The named questions each slot is made of, when a slot is several. /// /// Empty is a slot that is one question, which is every repeating question /// written before this member existed. See [`Question`] for why the shape /// is declared here and only the answers are per slot. pub parts: Vec, /// What adds a slot to this question. /// /// [`Adds::Control`] is the ordinary answer and is a control of the /// question's own. [`Adds::Elsewhere`] is a question whose slots arrive /// from another control on the same screen, which is MNW's version queue: /// the reader picks files and each picked file is a slot, so a control /// offering a blank row has nothing to offer. pub add: Adds, /// What the control that takes one away is called. "Remove". pub remove: String, } /// One answer to a repeating question. /// /// The value and the error, and nothing else. Both have the lifecycle /// [`Field::value`] and [`Field::error`] have and for the same reasons: what to /// re-offer after a refusal, and what whoever validated said about it. /// /// **This is the per-instance validation the ruling asked for.** /// [`Field::error`] is one string on one field, so a repeating question with /// only that could say the whole question was wrong and never which answer was. /// A message here belongs to this slot, and [`Field::error`] keeps the fact /// about the set: "at most eight reminders" is not about any one of them. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Instance { /// What this slot holds, on [`Field::value`]'s terms. /// /// The slot's own answer, for a slot that is one question. A slot that is /// several leaves this `None` and fills [`parts`](Self::parts) instead: a /// slot answers one way or the other, never both, and /// [`grouped`](Self::grouped) is which one it is. pub value: Option, /// What is wrong with *this* answer. Supplied by whoever validated. /// /// About the slot as a whole either way. A slot that is several questions /// puts what is wrong with *one* of them on that [`Answer::error`], and /// keeps this for what is wrong with the slot: the upload that failed, not /// the name that was too long. pub error: Option, /// What this slot answered for each of [`Repeat::parts`], in their order. /// /// Empty is a slot that is one question, and also a slot of a grouped /// question that has answered nothing yet. [`Repeat::parts`] is what says /// which of the two, because that is where the shape lives. pub parts: Vec, /// What this slot is called, when it is not called by its position. /// /// `None` is [`Repeat::ordinal`]: "Reminder 1", "Reminder 2", which is /// right for slots that differ only by where they are in the list. /// /// A picked file is the case that is not. A queue's slots are named by /// what is in them -- "track.wav" -- and numbering them would put a second, /// less useful name where the useful one goes. A slot that names itself /// keeps that name when the slots above it are removed, which is the whole /// reason it is on the slot rather than derived from its index. pub named: Option, /// How far the work on this slot has got, when work happens to it. /// /// [`Progress::Idle`] is a slot nothing is happening to, which is the /// default and every slot of a question that carries no work. pub progress: Progress, } impl Instance { /// A slot holding this value and nothing wrong with it. #[must_use] pub fn new(value: impl Into) -> Self { Self { value: Some(value.into()), error: None, parts: Vec::new(), named: None, progress: Progress::Idle, } } /// An empty slot. #[must_use] pub const fn blank() -> Self { Self { value: None, error: None, parts: Vec::new(), named: None, progress: Progress::Idle, } } /// A slot answering [`Repeat::parts`], in their order. /// /// [`value`](Self::value) stays `None`: the answers are on the parts, and a /// slot holding both would be two shapes behind one word, which is what /// [`Field::multiple`] was refused for. #[must_use] pub fn grouped(parts: impl IntoIterator) -> Self { Self { value: None, error: None, parts: parts.into_iter().collect(), named: None, progress: Progress::Idle, } } /// What this slot answered for the `at`th of [`Repeat::parts`]. /// /// A slot that has not answered that far is blank there, which is what a /// slot the reader just added is. #[must_use] pub fn part(&self, at: usize) -> Answer { self.parts.get(at).cloned().unwrap_or_default() } /// The same slot, called this rather than called by its position. #[must_use] pub fn called(mut self, name: impl Into) -> Self { self.named = Some(name.into()); self } /// The same slot, with the work on it standing where this says. #[must_use] pub fn getting(mut self, progress: Progress) -> Self { self.progress = progress; self } /// The same slot, with this said about it. #[must_use] pub fn wrong(mut self, message: impl Into) -> Self { self.error = Some(message.into()); self } } /// One of the named questions a slot is made of. /// /// A slot of a repeating field is ordinarily one answer to one question, and /// [`Instance::value`] is it. A picked-file queue is the shape that is not: a /// slot is a file, and a file is a name and a size and something that may have /// failed on its own. /// /// This is the *question* half and it lives on [`Repeat::parts`], declared once /// for the whole repeating question, for the reason [`Field::kind`] and the /// bounds live there: the question is what repeats, and only the answer is per /// slot. [`Answer`] is the other half. Declaring it once is also what gives a /// renderer the blank slot to offer when the reader adds one, which a shape /// living only on the answers could not. /// /// # The wire /// /// `name[0].size`, written by [`Repeat::part_at`] and read back by /// [`Repeat::part_of`] and [`Params::repeated_part`]. Beside `name[0]` rather /// than instead of it, which is the growth [`Repeat`]'s header sketched and /// left open: a slot that is one question still submits under the bare indexed /// name, so nothing written before this existed changed shape. /// /// [`Params::repeated_part`]: crate::Params::repeated_part #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Question { /// What this question is called within the slot: the `size` of /// `name[0].size`. /// /// A suffix rather than a whole name. The repeating question's own name and /// the slot index are the caller's, and [`Repeat::part_at`] is the only /// place the three are joined. pub name: String, /// What it is called on screen. /// /// Not numbered. [`Repeat::ordinal`] numbers the slot, and numbering the /// questions inside it as well would read as "Name 2" for the second file. pub label: String, } impl Question { /// One named question of a slot. #[must_use] pub fn new(name: impl Into, label: impl Into) -> Self { Self { name: name.into(), label: label.into(), } } } /// What one slot answered for one of [`Repeat::parts`]. /// /// The answer half of [`Question`], positional against it: the `n`th answer is /// to the `n`th question. Fewer answers than questions is a slot that has not /// answered the rest, which is what a slot the reader just added is, and every /// reader here tolerates it. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Answer { /// What this question of this slot holds, on [`Field::value`]'s terms. pub value: Option, /// What is wrong with *this* question of this slot. /// /// The per-slot error one level finer. [`Instance::error`] stays what is /// wrong with the slot as a whole. pub error: Option, } impl Answer { /// An answer holding this. #[must_use] pub fn new(value: impl Into) -> Self { Self { value: Some(value.into()), error: None, } } /// An unanswered question of a slot. #[must_use] pub const fn blank() -> Self { Self { value: None, error: None, } } /// The same, with this said about it. #[must_use] pub fn wrong(mut self, message: impl Into) -> Self { self.error = Some(message.into()); self } } /// What adds a slot to a repeating question. /// /// A repeating question used to assume one answer: the reader presses a /// control of the question's own and a blank slot appears. That is still the /// ordinary case and still the default. /// /// MNW's version-upload queue is the case it could not say. Its slots are /// picked files, made by the upload field above the table, and a blank row is /// not something a reader can fill: there is no way to type a file. A question /// there wants no add control at all, and no renderer may invent one. /// /// # Not [`Repeating::add`] /// /// That is an [`Act`], because a repeating *group*'s slots are the app's and /// adding one is a route. Neither of these is: a repeating field's slots live /// in the renderer's own view until they submit, so what makes one is either /// this renderer's own control or another control on the same screen. No /// route either way. #[derive(Debug, Clone, PartialEq, Eq)] pub enum Adds { /// A control of the question's own, called this. "Add reminder". Control(String), /// Another control on the screen, named by whatever the renderer addresses /// controls by, which in a form is the field's name. /// /// The question draws no add control of its own. What the named control /// does with the slots it makes is that control's business and this says /// nothing about it: a renderer that cannot reach the named control draws /// the slots it was given and no way to make more, which is the honest /// answer for a terminal asked to display a file queue. Elsewhere(String), } impl Default for Adds { fn default() -> Self { Self::Control(Repeat::ADD.to_owned()) } } impl Adds { /// What the control is called, when the question offers one. /// /// `None` is a question whose slots come from elsewhere, and is every /// renderer's signal to draw no add control. #[must_use] pub fn label(&self) -> Option<&str> { match self { Self::Control(label) => Some(label), Self::Elsewhere(_) => None, } } /// The control the slots come from, when they come from another. #[must_use] pub fn from(&self) -> Option<&str> { match self { Self::Control(_) => None, Self::Elsewhere(name) => Some(name), } } } /// How far the work on one slot has got. /// /// [`Instance::error`] says what is wrong with a slot and cannot say *when*: /// an answer that failed to upload and an answer nobody has started uploading /// are both "no value yet" to a renderer reading the slot alone. A queue of /// picked files is the screen that needs the difference, and needs it per slot /// rather than per question, because one file failing says nothing about the /// other six. /// /// Not [`layout::Readiness`], which is a region's word for whether its /// *content* arrived and carries an `Empty` that means nothing about a slot. /// Two meanings behind one word is what [`Field::multiple`] was refused for. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub enum Progress { /// Nothing is happening to this slot. /// /// The default, and every slot of every repeating question that carries no /// work of its own. #[default] Idle, /// Work on this slot began and has not finished. /// /// The [`Meter`] is how far, for a host that can say. `None` is work whose /// extent nobody can state yet, which is a file that has been handed over /// and has reported no bytes. Working(Option), /// The work finished and this slot holds what it produced. Done, /// The work did not finish. /// /// What went wrong is [`Instance::error`]. Kept apart from it so that a /// slot may carry a message without being a failure, which is an ordinary /// validation refusal on a queue nobody has submitted yet. Failed, } impl Progress { /// Whether this slot is waiting on work that has not finished. /// /// Asked by every renderer before it draws a slot's remove control: a slot /// mid-flight is one the reader may not pull out from under the work. #[must_use] pub const fn busy(&self) -> bool { matches!(self, Self::Working(_)) } /// Whether the work on this slot ended badly. #[must_use] pub const fn failed(&self) -> bool { matches!(self, Self::Failed) } } impl Repeat { /// What the control that adds a slot is called when nothing else is said. pub const ADD: &'static str = "Add"; /// What the control that removes one is called when nothing else is said. pub const REMOVE: &'static str = "Remove"; /// A question nobody has answered yet, with no floor and no ceiling. #[must_use] pub fn new() -> Self { Self { instances: Vec::new(), least: 0, most: None, parts: Vec::new(), add: Adds::default(), remove: Self::REMOVE.to_owned(), } } /// The named questions each slot is made of. /// /// A slot answers these rather than answering once itself, and /// [`Instance::grouped`] is how one supplies them. #[must_use] pub fn of(mut self, parts: impl IntoIterator) -> Self { self.parts = parts.into_iter().collect(); self } /// The slots this question already stands in, supplied whole. /// /// [`answered`](Self::answered) for a grouped question, where a slot is an /// [`Instance::grouped`] rather than a single value, and for any slot that /// carries a [`Progress`] the plain values cannot express. #[must_use] pub fn instances_of(mut self, instances: impl IntoIterator) -> Self { self.instances = instances.into_iter().collect(); self } /// Whether a slot of this question is several questions rather than one. #[must_use] pub fn grouped(&self) -> bool { !self.parts.is_empty() } /// A question already answered these many times. /// /// What a form being offered for editing carries: one slot per value the /// record holds. #[must_use] pub fn answered(values: impl IntoIterator>) -> Self { Self { instances: values.into_iter().map(Instance::new).collect(), ..Self::new() } } /// Leave at least this many slots standing. #[must_use] pub const fn least(mut self, least: usize) -> Self { self.least = least; self } /// Take at most this many answers. #[must_use] pub const fn most(mut self, most: usize) -> Self { self.most = Some(most); self } /// What the control that adds a slot is called. #[must_use] pub fn adding(mut self, label: impl Into) -> Self { self.add = Adds::Control(label.into()); self } /// The slots come from another control on the screen, named here. /// /// The question offers no add control of its own. See [`Adds::Elsewhere`]. #[must_use] pub fn added_by(mut self, control: impl Into) -> Self { self.add = Adds::Elsewhere(control.into()); self } /// What the control that takes a slot away is called. #[must_use] pub fn removing(mut self, label: impl Into) -> Self { self.remove = label.into(); self } /// Say what is wrong with one answer, leaving the rest alone. /// /// Grows the list to reach it, because a refusal naming the fourth answer /// of a form that came back with three is a description bug worth seeing on /// the screen rather than a message silently dropped. #[must_use] pub fn wrong(mut self, at: usize, message: impl Into) -> Self { if self.instances.len() <= at { self.instances.resize(at + 1, Instance::blank()); } self.instances[at].error = Some(message.into()); self } /// The name the `at`th slot submits under: `name[at]`. /// /// The one place the wire naming is spelled. See the type's header for why /// it is an index rather than N values under one name. #[must_use] pub fn at(name: &str, at: usize) -> String { format!("{name}[{at}]") } /// The name the `part` of the `at`th slot submits under: `name[at].part`. /// /// [`at`](Self::at) with a part on the end, and the only place that join is /// spelled, for [`at`](Self::at)'s reason: the three renderers and whatever /// reads the submission back have to agree, and [`part_of`](Self::part_of) /// is the reader. #[must_use] pub fn part_at(name: &str, at: usize, part: &str) -> String { format!("{name}[{at}].{part}") } /// The question, the slot and the part a wire name belongs to, if it is /// one. /// /// [`part_at`](Self::part_at) read backwards. `None` for a bare indexed /// name, which [`instance_of`](Self::instance_of) is the reader for, and /// `None` for every ordinary field name: the two never both answer, so a /// host may ask either of any name it holds. #[must_use] pub fn part_of(wire: &str) -> Option<(&str, usize, &str)> { let (indexed, part) = wire.split_once('.')?; if part.is_empty() || part.contains('.') { return None; } let (name, at) = Self::instance_of(indexed)?; Some((name, at, part)) } /// The question and the slot a wire name belongs to, if it is one. /// /// [`at`](Self::at) read backwards, for a host holding a name and asking /// what it is. `None` for every ordinary field name, which is what makes it /// safe to ask of any of them. #[must_use] pub fn instance_of(wire: &str) -> Option<(&str, usize)> { let (name, rest) = wire.split_once('[')?; let index = rest.strip_suffix(']')?; // Refused rather than parsed loosely: `name[+1]` and `name[ 1]` both // parse as 1 through `str::parse` on some inputs a caller would not // expect, and a name this did not write is not a slot. if index.is_empty() || !index.bytes().all(|byte| byte.is_ascii_digit()) { return None; } Some((name, index.parse().ok()?)) } /// What the `at`th slot is called on screen: "Reminder 2". /// /// Counted from one, because it is read by a person. Spelled here so the /// three renderers cannot number the same slot differently, which is the /// same reason [`at`](Self::at) is here: an error reported against the /// third answer has to name the third box on every host. #[must_use] pub fn ordinal(label: &str, at: usize) -> String { format!("{label} {}", at + 1) } /// Whether another slot may be added when this many are standing. #[must_use] pub fn more(&self, standing: usize) -> bool { self.most.is_none_or(|most| standing < most) } /// Whether a slot may be taken away when this many are standing. #[must_use] pub const fn fewer(&self, standing: usize) -> bool { standing > self.least } /// How many slots stand before the reader has touched anything. /// /// The described count, floored at [`least`](Self::least): a question that /// must be answered twice opens with two boxes rather than with none and a /// refusal on submit. #[must_use] pub fn standing(&self) -> usize { self.instances.len().max(self.least) } /// What the `at`th slot holds, if the description offered anything. #[must_use] pub fn holds(&self, at: usize) -> Option<&str> { self.instances.get(at)?.value.as_deref() } /// What is wrong with the `at`th answer, if anything is. #[must_use] pub fn amiss(&self, at: usize) -> Option<&str> { self.instances.get(at)?.error.as_deref() } } /// One field of a form, owned. /// /// The borrowed original is [`layout::Field`], and everything it says about /// what a field carries applies unchanged, with one addition that does not /// travel down to it: [`value`](Self::value). /// /// # Why the value lives here and not in `makeover-layout` /// /// [`layout::Field`] refuses to carry the current value, and that refusal is /// right: an immediate-mode renderer writes through a `&mut String` the app /// owns, and a terminal keeps an edit buffer, so a description carrying a live /// value would need a way to write it back and would then be a form model. /// /// What is carried here is not a live value. It is what to re-offer after a /// submission was refused, and it has [`error`](Self::error)'s lifecycle rather /// than a live value's: per-submission, one way, supplied by whoever validated, /// gone on the next request. `error` already sits in this struct on exactly /// those terms. /// /// The reason it is this crate's field and not the vocabulary's is that only a /// stateless request and response destroys the value. In egui and in a terminal /// the buffer never went anywhere, so nothing is lost and there is nothing to /// re-offer. This is the layer where the loss happens, so this is the layer that /// repairs it. /// /// # A field's described state is its value, and the caret is the renderer's /// /// Nothing here carries a caret position, and nothing in [`layout::FieldKind`] /// does either. A description names the field and, where it has one, its /// completion source. Where the caret sits is how a renderer decides what to /// offer from that source. /// /// The question came from goingson's `search.js`, whose completion list depends /// on which token the caret is inside rather than on the value: it reads /// `selectionStart`, listens for caret moves that change nothing else, and /// writes the caret back when a suggestion is applied. That is a real /// dependency, and it still does not belong here. A caret is where the user is /// pointing inside a control, the same class of fact as a scroll offset and a /// focus position, and this stack already puts those in the renderer's view /// rather than in the description (`quasi-tui`'s `View`). /// /// Growing this struct to (value, caret) was rejected: it is the most-consumed /// member in the vocabulary, every renderer would owe it an answer, and a /// terminal's answer would be a second cursor concept beside the one the runtime /// already holds. The measured demand was one file. /// /// Reversible if a second consumer appears that needs the caret described rather /// than held, such as a completion that has to survive a fragment swap. That is /// a member here and a cascade, the same shape as every other addition. /// /// No `Hash`, for the reason [`Tag`] has none: it can hold an [`Action`], which /// holds [`Params`], which is a `Vec`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Field { /// What kind of value it takes. pub kind: layout::FieldKind, /// The name the value is submitted under, and the name the handler reads /// back out of [`Params`]. /// /// The *lower* end's name for a [`layout::FieldKind::Interval`], whose upper /// end is [`upper_name`](Self::upper_name). pub name: String, /// The name a [`layout::FieldKind::Interval`]'s upper end is submitted /// under. /// /// The borrowed original is [`layout::Field::upper_name`], and everything it /// says applies: stated rather than derived, because the two measured sites /// disagree about affix order, and which member a name sits in is what says /// which end it is. /// /// `None` for every other kind. [`Field::interval`] is what makes an /// interval without one unsayable. pub upper_name: Option, /// What the user is asked for. pub label: String, /// Standing help. pub hint: Option, /// What is currently wrong with the value. Supplied by whoever validated; /// nothing here decides that a value is wrong. pub error: Option, /// A consequence of the answer the user has given, carrying its own tone. /// /// The third message channel, between [`hint`](Self::hint) and /// [`error`](Self::error) and overlapping neither: the value is acceptable /// and choosing it costs something worth saying. It does not make the field /// [`invalid`](Self::invalid). /// /// Precedence for a renderer with room for one line, decided in /// [`layout::Field::note`]: error, then note, then hint. pub note: Option<(layout::Tone, String)>, /// Ghost text shown while the field is empty. pub placeholder: Option, /// The options offered, in order. Empty for kinds that offer none. pub options: Vec, /// What a request decides about runs of [`options`](Self::options) and /// [`themes`](Self::themes). /// /// Empty answering a request, filled by a staged twin. See [`Slot::marks`]. pub marks: crate::stage::Marks, /// The themes offered, in the order they are offered. /// /// The borrowed original is [`layout::Field::themes`], and everything it /// says applies. Empty for every kind /// [`layout::FieldKind::offers_themes`] rejects, and a real answer for the /// one that accepts it. /// /// **The order is the grouping**, and nothing here sorts. The order arrives /// from whoever measured the tiers — `makeover::theme_options` is what /// produces it — and re-sorting at this layer would be deciding a question /// it cannot see the inputs to. pub themes: Vec, /// The entry that follows the ambient mode instead of naming a theme. /// /// The borrowed original is [`layout::Field::follows`]. A [`Choice`] rather /// than a bare label because the value belongs to the app's own store, and /// `None` is a real answer for a host with no ambient mode to follow. pub follows: Option, /// What a file field takes. Empty for kinds that take no files, and also a /// real answer for one that does: a field listing nothing takes any file. /// /// The borrowed original is [`layout::Field::accept`]. It filters the picker /// and it says which disclosure the field earns — a preview, a duration — /// which is why it is a list of [`Accepted`] rather than the comma-joined /// string a template holds. pub accept: Vec, /// Whether more than one file may be picked at once. /// /// The borrowed original is [`layout::Field::multiple`]. Read only by a kind /// [`layout::FieldKind::takes_files`] accepts. pub multiple: bool, /// Whether the form refuses to submit without it. pub required: bool, /// The longest the value may be, in characters. /// /// The borrowed original's [`layout::Field::max_length`], and everything it /// says applies: the description carries the rule, the renderer emits its /// host's idiom, and deciding a value is wrong stays with whoever validated. pub max_length: Option, /// The lowest value accepted, written the way the host writes one. pub min: Option, /// The highest value accepted. See [`min`](Self::min). pub max: Option, /// The granularity a *typed* value moves in, written the way the host writes /// one. /// /// The borrowed original is [`layout::Field::step`]. Absent means the /// host's own granularity, which is a real answer rather than a missing /// one. A [`layout::FieldKind::Range`] keeps its own on /// [`curve`](Self::curve) instead, as of makeover-layout 0.32.0. pub step: Option, /// How a slider's position becomes its value, and how finely it moves. /// /// The borrowed original is [`layout::Field::curve`]. A /// [`layout::FieldKind::Range`] reads its granularity here; every other /// kind reads [`step`](Self::step). See [`Curve`]. pub curve: Curve, /// What the number is measured in: `s`, `ms`, `dB`, `GiB`. /// /// The borrowed original is [`layout::Field::unit`], and everything it says /// applies. A fact about the value rather than part of the question's name, /// which is the distinction the member exists for: the two come apart the /// moment a handler reads a field back instead of a renderer drawing it. /// /// Read only by a kind [`layout::FieldKind::measurable`] accepts. The symbol /// alone, no brackets and no leading space; the spacing is the renderer's. pub unit: Option, /// Whether the field lives behind a "more options" disclosure. pub extended: bool, /// Whether this local wall-clock value is submitted as an absolute instant. /// /// The borrowed original is [`layout::Field::as_instant`], and everything it /// says applies. A [`layout::FieldKind::DateTime`] asks for a time the way a /// person says one, which names a different moment in each zone; this says /// the description wants the renderer to convert it, because the renderer is /// the only party that knows what its host's clock and zone are. /// /// No wire contract moves when a site adopts it. The route was already /// receiving an instant; what changes is who computed it. pub as_instant: bool, /// How much of its row the control asks for. /// /// Fill is determined at the description stage. [`Column::width`] has said /// this about a table cell since the beginning and [`layout::Share`] says /// it about a region, so the vocabulary already accepted that an app has /// an opinion about which of several things expands. A leaf control having /// no way to say it was an inconsistency in where the line sat rather than /// a principle being upheld, and this is the correction. /// /// What it is not is a measurement. [`layout::Width`] is an intent — /// content-sized, fixed, or take the rest — and the actual floor stays with /// `makeover-geometry`, which is the same division `Column` makes. A field /// that carried pixels would be the thing this replaces: audiofiles' /// toolbar kept a measured `trailing_width` in renderer memory, corrected /// it a frame late, and needed two constants to survive the first frame, /// all to say [`Fill`](layout::Width::Fill). /// /// [`layout::Share`]: crate::layout::Share pub width: layout::Width, /// What to put back in the box: what was submitted, when a submission was /// refused and the form is being offered again. /// /// `None` on a first showing, which is every form that is not answering a /// refusal. A checkbox is here by presence, the way HTML submits one: a /// value means ticked and `None` means not. /// /// A [`layout::FieldKind::Secret`] never gets one. [`Field::value`] refuses /// to set it and every renderer refuses to emit it, so the guarantee does /// not rest on either alone. pub value: Option, /// What to put back in the *upper* box of a /// [`layout::FieldKind::Interval`], on the same terms as /// [`value`](Self::value). /// /// A second value rather than a separator convention inside the first. An /// interval submits two names, so a refusal has two values to hand back, and /// joining them into one string would make this layer own a delimiter that /// any value could contain. /// /// Either end may be absent while the other stands, which is what an /// open-ended interval is: "over 120 BPM" is a lower end and no upper one, /// and it is a real answer rather than a half-filled form. /// /// `None` for every other kind, and never set for a /// [`layout::FieldKind::Secret`] for [`value`](Self::value)'s reason. pub upper_value: Option, /// What setting this calls, for a control that writes as it is set rather /// than waiting for a submit. /// /// A field inside a [`Node::Form`] submits with the form and needs nothing /// here. A settings toggle is the other kind: there is no submit, and /// setting the control *is* the write. goingson had 13 of these and /// reached them through `dispatch.js`, 109 lines of its own event /// plumbing, because nothing in the description could say it. No version /// of spinning up an app quickly has each app hand-rolling a dispatcher. /// /// The route receives the value under this field's [`name`](Self::name), /// which is the same name a submit would have sent it under. Nothing else /// changes about the field. /// /// # What it is for /// /// A value that is written as it is set, where no commit point is wanted: a /// draft field that autosaves, a setting, a per-field select on a detail /// view. The reader alters one control, and that alteration is the whole /// interaction. /// /// # What it is not for /// /// **Applying a value to a selection.** That wants a commit affordance, per /// wiki `explicit-commit-affordance`: the reader ticks rows, chooses a /// value, and the write lands on every row ticked. That is a large enough /// act to deserve a control that says so before it happens. /// /// **Anything that does not write.** A filter, a sort, a re-ask: a control /// whose action fetches a different view of data it leaves alone. Saying it /// here states something false about the control in every renderer that /// reads the description, and the falsehood is invisible because the /// request still goes out and the screen still updates. /// /// # Why the name is enough /// /// This is the only member on [`Field`] carrying an [`Action`] of its own, /// so a field without it is written by its form's submit and a field with /// it writes by itself. The presence of the member is the statement. There /// is nothing to add and no second member saying a field does not write. /// A [`Consult`] carries an action too and is not a counterexample: it /// names a question about the value, and the question is what its type /// says. /// /// # When it fires: the change is *complete*, not on the way to it /// /// A value the reader builds up, typed into or dragged across, writes /// once, when they are finished with the control: a webview on the /// browser's `change` event, a terminal when the caret walks off the box, /// egui on `lost_focus` or `drag_stopped`. A value chosen in one go (a /// select, a radio, a file) was complete the moment it changed and writes /// then. /// /// **This was already what a webview did and what the other two did not.** /// `quasi-webview` emits `hx-trigger="change"`, so it has meant this since /// the member existed; `quasi-immediate` fired on every frame the buffer /// differed and `quasi-tui` on every keystroke. So a search box was one /// request per letter on two hosts and one per search on the third, and /// audiofiles' bounded `row_height` posted a row height of 3 on the way to /// 30, outside the bounds the field's own hint states. /// /// Leaving a control nobody altered writes nothing, which is the other half /// of what `change` promises: walking through a form must not write every /// box it passes. /// /// # A question asked while typing is [`consults`](Self::consults) /// /// Nothing above costs live search anything, and that is what makes the /// rule coherent rather than a restriction. A box that asks a route about /// what is being typed carries a [`Consult`], which has its own /// [`after`](Consult::after): a wait the description states rather than a /// number each renderer picks. Writes complete; questions debounce. pub writes: Option, /// What this asks while the user is still typing, and how long it waits. /// /// See [`Consult`]. Distinct from [`writes`](Self::writes) in the two /// ways that matter: it fires while the value is still being written rather /// than once it settles, and what comes back is an answer about the value /// rather than the result of writing it. /// /// # Several, because one box can raise more than one question /// /// `N8` on MNW's discover screen: `#search-input` carries `hx- /// get="/discover/results"` with one wait, and the suggestion list beside /// it is a hand-written `fetch('/discover/suggestions?q=…')` with another. /// Two questions about one value, asked at two rates, and only one of them /// was sayable — so the other stayed as JS. /// /// Order is the description's and each renderer keeps it, though nothing /// depends on it: the answers land where each /// [`Action::replacing`] says, and two consults pointing at one place is a /// description arguing with itself rather than an ordering question. /// /// # "Typing" is the common case, not the rule /// /// A select, a radio, a checkbox and a slider all consult, and the /// majority of the measured sites are selects: a folder picker that re- /// reads a list of mail asks a route about a value and stores nothing, /// which is this member entire. It read as typing-only because the first /// four sites were wizard boxes and the webview hung the question on /// `keyup`, which a select never raises. /// /// Each renderer raises it in its host's idiom, the way it already does for /// [`Slot::consults`], and the wait stays the description's: /// [`Consult::after`] of zero is a real number for a control chosen in one /// gesture rather than a meaningless one. pub consults: Vec, /// The question whose answer is this field's own list of candidates. /// /// The field owns the list. MNW's discover screen spends ~120 lines of /// `page-discover.js` on exactly that wiring, none of which is about /// suggestions. /// /// # Why a [`Consult`] and not a second kind of member /// /// A suggestion source is a route asked as the value is typed, with a wait /// and a floor, which is [`Consult`] entire. What is added here is not a /// second mechanism but an owner: the answer to *this* question is a list /// of candidates for *this* value, and every renderer therefore knows where /// to draw it, what it is called, and what picking one does. /// /// # What comes back, and this one is described /// /// [`Outcome::Suggestions`](crate::Outcome::Suggestions), a list of /// [`Candidate`] — its own type rather than the [`Choice`] that /// [`options`](Self::options) carries. Both /// submit one string and read as another; a candidate also says what tells /// it apart from a row that reads alike, and what picking it does. /// Deliberately unlike /// [`consults`](Self::consults), where the answer is undescribed and the /// renderer picks the wire format: that works for a verdict landing in a /// region a description already named, and it cannot work here. A list /// nobody described is a list a terminal cannot draw, and one route /// answering three wire formats is the per-host branching this stack /// exists to end. /// /// # Picking is local by default /// /// [`Destination::Local`] says a suggestion's pick action sets the field. /// Ownership is what makes that sayable without an action at all: the list /// belongs to this field, so picking an entry writes its /// [`Candidate::value`] into this field, and no renderer has to be told /// which box to write to. Moving the highlight is local for the same /// reason. /// /// By default and not by definition after the two sites this member was /// designed from were held against it and neither picked locally. A /// candidate carrying /// [`picks`](Candidate::picks) has that action performed instead. Local /// stays the default, so every site that exists today is unchanged and /// nothing that already works has to say anything new. /// /// # Addressed by the field's name /// /// No id is authored anywhere. The field already has a /// [`name`](Self::name) — what a submit sends the value under, what a /// [`Consult`] sends it under — and that is what the answer names. A /// renderer that needs a document id derives one from it, which is the /// difference between a list a field owns and two elements a description /// has to keep pointing at each other. /// /// Beside [`consults`](Self::consults) rather than inside it: MNW's /// discover box asks two questions about one value, and only one of them is /// its suggestions. The other re-reads the results under the current /// filters and lands in a region, which is what [`consults`](Self::consults) /// has always been for. /// /// [`Destination::Local`]: crate::Destination::Local pub suggests: Option, /// Whether what is in this control belongs to the reader rather than to the /// answer that drew it. /// /// Say it, one member, on the field. /// /// The fact is that the server never sent this value and cannot send it /// again, so a redraw that clears the control loses something only the /// reader had. A tag typeahead is the measured case: type three letters, /// tick an unrelated facet, and the surrounding region is swapped out of /// band with the box in it. /// /// # Why it has to be said rather than assumed /// /// Because only one host's default is destructive, which is the usual /// reason a fact enters this vocabulary. A terminal keeps its own buffer /// and egui keeps widget state by id, so both were already right; a browser /// replaces the element and takes what was typed with it. /// /// # Why not on the region /// /// Which is what the original task asked for, and it cannot be /// implemented. Preservation is per-element, so a renderer told "redraw /// this region but keep the reader's half" has no way to know *which* /// elements hold reader state, and "preserve every input in here" would /// keep a facet control the answer legitimately reset. The field is the /// only place that knows. /// /// # Why not inferred /// /// A rule like "no value, plus [`suggests`](Self::suggests) or /// [`consults`](Self::consults), means reader-owned" would cover the /// measured site exactly and cost nothing to write. Declined in the same /// ruling: it changes behaviour when a field gains or loses an unrelated /// member, so a description that starts preserving because somebody added a /// consult is a surprise nobody wrote down. /// /// # What it is not /// /// Not [`value`](Self::value), which is what the answer offers and what a /// refusal re-offers. A field can have both: the answer says what it /// starts as, and this says nobody may take it away afterwards. pub keeps_value: bool, /// The slots this question is answered in, when it is answered more than /// once. /// /// `None` is the ordinary field, asked once and answering once, which is /// every other field in the tree. See /// [`Repeat`] for the wire naming, the per-slot errors and what this /// deliberately is not. /// /// The members around it keep their meanings and apply to every slot: the /// [`kind`](Self::kind), the bounds, the [`placeholder`](Self::placeholder) /// and the [`hint`](Self::hint) describe the question, and the question is /// what repeats. The two that do not are [`value`](Self::value) and /// [`error`](Self::error), which are per-answer and live on the /// [`Instance`]; a repeating field's own `error` is what is wrong with the /// *set*, which is the fact "at most eight reminders" belongs to. /// /// [`Field::instance`] is how a renderer gets one slot as an ordinary /// field, so that everything a renderer already does to a field it does to /// each slot without a second emitter. pub repeats: Option, /// What brings this field out, when it is not simply out. /// /// Found by the consumer `079a011e` was ruled for. The region carries the /// condition and that is the shape for a block of several things; a form's /// questions are a flat list, so a *single* conditional question inside /// one had nowhere to put the same fact. goingson's event form is the /// measured site: `initTzKindConfig` shows one box, "Anchored to", on one /// of the three zone kinds, and the box has to submit with the form around /// it. /// /// The condition sits on the thing revealed, which is the direction /// [`Reveal`] was ruled in: what is refused there is the condition living /// on the *watched* control, and that is refused here too. A renderer /// answers this exactly as it answers a region's, from what it already /// holds, and makes no request. /// /// A hidden field keeps its value and still submits it, which is what a /// browser does with an input inside a hidden element and what the client /// renderers already do with a region the reader has closed. /// /// `None` is the ordinary question, which is every other field in the tree. pub revealed_by: Option>, } /// A question asked while the reader is still working, rather than when they /// are done. /// /// N14 and N8: the description names the **source** — a route and how long to /// wait — and says nothing about what comes back. Both gaps were filed /// separately, one asking for "pending / ok / taken" and one for a suggestion /// list, and they are the same member: a field that consults something as it /// is written. Naming the two shapes instead would have put two mechanisms in /// the vocabulary for one idea, which is how a vocabulary drifts. /// /// # Two places it sits, one mechanism /// /// [`Field::consults`] asks about one control's value. [`Slot::consults`] asks /// about the values of every question inside a region, which is the pricing /// calculator's shape: five dials that are peers, and a panel that recomputes /// when any of them moves. Neither is about typing -- a select is the /// commonest asker of both. — and decided as /// *this* type rather than as a second member, because a route, a debounce and /// a floor is `Consult` entire and a second timing mechanism for the same idea /// is the drift the paragraph above is about. /// /// The three fields read the same in both places, against the value that just /// moved: [`after`](Self::after) is how long it must stand still, /// [`at_least`](Self::at_least) is how much of it there must be, and /// [`sends`](Self::sends) is what rides along *beyond* what the position /// already gathers. What the position gathers differs, and that is the whole of /// the difference: a field sends its own value, and a region sends the values /// of the questions it contains. /// /// # What comes back is not described, deliberately /// /// The route answers with whatever it answers with, and the renderer decides /// the wire format: a fragment for a webview, values for a terminal or egui. /// A three-way verdict and a list of suggestions are then the same member with /// two different routes behind it, and adding a third kind of answer costs /// nothing here. /// /// Where the answer lands is [`Action::replacing`], which already says that /// about every other action. Nothing new is needed to point a verdict at the /// status line beside the box. /// /// # The accepted costs, so they are not re-argued /// /// A field points at a route for the first time in the vocabulary, and a host /// with nothing to ask — no HTTP, no local responder — cannot honour one. Both /// were weighed and taken: the alternative was leaving four wizard fields and /// two comboboxes as host code forever. /// /// # Two numbers, not one /// /// [`after`](Self::after) says the value has stopped moving and /// [`at_least`](Self::at_least) says there is enough of it to be worth asking /// about. Both are the description's for the same reason — a renderer picking /// either is a renderer the other renderers disagree with — and the measured /// sites carry both: MNW's tag typeahead waits 150ms above two characters and /// its search box 200ms above two, while the four wizard fields wait 500ms and /// ask about a single letter, because one letter can already be taken. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Consult { /// The route asked, receiving the value under the field's /// [`name`](Field::name) — the same name a submit would send it under. pub action: Action, /// How long the value must stand still before asking. /// /// A property of the question rather than of the renderer, which is the /// half quasi-tui went without: its runtime fires a /// [`writes`](Field::writes) write on every keystroke and its own comment /// says why it cannot do otherwise — "nothing in the description says a /// delay is allowed", and a number the renderer picked would be a number /// the webview and the terminal disagree about. pub after: std::time::Duration, /// How many characters the value must carry before the route is asked at /// all, counted in `char`s. /// /// A floor rather than a wait, and the two are not the same question: a /// debounce says the value has stopped moving, and this says there is /// enough of it to be worth asking about. Both measured sites spell both /// numbers and they differ: MNW's tag typeahead waits 150ms and refuses /// under two characters, its search box waits 200ms and refuses the same. /// /// It is the description's for the reason the wait is: a route answering /// `a%` over every project and item in the catalogue is the expensive /// question, and neither renderer nor route can know how expensive without /// being told. Both MNW suggestion routes guard emptiness and nothing else, /// so a described field that dropped this would ask them on the first /// letter, which is the query the floor exists to refuse. /// /// `0`, the default, asks whatever is there. pub at_least: usize, /// Other controls whose values ride along with the question, named the way /// a submit names them. /// /// `N8`. A box that asks about its own value alone is the common case and /// leaves this empty. MNW's discover search is the other one: the results /// it re-reads are the results *under the current filters*, so the question /// is not "what matches `q`" but "what matches `q`, in this mode, sorted /// this way, under these tags". Asked without them the route answers about /// a screen the user is not looking at. /// /// # Named by field name, not by a group /// /// The shipped markup groups them with `hx-include=".discover-filter"`, a /// CSS class, which is the option this rejects. A class is a fact about the /// document and a terminal has none; naming the fields names something /// every host already has, since [`Field::name`] is what a submit sends a /// value under and what a [`Consult`] sends the typed value under. So a /// route reads all of them out of one bag, under the names it already /// expects, whichever host asked. /// /// A name that no field on the screen carries sends nothing. That is a /// description bug and every renderer treats it as one value fewer rather /// than an error, for [`Screen::replace`]'s reason: a miss is worth being /// able to see, and is not worth refusing to draw a screen over. /// /// # On a region, this is what rides along from *outside* it /// /// [`Slot::consults`] gathers the questions the region contains, by /// containment rather than by name, so the common region consult leaves /// this empty too. A dial that sits outside the panel it recomputes is what /// names itself here, and it names itself the same way: by /// [`Field::name`], which is the one address every host has. pub sends: Vec, } impl Consult { /// What the four measured sites already wait, and what /// [`Field::consults`] uses when nothing else is said. /// /// All four MNW wizard fields spell `delay:500ms` by hand, so this is the /// corpus' own number rather than a chosen one. pub const SETTLES: std::time::Duration = std::time::Duration::from_millis(500); /// Ask this route once the value has stood still for [`SETTLES`](Self::SETTLES). #[must_use] pub const fn new(action: Action) -> Self { Self { action, after: Self::SETTLES, at_least: 0, sends: Vec::new(), } } /// Ask this route the moment the value moves. /// /// [`new`](Self::new)'s wait is a typing wait, and a control chosen in one /// gesture has nothing to wait out: a select is at its next value or its /// last one, never on the way between them. Zero is a real number here /// rather than a missing one, which is why this is a constructor and not /// an `Option`. /// /// Not the default for a discrete kind. The description says the wait, the /// renderer says the event ([`Field::consults`]), and a kind deciding the /// wait would be the third party to the same question -- a radio group /// inside a [`Slot::consults`] already waits on purpose. #[must_use] pub const fn at_once(action: Action) -> Self { Self::new(action).after(std::time::Duration::ZERO) } /// Ask this route once the value has stood still for `after`. #[must_use] pub const fn after(mut self, after: std::time::Duration) -> Self { self.after = after; self } /// Whether a value has enough in it to be worth asking about. /// /// The floor stated once rather than in each renderer: three copies of /// `chars().count() >= n` are three places to disagree about whether the /// count is bytes or characters, and the terminal is the host where that /// difference is a wrong answer rather than a slow one. /// /// Says nothing about the wait, which is the half a renderer cannot be /// spared: htmx delays it, the TUI hands it to the host, egui keys it on a /// deadline it repaints for. /// /// On a [`Slot::consults`] the value is the one that just moved, never the /// gathered set. A floor over a set has no meaning a reader could predict — /// five dials holding one character each are not five characters — and the /// browser says the same thing in its own words, where the filter reads /// `event.target.value`. #[must_use] pub fn asks_about(&self, value: &str) -> bool { value.chars().count() >= self.at_least } /// Do not ask at all until the value carries `chars` characters. /// /// See [`at_least`](Self::at_least). Combines with /// [`after`](Self::after): the value has to be long enough *and* to have /// stood still. #[must_use] pub const fn at_least(mut self, chars: usize) -> Self { self.at_least = chars; self } /// Send these other fields' values with the question. /// /// See [`sends`](Self::sends). The typed value goes under the asking /// field's own name whether or not this is set, so a call names only what /// it needs *beside* that. /// /// Replaces rather than appends, which is the reading a builder has to /// have: a second call is a correction of the set, and a set built up over /// two calls would depend on where in the chain each one sat. #[must_use] pub fn sending(mut self, names: impl IntoIterator>) -> Self { self.sends = names.into_iter().map(Into::into).collect(); self } } impl Field { /// A plain optional field of the given kind. pub fn new(kind: layout::FieldKind, name: impl Into, label: impl Into) -> Self { Self { kind, name: name.into(), upper_name: None, label: label.into(), hint: None, error: None, note: None, placeholder: None, options: Vec::new(), themes: Vec::new(), marks: crate::stage::Marks::none(), follows: None, accept: Vec::new(), multiple: false, required: false, max_length: None, min: None, max: None, step: None, curve: Curve::Linear { step: None }, unit: None, extended: false, as_instant: false, // `Fill`, matching `Column::new` and matching what every renderer // did before this member existed. A default of `Content` would have // been the tidier reading and would have silently narrowed every // described field in every app on the day it landed, which is the // one thing an additive member must not do. width: layout::Width::Fill, value: None, upper_value: None, writes: None, consults: Vec::new(), suggests: None, keeps_value: false, repeats: None, revealed_by: None, } } /// A file field taking the given accept list. /// /// [`layout::Field::upload`]'s counterpart and it earns a constructor for /// the same reason: a file field with no list is not broken, it is one that /// takes anything, so an accidental omission looks exactly like a deliberate /// choice unless the list is an argument. Pass an empty slice to mean any /// file and mean it. #[must_use] pub fn upload( name: impl Into, label: impl Into, accept: impl IntoIterator, ) -> Self { Self { accept: accept.into_iter().collect(), ..Self::new(layout::FieldKind::File, name, label) } } /// Say that what is in this control is the reader's and survives a redraw. /// /// See [`keeps_value`](Self::keeps_value). The measured case is a box the /// reader types into whose surrounding region is swapped by something else /// on the screen. #[must_use] pub const fn keeping_value(mut self) -> Self { self.keeps_value = true; self } /// Several files at once, not one. #[must_use] pub const fn many(mut self) -> Self { self.multiple = true; self } /// Whether anything in [`accept`](Self::accept) names a media family. /// /// [`layout::Field::accepts_media`] asked of the owned form: the question a /// renderer asks before it keeps room for a preview, answered of the whole /// list because a dropzone taking `image/*,video/*` has two families and /// still has a disclosure to offer. #[must_use] pub fn accepts_media(&self) -> bool { self.accept .iter() .any(|one| one.as_layout().family().is_some()) } /// How much of its row this asks for. #[must_use] pub const fn width(mut self, width: layout::Width) -> Self { self.width = width; self } /// Changing this writes, without waiting for a submit. #[must_use] pub fn writes(mut self, action: Action) -> Self { self.writes = Some(action); self } /// Ask this route about the value as the reader works on it, once it has /// stood still for [`Consult::SETTLES`]. /// /// Not only while it is typed: a select, a radio or a slider asks the same /// question, and [`consulting`](Self::consulting) with /// [`Consult::after`] of zero is what one of those usually wants, since its /// value was complete the moment it moved. /// /// Point the answer somewhere with [`Action::replacing`]; what it contains /// is between the route and the renderer. Use [`consulting`](Self::consulting) /// for a different interval, or for a floor under which nothing is asked at /// all — see [`Consult::at_least`]. /// /// Adds a question rather than replacing the ones already asked, which is /// what a field with several of them needs and what a builder that took the /// last call would make unwritable. #[must_use] pub fn consults(mut self, action: Action) -> Self { self.consults.push(Consult::new(action)); self } /// [`consults`](Self::consults) with the wait, the floor or what rides /// along named. /// /// Adds, for [`consults`](Self::consults)' reason. Chain it twice for a box /// that asks two routes at two rates, which is what MNW's discover search /// does. #[must_use] pub fn consulting(mut self, consult: Consult) -> Self { self.consults.push(consult); self } /// This field owns a suggestion list, filled from this route once the value /// has stood still for [`Consult::SETTLES`]. /// /// [`suggests`](Self::suggests). Use [`suggesting`](Self::suggesting) to /// name the wait, the floor, or what rides along — both measured MNW sites /// spell a floor, and a suggestion route asked on the first letter is the /// `ILIKE 'a%'` over the whole catalogue the floor exists to refuse. /// /// Replaces rather than adds, unlike [`consults`](Self::consults): a field /// owns one list, and a second call is a description changing its mind /// rather than asking a second question. #[must_use] pub fn suggests(mut self, action: Action) -> Self { self.suggests = Some(Consult::new(action)); self } /// [`suggests`](Self::suggests) with the wait, the floor or what rides /// along named. #[must_use] pub fn suggesting(mut self, consult: Consult) -> Self { self.suggests = Some(consult); self } /// This question is answered zero or more times, in slots the reader adds /// and removes. /// /// See [`Repeat`], which carries the answers standing now, the floor and /// ceiling on how many there may be, and what the two controls are called. #[must_use] pub fn repeating(mut self, repeats: Repeat) -> Self { self.repeats = Some(repeats); self } /// This question only applies while another control holds a value. /// /// [`revealed_by`](Self::revealed_by). The counterpart of /// [`Slot::revealed_by`], for the one question inside a form rather than /// for a block of several things, and it is answered the same way by every /// renderer. /// /// Calling it twice replaces the condition, which is the reading every /// builder here has: the second call is a correction. /// /// ``` /// use quasi_router::{Field, Reveal, layout::FieldKind}; /// /// let zone = Field::new(FieldKind::Text, "timezone", "Anchored to") /// .revealed_by(Reveal::holding("tz_kind", "local")); /// /// assert!(zone.revealed(Some("local"))); /// assert!(!zone.revealed(Some("relative"))); /// assert_eq!(zone.watches(), Some("tz_kind")); /// ``` #[must_use] pub fn revealed_by(mut self, reveal: Reveal) -> Self { self.revealed_by = Some(Box::new(reveal)); self } /// Whether this question applies, given what its control is holding. /// /// `true` for a field that named no condition, which is nearly all of /// them. [`Slot::revealed`]'s counterpart, and what a renderer does with /// `false` is the renderer's on the same terms. #[must_use] pub fn revealed(&self, held: Option<&str>) -> bool { self.revealed_by .as_ref() .is_none_or(|reveal| reveal.satisfied_by(held)) } /// The name of the control this question is watching, if it watches one. #[must_use] pub fn watches(&self) -> Option<&str> { self.revealed_by .as_ref() .map(|reveal| reveal.control.as_str()) } /// One slot of a repeating question, as an ordinary field. /// /// The name is [`Repeat::at`] of this field's, the label is /// [`Repeat::ordinal`] of this field's, and the value and the error are /// that slot's own. Everything else is the question's and is carried /// through unchanged, which is the point: a renderer draws a slot with /// whatever it already does to a field, and there is no second field /// emitter anywhere in the stack. /// /// [`repeats`](Self::repeats) is cleared on the way out, so a renderer that /// loops over the slots cannot recurse into them. /// /// A slot past the end of what the description offered is an empty box /// under the right name, which is exactly what a slot the reader has just /// added is. That is why this takes an index rather than an /// [`Instance`]: the reader's slots outnumber the description's the moment /// the add control is pressed, and both are drawn the same way. /// /// Answered for a field that repeats nothing too, and the answer is the /// field itself with its name indexed. Nothing calls it that way, and /// refusing would make every renderer branch before it could loop. #[must_use] pub fn instance(&self, at: usize) -> Self { let held = self .repeats .as_ref() .and_then(|repeat| repeat.instances.get(at)); Self { name: Repeat::at(&self.name, at), label: held .and_then(|slot| slot.named.clone()) .unwrap_or_else(|| Repeat::ordinal(&self.label, at)), value: held.and_then(|slot| slot.value.clone()), error: held.and_then(|slot| slot.error.clone()), repeats: None, // The condition belongs to the question, and a renderer that has // reached the slots has already answered it once for the whole // group. A slot carrying it would have every renderer asking the // same question once per box. revealed_by: None, ..self.clone() } } /// One named question of one slot, as an ordinary field. /// /// [`instance`](Self::instance) one level finer, and for its reason: a /// renderer draws a part with whatever it already does to a field, so a /// slot that is several questions needs no second emitter either. The name /// is [`Repeat::part_at`], the label is the part's own and is not numbered, /// and the value and the error are the part's. /// /// [`repeats`](Self::repeats) is cleared for /// [`instance`](Self::instance)'s reason, and so is /// [`revealed_by`](Self::revealed_by). #[must_use] pub fn instance_part(&self, at: usize, part: usize) -> Self { let repeat = self.repeats.as_ref(); let question = repeat.and_then(|repeat| repeat.parts.get(part)); let answered = repeat .and_then(|repeat| repeat.instances.get(at)) .map(|slot| slot.part(part)) .unwrap_or_default(); Self { name: Repeat::part_at( &self.name, at, question.map_or("", |question| question.name.as_str()), ), label: question.map_or_else(String::new, |question| question.label.clone()), value: answered.value, error: answered.error, repeats: None, revealed_by: None, ..self.clone() } } /// Every field one slot draws: the slot itself, or one per part. /// /// The hinge the three renderers turn on, so that none of them carries the /// branch. A slot of an ordinary repeating question is one field and this /// answers one; a slot of a grouped one is [`Repeat::parts`] fields in /// their order. Four walks in the TUI alone read this -- the height, the /// drawing, the caret and the submitted names -- and they agree because /// they ask the same question rather than each looping the parts. #[must_use] pub fn instance_fields(&self, at: usize) -> Vec { match self.repeats.as_ref() { Some(repeat) if repeat.grouped() => (0..repeat.parts.len()) .map(|part| self.instance_part(at, part)) .collect(), _ => vec![self.instance(at)], } } /// How many slots this question stands in right now, as described. /// /// `1` for the ordinary field, which is the count a caller with no interest /// in repetition can loop over without asking whether there is any. #[must_use] pub fn slots(&self) -> usize { self.repeats.as_ref().map_or(1, Repeat::standing) } /// This field as a control asks it: a question, and nothing that writes. /// /// What every renderer draws for a member of [`Act::asks`]. A field there /// is answered by the press that asked for it, so a /// [`writes`](Self::writes) route on it would fire a second write for the /// same value. [`consults`](Self::consults) survives: asking whether a tag /// slug is taken is a question about the value, not a write of it. #[must_use] pub fn as_asked(&self) -> Self { Self { writes: None, ..self.clone() } } /// A select offering the given options. /// The options offered, appended to the ones already there. /// /// The accreting half of [`select`](Self::select) and [`radio`](Self::radio), /// which take the whole list as an argument. Beside them for the reason /// [`Table::column`] sits beside `Table::new`: a caller with no expression /// to hold a list in still has to be able to offer options, and `options` /// was reachable only by assigning the field. /// /// Says nothing about the kind. A kind that offers no options ignores them, /// which is what [`layout::FieldKind::offers_options`] already decides. #[must_use] pub fn options(mut self, options: impl IntoIterator) -> Self { self.options.extend(options); self } /// Offer one more option, chaining. /// /// The accreting half of [`Self::options`], which takes the whole list. The /// fifth constructor of exactly this shape, after [`Table::column`], /// [`Row::cell`], [`Self::options`] itself and [`Node::figure`]: a caller /// building its options one at a time has no expression to hold a list in. /// MNW's repository bar is the site -- its ref chooser offers a branch or a /// tag per ref, and which of the two decides only the label. #[must_use] pub fn option(mut self, option: Choice) -> Self { self.options.push(option); self } pub fn select(name: impl Into, label: impl Into, options: Vec) -> Self { Self { options, ..Self::new(layout::FieldKind::Select, name, label) } } /// A radio group offering the given options. pub fn radio(name: impl Into, label: impl Into, options: Vec) -> Self { Self { options, ..Self::new(layout::FieldKind::Radio, name, label) } } /// A theme picker over the themes the host resolved. /// /// A constructor for [`layout::Field::theme`]'s reason: the list is the one /// thing this kind takes that a call site can get wrong by *substitution*, /// since [`options`](Self::options) is right there and reads as if it would /// work. A renderer walking `options` for a theme picker draws an empty /// control. /// /// [`following`](Self::following) is a builder rather than a fourth /// argument: a picker with no follow-the-system row is a real picker. pub fn theme( name: impl Into, label: impl Into, themes: Vec, ) -> Self { Self { themes, ..Self::new(layout::FieldKind::Theme, name, label) } } /// Offer these themes, chaining. /// /// The accreting half of [`theme`](Self::theme), which takes the whole list /// as an argument, and the same gap [`options`](Self::options) fills beside /// [`select`](Self::select): a caller with no expression to hold a list in /// still has to be able to offer themes. GoingsOn's Appearance section is /// the site. /// /// Says nothing about the kind, exactly as `options` does not. A renderer /// reads these only for [`layout::FieldKind::Theme`]. #[must_use] pub fn themes(mut self, themes: impl IntoIterator) -> Self { self.themes.extend(themes); self } /// The same picker, offering a row that tracks the ambient mode. /// /// The [`Choice`] carries the value the app's own store spells it with — /// `makeover::FOLLOW` for every store in the family today, and none of them /// is obliged to keep it. #[must_use] pub fn following(mut self, follow: Choice) -> Self { self.follows = Some(follow); self } /// A bounded number the user drags across its whole extent. /// /// The bounds are arguments for [`layout::Field::range`]'s reason: they are /// not a rule the answer is checked against, they are the control, so a /// range that forgot them has nothing to slide across. pub fn range( name: impl Into, label: impl Into, min: impl Into, max: impl Into, ) -> Self { Self { min: Some(min.into()), max: Some(max.into()), ..Self::new(layout::FieldKind::Range, name, label) } } /// One question with two ends, taking the name each end submits under. /// /// The names are arguments for [`layout::Field::interval`]'s reason: an /// interval built without the second one has an upper end with nowhere to be /// submitted, and nothing downstream can invent a name for it. /// /// The extent stays optional, unlike [`range`](Self::range)'s. An interval's /// bounds are a rule each end is checked against rather than the control, so /// a missing one is an open end rather than a control with nothing to slide /// across. pub fn interval( name: impl Into, upper_name: impl Into, label: impl Into, ) -> Self { Self { upper_name: Some(upper_name.into()), ..Self::new(layout::FieldKind::Interval, name, label) } } /// The name the upper end submits under, for an interval built by hand. /// /// [`interval`](Self::interval) is still the entry point a hand-written /// caller should take, and its doc says why the names are arguments there. /// This exists because a description cannot reach a three-argument /// constructor: `quasi-declare`'s `field` production says a kind, a name /// and a label, and everything else about a field is a setting in its body. /// audiofiles' filter panel is the site -- six intervals, and without this /// the only described spelling was the whole `Field` written out as an /// aggregate, which is the expression the form exists to refuse. /// /// Pairs with [`upper_value`](Self::upper_value), which names the same end's /// answer. #[must_use] pub fn upper_name(mut self, name: impl Into) -> Self { self.upper_name = Some(name.into()); self } /// The granularity the value moves in. /// /// A builder rather than a fourth argument to [`range`](Self::range): the /// host's own granularity is a real answer, and a stepped /// [`layout::FieldKind::Number`] wants this too. #[must_use] pub fn step(mut self, step: impl Into) -> Self { let step = step.into(); // Routed to wherever this kind keeps its granularity, rather than made // two builders. A range's lives on its curve as of makeover-layout // 0.32.0 and a typed value's stays here, and a call site saying "this // moves in steps of 0.001" means the same thing either way. if self.kind == layout::FieldKind::Range { self.curve = std::mem::take(&mut self.curve).with_step(Some(step)); } else { self.step = Some(step); } self } /// How this slider's position becomes its value. /// /// [`layout::FieldKind::Range`]'s, and it carries the granularity with it: /// passing a curve replaces whatever [`step`](Self::step) had set. #[must_use] pub fn curve(mut self, curve: Curve) -> Self { self.curve = curve; self } /// What the number is measured in. /// /// The symbol alone -- `s`, not `(s)` and not ` s`. Where it is drawn and /// how it is spaced is the renderer's, which is why the description carries /// neither. #[must_use] pub fn unit(mut self, unit: impl Into) -> Self { self.unit = Some(unit.into()); self } /// The form refuses to submit without it. #[must_use] pub fn required(mut self) -> Self { self.required = true; self } /// The wall-clock value is submitted as the moment it names. /// /// See [`as_instant`](Self::as_instant) for what it asks and who answers /// it. A [`layout::FieldKind::DateTime`]'s; sayable and ignored elsewhere. #[must_use] pub fn submits_instant(mut self) -> Self { self.as_instant = true; self } /// Standing help, shown whether or not anything is wrong. #[must_use] pub fn hint(mut self, hint: impl Into) -> Self { self.hint = Some(hint.into()); self } /// What is wrong with the value now. #[must_use] pub fn error(mut self, error: impl Into) -> Self { self.error = Some(error.into()); self } /// Ghost text shown while the field is empty. /// /// Was reachable only by assigning the field, which is /// The missing-builder defect met again and on the largest field in the /// vocabulary: 54 sites across the three shape trees fall out of a builder /// chain to write it, which is more than any other member here. /// /// Not [`hint`](Self::hint), and the two are worth keeping apart. A hint is /// standing help that survives the reader typing; a placeholder is a sample /// answer that disappears the moment they do, so it cannot carry anything /// they will need later. A renderer that had only one of them would have to /// pick which behaviour to give it. /// #[must_use] pub fn placeholder(mut self, placeholder: impl Into) -> Self { self.placeholder = Some(placeholder.into()); self } /// The longest the value may be, in characters. /// /// The description carries the rule and the renderer emits its host's /// idiom; deciding that a value is wrong stays with whoever validated it, /// which is [`max_length`](Self::max_length)'s own contract. #[must_use] pub const fn limited_to(mut self, characters: u32) -> Self { self.max_length = Some(characters); self } /// The extent a value is accepted within, both ends. /// /// One builder for the pair rather than two, on /// [`Row::ticking`]'s grounds: a bound is only meaningful against the /// other one, and two setters can disagree. [`Field::range`] already takes /// both together for exactly this reason, and this is how every other kind /// reaches what a range gets from its constructor. /// /// Written the way the host writes a value, which is what /// [`min`](Self::min) says: these are strings because a date bound and a /// number bound are the same fact about a field and only one of them is a /// number. /// #[must_use] pub fn within(mut self, min: impl Into, max: impl Into) -> Self { self.min = Some(min.into()); self.max = Some(max.into()); self } /// The lowest value accepted, leaving the upper end open. /// /// [`within`](Self::within) is the ordinary spelling and sets both. This /// exists because an open end is a real answer rather than a missing one -- /// [`Field::interval`] says so in as many words -- and a field with a floor /// and no ceiling cannot be written by a builder that insists on the pair. #[must_use] pub fn at_least(mut self, min: impl Into) -> Self { self.min = Some(min.into()); self } /// The highest value accepted, leaving the lower end open. /// /// See [`at_least`](Self::at_least). #[must_use] pub fn at_most(mut self, max: impl Into) -> Self { self.max = Some(max.into()); self } /// Put the field behind a "more options" disclosure. /// /// Presence is the whole of it, which is why it takes no argument: a /// description says a field is secondary or says nothing, and /// `extended(false)` would be a way of writing the default twice. #[must_use] pub const fn extended(mut self) -> Self { self.extended = true; self } /// What the answer the user has given costs, in the tone it deserves. /// /// Not a validation failure: the field stays valid and submittable. See /// [`note`](Self::note). #[must_use] pub fn note(mut self, tone: layout::Tone, note: impl Into) -> Self { self.note = Some((tone, note.into())); self } /// Whether the field is currently reporting a problem. #[must_use] pub fn invalid(&self) -> bool { self.error.is_some() } /// Put this back in the box when the form is offered again. /// /// A [`layout::FieldKind::Secret`] keeps `None` whatever it is handed. A /// password that comes back down the wire is a password in a page, in a /// proxy log and in a browser cache, and the field kind exists to say so. /// Silently rather than by a `Result`, because there is no answer a caller /// could give that would make echoing it right. #[must_use] pub fn value(mut self, value: impl Into) -> Self { if self.kind != layout::FieldKind::Secret { self.value = Some(value.into()); } self } /// Put this back in the upper box of an interval when the form is offered /// again. /// /// [`value`](Self::value)'s counterpart and it refuses a /// [`layout::FieldKind::Secret`] on the same terms, though no secret is an /// interval: the guarantee is written where the value is set rather than /// where the kinds happen not to overlap today. #[must_use] pub fn upper_value(mut self, value: impl Into) -> Self { if self.kind != layout::FieldKind::Secret { self.upper_value = Some(value.into()); } self } /// Re-offer whatever was submitted under this field's name. /// /// What a refused write calls, with the [`Params`](crate::Params) it was /// refusing. A name with nothing under it stays empty, which is what an /// unticked checkbox and an untouched box both are. #[must_use] pub fn refilled(self, params: &crate::Params) -> Self { let filled = match params.get(&self.name) { Some(value) => { let value = value.to_owned(); self.value(value) } None => self, }; // An interval was submitted under two names, so re-offering it reads // both. Either end staying empty is a real answer rather than a // half-filled form: "over 120 BPM" has no upper end. let Some(upper_name) = filled.upper_name.clone() else { return filled; }; match params.get(&upper_name) { Some(value) => { let value = value.to_owned(); filled.upper_value(value) } None => filled, } } /// Read this field as the description layer's own type. /// /// A callback rather than a return, because [`layout::Field`] holds its /// options as a slice and ours holds them as owned values, so the borrowed /// slice has to live somewhere for the duration of the read. Building it /// here means one allocation at the renderer's boundary instead of the /// borrow leaking into every caller's signature. pub fn with_layout(&self, f: impl FnOnce(layout::Field<'_>) -> R) -> R { let options: Vec> = self.options.iter().map(Choice::as_layout).collect(); let themes: Vec> = self.themes.iter().map(ThemeChoice::as_layout).collect(); let accept: Vec> = self.accept.iter().map(Accepted::as_layout).collect(); f(layout::Field { kind: self.kind, name: &self.name, upper_name: self.upper_name.as_deref(), label: &self.label, hint: self.hint.as_deref(), error: self.error.as_deref(), note: self.note.as_ref().map(|(t, n)| (*t, n.as_str())), placeholder: self.placeholder.as_deref(), options: &options, themes: &themes, follows: self.follows.as_ref().map(Choice::as_layout), accept: &accept, multiple: self.multiple, required: self.required, max_length: self.max_length, min: self.min.as_deref(), max: self.max.as_deref(), step: self.step.as_deref(), curve: self.curve.as_layout(), unit: self.unit.as_deref(), extended: self.extended, as_instant: self.as_instant, }) } } /// One run of source code, and what it is. /// /// The unit [`Node::Code`] is a list of, and the whole reason that node carries /// runs rather than a string: the app classified the source, and this is the /// classification crossing the seam. Decision `19d7602d`, 2026-09-02. /// /// Owned, for the reason stated at the top of this file: a router's answer /// outlives its handler, and the text here is usually built from state rather /// than found in it. [`layout::Syntax`] is `Copy`, so only the text allocates. /// /// # Why the text is carried and not an index /// /// A `(range, syntax)` pair over one source string would allocate less, and it /// was rejected: the ranges are byte offsets into a string the renderer must /// then re-slice, so every renderer would have to handle a range that does not /// land on a character boundary, and two of the three would get it wrong on the /// first file with a multi-byte character in it. The run carrying its own text /// cannot express that bug. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Lexeme { /// The characters, exactly as they appear in the source. /// /// Including whitespace. A classifier that strips it hands the renderer a /// file it cannot lay out, and indentation is most of what makes source /// readable. pub text: String, /// What the run is. pub syntax: layout::Syntax, } impl Lexeme { /// A run of ordinary code. /// /// [`layout::Syntax::Plain`], which is what a classifier that ran and found /// nothing says, and what a host with no classifier at all says about a /// whole file. #[must_use] pub fn plain(text: impl Into) -> Self { Self { text: text.into(), syntax: layout::Syntax::Plain, } } /// A run of a stated class. #[must_use] pub fn new(text: impl Into, syntax: layout::Syntax) -> Self { Self { text: text.into(), syntax, } } } /// One column of a table, owned. /// /// The borrowed original is [`layout::Column`]. The `name` is both the heading /// and the address a cell is found by, which is what replaces addressing /// columns by position. /// No `Hash`, for the reason [`Tag`] and [`Field`] have none: it can hold an /// [`Action`], which holds [`Params`], which is a `Vec`. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Column { /// The heading, and the name the cell is addressed by. pub name: String, /// How much room it asks for. pub width: layout::Width, /// What it is worth when room runs out. pub priority: layout::Priority, /// Which way the table is ordered by this column, if it is. pub sorted: Option, /// What pressing this heading calls. /// /// `makeover-layout` carries `Column::sortable`, a bare bool, because it /// cannot name an address; here the address *is* the sortability, so the /// two collapse into one field and cannot disagree. /// [`as_layout`](Self::as_layout) sets the bool from whether this is here. /// /// Reordering a table is a control that writes with no surrounding submit, /// which is `14612ed8`'s shape, and the renderer treats it the same way. pub reorder: Option, } impl Column { /// A column that absorbs slack and drops after the optional ones. pub fn new(name: impl Into) -> Self { Self { name: name.into(), width: layout::Width::Fill, priority: layout::Priority::Secondary, sorted: None, reorder: None, } } /// Pressing this heading reorders the table. #[must_use] pub fn reorder(mut self, action: Action) -> Self { self.reorder = Some(action); self } /// The table is currently ordered by this column, this way. #[must_use] pub const fn sorted(mut self, sort: layout::Sort) -> Self { self.sorted = Some(sort); self } /// Set how much room it asks for. #[must_use] pub fn width(mut self, width: layout::Width) -> Self { self.width = width; self } /// Set what it is worth when room runs out. #[must_use] pub fn priority(mut self, priority: layout::Priority) -> Self { self.priority = priority; self } /// Borrow as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::Column<'_> { layout::Column { name: &self.name, width: self.width, priority: self.priority, sortable: self.reorder.is_some(), sorted: self.sorted, } } } /// Which region this is, owned. /// /// The borrowed original is [`layout::Region`], and only one member borrows: /// [`layout::Region::Handover`] carries a name the app owns and this crate never /// interprets. /// /// # Two members are gone, and a row says what they said /// /// `Split` was two panes side by side and `Columns` was a row of peers. Both /// were arrangement spelled as containment: they said nothing about scroll, /// depth or what may be inside, only how the room is divided. A [`Run`] whose /// members carry a [`layout::Width`] says the same thing and says more of it, /// because the row also carries a [`layout::Fallback`] and so can state what /// happens when the room runs out, which neither variant could. Split is a run /// of [`Content`](layout::Width::Content) then [`Fill`](layout::Width::Fill); /// Columns is a run of fills, which divide equally by that type's own rule. /// Ruled by Max 2026-09-07 on quasicoherent `b1d4c5d7`, built as `cf981aaa`. /// /// [`Sidebar`](Self::Sidebar) was measured with them and stays, because it is /// not the same kind of fact: it marks *which* region is the side of a /// [`layout::Arrangement::SidebarContent`] screen, which `quasi-tui` and /// `quasi-immediate` both match on to place it, and no width on a row member /// can say that. `Band`, `Pane`, `Group`, `TabGroup` and `Modal` are untouched /// for their own reasons: the first is a separate judgement, and the rest are /// containment, visibility and z-order rather than arrangement. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum RegionKind { /// A full-width strip with a title slot and an actions cluster. Band, /// A persistent column beside the content, holding navigation. Sidebar, /// A region of content with its own scroll. Pane, /// Things that belong together, and nothing else. /// /// The block a [`Heading::Section`](layout::Heading::Section) names, which /// had no container until makeover-layout 0.27.0: a section heading is a /// leaf beside the things it names, so a description could say a section had /// started and never that one had ended. See [`layout::Region::Group`] for /// the count behind it. /// /// Claims no scroll and no depth of its own, which is what separates it from /// [`Pane`](Self::Pane) -- the member apps reached for instead, and the /// reason 28 of the 45 regions in the described screens were panes. A group /// in a pane is in a well; a group on the page is on the page. /// /// The heading is an ordinary node in the body rather than a field here, so /// a group of related toggles with no heading stays legal. Group, /// A set of panes, one visible at a time, and a strip that chooses /// between them. /// /// Says nothing about where the strip sits. A row over the panes, a column /// beside them, a wrapped run of links under them: all three are the same /// member drawn by a renderer that knows its host, the way the strip's /// overflow is. MNW's settings sub-nav is a column on a wide viewport and a /// wrapped row below the breakpoint, one component and one CSS breakpoint, /// and it is a tab group on every count this member makes. TabGroup, /// Content over a scrim, taking input until dismissed. /// /// A modal this screen *contains*, which is how a confirmation is drawn: it /// arrives with the screen and goes when the screen goes. The app-level one /// is [`Outcome::Over`](crate::Outcome::Over), which draws a whole screen /// over whatever is under it and is reachable from screens that know /// nothing about it. Modal, /// A place, and nothing else. The app fills it per host. /// /// Decision 4: the renderer hands the space over and the app puts a JS /// component, an egui closure or a TUI widget in it. The rejected /// alternative was giving the placeholder its own route and fetching a /// fragment for it, which is uniform on paper and wrong in currency: a byte /// payload is not what egui or a terminal wants. /// /// All three renderers offer the fill in their own currency: /// `Webview::with_fill` takes markup, `Tui::with_fill` takes a drawing that /// answers a height and paints into a rect, and `Immediate::with_fill` /// takes a closure over a `Ui`. Each is keyed by [`Slot::id`] rather than /// by the name here, since a screen of N rows each carrying a fill shares /// one name and has N ids. /// /// # A handover now, a widget eventually /// /// A behaviour with no described form takes this member rather than /// growing the vocabulary with members two of the three renderers could /// only degrade -- scrub, rate and chapters were the rejected shape -- and /// the surface is not ceded to the host permanently either. Once /// [`Widget`](Self::Widget) has grown, a behaviour already implemented /// once and tested behind a fill is a candidate to become one, and the /// media player is the first of them. /// /// The revisit trigger is the widget work rather than a count of consumers. /// /// # What separates it from [`Ceded`](Self::Ceded) /// /// A fill is owed here in every host's currency. See /// [`layout::Region::Handover`]; the short form is that a renderer handed /// one of these and given no fill is looking at a hole the app meant to /// fill, and should say so rather than draw an empty box. Handover { /// What the app calls it. Never interpreted here. name: String, }, /// A place the app fills, that no host is owed a fill for. /// /// The other half of what was one opaque member. A chart, a waveform, a /// rendered picture of domain data: the app has ruled the space is not the /// description's to fill and is not going to become so, so a renderer with /// nothing to put here draws nothing and is right to. /// /// See [`layout::Region::Ceded`]. The distinction is worth two members /// because the two want opposite behaviour from a renderer that cannot /// fill them. Ceded { /// What the app calls it. Never interpreted here. name: String, }, /// A named assembly of things the description already says. /// /// The third tier, and the one member that is a name *and* contents. See /// [`layout::Region::Widget`] for what separates it from the two either /// side of it; the short form is that a primitive has to be drawable by /// every host from scratch and a handover carries nothing under it, and a /// carousel is neither. /// /// The body is the assembly and it is ordinary description: a renderer that /// does not recognise the name walks it and draws primitives, which is why /// naming one costs no renderer release. Contrast /// [`Handover`](Self::Handover), whose body a renderer can draw but whose /// *fill* only the host has. Widget { /// What the assembly is called. Never interpreted here, and a renderer /// is free not to know it. name: String, }, } impl RegionKind { /// A place the app fills itself, and owes every host a fill for. /// /// Beside [`Slot::handover`], which builds the region and its id in one /// call. This is the kind on its own, which is what a caller holding the id /// separately needs: the three kinds that carry a name are the three a bare /// variant cannot spell. #[must_use] pub fn handover(name: impl Into) -> Self { Self::Handover { name: name.into() } } /// A place the app fills itself, that no host is owed a fill for. /// /// [`Slot::ceded`]'s kind on its own, for the same reason [`handover`] has /// one. /// /// [`handover`]: Self::handover #[must_use] pub fn ceded(name: impl Into) -> Self { Self::Ceded { name: name.into() } } /// Borrow as the description layer's own type. #[must_use] pub fn as_layout(&self) -> layout::Region<'_> { match self { Self::Band => layout::Region::Band, Self::Sidebar => layout::Region::Sidebar, Self::Pane => layout::Region::Pane, Self::Group => layout::Region::Group, Self::TabGroup => layout::Region::TabGroup, Self::Modal => layout::Region::Modal, Self::Handover { name } => layout::Region::Handover { name }, Self::Ceded { name } => layout::Region::Ceded { name }, Self::Widget { name } => layout::Region::Widget { name }, } } /// Whether the description can say anything about the contents. #[must_use] pub fn described(&self) -> bool { self.as_layout().described() } /// How the region sits on what is behind it. #[must_use] pub fn depth(&self) -> layout::Depth { self.as_layout().depth() } } /// A node, and what it is worth when there is not room for everything. /// /// The other half of "Any width, one answer" (`makeover-layout` 0.27.4). A /// renderer narrows by raising a cutoff over a declared total order, never by /// counting what fits, and the only placement that can declare its rank /// otherwise is a [`Column`]. So anything that is not a table hand-rolls its /// responsiveness, and hand-rolling means measuring, and measuring means /// keeping the measurement, which is how a layout becomes a function of the /// width you came from. audiofiles had four of those and one genuine /// measure-and-correct loop before its screens were described. /// /// # Why the rank sits here and not on the node /// /// [`Column`] already settled it: the priority is on the column and not on the /// cell's contents. The same node at [`layout::Priority::Essential`] in one /// band and [`layout::Priority::Optional`] in another is an ordinary thing to /// want, and a rank welded to the node could not say it. /// /// # Why not a wrapping [`Node`] member /// /// A `Node::Optional { priority, node }` would have been additive and needed /// no type change here, and it was refused. A renderer that has not learned a /// member draws it as one muted stand-in line, which is the right failure for /// an unknown leaf and exactly the wrong one for a member whose whole job is /// deciding what disappears: content wrapped for narrowing would go invisible /// in any renderer that had not caught up. Changing the type instead makes /// every renderer fail to compile, which is the failure that gets fixed. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Ranked { /// The thing itself. pub node: Node, /// What it is worth when room runs out. /// /// [`layout::Priority::Essential`] by default, which never drops. That is /// what makes the rank additive at a call site rather than a re-reading of /// every screen in the tree. pub priority: layout::Priority, /// How much of the row it asks for. /// /// The other half of what a [`Column`] has always been able to say. A rank /// answers "what goes first when there is not room for everything"; this /// answers "how is the room divided while there is". A row of two panes /// where the left takes what it needs and the right absorbs the rest is /// the shape [`RegionKind`] used to spell as a variant, and the variants /// are gone because this says it (quasicoherent `cf981aaa`, Max's ruling /// on `b1d4c5d7`). /// /// **[`layout::Width::Content`] by default, because that is what a member /// of a run already drew.** A run is a flex row whose members have /// `min-width: min-content` and no grow, a terminal gives each what it /// asks for, and egui allocates what a galley needs. So every member /// written before this existed keeps its drawing, and a screen opts into /// dividing the room by saying so. /// /// **Read on a run, ignored in a body.** A [`Run`] is a row and its /// members divide a width between them; a [`Slot::body`] is a stack and /// its members each get the whole of it, so there is nothing for a width /// to divide. The field is on `Ranked` rather than on the run's own list /// because the rank is, and splitting one of the two into a second type /// would be two spellings of a member. pub width: layout::Width, } impl Ranked { /// A node that never drops and takes what it needs. #[must_use] pub const fn new(node: Node) -> Self { Self { node, priority: layout::Priority::Essential, width: layout::Width::Content, } } /// A node, and what it is worth. #[must_use] pub const fn worth(node: Node, priority: layout::Priority) -> Self { Self { node, priority, width: layout::Width::Content, } } /// A node, what it is worth, and how much of the row it asks for. /// /// Beside [`Self::worth`] rather than replacing it: a width is the thing /// most members have no opinion about, and a constructor that demanded one /// would make every strip and every band say `Content` to mean "as before". #[must_use] pub const fn sized(node: Node, priority: layout::Priority, width: layout::Width) -> Self { Self { node, priority, width, } } /// Whether a region narrowed to this cutoff still shows it. /// /// [`layout::Column::kept_at`] said for a region member, and it has to be /// the same comparison or a screen's table and the band above it would /// disappear at different points. A renderer raises the cutoff and asks /// this; nothing counts, so inserting a member changes what is emitted /// rather than changing which member vanishes. #[must_use] pub const fn kept_at(&self, cutoff: layout::Priority) -> bool { (self.priority as u8) >= (cutoff as u8) } } /// The cutoffs a region narrows through, weakest first. /// /// The same sequence `makeover-tui`'s table keeps, and it is here rather than /// in each renderer so the three cannot drift into disagreeing about the order /// things go. [`layout::Priority`] is `#[non_exhaustive]`, so a tier added /// upstream has to be added here in its place in the sequence: the cost of /// missing one is a member that drops later than it should, which is visible, /// rather than a build that stops. /// /// *When* to raise the cutoff is the renderer's, and deliberately. A browser /// answers it with `@media` against CSS pixels, a terminal against cells, egui /// against points; those are three different units for one question and the /// description holds none of them. pub const CUTOFFS: [layout::Priority; 3] = [ layout::Priority::Optional, layout::Priority::Secondary, layout::Priority::Essential, ]; impl From for Ranked { fn from(node: Node) -> Self { Self::new(node) } } /// The members that share one row, and what the row does without enough of it. /// /// The gap it closes is goingson's, and it is worth stating exactly because /// the fix looks like a stylesheet bug and is not. `styles.css:702` pinned /// every `.page-header` over the pill strip with `position: absolute`, which /// is the one construction the description layer cannot audit: the toolbar /// left the flow, contributed no width to the row it shared, and so nothing /// could collide with it and nothing prevented the collision. At 913 CSS /// pixels the help control clipped off the edge; at 700 the search field sat /// on top of the "Contacts" pill; at 560 the new-contact button left the /// viewport and could not be reached at all. /// /// The rule that construction broke is the first of the four: every described /// member is in flow. What was missing was any way to say the thing the /// stylesheet was asserting -- that a [`RegionKind::Band`] and a /// [`RegionKind::TabGroup`]'s strip occupy one row. This says it. /// /// # What it does not carry /// /// **A size.** Not a minimum, not a breakpoint, not a count. The description /// says what the row holds and each renderer derives the minimum in its own /// units -- a webview from `min-content` under a container query, a terminal /// from cell widths, egui from the galley -- and composes them by the flow. /// A derived minimum cannot rot and an authored one always does. The single /// hardcoded number in the whole mechanism is makeover-geometry's 44px contact /// patch, which is a density fact and already lives there. /// /// **[`layout::Room`].** Whether the row is tight is measured, per render, by /// whoever is rendering. Nothing here authors it. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Run { /// What the row does when it is [`layout::Room::Tight`]. /// /// No `Default`, here or upstream, and that is the point rather than an /// omission: a row cannot be described without saying what it does when it /// runs out of room. A default would be this crate guessing, and the guess /// would be silently wrong on exactly the screens that made the guess /// necessary. pub fallback: layout::Fallback, /// What shares the row, after whatever the region puts in it itself. /// /// A [`RegionKind::TabGroup`] puts its strip there, generated from its /// children's [`labels`](Slot::labels), so a tab group's run is the strip /// and then these. Every other region puts nothing there, so its run is /// exactly these members in order. /// /// That asymmetry is what lets the tab-group case be described without a /// new [`layout::Region`] member and without the strip becoming a node. /// Describing the strip in its own right is a separate question, filed as /// makeover-layout `978d24f8`; it is not needed to say what shares a row /// with one. /// /// [`Ranked`] rather than [`Node`], because [`layout::Fallback::Shed`] and /// [`layout::Fallback::Menu`] both read a [`layout::Priority`] per member /// and the type already carries one. `Wrap` and `Stack` keep every member, /// so they ignore it, which is why the rank is not conditional on the /// fallback. pub members: Vec, } impl Run { /// A row that holds nothing yet, and knows what it does when it is tight. #[must_use] pub const fn new(fallback: layout::Fallback) -> Self { Self { fallback, members: Vec::new(), } } /// Put a node in the row, saying what it is worth when the row is tight. /// /// The member goes on the row and not on the region, and that is the whole /// of how rule 2 is held now. There is no receiver here that could be /// missing a fallback: reaching this method at all means holding a `Run`, /// and the only way to hold one is to have said what it does when it runs /// out of room. The rule is carried by the type rather than by a runtime /// check, which is what lets a row be handed to a function that knows /// nothing about where it came from. /// /// That handing-off is not hypothetical. Of the 70 places in the tree that /// put something in a row, 25 sit in a function that did not declare the /// row: audiofiles' toolbar declares it once in `body` and then threads it /// through `here`, `holding`, `leaving`, `looking` and `frames`, each of /// which adds members and none of which has any business restating what the /// bar does when it is tight. Under the old shape those 25 were correct /// only by convention, and a helper called on a region with no row was a /// panic no compiler could see coming. A `Run` parameter makes the same /// five functions say in their signatures what they were already relying /// on. /// /// The node goes in the row rather than in a region's /// [`body`](Slot::body), and the two are different places: the body stacks /// down the region, the run lies across its leading row. goingson's toolbar /// belongs here, beside the tab strip it was overlapping. #[must_use] pub fn beside(mut self, node: Node, priority: layout::Priority) -> Self { self.members.push(Ranked::worth(node, priority)); self } /// Put a node in the row, saying what it is worth and how much it asks for. /// /// [`Self::beside`] with the second half of what a [`Column`] says. Two /// methods rather than one with a width argument, for [`Ranked::sized`]'s /// reason: most members have no opinion about the division, and the ones /// that do are the two-pane rows the retired [`RegionKind`] variants used /// to spell. #[must_use] pub fn spread(mut self, node: Node, priority: layout::Priority, width: layout::Width) -> Self { self.members.push(Ranked::sized(node, priority, width)); self } /// Whether this row keeps every member it was given. /// /// True for [`Wrap`](layout::Fallback::Wrap) and /// [`Stack`](layout::Fallback::Stack), which rearrange; false for /// [`Shed`](layout::Fallback::Shed) and [`Menu`](layout::Fallback::Menu), /// which read [`Ranked::priority`] and take members out of the row. A /// renderer asks this before it bothers computing a cutoff. /// /// An unrecognised fallback reads as keeping everything. `Fallback` is /// `#[non_exhaustive]`, and the safe reading of a member this crate has not /// been taught is the one that draws too much rather than the one that /// silently removes something. #[must_use] pub const fn keeps_every_member(&self) -> bool { !matches!( self.fallback, layout::Fallback::Shed | layout::Fallback::Menu ) } /// The members a row narrowed to this cutoff still shows, in order. #[must_use] pub fn kept_at(&self, cutoff: layout::Priority) -> Vec<&Ranked> { if self.keeps_every_member() { return self.members.iter().collect(); } self.members.iter().filter(|m| m.kept_at(cutoff)).collect() } } /// A fallback on its own is the row that has said what it does and holds /// nothing yet. /// /// Here so that [`Slot::across`] reads as one thing at both of its call shapes. /// A tab strip that only needs the fallback writes `across(Fallback::Menu)` and /// a band that has members writes `across(Run::new(..).beside(..))`, and /// neither has to know that the other exists. Note which direction this goes: /// a fallback becomes a row, and a row is never anything but a row. There is /// no conversion that hands back a `Run` without one being named, because that /// is the description this vocabulary exists to make unwritable. impl From for Run { fn from(fallback: layout::Fallback) -> Self { Self::new(fallback) } } /// What a control has to be holding for the region that names it to be out. /// /// The four shapes the eight measured sites need, and no more: a ticked box, /// an unticked one, one value, or one of several. A predicate would cover all /// four and could not be read by a renderer that has to draw "not applicable /// right now" as words, which is the reading the shape was ruled on. /// /// The value a control holds is read the way a form reads it: a checkbox is /// there by presence, per [`Field::value`], so an unticked box holds nothing /// and an empty box holds nothing either. That is one convention across the /// three renderers rather than three readings of "empty". #[derive(Debug, Clone, PartialEq, Eq)] pub enum Held { /// Anything at all: a ticked box, a filled-in answer. Anything, /// Nothing: an unticked box, a box the reader has cleared, a select /// resting on an empty option. Nothing, /// Exactly this value. Value(String), /// Any one of these values. /// /// goingson's zone picker is the site: it is out on three of the four /// `TzKind` values, and saying that as three regions with one condition /// each would put the same body on the screen three times. OneOf(Vec), } /// What brings a region out, said by the region. /// /// A region names the control it is watching and the value that brings it out, /// and a renderer evaluates that without knowing what pressed what. /// /// # Why it is here rather than on the control /// /// The host with no cursor decided it. On a terminal a hidden region is not /// hidden the way a browser hides one: the honest reading is "this region is /// not applicable right now", which is a property of the region, and a /// renderer is free to dim it, omit it or explain it. Put the condition on the /// control and a terminal asking whether a section applies has to search /// outward across every control on the screen to find out. A region gated by /// two controls has one home for the condition under this shape and none under /// the other. /// /// Both alternatives were considered and rejected in the same ruling: the /// condition on the control, and both ends joined by a back-pointer, which /// saves every renderer a search and costs two members to keep in agreement /// plus a rule for what happens when they disagree. /// /// # What it is not /// /// It is not [`Field::writes`] plus [`Action::replaces`]. That expresses the /// same reveal and buys a round trip for it, and on a form the reader is /// midway through it re-renders a region holding uncommitted values. /// /// It is not a press either. A toggle that flips on every press is /// unconditional and does not track a value, which is why all six MNW sites /// wrap one in a function that reads `.checked` or `.value` first. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Reveal { /// The control being watched, by [`Field::name`]. /// /// By name rather than by id, for [`Prefill::field`]'s reason: the name is /// what the description carries and what a submit sends the value under, /// while an id is scoped per form instance by whoever is emitting. /// /// A name no control on the screen carries leaves the region holding a /// condition nothing can satisfy. Every renderer draws that the same way, /// as a region that is not applicable, which is a description bug and /// reports itself as one. pub control: String, /// What that control has to hold. pub when: Held, } impl Reveal { /// Out while the box is ticked. #[must_use] pub fn ticked(control: impl Into) -> Self { Self { control: control.into(), when: Held::Anything, } } /// Out while the box is not ticked. #[must_use] pub fn unticked(control: impl Into) -> Self { Self { control: control.into(), when: Held::Nothing, } } /// Out while the control holds this value. #[must_use] pub fn holding(control: impl Into, value: impl Into) -> Self { Self { control: control.into(), when: Held::Value(value.into()), } } /// Out while the control holds any one of these values. #[must_use] pub fn holding_one_of>( control: impl Into, values: impl IntoIterator, ) -> Self { Self { control: control.into(), when: Held::OneOf(values.into_iter().map(Into::into).collect()), } } /// Whether a control holding this is holding what the region asked for. /// /// Here rather than in each renderer, so the three cannot come apart over /// what an empty value means. [`None`] and `Some("")` are one answer: /// nothing is held. A terminal's unticked checkbox is an empty buffer, a /// browser's is an element with no value to read, and both are the same /// fact about the form. #[must_use] pub fn satisfied_by(&self, held: Option<&str>) -> bool { let held = held.filter(|value| !value.is_empty()); match &self.when { Held::Anything => held.is_some(), Held::Nothing => held.is_none(), Held::Value(wanted) => held == Some(wanted.as_str()), Held::OneOf(wanted) => held.is_some_and(|value| wanted.iter().any(|one| one == value)), } } } /// One member of a region that shows its members one at a time. /// /// # Why the label is here and not on the region /// /// A label is the name of the control that reveals a frame: the tab's text, the /// disclosure's summary. Only a region that discloses or steps through its /// members draws one, so only such a region can hold one, and that is what this /// type is for. /// /// It used to sit on [`Slot`], where any region could carry it and only some /// would ever render it. That is not a lint waiting to be written. A member is /// built before it is placed, so nothing about the member can say whether its /// label will be drawn, and the parent that decides may be in another /// `declare!` entirely -- which is exactly how MNW's `/use-cases` lost the /// titles of nine cards for weeks with a green suite (quasicoherent `2cdc6761`, /// wiki `quasi-declare-form` section 22). Max ruled it should not be /// representable, so the label moved to the only place that draws it. #[derive(Clone, Debug, PartialEq, Eq)] pub struct Frame { /// What the control that reveals this frame is called. /// /// Optional because a carousel's frames are [`Node::Image`] and have /// nowhere to put one, which is correct rather than a gap: a photograph has /// a caption, not a tab name. A renderer draws a strip when every frame is /// named and a prev/next row when they are not, which is /// [`Slot::labels`]'s all-or-nothing rule. pub label: Option, /// The member itself, ranked as any other member is. pub member: Ranked, } impl Frame { /// A frame with a name, which is what a tab or a disclosure is. #[must_use] pub fn named(label: impl Into, node: Node) -> Self { Self { label: Some(label.into()), member: Ranked::new(node), } } /// A frame with no name, which is what a carousel's are. #[must_use] pub fn unnamed(node: Node) -> Self { Self { label: None, member: Ranked::new(node), } } } /// Which of a selective region's members are up at once. /// /// [`layout::Showing`]'s two selective members, and only those. The third, /// `All`, is the other arm of [`Body`], so a selective body cannot say it: this /// exists so `Selective { showing: Showing::All, .. }` is unspellable rather /// than merely wrong. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Picks { /// Exactly one. A carousel, a tab group. One, /// One, or none. A disclosure, which is closed until it is opened. AtMostOne, } impl Picks { /// The vocabulary's word for this, for a renderer that asks in those terms. #[must_use] pub const fn showing(self) -> layout::Showing { match self { Self::One => layout::Showing::One, Self::AtMostOne => layout::Showing::AtMostOne, } } } /// A region's members, and how many of them are up. /// /// Two arms, because there are two shapes of region and they do not hold the /// same thing. A region showing everything holds ranked members. A region /// showing one at a time holds frames, each with the name of the control that /// reveals it, plus which one is up. /// /// Collapsing `body`, `showing` and `shown` into one field is what makes the /// combinations that never meant anything unspellable: a label on a member /// nothing reveals, and a `shown` index on a region that shows everything. Both /// were fields that some regions read and others silently ignored. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Body { /// Every member, in order. What every region did before `Showing` existed. All(Vec), /// One member at a time, each named for the control that reveals it. Selective { /// Whether showing nothing is a legal resting place. picks: Picks, /// Which frame the region opens on. `None` under [`Picks::AtMostOne`] /// is the closed state; read it through [`Slot::current`], which is /// where an index past the end is dealt with. shown: Option, /// The frames, in order. frames: Vec, }, } impl Body { /// How many members there are, whichever shape this is. #[must_use] pub fn len(&self) -> usize { match self { Self::All(members) => members.len(), Self::Selective { frames, .. } => frames.len(), } } /// Whether the region draws nothing at all. #[must_use] pub fn is_empty(&self) -> bool { self.len() == 0 } /// Every member in order, with its frame taken off if it had one. /// /// What a renderer walks when it is drawing content rather than chrome. The /// chrome is the caller's other question, answered by [`Slot::labels`] and /// [`Slot::current`]. pub fn members(&self) -> impl Iterator + '_ { // Two iterator types, so they are boxed into one. A region's member // count is small and this is not on any hot path: the residual seam // exists so that a served screen walks no `Node` tree at all. let iter: Box> = match self { Self::All(members) => Box::new(members.iter()), Self::Selective { frames, .. } => Box::new(frames.iter().map(|frame| &frame.member)), }; iter } /// The first member, whichever shape the body is. #[must_use] pub fn first(&self) -> Option<&Ranked> { self.get(0) } /// One member by position, whichever shape the body is. #[must_use] pub fn get(&self, at: usize) -> Option<&Ranked> { match self { Self::All(members) => members.get(at), Self::Selective { frames, .. } => frames.get(at).map(|frame| &frame.member), } } /// Every member in order. The same as [`members`](Self::members), under the /// name a caller reaches for when it is walking a list. pub fn iter(&self) -> impl Iterator + '_ { self.members() } /// Add one member, keeping whichever shape the body already has. pub fn push(&mut self, member: Ranked) { match self { Self::All(members) => members.push(member), Self::Selective { frames, .. } => frames.push(Frame { label: None, member, }), } } /// Add several, in order. pub fn extend(&mut self, members: impl IntoIterator) { for member in members { self.push(member); } } /// Every member, mutably, under the name a caller walking a list reaches /// for. pub fn iter_mut(&mut self) -> impl Iterator + '_ { self.members_mut() } /// Drop every member, keeping the shape. /// /// A selective body that is emptied stays selective: what it shows one at a /// time is a fact about the region, not about how many members it happens to /// hold right now. pub fn clear(&mut self) { match self { Self::All(members) => members.clear(), Self::Selective { frames, .. } => frames.clear(), } } /// Every member, mutably. pub fn members_mut(&mut self) -> impl Iterator + '_ { let iter: Box> = match self { Self::All(members) => Box::new(members.iter_mut()), Self::Selective { frames, .. } => { Box::new(frames.iter_mut().map(|frame| &mut frame.member)) } }; iter } /// The vocabulary's word for how much of this is up at once. #[must_use] pub const fn showing(&self) -> layout::Showing { match self { Self::All(_) => layout::Showing::All, Self::Selective { picks, .. } => picks.showing(), } } /// Which member the region opens on, before clamping. #[must_use] pub const fn shown(&self) -> Option { match self { Self::All(_) => None, Self::Selective { shown, .. } => *shown, } } /// Turn a body that shows everything into one that shows one at a time. /// /// Members it already holds become unnamed frames, which is a carousel: a /// name arrives with [`Slot::frame`] and nowhere else, so nothing here can /// invent one or drop one. fn select(&mut self, picks: Picks, shown: Option) { match self { Self::All(members) => { *self = Self::Selective { picks, shown, frames: std::mem::take(members) .into_iter() .map(|member| Frame { label: None, member, }) .collect(), }; } Self::Selective { picks: held, shown: at, .. } => { *held = picks; *at = shown; } } } } impl Default for Body { fn default() -> Self { Self::All(Vec::new()) } } /// A named region, and the thing a fragment is aimed at. /// /// The name is what decision 7 needs and [`layout::Region`] deliberately does /// not have: two panes in a split are both `Pane`, so the kind cannot be an /// address. A webview maps the id onto `hx-target`; egui and the terminal /// ignore it and redraw, which costs them nothing because they were redrawing /// anyway. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Slot { /// The address. Unique within a screen, and stable across responses, or a /// fragment lands nowhere. pub id: String, /// Which region it is. pub kind: RegionKind, /// Whether the region's own content is here or on its way. /// /// The loading axis, and only that. Emptiness is *not* said here, which /// looks like the obvious place for it and is not: a column with a heading /// and no rows is a region that has content — the heading — and a list that /// has none. Marking the region empty would hide the heading with it. See /// [`Node::StandIn`]. pub readiness: layout::Readiness, /// What is in it. /// /// Blocks, regions included, which is the nesting that was always accepted: /// a region inside a region is a nested rect on every host. Leaves are /// admitted too, and deliberately -- a fact under a heading is a /// [`Node::Text`] straight in a pane, and it is the commonest thing in the /// tree. /// /// # Why there is no bound here /// /// [`Cell::part`] and [`Row::part`] assert that what they are handed is a /// leaf, and this does not, which looks like an oversight and is the model /// working. The ladder forbids reaching *up*: a run may not hold a block, /// because a run has to be drawable on one wrapped line. A block holding a /// leaf is going down, and going down is what containment is for. There is /// no upward violation for [`with`](Self::with) to catch, so an assertion /// here would be a runtime check that can never fire. /// /// A region whose whole content is one badge is the case that made this /// look like a question. It is describable, and it should be: a status pane /// is a real screen. Whether it is a *good* screen is a judgement about /// that screen rather than a property of the vocabulary, and the bound is /// not the place to hold opinions about taste. /// /// # Why the members are [`Ranked`] and not bare nodes /// /// Each one carries what it is worth when the region runs out of room, so /// a renderer narrows a region the way it already narrows a table: raise a /// cutoff over a declared order. `Ranked`'s own docs carry the argument. pub body: Body, /// What a request decides about runs of [`body`](Self::body). /// /// Empty on every tree built to answer a request. A staged twin fills it, /// and it is what lets a renderer bracket the markup a guard, a loop or a /// dispatch controls rather than leaving a derivation to find those bounds /// by comparing renders. See [`stage::Mark`](crate::stage::Mark). pub marks: crate::stage::Marks, /// How many of [`body`](Self::body) are visible at once. /// /// [`layout::Showing::All`] by default, so a description written against /// the previous version says the same thing. /// /// This is the kind. The two fields below are the current answer and the /// per-child name, and they are here rather than in `makeover-layout` for /// the reason a [`Field`]'s value is: a layer that defers every address /// does not hold what is picked either. /// /// # A description never names client-only state /// /// A description says what a region *is*: these frames are peers, show /// one. State that lives only in the client and never reaches a handler is /// not part of that. Each renderer decides it, exactly as it already /// decides scroll position, focus and the duration of an undo window. /// /// The consequence is worth stating plainly rather than discovering later: /// **a screen has no way to say a preference should persist.** That is a /// deliberate limit. A description that wanted to say "remember which tab /// the reader was on across visits" would be naming storage, and storage is /// the host's. /// /// Three behaviours settle as renderer policy under this rule, recorded /// here because a reader meets the question at this field: /// /// - **Section tabs switched without a round trip.** This field already /// answers it: the description says the frames are peers and one is up, /// and the renderer toggles and rewrites the URL. MNW defines /// `switchSectionTab` verbatim in three bundles (`static/page-item-2.js`, /// `static/page-library-downloads.js`, `static/page-project.js`) and /// reaches a fourth site through the delegated handler in /// `static/actions-pages.js`. Three definitions, four call sites, and none /// of them is something a description should have been carrying. /// - **A persisted view preference.** The renderer decides whether it /// persists and where; nothing in the description mentions storage. The /// persist-across-swap behaviour has **one** implementation in MNW, /// `static/page-discover.js:176-178`, not the two an earlier count /// claimed. The count is recorded because it is what made a vocabulary /// addition look worth buying, and at one site it is not. /// - **Loading a region's contents on first reveal.** [`Readiness`] says /// what a region shows while it waits; what makes it *start* is the /// renderer's. MNW does it twice, license text on a `
` toggle /// and a video `src` on first `play`, each guarding with a `loaded` flag. /// /// One fragility worth knowing before a conversion, since a converter will /// arrive here first: MNW's `static/page-project-2.js` binds its /// `.view-btn` handlers directly at script load rather than by delegation. /// The container it binds into is never an htmx target today, so that is /// correct as written. If it ever becomes one, the handlers and the view /// state both break silently. /// /// [`Readiness`]: layout::Readiness /// What this region is called in its own right. /// /// Not [`label`](Self::label), which is the name a *parent* reads off this /// region when it is showing one child at a time -- the tab's name, /// gathered by [`labels`](Self::labels). Setting that on a tab strip /// itself is read by nothing, which is how MNW's library strip lost its /// `aria-label="Library sections"` when it converted and announced as an /// unnamed tab list. /// /// Counted before it was added, across MNW, goingson and Balanced /// Breakfast: 199 `aria-label` sites, and the ones on containers -- /// `table`, `nav`, `ul`, `section`, `aside` -- are this. "Tag breadcrumbs", /// "Selected tags", "Waitlist entries", "Sales per month". The ones on /// controls are a different thing and need nothing new: an icon-only button /// already has [`Act::label`], and a renderer drawing it as a glyph is the /// renderer's choice about how to spell a name it was given. /// /// Every renderer has somewhere to put it, which is what separates it from /// [`Act::hint`]: a webview writes `aria-label`, a terminal a rule caption /// or a heading line, egui a frame's title. None of them has to invent /// copy, and none of them may draw it *instead of* a heading the body /// already carries -- a region with a `Heading` in it is named twice on /// purpose, once for the eye and once for the reader that cannot see it. /// /// `None` is a region with no name of its own, which is most of them and is /// what every region did before this field existed. pub name: Option, /// The call that fills this region, when the region's content is not here /// yet. /// /// The region half of [`layout::Awaiting`]: a screen that is mostly local /// reads plus one slow part says so here instead of being hand-split into /// a second route, which is what MNW's user dashboard does with its payout /// summary because that one tab calls a payment provider and the rest of /// it reads the database. /// /// [`readiness`](Self::readiness) is [`Pending`](layout::Readiness::Pending) /// while this is set, and [`Screen::replace`] clears it when the answer /// lands, so a retained-screen host cannot ask twice for one region. /// /// The wait's size, if it has one, is on the action rather than here. A /// region is fed by a call and the call is what knows. /// /// Boxed, which is the one place in this file that is: [`Node::Region`] /// holds a [`Slot`] by value, so every node in every tree would carry an /// [`Action`]'s width for a field almost no region sets. Reach for /// [`fed_by`](Self::fed_by) and [`awaiting`](Self::awaiting) rather than the /// field, and the box is not something a caller has to think about. pub fed_by: Option>, /// Whether this region's contents change without the user. /// /// The other word beside [`layout::Awaiting`], and the two divide by /// whether the waiting ends: awaiting resolves once, in finite time, and /// this never resolves. A sync panel whose state moves when an OAuth /// callback lands in another process is the case, and no measure of /// progress can carry it — the transition that prompted the question is /// `Authenticating -> NeedsEncryption`, which is not an amount of /// anything. /// /// A bool rather than a rate. The description says the contents move; how /// often to look is the renderer's, picked once per host rather than per /// screen, which is the whole of what this buys. MNW hand-writes /// `hx-trigger="every 10s"` in two templates and audiofiles re-reads every /// frame, and nothing makes those two comparable today. /// /// Orthogonal to [`readiness`](Self::readiness). A live region can be /// [`Ready`](layout::Readiness::Ready), [`Pending`](layout::Readiness::Pending) /// or [`Failed`](layout::Readiness::Failed); those four stay mutually /// exclusive states about whether there is content yet, and liveness is a /// fact about the content after it arrives. /// /// What a host does with it depends on where the content comes from. A /// region that also names a [`fed_by`](Self::fed_by) is re-asked on the /// renderer's cadence — see [`Screen::refreshes`], which is the walk that /// finds them and the reason [`Screen::replace`] leaves a live region's /// feed in place instead of clearing it. A live region with no call reads /// something the host already holds, and the cadence is a repaint. pub live: bool, /// What brings this region out, when it is not simply out. /// /// The region names a control and the value that reveals it, and every /// renderer answers it from what it already holds: no request, no /// fragment, no re-render of a form the reader is midway through. See /// [`Reveal`] for why the condition lives here and not on the control. /// /// `None` is a region that is always out, so a description written against /// the previous version says the same thing. /// /// Boxed, for [`fed_by`](Self::fed_by)'s reason and measured the same way: /// [`Node::Region`] holds a [`Slot`] by value, so an unboxed condition puts /// a `String` and a `Vec` on every node in every tree for a field a handful /// of regions in a screen set. pub revealed_by: Option>, /// Whether this region leads with a row that things share, and what that /// row does when it is [`layout::Room::Tight`]. /// /// `None` says it does not and is why the field is additive at a call site /// rather than a re-reading of every screen in the tree. It is not a /// defaulted fallback and must not be read as one: a region with no run /// has no row to fall back, so there is nothing for it to have failed to /// say. /// /// Set it with [`across`](Self::across), which takes the row itself. The /// only way to make a [`Run`] is [`Run::new`], and that takes the /// fallback, so there is no path to a row here that failed to say what it /// does when it is tight. /// /// Boxed, for [`fed_by`](Self::fed_by)'s reason and measured the same way: /// [`Node::Region`] holds a [`Slot`] by value, so an inline `Run` puts a /// `Vec`'s width on every node in every tree for a field one region in a /// screen sets. pub run: Option>, /// What this region asks when the questions inside it move. /// /// The region names the route, the wait and the floor; the values it sends /// are the ones its own /// [`questions`](Self::questions) are holding; and the answer lands where /// [`Action::replaces`] points, which is usually a region inside this one. /// MNW's fee calculator is the site: five dials and a results panel that /// recomputes when any of them moves. /// /// # Why the region and not one of the dials /// /// The cheaper shape was a [`Consult::sends`] on one field naming its /// peers, and it was rejected for what it says rather than for what it /// costs: it nominates one of five equals as the owner of the recompute, /// and whichever is picked reads as arbitrary to the next person to open /// the file. It is also asymmetric on a host with focus, where the caret /// would have to be in the owning box for anything to happen. /// /// # What is gathered /// /// Every question this region contains, at any depth, nested regions /// included — [`questions`](Self::questions) is the walk, so the three /// renderers cannot disagree about what "inside" means. Beside that, /// whatever [`Consult::sends`] names, which is how a dial outside the panel /// joins in. /// /// Several, for [`Field::consults`]' reason: two frames recomputing from /// one set of dials at two rates are two questions, and one is not a /// special case of the other. Empty for nearly every region, which is every /// region written before this field existed. pub consults: Vec, /// The children are answers to one question, and the reader may add and /// take them away. /// /// A region member, not a rework of [`Repeat`]. See [`Repeating`] for what /// separates the two -- briefly, `Repeat` is one *field* answered N times /// and this is one /// *group* answered N times, which is the shape nothing could say. /// /// Boxed for [`fed_by`](Self::fed_by)'s reason: `Node::Region` holds a /// `Slot` by value, so an unboxed member is paid for by every region in /// every tree for a field almost none of them set. pub repeating: Option>, /// What taking *this* slot away calls, when this region is one. /// /// On the child rather than on the parent, which is the one place this /// diverges from the sketch in the task. A parent-level remove would have /// to reach a particular slot, and the two ways to do that are both worse: /// a renderer building `.../{at}/remove` is route construction in a /// renderer, and one route plus an index under an agreed name is a third /// word beside [`Node::SELECTED`] and [`Node::TICKED`] for one screen's /// benefit. A child carrying the action that concerns it is /// [`fed_by`](Self::fed_by)'s shape, already here and already understood. /// /// What the parent's [`Repeating::least`] adds is the *enforcement*: a /// renderer disables this once the floor is reached, so "at least one /// condition" stops being an app disabling its own button. /// /// Boxed with `repeating` and for its reason. pub removes: Option>, } /// What a region says when its children are answers to one question. /// /// [`Repeat`] describes a repeating *field*: an [`Instance`] is one value and /// one error, so a slot is one answer to one question. Nothing described a /// repeating *group*, and audiofiles' rule editor has two of them on one /// screen -- a condition is three questions that only mean anything together, /// and an action is two. /// /// # Why this and not a wider `Repeat` /// /// The regions already exist: 23 [`RegionKind::Group`] sites in the tree when /// this was measured, so a region member attaches to something with consumers, /// where widening `Repeat` would have been a redesign of vocabulary that has /// none. /// /// # What it deliberately does not carry /// /// A per-slot error, which is [`Instance::error`]'s job for a repeating field. /// A group's slots can be wrong in ways a string on the group cannot say, and /// no screen had needed one: **that was the reopening condition, and it /// fired.** MNW's version-upload queue wanted a status and an error against one /// slot, so [`Repeat`] grew [`Instance::parts`] and [`Progress`]. /// /// This member still stands, and the line between the two did not move. A /// repeating *field* holds its slots in the renderer's own view until one /// submit carries all of them, which is why they need a wire naming and a /// per-slot status at all. These regions each carry their own fields and their /// own writes, so a slot is already addressed by the routes inside it and there /// is nothing to take apart on the way back. The queue was the first shape and /// it is a field, not a group. /// /// # The wire /// /// Nothing here. A repeating field names its answers `name[0]`, `name[1]`, /// because one submit carries all of them; these regions each carry their own /// fields with their own writes, so every slot is already addressed by the /// routes inside it. There is no set to take apart on the way back and so no /// naming convention to agree. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Repeating { /// What one of them is called, singular. "Condition", "Action". /// /// The renderer numbers them from it -- "Condition 1", "Condition 2" -- /// which is why this is the singular noun and not a heading. A description /// that wrote the numbers itself would be numbering for a screen it cannot /// see, and would go stale the moment a slot was removed from the middle. pub one: String, /// The fewest slots the reader may leave standing. /// /// [`Repeat::least`]'s meaning, one level up. `1` is audiofiles' rule /// editor, whose conditions cannot go to zero; `0` is the ordinary answer. /// /// **This is the enforcement the member exists for.** The editor said it by /// disabling the last Remove itself, which is a rule living in an app where /// every renderer needs it. pub least: usize, /// The most the reader may add, if there is a ceiling. `None` is none. pub most: Option, /// The control that adds a slot. /// /// A whole [`Act`] rather than a label, because adding a slot here is a /// route: the group's state is the app's, unlike a repeating field's, whose /// slots live in the renderer's own view until they are submitted. pub add: Act, } impl Repeating { /// A repeating group with no floor and no ceiling. #[must_use] pub fn new(one: impl Into, add: Act) -> Self { Self { one: one.into(), least: 0, most: None, add, } } /// The same, with a floor on how many may be left standing. #[must_use] pub const fn least(mut self, least: usize) -> Self { self.least = least; self } /// The same, with a ceiling on how many may be added. #[must_use] pub const fn most(mut self, most: usize) -> Self { self.most = Some(most); self } /// Whether a group holding this many may lose one. /// /// Asked by every renderer before it draws a slot's remove control, so the /// three cannot disagree about what the floor means. #[must_use] pub const fn may_remove(&self, standing: usize) -> bool { standing > self.least } /// Whether a group holding this many may gain one. #[must_use] pub const fn may_add(&self, standing: usize) -> bool { match self.most { Some(most) => standing < most, None => true, } } } impl> crate::stage::Spliced for I { fn splice_into(self, container: &mut Slot) { for node in self { container.push(node); } } } impl crate::stage::Spliced for crate::stage::Staged> { /// The members, and their marks moved to where they landed. /// /// The child numbered its members from its own zero and they are now at an /// offset in this region, so every bound moves by the same amount. Done /// here because the container is the only place that offset is known. fn splice_into(self, container: &mut Slot) { use crate::stage::Marking as _; let at = container.placed(); for node in self.value { container.push(node); } container.absorb(&self.marks, at); } } impl crate::stage::Marking for Node { /// However many members this kind of node holds. /// /// A node is a container only in the five shapes that hold a list. Anything /// else has no members, so a run of them cannot be marked and `placed` says /// zero -- which makes any mark on one name members it did not draw, and /// the renderer refuses it by name rather than compiling something wrong. fn placed(&self) -> usize { match self { Self::Form { fields, .. } => fields.len(), Self::Table { columns, rows, .. } => columns.len() + rows.len(), Self::Timeline { entries, .. } => entries.len(), Self::Chart { bars, .. } => bars.len(), Self::Stats { figures, .. } => figures.len(), Self::Region(slot) => slot.body.len(), Self::Canvas(canvas) => canvas.within.len(), // One member, and it is one: `offering` places the way out and a // guard on it is a run of one. Self::StandIn { act, .. } => usize::from(act.is_some()), _ => 0, } } fn mark(&mut self, mark: crate::stage::Mark) { let marks = match self { Self::Form { marks, .. } | Self::Table { marks, .. } | Self::Timeline { marks, .. } | Self::Chart { marks, .. } | Self::Stats { marks, .. } | Self::StandIn { marks, .. } => marks, Self::Region(slot) => &mut slot.marks, // A node that holds no list has nowhere to keep this, and a twin // that marked one has marked something it did not build. other => panic!( "this node holds no members, so there is no run for a mark to \ cover: {other:?}" ), }; marks.add(mark); } } impl crate::stage::Marking for Field { /// The options it offers, of whichever kind: a picker's choices and a theme /// picker's themes are one list drawn one way, never both at once. fn placed(&self) -> usize { self.options.len() + self.themes.len() } fn mark(&mut self, mark: crate::stage::Mark) { self.marks.add(mark); } } impl crate::stage::Marking for Slot { /// Members of the body, whichever shape it has. fn placed(&self) -> usize { self.body.len() } fn mark(&mut self, mark: crate::stage::Mark) { self.marks.add(mark); } } impl crate::stage::Marking for Row { /// Cells first, then the menu, which is the order a row is drawn in. /// /// One index space over both lists, which is the rule for every container /// that draws from more than one: the numbering is the render order, so a /// mark reads the same to the renderer as it did to the twin that made it. fn placed(&self) -> usize { self.cells.len() + self.menu.len() } fn mark(&mut self, mark: crate::stage::Mark) { self.marks.add(mark); } } impl Slot { /// An empty region under this address. pub fn new(id: impl Into, kind: RegionKind) -> Self { Self { id: id.into(), kind, readiness: layout::Readiness::Ready, body: Body::All(Vec::new()), marks: crate::stage::Marks::none(), name: None, fed_by: None, live: false, revealed_by: None, run: None, consults: Vec::new(), repeating: None, removes: None, } } /// A place the app fills itself, and owes every host a fill for. /// /// See [`RegionKind::Handover`]. Use [`ceded`](Self::ceded) instead when /// no host is owed one. pub fn handover(id: impl Into, name: impl Into) -> Self { Self::new(id, RegionKind::Handover { name: name.into() }) } /// A place the app fills itself, that no host is owed a fill for. /// /// See [`RegionKind::Ceded`]. A chart, a waveform: a renderer with nothing /// to put here draws nothing and is right to. pub fn ceded(id: impl Into, name: impl Into) -> Self { Self::new(id, RegionKind::Ceded { name: name.into() }) } /// Things that belong together. /// /// Named rather than left to [`new`](Self::new) because this is the member /// a run of siblings under a heading should reach for, and the one it /// reached for instead -- [`RegionKind::Pane`] -- is what `Slot::new` makes /// easy. The heading goes in the body, not here. pub fn group(id: impl Into) -> Self { Self::new(id, RegionKind::Group) } /// A named assembly, whose body says what it is made of. /// /// The body is not optional in spirit, though nothing here enforces it: a /// widget with an empty body is a [`handover`](Self::handover) that has /// mislaid its host fill, and a renderer that does not know the name will /// draw nothing at all. Assemble it out of members the description already /// has, the way [`layout::Region::Widget`] describes. pub fn widget(id: impl Into, name: impl Into) -> Self { Self::new(id, RegionKind::Widget { name: name.into() }) } /// Say what brings this region out. /// /// The condition is the region's own, so a form with several conditional /// sections reads as a list of regions each stating its precondition /// rather than as controls reaching across the form. /// /// Calling it twice replaces the condition, which is the reading every /// builder here has: the second call is a correction. /// /// ``` /// use quasi_router::{RegionKind, Reveal, Slot}; /// /// let settings = Slot::group("pwyw-settings").revealed_by(Reveal::ticked("pwyw")); /// let custom = Slot::new("dash-custom-license", RegionKind::Pane) /// .revealed_by(Reveal::holding("license", "custom")); /// /// assert!(settings.revealed(Some("on"))); /// assert!(!settings.revealed(None)); /// assert!(custom.revealed(Some("custom"))); /// assert!(!custom.revealed(Some("all-rights-reserved"))); /// ``` #[must_use] pub fn revealed_by(mut self, reveal: Reveal) -> Self { self.revealed_by = Some(Box::new(reveal)); self } /// Whether this region is out, given what its control is holding. /// /// `true` for a region that named no condition, which is nearly all of /// them: a region says when it is *not* applicable, and saying nothing is /// saying it always is. /// /// What a renderer does with `false` is the renderer's. A browser hides the /// element, and a host with no cursor may dim the region, leave it out, or /// say in words that it does not apply right now — the reading the shape /// was ruled for. #[must_use] pub fn revealed(&self, held: Option<&str>) -> bool { self.revealed_by .as_ref() .is_none_or(|reveal| reveal.satisfied_by(held)) } /// The name of the control this region is watching, if it watches one. #[must_use] pub fn watches(&self) -> Option<&str> { self.revealed_by .as_ref() .map(|reveal| reveal.control.as_str()) } /// Add a node, chaining. /// /// At [`layout::Priority::Essential`], so it never drops. The signature is /// unchanged from before [`Ranked`] existed and so is what it means, which /// is why the whole tree kept building. #[must_use] pub fn with(mut self, node: Node) -> Self { self.push(node); self } /// [`with`](Self::with) for a caller holding this by reference. /// /// The splicing half needs it: a run of members is put in one at a time and /// the marks that came with them are moved afterwards, which a chaining /// call cannot express. fn push(&mut self, node: Node) { match &mut self.body { Body::All(members) => members.push(Ranked::new(node)), // A member added to a selective region is a frame with no name, // which is a carousel's. A name arrives through [`Self::frame`] and // nowhere else. Body::Selective { frames, .. } => frames.push(Frame::unnamed(node)), } } /// Add every node a shape answered with, chaining. /// /// [`with`](Self::with)'s plural, and the whole of what `include each` /// emits. A shape answering `Vec` has no region of its own -- what it /// returns is a run of members and not one node -- so before this a caller /// spread it by hand, with a loop whose body was `include node;`. /// /// That loop is why this exists. It reads as a loop over data and is a /// splice, and a staged shape cannot make sense of it: the `include` names /// a binding rather than a shape, so there is no twin to retarget to and no /// filler to call. Said as one `include each`, the caller splices a /// reference the way every other `include` does. #[must_use] pub fn with_all(mut self, nodes: impl crate::stage::Spliced) -> Self { nodes.splice_into(&mut self); self } /// A named frame: a tab, or a disclosure's one panel. /// /// The only way to write a label, and it places the member at the same /// time. That is the whole of the fix in quasicoherent `2cdc6761`: a name /// cannot be written on something that will not draw it, because the name /// and the thing that draws it are one call. /// /// Naming a frame on a region that shows everything makes it show one at a /// time, since a named frame is a claim that something reveals it. Say /// [`showing_one`](Self::showing_one) or /// [`showing_at_most_one`](Self::showing_at_most_one) first to choose /// which; unstated, a disclosure is the safer default because it has a /// resting state that shows nothing. #[must_use] pub fn frame(mut self, label: impl Into, node: Node) -> Self { if matches!(self.body, Body::All(_)) { self.body.select(Picks::AtMostOne, None); } match &mut self.body { Body::Selective { frames, .. } => frames.push(Frame::named(label, node)), Body::All(_) => unreachable!("just selected"), } self } /// Lead with a row that things share. /// /// Takes the row itself, so the fallback arrives with it: a /// [`layout::Fallback`] converts, which is the empty row this reads as, and /// a [`Run`] built up with [`Run::beside`] is the row that already holds /// something. Either way the thing handed over has said what it does when /// it is tight, because [`Run::new`] is the only way to make one and it /// takes the answer. That is rule 2, and it is now held by the type of the /// argument rather than by a check inside a method that could only fire /// after the description was already written. /// /// The empty form is not a degenerate case. Five tab strips in MNW declare /// a fallback and no members at all: a [`RegionKind::TabGroup`] puts its /// own strip in the row, generated from its children's /// [`labels`](Self::label), so the row is full without anything being put /// in it and the fallback is the only thing left to say. /// /// Calling it twice replaces the row, members and all. The row is one /// value now, so a second call is a second row rather than a correction to /// the first, and correcting a fallback means correcting it on the `Run` /// before the region is ever told about it. #[must_use] pub fn across(mut self, row: impl Into) -> Self { self.run = Some(Box::new(row.into())); self } /// Add a node, saying what it is worth when room runs out. /// /// The narrowing member. Spelled as a second constructor rather than as a /// builder on the placement, the way [`Column::priority`] is, because a /// `Ranked` is built at the point it is inserted and there is nothing to /// hold between building it and pushing it. #[must_use] pub fn with_ranked(mut self, node: Node, priority: layout::Priority) -> Self { self.body.push(Ranked::worth(node, priority)); self } /// Add several nodes, chaining. /// /// Takes anything that becomes a [`Ranked`], so a run of bare nodes still /// works and reads as before, and a run of ranked ones needs no second /// method. #[must_use] pub fn extend(mut self, nodes: impl IntoIterator>) -> Self { self.body.extend(nodes.into_iter().map(Into::into)); self } /// The content is on its way rather than here. #[must_use] pub fn pending(mut self) -> Self { self.readiness = layout::Readiness::Pending; self } /// The content arrives from this call rather than with the screen. /// /// Sets [`readiness`](Self::readiness) to /// [`Pending`](layout::Readiness::Pending) in the same breath, because a /// region that says where its content is coming from is by construction a /// region that does not have it yet, and the two disagreeing is a state no /// renderer could draw honestly. /// /// Mark the action [`awaiting`](Action::awaiting) unless there is a reason /// not to. Without the mark this is still a deferred load and every renderer /// still fetches; what is lost is the size of the wait, so the stand-in has /// no proportion to draw. #[must_use] pub fn fed_by(mut self, action: Action) -> Self { self.readiness = layout::Readiness::Pending; self.fed_by = Some(Box::new(action)); self } /// The contents change without the user, so a renderer keeps looking. /// /// See [`live`](Self::live). Says nothing about how often: the cadence is /// the renderer's, and a description that named one would be a description /// the webview and the terminal disagreed about. /// /// Combines with [`fed_by`](Self::fed_by) rather than replacing it. Called /// after it, the region is asked for once and then re-asked; called on a /// region with no call, the host re-reads whatever it is drawing from. #[must_use] pub const fn live(mut self) -> Self { self.live = true; self } /// Ask this route when the questions inside this region move. /// /// See [`consults`](Self::consults). Appends rather than replaces, which is /// the reading [`Field::consulting`] has for the same reason: two questions /// about one set of dials are two questions, and a second call adding a /// second one is what a builder reads as. /// /// ``` /// use quasi_router::{Action, Consult, Slot}; /// /// let calculator = Slot::group("pricing-calculator").consulting( /// Consult::new(Action::get("/pricing/compare").replacing("results-panel")) /// .after(std::time::Duration::from_millis(300)), /// ); /// /// assert_eq!(calculator.consults.len(), 1); /// ``` #[must_use] pub fn consulting(mut self, consult: Consult) -> Self { self.consults.push(consult); self } /// What this region is waiting on, when it is waiting on something. /// /// Asked once here rather than reached through the action in each renderer. #[must_use] pub fn awaiting(&self) -> Option { self.fed_by.as_ref().and_then(|action| action.awaiting) } /// Show one child at a time, starting at this one. /// /// The carousel and the tab group, which are one thing said twice: whether /// a host draws a strip of names or a prev/next row falls out of whether /// the children carry a [`label`](Self::label), never out of the widget's /// name. /// /// `shown` is where the region starts, not where it stays. Whether a later /// visit comes back to the same child is the renderer's, and there is no /// way to say otherwise here. See [`showing`](Self::showing) for the rule /// and what it settles. #[must_use] pub fn showing_one(mut self, shown: usize) -> Self { self.body.select(Picks::One, Some(shown)); self } /// Show one child or none, starting closed unless a child is named. /// /// Disclosure. `None` is the closed state and is a legal resting place, /// which is the whole of what separates this from /// [`showing_one`](Self::showing_one). /// /// A closed disclosure that fetches its contents when it first opens is /// describable as `None` here plus a [`Readiness`](layout::Readiness) for /// the wait. What makes the fetch start is the renderer's, under the rule /// on [`showing`](Self::showing). #[must_use] pub fn showing_at_most_one(mut self, shown: Option) -> Self { self.body.select(Picks::AtMostOne, shown); self } /// Say that this region's children are answers to one question. /// /// See [`Repeating`], and put [`removes`](Self::removes) on each child: the /// two are halves of one description and a group with neither is a group /// nobody can shrink. #[must_use] pub fn repeating(mut self, repeating: Repeating) -> Self { self.repeating = Some(Box::new(repeating)); self } /// Say what taking this slot away calls. /// /// Only meaningful on a child of a [`repeating`](Self::repeating) region. /// Ignored elsewhere rather than refused, for the reason every other /// builder here is a no-op off its own member: a description that says /// something no renderer reads is a description bug, and a panic in a /// builder chain is a worse way to find one than a control that is not /// drawn. #[must_use] pub fn removes(mut self, act: Act) -> Self { self.removes = Some(Box::new(act)); self } /// Name this region in its own right. See [`name`](Self::name) for what /// separates it from [`label`](Self::label). #[must_use] pub fn named(mut self, name: impl Into) -> Self { self.name = Some(name.into()); self } /// Which child to draw, once [`shown`](Self::shown) is read against the body. /// /// `None` means draw them all, which is both [`layout::Showing::All`] and a /// closed disclosure — the two cases differ in what chrome sits around them /// and not in what a renderer does with the body, so they answer the same /// here. /// /// An index past the end is clamped rather than refused. A description /// pointing at a frame that is not there is a bug in the app, and a renderer /// that answers it by drawing nothing reports it as a region that vanished, /// which is the hardest kind of bug to find from what is on the screen. /// [`layout::Share::percent`] clamps for the same reason. /// /// The clamp is [`layout::Window`]'s, not this method's. A frame is a window /// of one over children that are all present, which is the same arithmetic a /// paged list runs over rows that are not — see [`Rest`]. Sharing it is what /// stops the two disagreeing about which frame is last. #[must_use] pub fn current(&self) -> Option { let Body::Selective { picks, shown, frames, } = &self.body else { return None; }; if frames.is_empty() { return None; } let frame = |at: usize| layout::Window::frame(at, frames.len()).clamped().from; match picks { Picks::One => Some(frame(shown.unwrap_or(0))), Picks::AtMostOne => shown.map(frame), } } /// How much of this region is up at once, in the vocabulary's words. #[must_use] pub const fn showing(&self) -> layout::Showing { self.body.showing() } /// Which member the region opens on, before clamping. /// /// Read [`current`](Self::current) instead when drawing, which is where an /// index past the end of the body is dealt with. #[must_use] pub const fn shown(&self) -> Option { self.body.shown() } /// Every member in order, whichever shape the body is. pub fn members(&self) -> impl Iterator + '_ { self.body.members() } /// The children's names, when they have them. /// /// Empty unless *every* child is a named region, which is the test a /// renderer applies before drawing a strip: a strip with a hole in it is /// worse than the prev/next row it would otherwise have drawn, and a /// half-labelled body is an app bug rather than a third idiom. #[must_use] pub fn labels(&self) -> Vec<&str> { let Body::Selective { frames, .. } = &self.body else { // A region that shows everything reveals nothing, so it has no // controls to name. Since `2cdc6761` it cannot hold a name either. return Vec::new(); }; let named: Vec<&str> = frames .iter() .filter_map(|frame| frame.label.as_deref()) .collect(); if named.len() == frames.len() { named } else { Vec::new() } } /// What this region and the regions inside it offer under a control name. /// /// The described half of a [`Reveal`]: what the box was handed to the /// reader holding, before anything was typed into it. A renderer that /// holds edits reads those first and falls back to this, which is the same /// order a submit reads a form in. #[must_use] pub fn holds(&self, name: &str) -> Option<&str> { self.run .iter() .flat_map(|run| run.members.iter()) .chain(self.body.iter()) .find_map(|placed| placed.node.holds(name)) } /// Every question inside this region, at any depth, in draw order. /// /// What a [`consults`](Self::consults) gathers, and it lives here rather /// than in each renderer for [`holds`](Self::holds)' reason: three walks /// over this crate's own tree are three chances to disagree about what /// "inside this region" means, and the browser's answer — every control /// the element contains — is not one a terminal can copy without being /// told the same shape. /// /// The run as well as the body: a filter bar that moved into the region's /// leading row is still a dial of that region, and a walk that skipped the /// run would recompute without it. /// /// A repeating question ([`Field::repeats`]) is one entry, under the name /// the description gave it. How many boxes that is standing right now is /// the renderer's, since it is the renderer that holds the count. #[must_use] pub fn questions(&self) -> Vec<&Field> { let mut found = Vec::new(); for placed in self .run .iter() .flat_map(|run| run.members.iter()) .chain(self.body.iter()) { placed.node.questions(&mut found); } found } /// Whether a control anywhere in this region carries this /// [`Act::id`](Act::id). /// /// The run as well as the body, for [`find`](Self::find)'s reason: a control /// in a toolbar is as reachable as one in a pane, and a walk that skipped /// the run would answer `false` for half the screens in the tree. #[must_use] pub fn names(&self, id: &str) -> bool { self.body .iter() .chain(self.run.iter().flat_map(|run| run.members.iter())) .any(|placed| placed.node.names(id)) } /// This slot, or the first slot under this address anywhere inside it. #[must_use] pub fn find(&self, id: &str) -> Option<&Self> { if self.id == id { return Some(self); } // The run is walked with the body, and it has to be: goingson's toolbar // is a region that moved out of the pane and into the tab strip's row, // and a region a fragment cannot be aimed at is a region that stops // updating. Body first, because that is draw order for everything that // is not a tab group and the one that is has no body a fragment names. self.body .iter() .chain(self.run.iter().flat_map(|run| run.members.iter())) .find_map(|placed| match &placed.node { Node::Region(slot) => slot.find(id), _ => None, }) } /// Whether this region, or one inside it, holds a row of a live selection. /// /// [`find`](Self::find)'s walk asking a different question. See /// [`Screen::chooses`], which is the only caller and carries the reasoning. #[must_use] pub fn chooses(&self) -> bool { self.body .iter() .chain(self.run.iter().flat_map(|run| run.members.iter())) .any(|placed| match &placed.node { Node::Region(slot) => slot.chooses(), Node::Table { rows, .. } => rows.iter().any(|row| row.chosen.is_some()), _ => false, }) } /// The mutable half of [`find`](Self::find). /// /// Same walk, and it has to be a second function rather than the same one /// generic over mutability: a `&mut` borrow of `self` cannot be handed to /// the recursive call and kept, which is what `find_map` does on the shared /// side. /// Every call this region and the regions inside it are waiting on, in draw /// order. /// /// Here rather than in each renderer because it is a walk over this crate's /// own tree, and two hosts writing it separately is two answers to "which /// regions have not arrived". A webview needs none of it: the markup carries /// a trigger per region and the browser does the walk. Every host that /// retains the description rather than the markup does need it. /// # A tab's panel is not waiting, it is unasked /// /// A labelled region that shows one child at a time is a tab strip, and a /// child of one carrying [`fed_by`](Self::fed_by) is naming the tab's /// address rather than saying its content is on its way. Those are not /// returned here: a host that asked for all of them would fetch every /// panel the moment the screen went up, which is what the reader pressing /// the tabs exists to avoid — measured on MNW's library page as five /// database reads per view where there had been one. /// /// The walk still descends, because a panel that has arrived may hold a /// region of its own that genuinely is waiting, and that one is nobody's /// tab. fn feeds_into<'a>(&'a self, out: &mut Vec<&'a Action>) { // A live region's call is a cadence rather than an arrival, so it is // [`Screen::refreshes`]'s and not this walk's. Returning it here would // have a retained host ask again the moment the answer landed, which is // a poll at whatever speed the event loop happens to run at. if let Some(action) = &self.fed_by && !self.live { out.push(action); } let tabbed = self.showing().selective() && !self.labels().is_empty(); for placed in self .body .iter() .chain(self.run.iter().flat_map(|run| run.members.iter())) { if let Node::Region(slot) = &placed.node { if tabbed { slot.panel_feeds_into(out); } else { slot.feeds_into(out); } } } } /// Every call this region and the regions inside it re-ask on a cadence. /// /// [`feeds_into`](Self::feeds_into)'s counterpart, split by /// [`live`](Self::live) so that the two answers never overlap: a call is /// one or the other and no host has to work out which. /// /// A tab's unopened panel is skipped here for the reason it is skipped /// there. A panel nobody has asked for is not being kept up to date either. fn refreshes_into<'a>(&'a self, out: &mut Vec<&'a Action>) { if let Some(action) = &self.fed_by && self.live { out.push(action); } let tabbed = self.showing().selective() && !self.labels().is_empty(); for placed in self .body .iter() .chain(self.run.iter().flat_map(|run| run.members.iter())) { if let Node::Region(slot) = &placed.node { if tabbed { slot.panel_refreshes_into(out); } else { slot.refreshes_into(out); } } } } /// This region and the regions inside it that ask a question of their own. /// /// [`refreshes_into`](Self::refreshes_into)'s shape for /// [`consults`](Self::consults), and here for its reason: what counts as a /// region of this screen is this crate's answer, and a renderer walking the /// tree itself is a second answer waiting to disagree. /// /// A tab's unopened panel is **not** skipped, unlike the two walks above. /// Those find calls a host would make on its own; this finds the questions /// a control the reader touched sets off, and a control inside a panel /// nobody has opened is a control nobody has touched. Skipping it would /// cost a walk and rule out nothing. fn consulting_into<'a>(&'a self, out: &mut Vec<&'a Self>) { if !self.consults.is_empty() { out.push(self); } for placed in self .body .iter() .chain(self.run.iter().flat_map(|run| run.members.iter())) { if let Node::Region(slot) = &placed.node { slot.consulting_into(out); } } } /// Whether this region or anything inside it changes without the user. /// /// Wider than [`refreshes_into`](Self::refreshes_into), which finds only the /// live regions that name a call. A host asks this to decide whether to keep /// redrawing at all, and a live region reading state the host already holds /// is exactly the case that has no call to find. fn live_within(&self) -> bool { self.live || self .body .iter() .chain(self.run.iter().flat_map(|run| run.members.iter())) .any(|placed| match &placed.node { Node::Region(slot) => slot.live_within(), _ => false, }) } /// [`refreshes_into`](Self::refreshes_into) for a tab's panel: the panel's /// own call is its address, not a cadence. fn panel_refreshes_into<'a>(&'a self, out: &mut Vec<&'a Action>) { for placed in self .body .iter() .chain(self.run.iter().flat_map(|run| run.members.iter())) { if let Node::Region(slot) = &placed.node { slot.refreshes_into(out); } } } /// [`feeds_into`](Self::feeds_into) for a tab's panel: whatever is inside it /// is waiting, and the panel itself is not. fn panel_feeds_into<'a>(&'a self, out: &mut Vec<&'a Action>) { for placed in self .body .iter() .chain(self.run.iter().flat_map(|run| run.members.iter())) { if let Node::Region(slot) = &placed.node { slot.feeds_into(out); } } } /// The call that fills this region when something above it asks on its /// behalf, which is a tab strip pressing one of its own tabs. /// /// The address a panel carries but does not act on, so the one party that /// does act on it does not have to reach into the field and decide for /// itself what the field means here. /// /// # Only while it is empty, and only a retained-screen host can tell /// /// A panel that has been read is not asked for again, which is what going /// back to a tab means. That is the answer for a host holding the /// description, and it is not the answer a webview gives: the markup keeps /// the address on the button after [`Screen::replace`] has dropped it from /// the tree, so a browser re-reads the panel on every press. The divergence /// is `replace` clearing [`fed_by`](Self::fed_by), which predates this and /// is the rule that stops a retained screen asking twice on one paint. /// /// Neither is wrong and the difference is visible only as freshness, so it /// is recorded here rather than papered over. What both agree on is the one /// thing that mattered: pressing a tab is what fetches it, and four unpressed /// frames fetch nothing. #[must_use] pub fn asked_for(&self) -> Option<&Action> { if self.body.is_empty() { self.fed_by.as_deref() } else { None } } fn find_mut(&mut self, id: &str) -> Option<&mut Self> { if self.id == id { return Some(self); } self.body .iter_mut() .chain(self.run.iter_mut().flat_map(|run| run.members.iter_mut())) .find_map(|placed| match &mut placed.node { Node::Region(slot) => slot.find_mut(id), _ => None, }) } } /// A value an act puts into a field on the same screen. /// /// An act names a destination field, and the renderer decides where in it the /// value lands. Measured on MNW's media picker, where the whole point of the /// button is to put an image reference into the box the reader is already /// typing in, and where the three surfaces it appears on lose between 30 /// seconds and the entire unsaved draft if the server does the appending /// instead. /// /// # Stated as a destination and never as a caret /// /// The description says "into `body`". It never says "at the caret", because /// where inside a field a value lands is the renderer's, the same class of fact /// as how a menu overflows or when a toast clears. A webview inserts at the /// selection, a terminal and an immediate-mode host append, and neither is /// wrong. This is what keeps `d52884b0` — "a field's described state is its /// value, and the caret is the renderer's" — standing rather than reopened: /// nothing here carries a caret in either direction. /// /// # Both halves, because an act has no value of its own /// /// [`field`](Self::field) is addressed by [`Field::name`], which is already how /// [`Field::writes`] and a submit name a field, so no id relationship is /// invented. [`value`](Self::value) is here because an [`Act`] carries a label /// and an address and nothing else a renderer could put anywhere: the picker's /// card reads as a file name and deposits `![](media/kick.png)`, and the two /// are not the same string. /// /// # It does not replace the act's action /// /// A renderer fills first and then dispatches [`Act::action`] as it always /// would. An act that only fills says so with [`Destination::Local`], which is /// the vocabulary's existing way to say that no request goes out, and is what /// every measured site wants. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Prefill { /// The [`Field::name`] that receives it. pub field: String, /// What lands there. pub value: String, } /// A control that calls a route. /// /// A button, a link and a menu item are the same thing to a description: a /// label, an address, and how loudly it is saying it. Which of the three a /// renderer draws is a renderer decision. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Act { /// What it is called. pub label: String, /// What it calls. pub action: Action, /// What it is saying. [`layout::Tone::Danger`] is what marks the button /// that destroys something. pub tone: layout::Tone, /// Focused, disabled, or neither. pub state: Option, /// What to ask before doing it, if it should be asked. /// /// The prompt only. The word on the agreeing button is /// [`label`](Self::label), because it already is — goingson's `confirmDelete` /// passes `confirmText: 'Delete'` for an act labelled "Delete" — and a /// second string would be the same word twice with a chance to disagree. /// [`tone`](Self::tone) already says whether the dialog is a dangerous one. /// /// `Region::Modal` names the box a confirmation appears in and does not name /// the pattern. This is the pattern: a webview raises a dialog, a touch host /// an action sheet, a terminal a y/n line, and none of them is a route to a /// modal screen and back, which is a different interaction. pub confirm: Option, /// The key that reaches it, written the way a user would say it. /// /// An `Act` had a label and a destination and nothing said which key gets /// there, so goingson's 279-line `keyboard.js` holds the table beside the /// description, and the help overlay that lists the shortcuts is a second /// hand-written copy that can drift from it. /// /// A terminal makes the case sharper than a webview does: there the key *is* /// the affordance, so a description that cannot name one cannot describe the /// screen's primary interaction at all. /// /// Text rather than a modelled chord — "n", "ctrl+k", "?" — because the /// vocabulary of keys is the host's and a description that modelled it would /// be naming one host's keyboard. A renderer that does not know a name /// ignores it, which is what a webview does with a key a terminal wants. /// /// Screen-scoped, because a screen is what this describes. An app-wide /// shortcut belongs to the app and is not a fact about any one screen: /// that is [`Chrome::bindings`](crate::Chrome::bindings), held beside the /// router rather than inside any answer. A renderer matches those first, so /// a screen cannot capture the key that opens the palette. pub key: Option, /// The [`Screen::selection`] this acts on, if it acts on one. /// /// Every ticked [`Row::value`] is sent under [`Node::TICKED`], repeated /// once per member. Repeated rather than joined, because a name appearing /// many times is what [`Params::get_all`] is for and a delimiter would have /// to be one no value can contain. /// /// # A handler still answers for an empty set /// /// Every renderer draws a control over an empty selection as disabled and /// refuses the press, so the ordinary way to reach a handler with no ticks /// is gone. It is not the only way: a hand-typed request has none, and a /// webview host that does not serve /// [`SELECTION_JS`](https://makenot.work/git/max/quasi) leaves the control /// live. A bulk write over nothing should still answer the screen rather /// than erroring — a renderer's refusal is an affordance, not a guarantee /// about what arrives. /// /// # The name does not select between sets yet, and cannot /// /// A screen holds one selection ([`Screen::selection`]), so being set at /// all is what makes a control a commit control, and the name is what makes /// it *readable* — "Archive" over `chosen` is a different sentence from /// "Archive" on a row. /// /// Matching it against the screen's name was the first shape and it does /// not work, because a renderer does not always have the screen: an /// [`Outcome::Fragment`] replaces a region and carries no screen at all, so /// a webview rendering one would have had to guess and a terminal, which /// keeps the screen beside it, would not. The two hosts would then disagree /// about a typo, which is exactly the drift this vocabulary exists to stop. /// So both read it the same way, and the name starts choosing between sets /// on the day [`Screen::selection`] becomes a map. /// /// [`Params::get_all`]: crate::Params::get_all /// [`Outcome::Fragment`]: crate::Outcome::Fragment pub over: Option, /// What the press asks for before the call goes out, if it asks for /// anything. /// /// [`confirm`](Self::confirm) is the yes/no shape of this moment, a /// question raised after the press and before the call, and this is the /// shape that comes back with a value. Measured on MNW's content table, /// where five verbs sit over the selection and two of them, "Set Price" /// and "Add Tag", reveal a small form first: a label, one box, an Apply /// and a hint, hidden until the verb is pressed. /// /// Every field is sent under its own [`Field::name`], the way a submit /// sends it, and the ticks ride along under [`Node::TICKED`] when /// [`over`](Self::over) names a selection. A handler reads a verb that /// asked for a value the same way it reads one that did not. /// /// Empty is the ordinary control, which is nearly all of them. /// /// # Why it is not a form /// /// Said as a [`Node::Form`] the verb is lost: the description holds a /// route, a submit label and a box, and nothing says the box belongs to /// "Set Price" rather than to the screen. Said as a route that answers a /// form fragment it is a round trip to ask a question whose shape the /// screen already knows, and a fragment is markup a terminal or an egui /// host has nowhere to put. /// /// A field here carries no [`Field::writes`]. It is answered by the control /// that asked for it, so a renderer that honoured a write on it as well /// would fire twice for one value. /// /// # The disclosure is the renderer's /// /// quasi-webview puts the fields in a `
` under the control, which /// is the shape MNW's button-then-form already has. quasi-tui and /// quasi-immediate draw them beside the control instead: a terminal has no /// popover, and "first paint is final paint" is worth more there than /// hiding two boxes. Both send the same values, which is the part the /// description states. pub asks: Vec, /// The field on this screen that receives the act's value, if it has one. /// /// See [`Prefill`] for the ruling and for why both halves are there. Absent /// on nearly every control, which is what makes this additive: a renderer /// that finds nothing here draws exactly what it drew before the member /// existed. /// /// Not a second address. [`over`](Self::over) names a selection the act /// reads and this names a field the act writes, and an act may carry both: /// the picker's card names neither, and a verb that gathers ticks and /// deposits a summary somewhere would name both without either meaning the /// other. pub fills: Option, /// Standing help about the control, when the label does not carry it. /// /// The same member as [`Field::hint`] and it means the same thing: a /// sentence that is always true of this control, shown rather than hunted /// for. [`confirm`](Self::confirm) is a question asked at the press and /// [`asks`](Self::asks) is a value collected at it; both are about the /// moment, and this is about the control. /// /// Counted before it was added: 78 `title` attributes in MNW, 12 in /// Balanced Breakfast and 14 `on_hover_text` calls in audiofiles, and where /// they sit is the finding -- `button` and `a` outnumber every other /// element carrying one. audiofiles' storage section is the clearest single /// site, where six acts wanted one and three said something the label could /// not: "Local-only: other synced devices keep their own copies.", /// "Runs in the background: keep working", "the result appears in the /// status line." /// /// # Not a tooltip /// /// Half the hosts have no pointer. The shipped apps spelled this as a hover /// because egui and a browser both had one, and the hover is the spelling /// rather than the thing. A terminal puts it on a help line, egui may keep /// its hover, a webview writes `title` *and* stays free to draw it: what /// the description says is that the sentence is true, not that it is /// hidden. /// /// A renderer that draws it must not also drop it from the accessible tree, /// which is the failure `title` alone has on a browser. /// /// # It lives one layer down as of makeover-layout 0.40.0 /// /// `makeover_layout::Act::hint` is where the member is now, and this one /// mirrors it across [`as_layout`](Self::as_layout). /// /// # Why not prose beside the act /// /// A [`Node::Text`] next to a control reads well and says nothing about /// which control it belongs to, so a renderer laying the region out its own /// way separates them. That is the same loss [`Rest`] has beside a table, /// and it is why three sites were enough to file this and 104 are enough to /// build it. /// /// `None` is a control whose label is the whole of it, which is nearly all /// of them. pub hint: Option, /// The value the press puts on the clipboard, if that is what it does. /// /// Measured on the MNW server: seven of the 67 `window.` globals are /// this, across 14 sites -- `copyElementText` (4), `copyEmbedBtn` (4), /// `copyText` (2 files), `copyFeedUrl`, `copyItemLink`, `copyKeyCode`, /// `onCopyItemId`. /// /// # Why it is a member here and not a [`Destination`] /// /// A copy asks no route, so its action is [`Action::local`]. That variant's /// own rule is the reason this member exists: *what happens locally is /// named by the member carrying the action, never by the variant*. A bare /// act with a local destination says only "the renderer's own affordance /// happens", so without a member saying what, a described copy button is /// `data-action="copyThing"` in a new hat -- the exact thing /// [`Destination::Local`] was written to refuse. /// /// # The value, not its source /// /// All 14 measured sites copy something the server already rendered, and /// six of the seven scrape it back off the DOM at press time. So this /// carries the string. Naming a source element instead would make two /// elements point at each other by id for a value the description is /// holding anyway, which is what [`Field::suggests`] declined. /// /// The cost, stated: a value the *reader* has since edited cannot be /// copied this way. No measured site is one. A control that wants the live /// contents of a field is a different member and should be filed when a /// second site asks for it. /// /// # The acknowledgement is not here /// /// Every one of the seven relabels itself to "Copied!" and reverts, six at /// 1500ms and `copyFeedUrl` at 2000ms. That is a temporary label, which is /// mnw-server `033c722f`'s class and belongs to `makeover-timing` rather /// than to this member. Deliberately separable: a host with no notion of a /// reverting label still needs to be told the act copies something. pub copies: Option, /// The name an [`Outcome::Anchored`] reaches this control by. /// /// [`Anchor::Control`] names a control and an [`Act`] had no name to be /// named by: a label is what it says and can be the same word twice on one /// screen, and an action is where it goes, which two controls may share. /// /// `None` on nearly every control, which is what makes this additive: a /// control nothing anchors to needs no name, and a renderer that finds none /// draws exactly what it drew before the member existed. /// /// Screen-scoped and stable, the same contract [`Slot::id`] carries and for /// the same reason — an answer aimed at it lands nowhere if it moves. /// /// [`Outcome::Anchored`]: crate::Outcome::Anchored /// [`Anchor::Control`]: crate::Anchor::Control /// [`Slot::id`]: Slot::id pub id: Option, /// The picture this control shows, when it shows one. /// /// [`label`](Self::label) stays what the control *says* and this is what /// it *shows*; a renderer draws both, because a control drawing only a /// picture and hiding its name is a control with no accessible text. /// /// # What the containment model already covers /// /// [`Row::part`] and [`Cell::part`] take any leaf, [`Node::Image`] is one, /// and a webview puts an activated row's whole run inside the anchor, so a /// picture in an activated row is already pressable with no member here. /// /// What that leaves is the case where the *control itself* is the picture, /// which a row cannot be: /// /// - `MNW/server/src/quasi/media_picker.rs`, `fn card`. A tile is a region /// holding a [`Node::Image`] and an [`Act`] as siblings, so only the file /// name answers a press and a reader aiming at the thumbnail hits /// nothing. It cannot be an activated row instead, because the press /// deposits a value and [`fills`](Self::fills) lives here rather than on /// [`Row`]. /// - `MNW/server/templates/pages/project.html`, the storefront item card: /// ``, with a second link on the title /// going to the same place. One act showing the cover and labelled with /// the title is one control where the markup has two. /// /// # What each renderer does /// /// A webview draws the picture inside the control. quasi-immediate draws it /// above the button, outside `makeover_immediate::widget::act`, because /// `makeover_layout::Act` does not carry a picture -- which was /// [`hint`](Self::hint)'s reason too until makeover-layout 0.40.0 moved /// that one down. quasi-tui ignores it and draws the label, which is /// already the honest terminal answer -- a picture's alt text is what a /// terminal has, and the label is saying it. pub shows: Option, } impl Act { /// A neutral control calling this route. pub fn new(label: impl Into, action: Action) -> Self { Self { label: label.into(), action, tone: layout::Tone::Neutral, state: None, confirm: None, key: None, over: None, asks: Vec::new(), fills: None, hint: None, copies: None, shows: None, id: None, } } /// Standing help about this control. See [`hint`](Self::hint). #[must_use] pub fn hint(mut self, hint: impl Into) -> Self { self.hint = Some(hint.into()); self } /// Pressing this puts that value on the clipboard. /// /// Sets the destination to [`Action::local`] as well, because a copy asks /// no route and the two facts are one sentence. See /// [`copies`](Self::copies). #[must_use] pub fn copying(mut self, value: impl Into) -> Self { self.action = Action::local(); self.copies = Some(value.into()); self } /// The picture this control shows. See [`shows`](Self::shows). /// /// The label is untouched and stays the control's name, which is what a /// renderer that draws no pictures reads and what a screen reader /// announces. #[must_use] pub fn showing(mut self, picture: Image) -> Self { self.shows = Some(picture); self } /// The name an anchored screen reaches this control by. /// /// See [`id`](Self::id). Nothing else reads it: it is not a class, not a /// test hook and not a second address, and a control that is never anchored /// to should not carry one. #[must_use] pub fn id(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } /// This acts on the screen's selection, by name. /// /// The commit half of a staged tick. See [`over`](Self::over) for what /// reaches the handler, and [`Screen::selection`] for why a tick stages /// rather than writes. #[must_use] pub fn over(mut self, selection: impl Into) -> Self { self.over = Some(selection.into()); self } /// Ask for this value before doing it, chaining. /// /// Adds rather than replaces, for [`Field::consults`]' reason: MNW's two /// verbs ask for one value each, and a builder that took the last call /// would make a verb wanting two unwritable. See [`asks`](Self::asks). #[must_use] pub fn asking(mut self, field: Field) -> Self { self.asks.push(field); self } /// Put this value into that field when it is pressed. /// /// Replaces rather than appends, unlike [`asking`](Self::asking): an act /// deposits one value, and a control writing into two boxes at once is a /// description doing two things under one press. See [`Prefill`]. #[must_use] pub fn filling(mut self, field: impl Into, value: impl Into) -> Self { self.fills = Some(Prefill { field: field.into(), value: value.into(), }); self } /// Ask this before doing it. #[must_use] pub fn confirm(mut self, prompt: impl Into) -> Self { self.confirm = Some(prompt.into()); self } /// The key that reaches it. #[must_use] pub fn key(mut self, key: impl Into) -> Self { self.key = Some(key.into()); self } /// Set what it is saying. #[must_use] pub fn tone(mut self, tone: layout::Tone) -> Self { self.tone = tone; self } /// Present, visible, and not answering. #[must_use] pub fn disabled(mut self) -> Self { self.state = Some(layout::State::Disabled); self } /// Whether the control currently answers input. #[must_use] pub fn interactive(&self) -> bool { !self .state .is_some_and(layout::State::suppresses_interaction) } /// Borrow as the description layer's own type. /// /// [`action`](Self::action) and [`confirm`](Self::confirm) do not survive /// the crossing, and that is what the two layers disagree about rather than /// an oversight. An address is quasi's — every host follows one differently /// — and a confirmation is a question asked after the press, so it belongs /// to whoever is holding the interaction. What is left is what a renderer /// needs to *draw* the control, which is all `layout::Act` claims to be. #[must_use] pub fn as_layout(&self) -> layout::Act<'_> { layout::Act { label: &self.label, key: self.key.as_deref(), tone: self.tone, state: self.state, hint: self.hint.as_deref(), } } } // `Part` was merged into `Cell` on 2026-09-05. // // A part was a cell that carried its own role because a list had no columns to // carry it. Now a list declares columns like a table does, so the role is // `CellKey::Role` and there is one type. `Part::worth` is `Cell::priority` // falling back to its key. /// One row of a list. /// /// # The run /// /// A row's content is an inline run of [`Part`]s, in the order the description /// says them, the same way a [`Cell`]'s is. /// -- each of which arrived as a counted-sites argument, a member here, a /// [`layout::RowPart`] variant and a release: `RowPart::Tokens` at /// makeover-layout 0.9.0 for a badge in a row, `RowPart::Proportion` at 0.11.0 /// for a bar in one. A link in a row was simply not sayable, and a figure in /// one was not either. Under the run both are already sayable and cost nothing. /// /// The bound is that every part is a leaf, so a row is drawable on one wrapped /// line without a renderer knowing what is in it. [`Row::part`] is where that /// bites at a call site. /// /// Order is the description's: a part draws where it was put, so a tag between /// two facts stays between them. /// /// # What stayed a field /// /// [`activate`](Self::activate), [`current`](Self::current), /// [`selected`](Self::selected), [`menu`](Self::menu) and /// [`toggle`](Self::toggle) are facts *about* the row rather than content in /// it. A run of things on a line is not where "this row is the one the detail /// pane is showing" belongs. /// /// # The cost /// /// [`primary()`](Self::primary) is no longer guaranteed to be one string, which /// is what let a constrained renderer right-align a row cheaply. It answers the /// text of the primary parts joined, and a row built the ordinary way still has /// exactly one. /// A row and when it happens. /// /// The pairing [`Node::Timeline`] is made of. Deliberately a pair rather than /// members on [`Row`]: a row does not become a different kind of thing by /// being placed, and every list, table and detail pane in the tree would /// otherwise carry two integers it has no use for. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Placed { /// Where it sits on the axis, and for how long. pub placement: layout::Placement, /// The thing itself, said the ordinary way. pub row: Row, } impl Placed { /// A row at a start and a duration, both in minutes. #[must_use] pub const fn new(at: u16, minutes: u16, row: Row) -> Self { Self { placement: layout::Placement::new(at, minutes), row, } } /// Whether this and another cover any of the same time. /// /// Forwarded so a renderer laying out collisions does not reach through to /// the placement and, in doing so, decide for itself what overlapping /// means. #[must_use] pub const fn overlaps(&self, other: &Self) -> bool { self.placement.overlaps(other.placement) } } #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Row { /// What is in the row, in order, one entry per column it says anything in. /// /// Built by [`Row::new`], [`secondary`](Row::secondary), /// [`meta`](Row::meta), [`token`](Row::token), [`act`](Row::act), /// [`meter`](Row::meter) for the default column set, and by /// [`cells`](Row::cells), [`at`](Row::at) and [`cell`](Row::cell) for a /// declared one. /// /// A cell says which column it answers to through [`Cell::key`], so a row /// is no longer two collections with a private staging vector between /// them. A cell keyed [`CellKey::Named`] is unresolved until /// [`Table::row`] sees it. pub cells: Vec, /// What a request decides about runs of [`cells`](Self::cells). /// /// Empty answering a request, filled by a staged twin. See /// [`Slot::marks`], which carries the argument for both. pub marks: crate::stage::Marks, /// The route that selects this row, if selecting it does anything. pub activate: Option, /// Whether this is the row the detail side is currently showing. /// /// This one is the app's own pointer into a set: what a list-detail /// arrangement highlights because its pane is showing it, and what a /// webview says with `aria-current`. The user's tick is /// [`selected`](Self::selected), and conflating them meant a screen with /// bulk actions could not describe its checkboxes at all. pub current: bool, /// Whether this row is part of a selection that is already in force. /// /// Three states for [`selected`]'s reason, the app owns the set, the /// renderer contributes the gesture through [`Choosing`], and it draws as a /// highlighted row rather than as a checkbox. /// /// [`selected`]: Self::selected pub chosen: Option, /// Whether the user has ticked this row, and whether they can. /// /// Three states in one field, which is why it is not a `bool`. `None` means /// the row is not selectable and no affordance should be drawn; `Some(false)` /// means it can be ticked and is not; `Some(true)` means it is. A plain bool /// cannot tell "not ticked" from "not tickable", so every renderer would /// have had to be told selectability some other way, and each would have /// picked a different way. /// /// This is the user's selection, as distinct from /// [`current`](Self::current). goingson's contacts and tasks screens both /// drive bulk actions from it. pub selected: Option, /// Everything else that can be done to this row. /// /// The [`Actions`](layout::RowPart::Actions) parts of the run are what the /// row shows; this is what it *offers*, reached by right-click on a /// pointer host, long-press on a touch one, and a key in a terminal. That /// split is the whole reason it belongs in the description rather than in /// a renderer: one description has to become a context menu, an action /// sheet and a key-driven menu, and no single renderer can be the place /// where it is said. /// /// goingson opens one at 14 sites and Balanced Breakfast at 9, on top of /// 680 lines of generic menu machinery between `components.js` and /// `context-menus.js`. /// /// A field rather than a role in the run, because a menu is not on the /// line. The run is what the row draws; this is what it holds back until /// the host asks, and no renderer draws it in sequence with the primary. pub menu: Vec, /// What ticking this row calls, if ticking it is the write. /// /// Part of it. [`selected`](Self::selected) says whether the row is ticked /// and whether it can be, and that was the whole story for a bulk /// checkbox, whose tick is client state feeding a later action. A /// checklist is the other case: the tick *is* the write, and it is the /// only affordance the screen offers for it. Described without this, the /// port drew the tick inert and put the toggle on a button beside it, /// which is a user clicking a button next to a checkbox that ignores /// clicks. /// /// Two fields rather than a `Selection` struct, matching how /// [`activate`](Self::activate) sits beside [`current`](Self::current): /// state and behaviour are separate facts about the row. They do have to /// agree — a `toggle` with no [`selected`](Self::selected) is a route on a /// control nothing draws — and [`Row::toggling`] is the constructor that /// makes them agree. pub toggle: Option, /// What this row's tick contributes to the screen's selection. /// /// [`selected`](Self::selected) says the row can be ticked; this says what /// ticking it *means*, which is the half that was missing. A set of ticks /// with nothing in them is not a selection, so a renderer holding /// [`Screen::selection`] holds these. /// /// `value` rather than `id`, matching [`Choice::value`]: throughout this /// vocabulary it is the word for what a control contributes when it is /// chosen, and a row's tick is the same kind of fact. /// /// A selectable row without one is the dead affordance this member exists /// to end, and [`Row::ticking`] is the constructor that cannot produce it. /// It is not enforced here, for [`toggle`](Self::toggle)'s reason: a /// description layer that refused to hold a half-built row would refuse it /// at the moment the app is still building it. /// /// [`Choice::value`]: Choice::value /// [`Screen::selection`]: Screen::selection pub value: Option, /// The address a link reaches this row by. /// /// Not [`value`](Self::value): a value is unique within its list and an /// address is unique in the document, because an address is what a reader /// copies and sends on. /// /// `None` on nearly every row, and a renderer that finds none draws what it /// drew before. /// pub address: Option, /// How far into a hierarchy this row sits. 0 is top level. /// /// A hierarchy of rows is a **flat list of rows each saying how deep it /// is**, not rows holding rows. audiofiles' tags are dotted -- /// `drums.kick`, `genre.house` -- and the shipped sidebar builds a real /// tree with a recursive draw over it; described the other way, every tag /// was one row at its full dotted path and a vault with two hundred tags /// drew a wall where the shipped one drew an outline. /// /// Flat rather than nested for the reason the decision turned on: a /// renderer that has never heard of this member still draws the list it /// drew before, in order, with nothing missing. `Row::children` would have /// made every renderer's list walk recursive on pain of dropping rows /// silently, and a description that a renderer can only half-implement by /// losing content is not one this vocabulary should be able to say. /// /// A row deeper than the row above it is that row's child. Nothing checks /// that, and nothing should: a list whose first row is at depth 3 is a /// branch shown on its own, which is a screen somebody will write. /// /// Not [`Node::Heading`]'s level, which says how far down the *document* a /// title sits. That is depth in prose; this is containment in a set. pub depth: layout::Nesting, /// Whether this row has a disclosure, and whether it is currently open. /// /// `None` means no disclosure at all and no affordance drawn -- the state /// of every row written before this member existed, and the right answer /// for a leaf. `Some(false)` is a closed branch and `Some(true)` an open /// one. Three states in one field for [`selected`](Self::selected)'s /// reason: a bool cannot tell a leaf from a branch that happens to be shut. /// /// # What a renderer does with it, and what it does not /// /// It draws a disclosure, **as a separate hit target from the label**. The /// shipped egui sidebar already separates them deliberately, and it is the /// behaviour being described rather than an improvement on it: pressing a /// tag filters by it, pressing its chevron does not. /// /// A closed row's descendants -- the rows after it at a greater /// [`depth`](Self::depth), up to the next row at its own depth or less -- /// are not drawn. That is computable from the flat list, which is what /// makes the flat list enough. /// /// # The gesture is the renderer's /// /// This says where the outline starts, not where it stays. /// [`Region::showing_at_most_one`] states the same rule for a disclosure /// around a region and every word of it applies here: what a second press /// does, and whether a later visit comes back to the same shape, is the /// renderer's. A description that had to be re-asked for to fold a branch /// would put a round trip on a gesture that changes nothing anyone else can /// observe. /// /// So there is no route beside this the way [`toggle`](Self::toggle) sits /// beside [`selected`](Self::selected). A tick is a write and has to reach /// the app; folding a branch is the reader tidying their own view. If a /// screen turns up whose open branches are app state worth persisting, that /// is the consumer that earns the third member, and it has not turned up. /// /// [`Region::showing_at_most_one`]: Region::showing_at_most_one pub open: Option, /// Which side of a change this row is on, when the table is a diff. /// /// Decision `19d7602d` (2026-09-02, option d). A diff is a table of lines, /// and the only thing the vocabulary was missing was a way for a line to /// say whether it was added, removed or unchanged. So it is a member here /// rather than a `Node::Diff` carrying git's data model into a vocabulary /// shared by a task manager and a sample browser. /// /// `None` is not [`Change::Context`]. `None` means this table is not a diff /// and no renderer should tint it; `Some(Context)` means it is a diff and /// this line did not change. Two facts, and a table of ordinary rows must /// not read as a diff whose every line is context. /// /// What a renderer does with it is [`Change`]'s `Intent` impl and its own /// palette. A terminal with two colours to spend gets the same three /// answers a browser does. pub change: Option, } impl Row { /// A row with only its primary text. /// /// An empty string is an empty run rather than a run holding an empty /// string, so `Row::new("")` and [`Row::default`] are the same value. Same /// rule as [`Cell::new`], and for the same reason. pub fn new(primary: impl Into) -> Self { let primary = primary.into(); Self { cells: if primary.is_empty() { Vec::new() } else { vec![Cell { key: CellKey::Role(layout::RowPart::Primary), content: vec![Node::text(primary)], flow: None, priority: None, span: 1, }] }, ..Self::default() } } /// Put this row at an indent level, 0 being top level. /// /// [`depth`](Self::depth) is the member and this is how a call site says /// it. audiofiles' tag sidebar counts the dots in `drums.kick` and hands /// a Nesting built from it here. #[must_use] pub const fn depth(mut self, depth: layout::Nesting) -> Self { self.depth = depth; self } /// Give this row a disclosure, and say whether it is open. /// /// A branch. Without it the row is a leaf and draws no chevron, which is /// what every row written before [`open`](Self::open) existed is. Which /// rows it folds away is [`open`](Self::open)'s to say and the renderer's /// to do. #[must_use] pub const fn disclosing(mut self, open: bool) -> Self { self.open = Some(open); self } /// Something else that can be done to this row, not shown inline. #[must_use] pub fn offers(mut self, act: Act) -> Self { self.menu.push(act); self } /// Offer these acts on the row itself, rather than inside a part. /// /// The plural of [`offers`](Self::offers). A table row got this pair on /// 2026-09-02 and a list row did not, though both already carried the same /// facts under the same names, so a screen mixing a list and a table had to /// write one of them in the chain and the other by assignment. That gap is /// what the 2026-09-05 collapse closed by making them one type. /// /// The whole menu at once, because the measured sites hand over a `Vec` /// they already have: audiofiles' file list builds one conditionally on /// how many rows are chosen, and pushing it act by act would take the /// condition apart. /// #[must_use] pub fn menu(mut self, acts: impl IntoIterator) -> Self { self.menu.extend(acts); self } /// This is the row being shown elsewhere. /// /// Was reachable only by assigning the field, which is why a row built by /// a chain had to fall out of the chain to say it. A table row carried the /// same fact under the same name and got its builder first; this is the /// other half of that fix. /// /// The app's own pointer into a set, as distinct from /// [`selected`](Self::selected), which is the user's tick. Conflating them /// is what stopped a screen with bulk actions describing its checkboxes at /// all, and that argument is on the field. #[must_use] pub const fn current(mut self, current: bool) -> Self { self.current = current; self } /// How much of this row's set is done. #[must_use] pub fn meter(mut self, meter: Meter) -> Self { self.set(layout::RowPart::Proportion, Node::Meter(meter)); self } /// A tick that is the write, in the state it is currently in. /// /// Sets [`selected`](Self::selected) and [`toggle`](Self::toggle) together, /// because a route on a tick nothing draws is the one way the two fields can /// disagree. A checklist item is what this is for; a bulk checkbox sets /// `selected` alone and keeps its meaning as client state. #[must_use] pub fn toggling(mut self, ticked: bool, action: Action) -> Self { self.selected = Some(ticked); self.toggle = Some(action); self } /// Supporting text under the primary. #[must_use] pub fn secondary(mut self, text: impl Into) -> Self { let node = match text.into() { Prose::Text(text) => Node::text(text), Prose::Rich(source) => Node::rich(source), }; self.set(layout::RowPart::Secondary, node); self } /// A short trailing fact. #[must_use] pub fn meta(mut self, text: impl Into) -> Self { self.set(layout::RowPart::Meta, Node::text(text)); self } /// Add a token, chaining. #[must_use] pub fn token(mut self, tag: Tag) -> Self { self.cells.push(Cell { key: CellKey::Role(layout::RowPart::Tokens), content: vec![Node::Token(tag)], flow: None, priority: None, span: 1, }); self } /// Make the row tickable, and say whether it is ticked. /// /// A row is not selectable until something says so, which is what keeps a /// checkbox off every list in the app. /// /// Says nothing about what the tick contributes, so on a screen with a /// [`selection`](Screen::selection) it draws a box that joins no set. Reach /// for [`ticking`](Self::ticking) instead; this stays for the screens whose /// tick is the write, beside [`toggling`](Self::toggling). #[must_use] pub const fn selectable(mut self, ticked: bool) -> Self { self.selected = Some(ticked); self } /// Make the row part of a live selection under this value, and say whether /// it is chosen. /// /// The same pairing [`ticking`](Self::ticking) makes one member along: a /// row that can be chosen and names nothing is a dead affordance, because /// the value is what the app reads back to know which row the press was /// about. #[must_use] pub fn choosing(mut self, value: impl Into, chosen: bool) -> Self { self.value = Some(value.into()); self.chosen = Some(chosen); self } /// Make the row tickable under this value, and say whether it is ticked. /// /// Sets [`selected`](Self::selected) and [`value`](Self::value) together, /// which is the pair a screen's [`selection`](Screen::selection) needs. /// The two halves exist separately for [`toggling`](Self::toggling)'s /// reason — state and identity are different facts about the row — and /// this is the constructor that stops them being written apart. #[must_use] pub fn ticking(mut self, value: impl Into, ticked: bool) -> Self { self.selected = Some(ticked); self.value = Some(value.into()); self } /// Name the row without making it tickable. /// /// Identity and tickability are different facts about a row, and /// [`ticking`](Self::ticking) writes both because a screen's selection needs /// both; a row that is only ever pointed at needs the first alone. #[must_use] pub fn identified(mut self, value: impl Into) -> Self { self.value = Some(value.into()); self } /// Give the row a document address a link can reach it by. /// /// See [`address`](Self::address) for why it is not /// [`identified`](Self::identified), and write the name without the `#`. /// #[must_use] pub fn addressed(mut self, address: impl Into) -> Self { self.address = Some(address.into()); self } /// The route selecting this row. #[must_use] pub fn activate(mut self, action: Action) -> Self { self.activate = Some(action); self } /// A control acting on this row. #[must_use] pub fn act(mut self, act: Act) -> Self { self.cells.push(Cell { key: CellKey::Role(layout::RowPart::Actions), content: vec![Node::Act(act)], flow: None, priority: None, span: 1, }); self } /// Anything in this row, under the role it takes. /// /// The general form the constructors above are shorthands for, and the /// point of the model: a link in a row and a figure in a row became /// sayable at once, where each was previously a /// [`layout::RowPart`] variant, a member here, a renderer arm and a /// release. /// /// Appends rather than replacing, so a row can hold two of a role. The /// named constructors keep the single-valued roles single-valued, which is /// what their call sites already meant. /// /// # Panics /// /// If the node is not a leaf. A row is an inline run, so what goes in it /// has to be drawable on one wrapped line without the renderer knowing what /// it is -- the constrained-consumer bound, biting at a call site rather /// than in a doc comment. Same assertion as [`Cell::part`]. #[must_use] pub fn part(mut self, role: layout::RowPart, node: Node) -> Self { assert!( node.containment() == Containment::Text, "a row is an inline run and holds leaves; {node:?} holds {:?}", node.containment() ); self.cells.push(Cell { key: CellKey::Role(role), content: vec![node], flow: None, priority: None, span: 1, }); self } /// Let the part just added take two lines instead of one. /// /// Applies to the last part in the run, which is the one the call before it /// pushed: `Row::new(title).relaxed()` relaxes the title, and /// `.secondary(body).relaxed()` relaxes the body. Chaining is what makes /// "the last one" unambiguous at a call site, and it is why this is a /// method here rather than an argument on every constructor. /// /// A no-op on an empty run rather than a panic. `Row::new("")` is /// deliberately an empty run, so a caller that relaxes a title it turned /// out not to have is asking for nothing and gets nothing. /// /// What two lines means is [`layout::Flow::Relaxed`]'s to say, and what it /// costs each renderer is in that type's docs. #[must_use] pub fn relaxed(mut self) -> Self { if let Some(part) = self.cells.last_mut() { part.flow = Some(layout::Flow::Relaxed); } self } /// Say what the part just added is worth when the run does not fit. /// /// Applies to the last part in the run, the same way /// [`relaxed`](Self::relaxed) does. Without it the part is worth whatever /// its role is worth. /// /// A no-op on an empty run rather than a panic, for `relaxed`'s reason. #[must_use] pub fn worth(mut self, priority: layout::Priority) -> Self { if let Some(part) = self.cells.last_mut() { part.priority = Some(priority); } self } /// Set the one part taking a role, replacing it if it is already there. /// /// For the roles that are single-valued at every call site that has ever /// existed: the primary, the supporting line, the trailing fact, the bar. /// Building a row that calls `.meta` twice meant the second one won when /// `meta` was an `Option`, and it still does. fn set(&mut self, role: layout::RowPart, node: Node) { match self .cells .iter_mut() .find(|cell| cell.key == CellKey::Role(role)) { Some(cell) => cell.content = vec![node], None => self.cells.push(Cell { key: CellKey::Role(role), content: vec![node], flow: None, priority: None, span: 1, }), } } /// A row of a declared table, its cells in column order. /// /// Was the table row's own `new` before the 2026-09-05 collapse. It is a separate /// constructor from [`new`](Self::new) rather than an overload of it /// because the two say different things: `Row::new` names the primary /// column of the default set, and this answers a column list positionally. #[must_use] pub fn cells(values: impl IntoIterator>) -> Self { Self { cells: values .into_iter() .enumerate() .map(|(at, cell)| { let mut cell = cell.into(); cell.key = CellKey::Column(at); cell }) .collect(), ..Self::default() } } /// A cell naming the column it sits in, chaining. /// /// Safer than counting to a column in every way but one, which is why /// [`Table::row`] carries a debug assertion: a name no column has is /// dropped, so a typo renders an empty column rather than failing to /// compile. The row and the column list are usually written in different /// functions, so nothing above `Table::row` has both in hand to check. #[must_use] pub fn at(mut self, column: impl Into, cell: impl Into) -> Self { let mut cell = cell.into(); cell.key = CellKey::Named(column.into()); self.cells.push(cell); self } /// A cell in the next column along, chaining. #[must_use] pub fn cell(mut self, cell: impl Into) -> Self { let at = self.cells.len(); let mut cell = cell.into(); cell.key = CellKey::Column(at); self.cells.push(cell); self } /// Which side of a change this row is on, when the table is a diff. #[must_use] pub const fn changed(mut self, change: layout::Change) -> Self { self.change = Some(change); self } /// The parts taking one role, in order. pub fn role(&self, role: layout::RowPart) -> impl Iterator { self.cells .iter() .filter(move |cell| cell.key == CellKey::Role(role)) .flat_map(|cell| cell.content.iter()) } /// The row's primary text. /// /// A row built the ordinary way has one primary part and answers its /// string; one that was given two answers both, joined, in order. #[must_use] pub fn primary(&self) -> String { self.role(layout::RowPart::Primary) .filter_map(|node| match node { Node::Text { text, .. } | Node::Link { text, .. } => Some(text.as_str()), _ => None, }) .collect::>() .join(" ") } /// The controls the row shows. /// /// The same service [`primary`](Self::primary) does, for the member the run /// replaced. `actions` was a `Vec` before the run, and every consumer /// that read it now writes the same three lines: filter the run by role, /// match the one node kind that can be there, and collect. goingson wrote /// them twice in one file the day the member went away. /// /// Not what the row *offers*: that is [`menu`](Self::menu), which is held /// back until the host asks for it and is not on the line. /// Whether a control in this row's run or menu carries this /// [`Act::id`](Act::id). #[must_use] pub fn names(&self, id: &str) -> bool { self.menu.iter().any(|act| act.id.as_deref() == Some(id)) || self .cells .iter() .any(|cell| cell.content.iter().any(|node| node.names(id))) } pub fn acts(&self) -> impl Iterator { self.role(layout::RowPart::Actions) .filter_map(|node| match node { Node::Act(act) => Some(act), _ => None, }) } /// Every kind of time-derived readout in this row's run, added to `found`. fn clocks_into(&self, found: &mut BTreeSet) { for cell in &self.cells { for node in &cell.content { node.clocks_into(found); } } } /// The tags the row shows. /// /// [`acts`](Self::acts)' counterpart, for the same reason. pub fn tokens(&self) -> impl Iterator { self.role(layout::RowPart::Tokens) .filter_map(|node| match node { Node::Token(tag) => Some(tag), _ => None, }) } } /// One cell of a table row. /// /// A cell was a `String` until then, so a table whose rows carry a control /// could not be described at all and had to become a column-less table, /// losing its headers -- which is what the MNW server's /// SSH-keys tab did, and why it read worse than the Askama original it replaced. /// /// # Why the acts sit on the cell and not on the row /// /// Counted across MNW's templates, 30 table rows carry a control. 25 put it /// alone in the last cell, which a row-level `actions` list would have covered. /// The other five put it *beside a value*: `project_content`'s position cell is /// the number plus two reorder arrows, `project_synckit`'s slug cell is the slug /// plus "Set slug", `promo_codes_list`'s use count is a number that is itself /// the button opening the redemptions. A row-level list renders as an appended /// cell and cannot say any of those, and neither can an actions *column*, since /// a column is a column. The control belongs where it actually is. /// /// An empty [`value`](Self::value) with acts is the common case, and /// [`Cell::acts`] is the constructor for it. That is the trailing actions cell /// the markup already writes an empty `` for. /// /// A `Vec` and not a node: the ruling that a row holds no nodes holds /// here for the same reason. Acts carry their own tone, state and /// confirmation, and that is the whole of what these cells hold. /// Which column a cell answers to. /// /// The 2026-09-05 collapse: a list's parts were addressed by role and a table's /// values by column, and those were the same operation under two spellings, a /// lookup into a declared key set. The only difference was who declares the /// keys, and now that a list declares its own the difference is gone. #[derive(Debug, Clone, PartialEq, Eq)] pub enum CellKey { /// A column of the default set, which is what a container that declares no /// columns gets. /// /// [`layout::RowPart`]'s six variants are that set. A container spelled as /// a list is a table over them, which is why `list { row "x" { secondary /// "y" } }` still says what it always said. Role(layout::RowPart), /// A declared column, by position, already resolved. Column(usize), /// A declared column, by name, pending resolution by [`Table::row`]. /// /// This replaces the private staging vector a table row used to carry. A named /// cell now sits in the row with the others and says it is unresolved, /// rather than living in a second collection that had to be drained. Named(String), } impl Default for CellKey { fn default() -> Self { Self::Role(layout::RowPart::Primary) } } /// One cell: a run of leaf nodes, addressed by a key. /// /// **The 2026-09-05 collapse merged `Part` into this type.** A part was a cell /// that carried its own role because a list had no columns to carry it; a cell /// was a part whose column carried the role instead. One type now, with /// [`key`](Self::key) saying which column it answers to. /// /// A cell was a `String` before that, so a table whose rows carry a control /// could not be described at all and had to become a column-less table, losing /// its headers, which is what the MNW server's SSH-keys tab did and why it /// read worse than the Askama original it replaced. /// /// # Why the acts sit on the cell and not on the row /// /// Counted across MNW's templates, 30 table rows carry a control. 25 put it /// alone in the last cell, which a row-level `actions` list would have covered. /// The other five put it *beside a value*: `project_content`'s position cell is /// the number plus two reorder arrows, `project_synckit`'s slug cell is the slug /// plus "Set slug", `promo_codes_list`'s use count is a number that is itself /// the button opening the redemptions. A row-level list renders as an appended /// cell and cannot say any of those, and neither can an actions *column*, since /// a column is a column. The control belongs where it actually is. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Cell { /// What is in it, in order. /// /// An inline run: every entry is a leaf, so the whole cell is drawable on /// one wrapped line without a renderer knowing what is in it. That is the /// bound, and [`Cell::part`] is where it is enforced. It is also what /// [`CellKey::Role`] leans on: several tokens in one row are one cell /// holding several [`Node::Token`]s, not several cells fighting for a key. /// /// The run is what makes a meter in a cell or a figure in a cell sayable /// without a member each. pub content: Vec, /// Which column this cell answers to. pub key: CellKey, /// How much vertical room it may take, or `None` for its column's. /// /// An override rather than a duplicate: the column states the default and a /// cell may narrow it. BB clamps a feed row's title while its excerpt wraps /// freely underneath, which is two columns rather than one override, but a /// long value in one row of an otherwise tight column is the case that /// keeps this here. pub flow: Option, /// What it is worth when the row does not fit, or `None` for its column's. /// /// `None` means the description did not say, and the key answers instead: /// [`layout::RowPart::priority`] for a role, [`Column::priority`] for a /// declared column. There is no global default worth having, which is why /// this is an `Option` rather than a defaulted value. pub priority: Option, /// How many columns this cell covers. 1 is one column. /// /// **Defined and unread until the 2D work (`b1d4c5d7`).** It is here so /// spanning does not cost a second breaking change; nothing honours it yet, /// and a renderer meeting a value above 1 today should draw it as 1. pub span: u16, } impl Default for Cell { /// An empty cell in the primary column, covering one column. /// /// `span` is 1 rather than 0 here, which is why this is written out: a /// derived `Default` would produce a cell covering no columns, and nothing /// downstream would say so. fn default() -> Self { Self { content: Vec::new(), key: CellKey::default(), flow: None, priority: None, span: 1, } } } impl Cell { /// A cell holding text. /// /// An empty string is an empty run rather than a run holding an empty /// string, so an actions-only cell built through [`acts`](Self::acts) and /// one built as `Cell::new("").act(..)` are the same value. pub fn new(value: impl Into) -> Self { let value = value.into(); Self { key: CellKey::default(), flow: None, priority: None, span: 1, content: if value.is_empty() { Vec::new() } else { vec![Node::text(value)] }, } } /// A cell holding one tag and no text. /// /// What a status column is: the cell is the badge. `Cell::new("")` with a /// token would say the same thing and reads as an oversight. pub fn tag(tag: Tag) -> Self { Self { key: CellKey::default(), flow: None, priority: None, span: 1, content: vec![Node::Token(tag)], } } /// A tag in this cell, chaining. #[must_use] pub fn token(mut self, tag: Tag) -> Self { self.content.push(Node::Token(tag)); self } /// A cell holding controls and no text. pub fn acts(actions: impl IntoIterator) -> Self { Self { key: CellKey::default(), flow: None, priority: None, span: 1, content: actions.into_iter().map(Node::Act).collect(), } } /// A control in this cell, chaining. #[must_use] pub fn act(mut self, act: Act) -> Self { self.content.push(Node::Act(act)); self } /// How much of a set this cell's row is through, chaining. /// /// [`Row::meter`]'s counterpart on the other container, and it is a setting /// for that one's reason: a proportion is a fact about the thing the cell is /// in rather than a leaf beside its text. `Node::Meter` has no member of its /// own and cannot get one -- `meter` is a setting here and on `Row`, so the /// name is taken -- which is what made a table's progress column reach for a /// supplier before this existed. goingson's task list is the site. #[must_use] pub fn meter(mut self, meter: Meter) -> Self { self.content.push(Node::Meter(meter)); self } /// Where this cell's value goes. /// /// The value becomes the link. A cell with no value and an `activate` is a /// link with nothing to press, so give it text. /// /// Under the run this rewrites the leading text into a [`Node::Link`] /// rather than setting a member beside it, which is the same fact said once /// instead of as a pair of fields that could disagree. A cell with no text /// to link gains nothing, because a link with no label is a control nothing /// draws. #[must_use] pub fn activate(mut self, action: Action) -> Self { if let Some(first) = self .content .iter_mut() .find(|part| matches!(part, Node::Text { .. })) && let Node::Text { text, .. } = first { *first = Node::Link { text: std::mem::take(text), action, }; } self } /// Anything in this cell, chaining. /// /// The general form the five constructors above are shorthands for, and the /// whole point of the model: a meter in a cell, a figure in a cell and a /// second linked value in a cell all became sayable at once, where each was /// previously a member, three renderer arms and a release. /// /// # Panics /// /// If the node is not a leaf. A cell is an inline run, so what goes in it /// has to be drawable on one wrapped line without the renderer knowing what /// it is -- that is the constrained-consumer bound, and this is where it /// bites at a call site rather than in a doc comment. #[must_use] pub fn part(mut self, node: Node) -> Self { assert!( node.containment() == Containment::Text, "a cell is an inline run and holds leaves; {node:?} holds \ {:?}", node.containment() ); self.content.push(node); self } /// What this cell is worth when the row does not fit. /// /// [`priority`](Self::priority) if the description said, and otherwise the /// key's: [`layout::RowPart::priority`] for a role. /// /// **A cell keyed to a declared column cannot answer alone**, and reports /// [`layout::Priority::Essential`] rather than guessing. Its column holds /// the real answer, and a renderer drawing a declared table should read /// [`Column::priority`] instead of calling this. Essential is the safe end /// of the scale on purpose: a caller that forgets to consult the column /// draws a cell it could have dropped, rather than dropping one it should /// have drawn. That asymmetry is the collapse being honest -- a table row /// was always meaningless without its columns, and now it says so. #[must_use] pub fn worth(&self) -> layout::Priority { self.priority.unwrap_or_else(|| match &self.key { CellKey::Role(role) => role.priority(), CellKey::Column(_) | CellKey::Named(_) => layout::Priority::Essential, }) } /// How much vertical room this cell may take. /// /// [`flow`](Self::flow) if the description said, and otherwise /// [`layout::Flow::Tight`], which is one line and is what every part did /// before the field existed. #[must_use] pub fn room(&self) -> layout::Flow { self.flow.unwrap_or_default() } /// The cell's text, with the parts that are not text left out. /// /// A cell that is one string answers that string; one that mixes answers /// the text between its tags and controls, in order. #[must_use] pub fn text(&self) -> String { self.content .iter() .filter_map(|part| match part { Node::Text { text, .. } | Node::Link { text, .. } => Some(text.as_str()), _ => None, }) .collect::>() .join(" ") } /// Whether anything in this cell answers a click. /// /// A badge is not one: it says something and answers nothing, which is why /// this asks the tag rather than counting tags. #[must_use] pub fn carries_control(&self) -> bool { self.content.iter().any(|part| match part { Node::Act(_) | Node::Link { .. } => true, Node::Token(tag) => tag.kind.interactive() && tag.action.is_some(), _ => false, }) } } impl From for Cell { fn from(value: String) -> Self { Self::new(value) } } impl From<&str> for Cell { fn from(value: &str) -> Self { Self::new(value) } } /// What a press on a row of a live selection meant. /// /// The renderer's half of [`Row::chosen`]: the description says which rows /// are chosen and the app owns the set, but *how a press was meant* is the one /// part only the renderer can know, because it is the host's idiom and a /// different idiom on each. /// /// Three members, which is what every file manager on every desktop has offered /// for thirty years and what audiofiles lost when its list was described. A host /// maps its own gesture onto them: /// /// | | pointer | touch | terminal | /// |---|---|---|---| /// | [`Only`](Self::Only) | click | tap | Enter | /// | [`Also`](Self::Also) | ctrl-click, cmd-click on macOS | long-press | Space | /// | [`Through`](Self::Through) | shift-click | drag over a run | none yet | /// /// The terminal's gap is stated rather than invented around: `quasi_tui::Key` /// carries no modifiers, so shift-Enter is not expressible without widening the /// key type every host driving that renderer maps onto. /// /// # What a reader holding both keys means /// /// Ruled here rather than left to each renderer, which is the whole point of /// the type: **shift wins**. Finder and Explorer both read ctrl-shift-click as /// "extend the run and keep what was already chosen", which is a fourth member /// and has no consumer asking for one. Of the three that exist, taking the run /// is nearer to what the reader asked for than toggling the single row they /// happened to land on -- and the one thing holding two keys cannot mean is the /// plain press. /// /// # Why this is in the vocabulary at all /// /// It looks like input state, which every other ruling here has refused to /// carry: a description says what is on the screen and never where, never how /// wide, never which key. The difference is that this is not the *gesture*, it /// is what the gesture **meant**, and the meaning is the same on every host /// while the gesture is not. A description that carried "ctrl was held" would /// be naming a keyboard; this names an intention a touch host can honour with /// no keyboard at all. /// /// The test it passes and a modifier would not: a terminal can implement it. /// /// # It travels in the payload, not in the address /// /// Under [`Node::CHOOSING`], beside whatever else the activation carries. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum Choosing { /// This row and nothing else. The plain press, and the default. #[default] Only, /// This row as well as whatever was already chosen, or out of it if it was /// already in. /// /// A toggle rather than an add, which is what every file manager does with /// ctrl-click and is the only reading that lets a reader correct a /// mis-click without starting over. Also, /// Every row from the one the app is pointing at to this one. /// /// The range's other end is the app's own -- `Row::current`, or whatever /// the app calls its focus -- and is deliberately not carried here. A /// renderer that named it would be answering with the row it *drew* as /// current, which is the description's answer from one frame ago; the app /// holds the live one. Through, } impl Choosing { /// The spelling that travels in a payload. #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::Only => "only", Self::Also => "also", Self::Through => "through", } } /// What a handler reads back, defaulting to [`Only`](Self::Only). /// /// **An unknown spelling is [`Only`](Self::Only) rather than an error**, and /// that is the same bargain every other read of a submitted value strikes /// here: a press that arrives saying something this version does not know is /// still a press on a row, and refusing it would break the ordinary act to /// protect the extraordinary one. The plain reading is the safe one -- it /// chooses the row that was pressed and nothing else. #[must_use] pub fn read(value: Option<&str>) -> Self { match value { Some("also") => Self::Also, Some("through") => Self::Through, _ => Self::Only, } } } /// A table: the columns, and the rows that answer to them. /// /// The columns arrive with the table, so a row is never built against a column /// list that does not exist yet. That is the rule [`Run::new`] holds for a /// shared row's fallback, here for the same reason: a table's invariant is that /// cells line up with columns, and the point a row is put in is the only place /// with enough context to keep that true. /// /// # Why a cell is addressed by name /// /// [`Column`] documented its `name` as "the heading, and the name the cell is /// addressed by ... what replaces addressing columns by position" while the row /// stayed positional, so the file held both answers at once. Position is the one /// that loses. A cell that appears on some rows and not others shifts every cell /// after it, so a conditional cell had to be written as a matched pair and /// nothing but care kept the pair matched. Under [`Row::at`] an absent cell is an /// empty cell in its own column, and a row whose arity depends on a runtime flag /// is an ordinary row. /// /// A row built by [`Row::cells`] is still positional and still works. The two /// are not mixed in one row: if a row names any column, the names are the row. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Table { columns: Vec, rows: Vec, more: Option, /// What a request decides about runs of the columns and the rows. /// /// Carried here and handed to [`Node::Table`] when the table becomes one, /// because the declared form accretes into this and the renderer reads /// that. See [`Slot::marks`]. marks: crate::stage::Marks, } impl crate::stage::Marking for Table { /// Columns, then rows, then the pager: the order a table is drawn in. /// /// The pager counts as a member because it is one in all but name: `more` /// is spelled as a setting and what it does is add markup at a position of /// its own, which is exactly what a member does and not at all what /// settling a tag does. fn placed(&self) -> usize { self.columns.len() + self.rows.len() + usize::from(self.more.is_some()) } fn mark(&mut self, mark: crate::stage::Mark) { self.marks.add(mark); } } impl Table { /// A table with these columns and no rows yet. pub fn new(columns: impl IntoIterator) -> Self { Self { columns: columns.into_iter().collect(), rows: Vec::new(), more: None, marks: crate::stage::Marks::none(), } } /// Add one column, for a caller building the table a piece at a time. /// /// Beside [`new`](Self::new) rather than instead of it: a table whose /// columns are a literal list still says so in one expression, and a table /// assembled by something that accretes (the declared form, which has no /// expression to hold a list in) says the same thing one column at a time. /// /// **Columns before rows.** [`row`](Self::row) resolves a named cell /// against the columns the table has when the row arrives, so a column /// added afterwards is invisible to every row already pushed. #[must_use] pub fn column(mut self, column: Column) -> Self { debug_assert!( self.rows.is_empty(), "a column added after a row cannot be seen by that row's named cells" ); self.columns.push(column); self } /// Add a row, resolving any cell that named its column. /// /// A named cell whose column this table does not have is dropped: the /// column list is the table's statement of what a row may say, and a row /// saying more than that is answered by the columns rather than by /// widening them. A column no cell named is empty in this row. #[must_use] pub fn row(mut self, mut row: Row) -> Self { if row .cells .iter() .any(|cell| matches!(cell.key, CellKey::Named(_))) { let placed: Vec<(String, Cell)> = std::mem::take(&mut row.cells) .into_iter() .filter_map(|cell| match &cell.key { CellKey::Named(name) => Some((name.clone(), cell.clone())), _ => None, }) .collect(); debug_assert!( placed .iter() .all(|(name, _)| self.columns.iter().any(|column| column.name == *name)), "a cell named a column this table does not have; the name is dropped and the \ cell is silently lost. Named: {:?}. Columns: {:?}", placed.iter().map(|(name, _)| name).collect::>(), self.columns .iter() .map(|column| &column.name) .collect::>(), ); debug_assert!( { let mut names = self .columns .iter() .map(|column| column.name.as_str()) .collect::>(); names.sort_unstable(); let before = names.len(); names.dedup(); names.len() == before }, "two columns share a name, so a named cell cannot say which it meant and both \ take the first one's value. Columns: {:?}", self.columns .iter() .map(|column| &column.name) .collect::>(), ); row.cells = self .columns .iter() .enumerate() .map(|(at, column)| { let mut cell = placed .iter() .find(|(name, _)| *name == column.name) .map_or_else(|| Cell::new(String::new()), |(_, cell)| cell.clone()); cell.key = CellKey::Column(at); cell }) .collect(); } self.rows.push(row); self } /// Add several rows, chaining. #[must_use] pub fn rows(mut self, rows: impl IntoIterator) -> Self { for row in rows { self = self.row(row); } self } /// Say what is not shown, and how to ask for it. #[must_use] pub fn more(mut self, rest: Rest) -> Self { self.more = Some(rest); self } /// The columns, in order. #[must_use] pub fn columns(&self) -> &[Column] { &self.columns } } impl From for Node { /// A region inside a region, without the enclosing one naming the variant. /// `From` below is the same courtesy for the same reason. fn from(slot: Slot) -> Self { Self::Region(slot) } } impl From for Node { /// The wrapping every caller of a control-returning function writes by /// hand. `From` below is the same courtesy for the same reason, and /// a shape that returns a control has to be placeable in a body without /// the caller naming the variant. fn from(act: Act) -> Self { Self::Act(act) } } impl From for Node { /// A question inside a body, without the enclosing container naming the /// variant. The third of these, after `From` and `From` above, /// and for their reason: a shape that returns a `Field` has to be placeable /// where a node goes. MNW's repository bar is the site -- its ref chooser /// is a shape of its own and the bar holds it beside the tab strip. fn from(field: Field) -> Self { Self::Field(::std::boxed::Box::new(field)) } } impl From
for Node { fn from(table: Table) -> Self { Self::Table { marks: table.marks, columns: table.columns, rows: table.rows, more: table.more, } } } /// A row that says where it sits in a hierarchy. /// /// [`Row`] carries [`depth`](Row::depth) and [`open`](Row::open), and /// [`folded`] is the one reading of them the three /// renderers must agree on. A trait rather than the function written twice: two /// copies of "which rows does a shut branch hide" is two answers waiting to /// disagree, and a reader who folds a branch in one host and finds a different /// list in another is being shown the disagreement. pub trait Outline { /// How far in the row sits. [`Row::depth`]. fn depth(&self) -> layout::Nesting; /// Its disclosure, if it has one. [`Row::open`]. fn open(&self) -> Option; /// What a renderer holding the reader's own folds keys this branch by. /// /// [`value`](Row::value) when the row names itself and its leading text /// otherwise, which is the same fallback `quasi_immediate::row_at` makes /// for the same reason: a row that names itself is identified by the app /// and one that does not is identified by what it says. /// /// Here rather than in each renderer, because a reader who folds a branch /// in a terminal and opens the same screen in a window is entitled to find /// the same branch. Two spellings of "which row is this" is two answers. fn key(&self) -> String; } impl Outline for Row { fn depth(&self) -> layout::Nesting { self.depth } fn open(&self) -> Option { self.open } fn key(&self) -> String { self.value.clone().unwrap_or_else(|| self.primary()) } } /// Which rows a closed branch folds away, one answer per row in order. /// /// A row is folded when a row above it is a closed branch shallower than it, /// with nothing at that branch's own depth or shallower in between. That is the /// whole of what makes a flat list an outline, and it is computed from the list /// rather than described, which is why `Row::children` was not needed to say a /// hierarchy. /// /// A row folded by one branch cannot un-fold under another inside it: a shut /// branch takes its whole subtree, including the open branches in it, and those /// come back in the state they were left when it opens. /// /// A renderer that does not call this draws every row flat, which is today's /// list and is the graceful degradation the flat model was chosen for. #[must_use] pub fn folded(rows: &[T]) -> Vec { folded_by(rows.iter().map(|row| (row.depth(), row.open()))) } /// [`folded`], for a renderer holding the reader's own answer about a branch. /// /// A terminal and an immediate-mode window keep the folds the reader made, the /// way they keep a scroll offset and a tick, and the description's `open` is /// where that starts rather than where it stays ([`Row::open`]). So they read /// each row's state off the reader first and hand the pairs here, and the /// walk itself stays in one place: two copies of "what does a shut branch /// cover" is two answers waiting to disagree across hosts. #[must_use] pub fn folded_by(levels: impl IntoIterator)>) -> Vec { let levels = levels.into_iter(); let mut folded = Vec::with_capacity(levels.size_hint().0); // The depth of the shallowest closed branch still folding, if any. One // variable and not a stack: a branch inside a folded subtree can never be // the reason a row is hidden, because it is hidden itself. let mut shut: Option = None; for (depth, open) in levels { if shut.is_some_and(|shallowest| depth <= shallowest) { shut = None; } let hidden = shut.is_some(); folded.push(hidden); if !hidden && open == Some(false) { shut = Some(depth); } } folded } /// Which way a readout is reckoned against the current time. /// /// The three time-derived members of [`Node`] say the same thing about /// themselves in three spellings, and this is how a renderer asks which one it /// is holding without matching all three everywhere it cares. Two things read /// it: the formatting, which differs per kind, and the cadence, which follows /// the granularity the format chose. /// /// Ordered so a renderer taking the minimum of a screen's kinds gets the finest /// one first, which is what [`Screen::clocks`] is usually asked for. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum Clock { /// Counting up from an instant. [`Node::Since`]. Since, /// Counting down to one. [`Node::Until`]. Until, /// How long ago one was. [`Node::Age`]. Age, } /// A thing on a screen. /// /// Every member composes something `makeover-layout` already names, and that is /// the admission test for a new one. A node with no counterpart there means the /// vocabulary is missing a word, and the fix is to add the word rather than to /// add a widget here. /// /// There is a second test, and a member passes both or neither: **a renderer /// must be able to size it before its content arrives.** A member that can only /// be laid out once it is filled makes the screen move under the reader, which /// is what "First paint is final paint" in `makeover-layout`'s header forbids. /// If a member cannot be sized in advance as written, it is missing the fact /// that would make it sizeable, and adding that fact is the fix — the same /// shape as the first test, one layer along. /// /// The failure this catches is an optional measurement standing in for a /// measurement that has not been taken. An `Option` on a count here means the /// host cannot count, for the life of the screen; it never means the count is /// still coming. A number that shows up after the first paint widens whatever /// prints it. /// /// `#[non_exhaustive]`, the pairing [`Region`](layout::Region) made when it grew /// [`Region::Widget`](layout::Region::Widget), and for the same reason: the /// member after this one should not be a lockstep event across three renderers. /// This is the enum that reason applies to most, since a renderer spells every /// member of it and nothing else. /// /// What the attribute buys is that a renderer *may* lag, not that it should. A /// wildcard arm here is a promise about one member, so write it the way the rest /// of the suite does: the arm says what the honest degradation is and why it is /// the safe read, rather than falling through to nothing without comment. The /// matches inside this crate stay exhaustive -- the attribute does not apply /// within it, and a wildcard here would only hide a member added without a /// spelling. #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum Node { /// A title, at one of three depths in the heading tree. Heading { /// How far down the tree it sits. level: layout::Heading, /// The text. text: String, }, /// Prose, with a tone. Text { /// The text. text: String, /// What it is saying. [`layout::Tone::Neutral`] is ordinary content. tone: layout::Tone, }, /// Prose the author wrote in markdown. /// /// What is carried is the **source**, never markup, which is the property /// that lets this exist at all. Every renderer has an honest answer /// because each renders the source its own way: a webview through a /// markdown-to-HTML pass, a terminal through markdown-to-ANSI, egui /// through its own. A `Node::Html` would have handed every one of them a /// string it could not honour, and would have broken /// [`Node::Text`]'s escaping guarantee for every consumer rather than the /// one that asked. That refusal stands; this is not it. /// /// Sanitising is the renderer's, at the point markup is produced, for the /// reason escaping already is: this holds text a user typed, and a /// description that sanitised would be deciding what a host can draw. /// /// Not available inside a [`Row`]: a row part holds no node, by the /// ruling. What a row can hold is [`Prose`], which carries the same /// markdown source under the same reasoning without being a node, so the /// projects card no longer keeps its raw markdown in `secondary`. Rich { /// The markdown, as written. source: String, /// How much of the format this source may use. /// /// Separate from [`trust`](Self::Rich::trust) on purpose, and the two /// correlate without being the same question: a creator's forum post is /// rich and untrusted, a button's own hint is plain and trusted. richness: Richness, /// How far the source is trusted. trust: Trust, }, /// Source code, already classified by whoever holds a lexer. /// /// Decision `19d7602d`, 2026-09-02, option (d). The node carries /// [`Lexeme`]s and never a bare string, which is the whole ruling: an app /// that browses source already has a lexer, a renderer does not and should /// not grow one, and three renderers each growing their own would disagree /// about the same file. /// /// # Why this is not [`Rich`](Self::Rich) /// /// `Rich` carries markdown and each renderer renders it its own way, which /// is right for prose and wrong here: markdown's fenced code block says a /// run is code and says nothing about what is in it, so a renderer would /// still be choosing whether to lex. This says what the runs are and leaves /// only the colouring. /// /// # Inline against block, and why one member rather than two /// /// A clone URL in a sentence and a file in a source browser are the same /// claim about the same text; what differs is whether it sits in the line or /// owns one. That is a containment fact, so it is a flag here and /// `Node::containment` reads it: `inline` is a leaf and may sit in a [`Row`] /// or a [`Cell`], and a block is not and may not. Two members would have /// made one distinction twice, once in the type and once in the bound. /// /// # The language is carried even though the runs are classified /// /// Not redundant. The classification is fixed at what [`layout::Syntax`] /// can say, and a renderer with an opinion of its own -- a terminal that /// already ships a highlighter, a host that wants a language badge over the /// block -- has nothing else to read. A hint, and no renderer is obliged to /// use it. Code { /// The source, run by run, in order. Concatenating every `text` gives /// the file back exactly, whitespace included. runs: Vec, /// What language it is, when the app knows. A hint; see above. language: Option, /// Whether it sits in a line rather than owning one. inline: bool, }, /// A control that calls a route. Act(Act), /// Text that goes somewhere. /// /// The containment model predicted this before anything asked for it: the /// composability matrix had a `Link` row whose "as a `Node`" cell was a /// dash, and the only way to say it was [`Cell::activate`], a member on one /// container. Once a cell is a run of leaves, the run needs a leaf that /// means "this text is a link" or the thing stops being sayable at all. /// /// Distinct from [`Act`](Self::Act), and the difference is what the reader /// sees rather than what the route does. An act is a control drawn as one, /// which is right for `Edit` and wrong for a title: making every linked /// value a button would put a row of bevels down the first column of half a /// dashboard. Both call a route; only one of them looks like a button. Link { /// What it says. text: String, /// Where it goes. action: Action, }, /// One figure, on a line. /// /// The second thing the containment model found, and it found it by /// refusing: a cell is a run of leaves, [`Stats`](Self::Stats) is a /// collection, so putting a figure in a cell failed the bound rather than /// quietly working. That is the check doing its job -- the matrix had /// "figure in a cell" as a dash nobody had attempted, and the dash turns /// out to have been hiding a missing member rather than a missing renderer /// arm. /// /// Not a duplicate of a one-element [`Stats`](Self::Stats), and the /// difference is the claim being made. A strip says "this is a row of /// tiles", which is why the set is the node there: a renderer handed one /// tile at a time cannot tell it is looking at a set. This says "this /// number sits on this line", where the run is already the grouping and /// there is nothing for a set to add. A dashboard strip of one is still a /// strip; a revenue column is not. Figure(Figure), /// Time elapsed since an instant, counting up. /// /// The three time-derived members are this, [`Until`](Self::Until) and /// [`Age`](Self::Age), and the ruling they implement is one sentence: **the /// renderer owns the clock.** What is carried here is an instant and which /// way the readout runs against now; the cadence and the spelling are the /// renderer's, and the description names neither. /// /// A stopwatch, an uptime, a running timer. goingson's time-tracking widget /// is the measured case: 564 lines of JS whose whole job is to subtract a /// start time from now every second, in two places, for a readout the /// description could not say at all. /// /// # Why a kind and not a format string /// /// A format string is presentation living in a description, and it would /// bind three renderers with different width budgets to one spelling. A /// bare instant is worse in the other direction: a stopwatch and a /// last-modified stamp become indistinguishable, so a renderer cannot pick /// a cadence and would tick a static date once a second. /// /// The kind answers many-readouts-one-tick for free. Every `Since` on a /// screen refreshes together because they share a kind, which is what /// `updateRowElapsed` walking `.task-timer-elapsed[data-started]` does by /// hand today, and [`Screen::clocks`] is how a renderer asks which kinds it /// is holding. /// /// # Why a duration is not carried /// /// [`Consult::after`] carries one, and the line between them is stated /// there: a duration is the description's when it is a property of the /// subject that renderers would otherwise disagree about, and the /// renderer's when it is presentation policy. A debounce is the route's own /// expense and no renderer could know it. A tick cadence follows the /// displayed granularity, so two renderers showing the same readout do not /// meaningfully disagree. /// /// A leaf, so it may sit in a row's run or a table cell. That is the half /// the goingson row readout needs: the elapsed time is one part of a task /// row beside its title, not a block of its own. Since { /// The instant it counts from. at: std::time::SystemTime, }, /// Time remaining until an instant, counting down. /// /// [`Since`](Self::Since) run the other way, under the same ruling and with /// the same reasoning. A deadline, an expiry, a lock that lifts. /// /// What a renderer does once the instant is past is the renderer's, for the /// reason the spelling is. Nothing here says whether that reads as a /// negative countdown or as zero, because a description that said would be /// picking one of those for a terminal it has never seen. Until { /// The instant it counts down to. at: std::time::SystemTime, }, /// How long ago an instant was. /// /// [`Since`](Self::Since)'s coarse sibling, and the reason it is a third /// member rather than the same one: "3h ago" and a running `h:mm:ss` are /// the same subtraction shown at granularities two orders apart, and the /// granularity is what decides the cadence. A stamp drawn as a stopwatch /// ticks a number that has not changed once a second for a day. /// /// A last-modified, a posted-at, a synced-at. Age { /// The instant being aged. at: std::time::SystemTime, }, /// A picture, at a source this crate holds and the description does not. /// /// The [`Act`](Self::Act) split, and `layout::Image`'s own docs carry the /// argument: an address is not the description's to hold, so the shape and /// the alt text live there and the URL lives here. /// /// A leaf, so it may sit in a run the way [`Link`](Self::Link) does. What /// it may *not* do is stand in for a region: a picture is one thing on the /// page, and a gallery of them is a widget assembled out of several. Image(Image), /// A small labelled thing sitting inside something else. Token(Tag), /// Something the app is telling the user, unprompted. Notice { /// Transient and stacked, or persistent and in flow. kind: layout::Notice, /// What it is saying. tone: layout::Tone, /// The message. text: String, /// One thing to do about it, if there is one. /// /// [`Message::undo`] says a response can offer a way back, and a host /// that draws a `Message` by converting it into one of these had /// nowhere to put it: quasi-tui's `announce` said so in a comment and /// dropped it. /// /// Grown here rather than answered by a holder beside the screen, which /// was the alternative. Every retained-screen host would have written /// the same holder, and both shapes are the same thing said from two /// ends -- which is what [`Message`]'s own docs already claim. /// /// [`StandIn`](Self::StandIn)'s member under the same name and for the /// same reason: a sentence about a situation, and the one thing to do /// about it, are one thing on the screen. /// /// [`Message`]: crate::Message /// [`Message::undo`]: crate::Message::undo act: Option, }, /// What stands where content would be, when there is none. /// /// goingson draws one at 27 sites across 12 files and Balanced Breakfast /// at 9, and the class families had already drifted into `empty-state-- /// error` against `error-state` for the same fact. Every one of those /// sites substitutes markup where a list would go, which is what makes /// this a node. /// /// # Why not on the region /// /// It was on [`Slot`] first, and a real screen killed it: the project /// dashboard's columns are a heading and a list, and a column with no rows /// is a region that has content and a list that has none. Marking the /// region empty took the heading down with the rows. The emptiness belongs /// to the thing that is empty. /// /// [`Slot::readiness`] keeps the loading axis and only that, which is what /// `aria-busy` is about. /// /// # The state is the vocabulary's and the sentence is not /// /// `makeover-layout` names the four states because "nothing here yet" and /// "this broke" mean the same thing in every app that will have them. "No /// projects yet" is content, and so is the button under it, so both are /// here. A [`layout::Readiness::Ready`] renders nothing at all: the state /// that shows content has no stand-in to draw. StandIn { /// Which of the states this is standing in for. state: layout::Readiness, /// The sentence. "No projects yet", "Failed to load events". message: String, /// The way out, if there is one. "Add your first project", "Try again". /// /// 2 of goingson's 27 have one and 25 say a sentence and stop, which is /// why it is optional rather than a second required string. act: Option, /// What a request decides about the way out. /// /// The one member a placeholder holds, and it is a member: `offering` /// places it, and a guard on it is a run of one. See [`Slot::marks`]. marks: crate::stage::Marks, }, /// One control, standing on its own. /// /// A [`Form`](Self::Form) is a set of questions asked together and /// answered at once. A settings screen is not that: goingson's is sections /// with headings between them, each holding one control that writes as /// soon as it changes, and wrapping those in a form would describe markup /// that is not there and a submit that does not exist. /// /// Almost always carries a [`Field::writes`], because a control with no /// form around it and no route on it collects a value nothing reads. /// /// Boxed because it is the only member holding a whole struct by value, and /// [`Field`] is the largest one here — every other member holds a `Vec`, a /// `String` or a small enum. Unboxed it decides the size of every [`Node`] /// in every list, and of the [`Response`](crate::Response) that carries one. Field(Box), /// Fields, and the route that submits them. Form { /// Where the answers go. Almost always a [`Method::Post`]. action: Action, /// What the submit control is called. submit: String, /// The questions, in order. fields: Vec, /// What a request decides about runs of [`fields`](Self::Form::fields). marks: crate::stage::Marks, }, /// Rows with named columns. /// /// # Which containers carry a [`Rest`], and which do not /// /// A container whose contents came from a query that can be partial carries /// one. That is this one -- in both its arrangements, since a list is this /// node with no columns -- and nothing else in this enum: /// [`Timeline`](Self::Timeline) is bounded by its `Track`, so more of /// it is a different window and that is navigation rather than paging; /// [`Stats`](Self::Stats) is a fixed set of figures; [`Region`](Self::Region) /// holds nodes rather than rows. Written down so the next container to /// arrive answers the question rather than inheriting an answer. /// /// This one went without for four minors. goingson's task list, the first /// described table anywhere, had to hang its paging off a separate /// [`Act`](Self::Act) under the table, and recorded the cost: the renderer /// could not tell the control belonged to the table above it. Table { /// The columns, in order. A cell keyed by position answers these in order. /// /// **Empty is a list.** That is the whole of what a list is since the /// 2026-09-06 collapse, and it is a fact the rows already carried rather /// than a flag added beside them: a row whose cells are keyed by /// [`CellKey::Role`] answers the default column set, and a table that /// declared no columns of its own is a table using that set. So there is /// no `look` member and no second variant to keep in step. /// /// A renderer reads it to choose an arrangement, not to choose a /// meaning. `columns.is_empty()` draws the flowed, one-per-line form a /// list has always drawn -- `
    ` in a webview -- and a declared column /// list draws a grid. Both are the same node holding the same rows. columns: Vec, /// The rows, in order. rows: Vec, /// What is not shown, if anything is. /// /// A described list of the first 50 of 400 tasks was indistinguishable /// from a described list of 50 tasks, so each app grew its own answer: /// goingson a 159-line pagination manager with two consumers that had /// each written it separately first, Balanced Breakfast four /// `loadMore` sites. Two idioms for one fact, and the fact is what /// belongs here -- how much more there is and how to ask for it. /// Whether that becomes numbered pages, a load-more button or an /// infinite scroll is the renderer's. more: Option, /// What a request decides about runs of the columns and the rows. /// /// Empty answering a request, filled by a staged twin. The index space /// is the columns and then the rows, which is the order a table is /// drawn in. See [`Slot::marks`]. marks: crate::stage::Marks, }, /// Rows placed by when they happen, rather than in order. /// /// The second of the two ways this vocabulary says "several of the same /// kind of thing", and the last one to arrive. /// [`Table`](Self::Table) puts them in order and, when it declares columns, /// lines their parts up under headings; this one puts them on a clock. /// /// It was the third of three until the 2026-09-06 collapse, when a list /// stopped being a node of its own. A timeline stayed: what it adds is /// [`layout::Placement`] on every entry, which is a fact a `Row` does not /// carry and a column cannot supply. /// /// A row here is an ordinary [`Row`] and gets no new members: the item /// bodies on goingson's day view are a title, a time, a tag and a tone, /// which the vocabulary already said. What it could not say is *where the /// row sits*, and that is [`layout::Placement`] — a start and a duration, /// two integers, which is the whole of what the timeline refusal was /// pricing as a component library. See `makeover-layout` 0.24.0. /// /// # What a renderer owes it /// /// Draw the span, put each row at its placement, and lay overlapping rows /// so both can be read. That last part is presentation and deliberately /// unspecified: a webview puts them in columns, a terminal may stack them /// with a marker, and neither is wrong. /// [`layout::Placement::overlaps`] is how a renderer finds the pairs /// without the description declaring them. /// /// # What it is not /// /// Not a calendar and not a kanban board. Both were refused alongside the /// timeline and neither has been measured; whoever needs one counts the /// members it is missing rather than reaching for this. Timeline { /// The axis: its window, its granularity, how often it labels itself. track: layout::Track, /// What sits on it, each with where it sits. /// /// Not sorted here, and a renderer must not assume it is. Sorting by /// start is presentation for anything that draws top to bottom, and /// meaningless for anything that does not. entries: Vec, /// What a request decides about runs of /// [`entries`](Self::Timeline::entries). marks: crate::stage::Marks, /// A moment worth bringing into view, if any. /// /// "Show me 09:00" rather than a scroll offset in pixels. goingson's JS /// hardcodes `targetHour = 9` inside the renderer, which is the shape /// this replaces: the app knows the interesting hour, the renderer /// knows how to get there. /// /// `None` means the renderer chooses, which is usually the span's /// start. focus: Option, }, /// How much of a set is done. Meter(Meter), /// A run of magnitudes read against one axis. /// /// The axis and the bars are separate fields rather than one struct holding /// both, for [`Table`](Self::Table)'s reason: the bars are the run a request /// varies the length of and the axis is one fact beside it, and a compiled /// template holds those as a loop and a hole outside it. Folding the bars /// into [`Chart`] would put the loop inside a value. Chart { /// What the bars are read against. axis: Chart, /// The magnitudes, in the order they should read. bars: Vec, /// What a request decides about runs of [`bars`](Self::Chart::bars). marks: crate::stage::Marks, }, /// A value with a caption, several of them as one strip. /// /// Against `makeover-layout`'s [`layout::Figure`], which arrived at 0.11.0 /// for this. The dashboard shape: a large value over a small caption, /// several in a row. goingson had five of them across five screens with /// five class vocabularies for the one shape, and the port had been making /// each out of a [`Row`] with the caption as `primary` and the figure as /// `meta`, which reads backwards — a row's primary slot means the thing /// itself, and here the thing is the number. /// /// # Why the set is the node and not each figure /// /// Four tiles in a strip and four tiles down a column are different things, /// and a renderer handed one at a time cannot tell it is looking at a set. /// The objection to that is real and is answered by what is already here: a /// node whose value is its grouping sounds like a layout instruction, and /// [`Table`](Self::Table) has been exactly that since the beginning without /// anyone calling it one. /// /// # Why the action is here and not on the figure /// /// One of goingson's five is a control — sync's "Not Applied: 3" opens the /// list. `makeover-layout` cannot name an action at all, so the figure it /// describes carries none, and this pairs the description with the address /// the same way [`Row`] pairs its parts with [`Row::activate`]. Stats { /// The figures, in order, and what each one calls if it calls anything. figures: Vec<(Figure, Option)>, /// What a request decides about runs of /// [`figures`](Self::Stats::figures). marks: crate::stage::Marks, }, /// A region inside a region. Region(Slot), /// Markup this vocabulary did not write, and the scope that isolates it. /// /// The measured consumer is MNW's custom pages: a creator writes HTML and /// CSS, the server sanitises both and re-scopes every selector under a /// canvas element, and what comes out is a document whose middle is /// opaque. Nothing in a vocabulary of rows, cards and fields can say that, /// and no amount of growing it will -- the whole point of the feature is /// that the platform does not know what the creator drew. /// /// # This is the one member a description does not describe /// /// Everywhere else, a description says what a thing *is* and a renderer /// decides what it looks like. Here the app hands over markup and the /// renderer writes it out. That is the concession, it is deliberate, and /// the way to keep it from spreading is to remember what earned it: markup /// that is *data*, authored by somebody who is not the app and stored /// rather than written. A screen reaching for this to avoid describing its /// own layout is a screen that has not been described. /// /// # Sanitised by whoever produced it /// /// This crate does not parse the markup, does not sanitise it, and has no /// opinion about what is in it. It cannot: what is safe depends on the /// document's own headers, and MNW's answer is an allowlist pass plus a /// `default-src 'none'` CSP on a cookieless host. An app putting reader /// input in here is putting reader input in a browser. /// /// # Every host but a markup one draws nothing /// /// A terminal has no use for a string of HTML and will not grow one, so it /// draws nothing here rather than drawing the tags. That is the same read /// [`Binding::key`] gets from a host that has never heard of the key, and /// it is honest for this member in a way it would not be for most: these /// are public web pages and no terminal is going to serve one. /// /// [`Binding::key`]: crate::chrome::Binding::key Canvas(Box), } /// Markup an app stored rather than wrote, and how it is kept to itself. /// /// The body half of what a custom page needs; the stylesheet half is /// [`Document::style`], because a sheet is true of the document rather than of /// a place in it. /// /// # The scope is markup-shaped on purpose /// /// [`class`](Self::class) and [`id`](Self::id) are the two hooks a stylesheet /// can be confined to, and they are named here in the markup's own words for /// [`Document`]'s reason: this is where a host keeps what its own taxonomy /// says, rather than where the vocabulary grows a word for it. MNW's sanitiser /// rewrites every creator selector to sit under `.user-canvas#uc-{owner}`, so /// the element the renderer writes has to carry that class and that id or the /// sheet it was scoped for matches nothing. Which strings those are is the /// app's to know; that they are a class and an id is the sanitiser's contract, /// not this crate's invention. /// /// Both optional, and both empty is a canvas that isolates nothing -- markup /// dropped into the page with no sheet aimed at it, which is a real shape for /// an app whose stylesheet is its own. /// /// # An empty [`markup`](Self::markup) is the other half of the same idea /// /// The scope is here because a class or an id can be a *contract with a /// stylesheet the app did not write*, and creator markup is only one way to /// arrive at one. The other is a block the app draws itself and has published /// a name for: MNW's guide tells creators that the buy block is `.mnw-buy`, /// the file list `.mnw-files` and the item block `.mnw-item`, with worked CSS /// against them. Those names are as load-bearing as the canvas id and for the /// same reason -- somebody outside the app has already written selectors /// against them -- and a renderer that prefixed or renamed them would silently /// stop every creator page that styles one. /// /// So a canvas holding no markup and only [`within`](Self::within) is an /// ordinary shape: described content, under a name the app has promised. What /// the two uses share is the whole of what this member is, which is why they /// are one member and not two. What they do not share is risk: markup is /// opaque and dangerous, and a name is neither. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Canvas { /// The markup, as whoever produced it left it. pub markup: String, /// The classes on the element that scopes it, space-separated. pub class: Option, /// The id on the element that scopes it. pub id: Option, /// Nodes drawn inside the scope, after the markup. /// /// Inside rather than beside, because that is what the measured consumer /// does and the difference is visible: MNW's project pages put the buy /// block and the file list in the scoping element after the creator's /// markup, which is what lets a creator style the platform's own blocks to /// match the page they wrote. Nodes placed outside would be a different /// page, quietly. /// /// Ordinary nodes. Nothing is injected into the middle of the markup and /// nothing can be: a canvas is opaque, so the only place the app's own /// nodes can go is after it. pub within: Vec, } impl Canvas { /// Markup with no scope on it yet. #[must_use] pub fn new(markup: impl Into) -> Self { Self { markup: markup.into(), ..Self::default() } } /// Put this class on the element that scopes the markup. #[must_use] pub fn classed(mut self, class: impl Into) -> Self { self.class = Some(class.into()); self } /// Put this id on the element that scopes the markup. #[must_use] pub fn identified(mut self, id: impl Into) -> Self { self.id = Some(id.into()); self } /// Draw this node inside the scope, after the markup. #[must_use] pub fn with(mut self, node: Node) -> Self { self.within.push(node); self } } impl Node { /// What this node offers under a control name, if anything below it does. /// /// The described half of a [`Reveal`]: a region names a control by /// [`Field::name`], and this is how a renderer finds what that control was /// handed to the reader holding. /// /// The match is exhaustive rather than a wildcard over the containers, /// which is what keeps a member that grows a body from quietly hiding /// fields from every conditional region on the screen. `Node` is /// `#[non_exhaustive]` outside this crate and not inside it, so the /// compiler is the reviewer here. /// /// Every describable choice is a [`Field`] and therefore has a name, which /// is what makes a watched choice reachable at all. There is one way to /// describe a choice. #[must_use] pub fn holds(&self, name: &str) -> Option<&str> { /// What one field offers, if it is the field being asked about. fn offered<'a>(field: &'a Field, name: &str) -> Option<&'a str> { (field.name == name) .then_some(field.value.as_deref()) .flatten() } match self { Self::Field(field) => offered(field, name), Self::Form { fields, .. } => fields.iter().find_map(|field| offered(field, name)), Self::Region(slot) => slot.holds(name), // The markup is opaque and holds nothing this crate can find; what // is walked is the app's own nodes inside the scope. Self::Canvas(canvas) => canvas.within.iter().find_map(|node| node.holds(name)), Self::Table { rows, .. } => rows.iter().find_map(|row| { row.cells .iter() .find_map(|cell| cell.content.iter().find_map(|node| node.holds(name))) }), Self::Timeline { entries, .. } => entries.iter().find_map(|placed| { placed .row .cells .iter() .find_map(|cell| cell.content.iter().find_map(|n| n.holds(name))) }), // Everything with no field under it. Written out rather than left // to a wildcard, for the reason above. Self::Heading { .. } | Self::Text { .. } | Self::Rich { .. } | Self::Act(_) | Self::Link { .. } | Self::Figure(_) | Self::Since { .. } | Self::Until { .. } | Self::Age { .. } | Self::Image(_) | Self::Token(_) | Self::Notice { .. } | Self::StandIn { .. } | Self::Meter(_) | Self::Chart { .. } // Classified text and nothing else: no field, no act, no clock. | Self::Code { .. } | Self::Stats { .. } => None, } } /// Every question under this node, in draw order, appended to `found`. /// /// [`holds`](Self::holds)' walk asking for the boxes themselves rather /// than for one of their values, which is what a /// [`Slot::consults`] gathers. Exhaustive over the containers for /// `holds`' reason, and it appends rather than returning so a region's walk /// over a body of nodes allocates once. pub fn questions<'a>(&'a self, found: &mut Vec<&'a Field>) { match self { Self::Field(field) => found.push(field), Self::Canvas(canvas) => { for node in &canvas.within { node.questions(found); } } Self::Form { fields, .. } => found.extend(fields), Self::Region(slot) => found.extend(slot.questions()), Self::Table { rows, .. } => { for row in rows { for cell in &row.cells { for node in &cell.content { node.questions(found); } } } } Self::Timeline { entries, .. } => { for placed in entries { for cell in &placed.row.cells { for node in &cell.content { node.questions(found); } } } } // Everything with no question under it. Written out rather than // left to a wildcard, so a member that grows a body has to answer // here instead of quietly contributing nothing to every region // consult on the screen. Self::Heading { .. } | Self::Text { .. } | Self::Rich { .. } | Self::Act(_) | Self::Link { .. } | Self::Figure(_) | Self::Since { .. } | Self::Until { .. } | Self::Age { .. } | Self::Image(_) | Self::Token(_) | Self::Notice { .. } | Self::StandIn { .. } | Self::Meter(_) | Self::Chart { .. } | Self::Code { .. } | Self::Stats { .. } => {} } } /// Whether an [`Act`] under this node carries this [`Act::id`]. /// /// [`holds`](Self::holds)'s shape for a different question, and it walks the /// same containers for the same reason: a control is anywhere a run, a row, /// a cell or a nested region can put one. /// /// A row's [`menu`](Row::menu) is walked too. A menu act is a control the /// description carries and a renderer draws, so a screen that answers `true` /// for one is telling the truth; whether that host has drawn it yet is the /// host's business. #[must_use] pub fn names(&self, id: &str) -> bool { match self { Self::Act(act) => act.id.as_deref() == Some(id), Self::Region(slot) => slot.names(id), // A control inside the markup is the creator's and carries no id // this crate handed out, so only the app's own nodes are walked. Self::Canvas(canvas) => canvas.within.iter().any(|node| node.names(id)), Self::Table { rows, .. } => rows.iter().any(|row| row.names(id)), Self::Timeline { entries, .. } => entries.iter().any(|placed| placed.row.names(id)), // Everything with no control under it. Written out rather than left // to a wildcard, so a node kind that gains one stops compiling here. Self::Heading { .. } | Self::Text { .. } | Self::Rich { .. } | Self::Field(_) | Self::Form { .. } | Self::Link { .. } | Self::Figure(_) | Self::Since { .. } | Self::Until { .. } | Self::Age { .. } | Self::Image(_) | Self::Token(_) | Self::Notice { .. } | Self::StandIn { .. } | Self::Meter(_) | Self::Chart { .. } | Self::Code { .. } | Self::Stats { .. } => false, } } /// The value a control sends when what it sends is presence rather than a /// number: a ticked checkbox, and anything else spelling "this one". /// /// Named once here rather than agreed by convention between each renderer /// and each handler, which is how a value arrives under `tab` in one screen /// and `selected` in the next. It is HTML's own convention for a checkbox /// read back out, which is why the word is `value`. /// /// A [`Field`] carries its own name, so a choice needs no shared word to /// arrive under. pub const SELECTED: &'static str = "value"; /// The parameter name an [`Act::over`] sends each ticked value under. /// /// [`SELECTED`](Self::SELECTED)'s sibling, named here for the same reason: /// a convention agreed separately by each renderer and each handler is a /// convention that holds until one of them is written by someone else. /// /// Distinct from `SELECTED` rather than shared with it, because the two /// carry different counts. A control saying "this one" sends one value and /// a handler reads it with [`Params::get`]; a selection sends however many /// are ticked, including none, and a handler reads it with /// [`Params::get_all`]. One name for both would make "the one thing picked" /// and "the first of the things ticked" the same read. /// /// [`Params::get`]: crate::Params::get /// [`Params::get_all`]: crate::Params::get_all pub const TICKED: &'static str = "ticked"; /// The parameter name a row activation sends its [`Choosing`] under. /// /// [`SELECTED`](Self::SELECTED)'s and [`TICKED`](Self::TICKED)'s third /// sibling, named here for the reason both of those are: a convention /// agreed separately by each renderer and each handler holds until one of /// them is written by someone else. /// /// Sent with **every** activation of a row that can be chosen, including the /// ordinary one, so a handler reads one parameter rather than branching on /// whether a parameter arrived. A row that carries no /// [`Row::chosen`] fact is not part of a selection and /// sends nothing, so a handler that never asks is unaffected. pub const CHOOSING: &'static str = "choosing"; /// A page title. pub fn page(text: impl Into) -> Self { Self::Heading { level: layout::Heading::Page, text: text.into(), } } /// A section title. pub fn section(text: impl Into) -> Self { Self::Heading { level: layout::Heading::Section, text: text.into(), } } /// A subsection title. /// /// The third of the three heading levels, which had no constructor while /// the other two did. Every site that wanted one wrote the variant out /// with its `level` and its `text`, which is the struct rather than the /// vocabulary. pub fn subsection(text: impl Into) -> Self { Self::Heading { level: layout::Heading::Subsection, text: text.into(), } } /// Ordinary prose. pub fn text(text: impl Into) -> Self { Self::Text { text: text.into(), tone: layout::Tone::Neutral, } } /// The same prose, saying something about itself. /// /// [`text`](Self::text) is this at [`layout::Tone::Neutral`], which is /// ordinary content, and it was the only constructor `Text` had: a line /// that is a warning wrote the variant out by hand. **21 sites in the tree /// do**, nine of them in MNW's commit view alone, which makes this the most /// repeated instance of the gap `Node::literal` and `Node::token` closed /// before it. /// /// Not a notice. A notice is a thing that happened and carries a way out; /// this is a sentence in the reading that is warning-coloured, which is /// what a diff's deletion count and an unverifiable signature both are. pub fn toned(text: impl Into, tone: layout::Tone) -> Self { Self::Text { text: text.into(), tone, } } /// Machine text in a line of reading. /// /// One unclassified run, inline, no language: a ref path, a fingerprint, a /// clone URL, a line of a file. [`Code`](Self::Code) is the only node with /// no constructor of its own, and six sites in the tree write this exact /// literal out -- one of them under a private helper called `literal`, /// which is where the name comes from. /// /// Nothing lexed it and nothing should, so what this buys is the monospace /// and not a colour. A block, or runs a lexer classified, still writes the /// variant; the screen that asks for either earns the constructor for it. pub fn literal(text: impl Into) -> Self { Self::Code { runs: ::std::vec![Lexeme::plain(text)], language: None, inline: true, } } /// Machine text a lexer has been over, inline. /// /// The constructor [`literal`](Self::literal) reserved for the screen that /// asked, and MNW's source browser is it: a file is drawn one row per line, /// because `#L42` is the address of a line and a block that owned its own /// lines would have nothing to hang one on. So every line is classified /// runs plus the extension, and `literal`'s one-plain-run shape cannot say /// it. /// /// Still inline. A block is the other half and is still unasked for. #[must_use] pub fn code(runs: Vec, language: Option) -> Self { Self::Code { runs, language, inline: true, } } /// Prose written in markdown, by somebody the app does not vouch for. /// /// [`Richness::Sentence`] and [`Trust::Untrusted`], which is what this member /// has always meant and what every call site written before the two axes /// existed still gets. Say otherwise with [`trust`](Self::trust) and /// [`richness`](Self::richness). pub fn rich(source: impl Into) -> Self { Self::Rich { source: source.into(), richness: Richness::Sentence, trust: Trust::Untrusted, } } /// Who wrote this markdown. See [`Trust`]. /// /// Does nothing to a node that is not [`Rich`](Self::Rich), rather than /// refusing: the same rule `Screen::replace` follows for a region it cannot /// find, and for the same reason -- a miss is worth seeing and is not worth /// refusing to draw a screen over. #[must_use] pub fn trust(mut self, trust: Trust) -> Self { if let Self::Rich { trust: current, .. } = &mut self { *current = trust; } self } /// How much of the format this markdown may use. See [`Richness`]. /// /// [`trust`](Self::trust)'s note about a node that is not `Rich` applies /// here too. #[must_use] pub fn richness(mut self, richness: Richness) -> Self { if let Self::Rich { richness: current, .. } = &mut self { *current = richness; } self } /// A control calling a route. pub fn act(label: impl Into, action: Action) -> Self { Self::Act(Act::new(label, action)) } /// A persistent message, dismissed by fixing what caused it. pub fn banner(tone: layout::Tone, text: impl Into) -> Self { Self::Notice { kind: layout::Notice::Banner, tone, text: text.into(), act: None, } } /// A transient message that dismisses itself. pub fn toast(tone: layout::Tone, text: impl Into) -> Self { Self::Notice { kind: layout::Notice::Toast, tone, text: text.into(), act: None, } } /// The same notice, with one thing to do about it. /// /// [`offering`](Self::offering)'s shape, and a no-op on anything else for /// the same reason [`and_more`](Self::and_more) is one. Named apart from /// `offering` because a stand-in's act is a way *out* of an empty screen and /// a notice's is a way *back* from what just happened. #[must_use] pub fn about(mut self, offer: Act) -> Self { if let Self::Notice { act, .. } = &mut self { *act = Some(offer); } self } /// A list of rows. /// /// A [`Table`](Self::Table) that declares no columns, which is what a list /// is since the 2026-09-06 collapse. The constructor stays because "a list /// of rows" is what the caller means and `Table { columns: vec![], .. }` is /// how the vocabulary spells it, not something every caller should have to /// spell. pub fn list(rows: impl IntoIterator) -> Self { Self::Table { marks: crate::stage::Marks::none(), columns: Vec::new(), rows: rows.into_iter().collect(), more: None, } } /// The same list, saying there is more of it. /// /// A no-op on anything that is not a [`Self::Table`], which is the one place /// this file allows that: the alternative is a constructor taking rows and a /// `Rest` together, and every call site that has no more rows then passes a /// `None` to say so. #[must_use] pub fn and_more(mut self, rest: Rest) -> Self { if let Self::Table { more, .. } = &mut self { *more = Some(rest); } self } /// A proportion of a set, untoned and unlabelled. #[must_use] pub const fn meter(done: u32, total: u32) -> Self { Self::Meter(Meter::new(done, total)) } /// Nothing here yet. pub fn empty(message: impl Into) -> Self { Self::StandIn { marks: crate::stage::Marks::none(), state: layout::Readiness::Empty, message: message.into(), act: None, } } /// This did not load. pub fn failed(message: impl Into) -> Self { Self::StandIn { marks: crate::stage::Marks::none(), state: layout::Readiness::Failed, message: message.into(), act: None, } } /// This is on its way. /// /// The third of the three drawn states, and the one nothing built until /// [`Outcome::Started`](crate::Outcome::Started) needed it. A host that /// retains the description says a region is waiting on /// [`Slot::readiness`] and needs no node; a host that swaps markup has /// nowhere to put an attribute and needs one, and this is what it puts /// there. /// /// [`Slot::readiness`] stays the axis either way. This is the sentence /// beside it, not a second way of saying the same thing: a region can be /// [`Pending`](layout::Readiness::Pending) with no words at all, which is /// every deferred load on every screen. pub fn pending(message: impl Into) -> Self { Self::StandIn { marks: crate::stage::Marks::none(), state: layout::Readiness::Pending, message: message.into(), act: None, } } /// The same stand-in, with a way out of it. /// /// A no-op on anything else, for the reason [`Self::and_more`] is one. #[must_use] pub fn offering(mut self, way_out: Act) -> Self { if let Self::StandIn { act, .. } = &mut self { *act = Some(way_out); } self } /// One control on its own, outside any form. pub fn field(field: Field) -> Self { Self::Field(Box::new(field)) } /// A strip of figures, none of which answers a click. pub fn stats(figures: impl IntoIterator) -> Self { Self::Stats { marks: crate::stage::Marks::none(), figures: figures.into_iter().map(|figure| (figure, None)).collect(), } } /// One more figure, beside [`stats`](Self::stats). /// /// `stats` takes the whole list, and every other container in this /// vocabulary accretes: `Table::column`, `Row::cell` and `Field::options` /// were all added for that reason and this is the fourth. A caller building /// figures one at a time, or conditionally, has nowhere to hold the list. /// /// Does nothing to a node that is not [`Stats`](Self::Stats), on /// [`trust`](Self::trust)'s rule. #[must_use] pub fn figure(mut self, figure: Figure) -> Self { if let Self::Stats { figures, .. } = &mut self { figures.push((figure, None)); } self } /// A chart's axis, with no bars on it yet. /// /// The bars accrete for [`figure`](Self::figure)'s reason: a caller building /// them one at a time, or conditionally, has nowhere to hold the list. MNW's /// revenue chart is the site. #[must_use] pub const fn chart(axis: Chart) -> Self { Self::Chart { marks: crate::stage::Marks::none(), axis, bars: Vec::new(), } } /// One more magnitude on the axis, beside [`chart`](Self::chart). /// /// Does nothing to a node that is not a [`Chart`](Self::Chart), on /// [`figure`](Self::figure)'s rule. #[must_use] pub fn bar(mut self, bar: Bar) -> Self { if let Self::Chart { bars, .. } = &mut self { bars.push(bar); } self } /// An axis with nothing on it yet. /// /// [`Timeline`](Self::Timeline) was the last variant with no constructor of /// its own, and its entries accrete for [`figure`](Self::figure)'s reason: /// a caller building them one at a time, or conditionally, has nowhere to /// hold the list. goingson's day view is the site. #[must_use] pub const fn timeline(track: layout::Track) -> Self { Self::Timeline { marks: crate::stage::Marks::none(), track, entries: Vec::new(), focus: None, } } /// One more thing on the axis, beside [`timeline`](Self::timeline). /// /// Does nothing to a node that is not a [`Timeline`](Self::Timeline), on /// [`figure`](Self::figure)'s rule. #[must_use] pub fn placed(mut self, entry: Placed) -> Self { if let Self::Timeline { entries, .. } = &mut self { entries.push(entry); } self } /// The moment the axis should bring into view, in minutes from its start. /// /// Does nothing to a node that is not a [`Timeline`](Self::Timeline), on /// [`figure`](Self::figure)'s rule. #[must_use] pub fn focus(mut self, at: u16) -> Self { if let Self::Timeline { focus, .. } = &mut self { *focus = Some(at); } self } /// A tag standing on its own, rather than inside a row or a cell. /// /// [`Token`](Self::Token) was the last node variant with no constructor of /// its own once [`literal`](Self::literal) landed. A chip that is a control /// carries its action and its latched state on the tag. #[must_use] pub fn token(tag: Tag) -> Self { Self::Token(tag) } /// Time counting up from an instant. #[must_use] pub const fn since(at: std::time::SystemTime) -> Self { Self::Since { at } } /// Time counting down to an instant. #[must_use] pub const fn until(at: std::time::SystemTime) -> Self { Self::Until { at } } /// How long ago an instant was. #[must_use] pub const fn age(at: std::time::SystemTime) -> Self { Self::Age { at } } /// Which way this node runs against the current time, and from when. /// /// `None` for everything that is not a time-derived readout, which is most /// of the vocabulary. A renderer reads this to format one; a host reads /// [`Screen::clocks`] to decide how often to draw again. #[must_use] pub const fn clock(&self) -> Option<(Clock, std::time::SystemTime)> { match self { Self::Since { at } => Some((Clock::Since, *at)), Self::Until { at } => Some((Clock::Until, *at)), Self::Age { at } => Some((Clock::Age, *at)), _ => None, } } /// Every kind of time-derived readout at or under this node, added to /// `found`. /// /// Walks into the containers rather than stopping at the top, because the /// measured case is a readout in a row: goingson puts an elapsed time on /// task rows, and a walk that only looked at the region's own blocks would /// answer that a screen full of running timers needs no clock. fn clocks_into(&self, found: &mut BTreeSet) { if let Some((clock, _)) = self.clock() { found.insert(clock); return; } match self { Self::Timeline { entries, .. } => { for placed in entries { placed.row.clocks_into(found); } } Self::Table { rows, .. } => { for row in rows { row.clocks_into(found); } } Self::StandIn { .. } => {} Self::Region(slot) => { for ranked in slot.body.iter() { ranked.node.clocks_into(found); } } // Only the app's own nodes: a readout the creator wrote is text in // a string, and no script this renderer loads is going to tick it. Self::Canvas(canvas) => { for node in &canvas.within { node.clocks_into(found); } } // The leaves, and the containers that hold no node. A form holds // questions, a strip holds figures, a control holds choices, and // none of those is a place a readout can be. Self::Heading { .. } | Self::Text { .. } | Self::Rich { .. } | Self::Act(_) | Self::Link { .. } | Self::Figure(_) | Self::Image(_) | Self::Token(_) | Self::Notice { .. } | Self::Field(_) | Self::Form { .. } | Self::Meter(_) | Self::Chart { .. } | Self::Code { .. } | Self::Stats { .. } | Self::Since { .. } | Self::Until { .. } | Self::Age { .. } => {} } } } /// A whole screen. /// /// [`Arrangement`](layout::Arrangement) is `makeover-layout`'s, and there are /// two of them because our apps have two: goingson is list-detail, Balanced /// Breakfast is sidebar plus content. Naming a third before an app has one is /// how a description becomes a framework. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Screen { /// What the screen is called. A window title, a tab title, a page heading. pub title: String, /// How the regions are laid out. pub arrangement: layout::Arrangement, /// The regions, in order. pub slots: Vec, /// Messages raised by whatever produced this screen. /// /// Separate from the slots because a notice belongs to the screen rather /// than to a place in it: which region a toast stacks in is the renderer's /// question, and a handler answering it would be describing a webview. pub notices: Vec, /// How this screen is found, shared and indexed. /// /// Not an `Option`. The default is meaningful — a screen nobody said /// anything about is an indexable website — and an `Option` would make /// "nobody said" and "indexable" two spellings of one thing. pub discovery: Discovery, /// Which of the app's places this screen is, if it is one of them. /// /// A [`Chrome`](crate::Chrome) is built once and held beside the router, /// so a `current` flag on a [`Place`](crate::chrome::Place) would be /// frozen at build time and could never point at where the user is. The /// nav says what the places are; this says which one is showing, and the /// renderer marks the place whose [`key`](crate::chrome::Place::key) /// matches. /// /// The same move [`Row::current`] makes one level down, and for the same /// reason: it is the app's own pointer at what is showing, said by the /// thing that knows. /// /// `None` for a screen that is not a place in the nav. A confirmation /// drawn over one, a detail reached from a row, an app with no nav at all: /// nothing is marked, rather than the last place staying lit. /// /// A key no [`Place`](crate::chrome::Place) carries marks nothing. Not an /// error, because the nav is the app's and so is this, and a renderer is /// the wrong place to discover that an app disagrees with itself. pub place: Option, /// The name of the set this screen's ticks go into, if it holds one. /// /// [`Row::selected`] said a row could be ticked and nothing said what the /// tick was *for*, so the tick had nowhere to go: a webview hid the hole /// because the browser owns a checkbox's checked state, and every app then /// wrote its own JS to gather the boxes back up. A terminal could not hide /// it. It drew the `[ ]`, bound the key, and the key did nothing, which is /// worse than not drawing the box. /// /// So the screen names the set, each [`Row::value`] is what that row's tick /// contributes, and [`Act::over`] is how a control says it acts on the /// whole of it. The renderer holds the set the way `quasi-tui` already /// holds an edit buffer and a scroll offset, and the commit control reads /// it by name. /// /// # Ticking never writes /// /// Wiki `explicit-commit-affordance`, the general rule: a change that /// happens with no obvious indication is confusing, so a tick stages and /// the commit control is what locks it in. [`Row::toggle`] describes the /// other thing — screens where the tick *is* the write — and is left alone /// here rather than removed, because stopping those screens is work in the /// apps that have them. /// /// # One set per screen /// /// A screen with two independent sets has not been measured. Naming one is /// the smallest thing that closes the hole, and the field grows to a map /// when an app turns up wanting two, on the same rule every other member /// here arrived under. /// /// [`Row::selected`]: Row::selected /// [`Row::value`]: Row::value /// [`Act::over`]: Act::over pub selection: Option, /// Which of this screen's questions the caret starts in, by /// [`Field::name`]. /// /// A statement about the screen rather than a flag on a field, and it is /// the same move [`place`](Self::place) and [`selection`](Self::selection) /// already make: the screen names the one thing that is true of it once, /// and a fact that cannot be said twice beats a field-local flag that can. /// Two fields each claiming the caret is a description arguing with itself, /// and there would be no honest way for a renderer to settle it. /// /// `None` for nearly every screen, which is every screen the reader arrives /// at to read. It is the sessionless form -- a login, a password reset -- /// where the only thing to do is type, and the caret starting anywhere else /// is a keystroke the reader has to spend before they can begin. /// /// A name no [`Field`] on the screen carries marks nothing. Not an error, /// for [`place`](Self::place)'s reason: the screen is the app's and so is /// the name, and a renderer is the wrong place to discover that an app /// disagrees with itself. /// /// # This is the initial caret, not [`Choosing::Through`]'s refusal /// /// The two get conflated because both are about focus, and they are about /// different focus. [`Choosing::Through`] declines to carry the *live* /// focus -- where the app is pointing right now, as a range is dragged -- /// on the grounds that a renderer naming it would be answering with the row /// it drew a frame ago while the app holds the moving one. This is the /// opposite fact: where the caret is before the reader has done anything, /// which nothing else knows and only the description can say. /// /// Nothing here moves the caret afterwards. A screen cannot pull focus back /// on a redraw, and a renderer that read this on every frame would take the /// caret away from wherever the reader had walked it to. /// /// # A renderer honours it once, on arrival /// /// `quasi-webview` emits `autofocus`, and only in a whole document: most of /// what a browser is answered with is a fragment, and moving the caret on a /// swap takes it out of whatever the reader was typing into. `quasi-tui` /// puts its caret on the matching stop when the screen arrives, and /// `quasi-immediate` asks egui for focus on the first frame of one. /// /// [`Field::name`]: Field::name /// [`Choosing::Through`]: Choosing::Through pub opens_at: Option, /// How wide this screen's content runs. /// /// Measured in the MNW server, where 69 of 72 templates carry one of three /// mutually exclusive CSS classes for it and nothing described it, so the /// choice lived in the template rather than in the screen. /// /// Beside [`arrangement`](Self::arrangement) and answering the level above /// it: that one divides the screen's width between regions, this says how /// much of the window the screen takes in the first place. Both are the /// description's, which is what answering `e0fd485e` and `0eccff0d` /// together settled. /// /// Not an `Option`, for [`discovery`](Self::discovery)'s reason. The /// default is meaningful -- a screen nobody said anything about uses the /// window it was given -- and an `Option` would make "nobody said" and /// "the whole width" two spellings of one thing. pub measure: layout::Measure, /// What this screen is about the document it is drawn into. /// /// Not an `Option`, for [`discovery`](Self::discovery)'s reason: an empty /// [`Document`] is what nearly every screen says, and it is meaningful. pub document: Document, } /// What a screen is about the document it is drawn into. /// /// Every /// [`Response`](crate::Response) variant changes what is inside ``, and /// nothing changed anything outside it: a webview host builds its shell once /// and hands it to the renderer as an `Arc` before app state exists, so the /// head and the `` tag were fixed for the process. A screen that needed /// its own could only be served by a route family of its own, which is how /// MNW's embeds ended up with a `document()` function outside the adapter. /// /// Chosen against `Response::Reload`, which was the other option. That is a /// host instruction rather than a description of a screen, and it is the line /// the vocabulary has held throughout; it also served only one of the two /// consumers. /// /// # What belongs here, and what does not /// /// The facts that are true of the whole document and that no region can carry. /// Both members are markup-shaped and that is deliberate: this is where a host /// keeps what its own taxonomy says, not where the vocabulary grows a word for /// it. A layout width is [`Screen::measure`] and belongs there; MNW's /// `admin-page` grouping and its 24 one-off screen-identity tokens are the /// app's own and belong here, which `16ba941e` settled by closing with no /// quasi crate gaining a field for them. /// /// # Every host but the webview ignores it /// /// A terminal has no document and neither does an egui frame, so both drop it /// whole -- the rule [`Binding::key`] already states for a key one host has /// never heard of. That is why this is here rather than in `makeover-layout`: /// it is not presentation, it is what the one host with a document is told /// about it. #[derive(Debug, Clone, Default, PartialEq, Eq)] pub struct Document { /// Classes for ``, space-separated, beside whatever the host's own /// shell puts there. /// /// Beside and not instead: a host's shell carries the part that is true of /// every page and this carries the part that is true of this one, and a /// screen that replaced the global half would be a screen that had to know /// it. pub body_class: Option, /// Attributes for the root element, by name. /// /// goingson's pinned theme is the measured consumer: once every theme ships /// in one sheet keyed by a root attribute, changing theme is setting an /// attribute rather than swapping a ``, and a screen answering a /// preference change is the thing that knows the new value. Until this /// existed the honest answer was to reload the app. /// /// A name a renderer will not write is dropped rather than refused, for /// [`Screen::replace`]'s reason: a miss is worth being able to see and is /// not worth refusing to draw a screen over. What counts as writable is /// each renderer's, since it is the one that knows what its document can /// hold -- see [`writable_root_attr`]. pub root: Vec<(String, String)>, /// A stylesheet this one document gets, on top of the host's own. /// /// For a document whose styling is data rather than build output: MNW's /// custom pages serve creator-authored CSS, sanitised and re-scoped per /// request, and a markup renderer's shell is built once before app state /// exists, so there was nowhere for it to go. The measured consumer is the /// whole of the reason this is here. /// /// # Opaque, and sanitised by whoever produced it /// /// This crate does not read the CSS, does not scope it, and does not /// sanitise it. It cannot: what counts as safe depends on the document's /// own headers, and MNW's answer is a `lightningcss` pass that rewrites /// every selector under a canvas id plus a `default-src 'none'` CSP. An /// app handing raw reader input here is handing raw reader input to a /// browser, and nothing below this line will save it. /// /// What a renderer does owe is that the string cannot end the element it is /// written into, which is a markup concern rather than a CSS one and is /// therefore the renderer's -- see `quasi-webview`'s `push_style`. /// /// # Last, so it wins /// /// A per-document sheet exists to override what every document shares, so a /// markup renderer writes it after the shell's own and outside the cascade /// layers the shell declares. Unlayered rules beat layered ones, which is /// what makes "on top of" true without this having to name a layer. /// /// # Every host but the webview ignores it /// /// A terminal has no stylesheet to add one to and draws the screen the way /// it draws every other, the same read [`Binding::key`] gets from a host /// that has never heard of the key. /// /// [`Binding::key`]: crate::chrome::Binding::key pub style: Option, } impl Document { /// Give this document a stylesheet of its own, on top of the host's. /// /// Replaces rather than adds, unlike [`rooted`](Self::rooted): a document /// has one sheet of its own, and two callers each setting one would be two /// answers to what this document looks like rather than two facts about it. #[must_use] pub fn styled(mut self, css: impl Into) -> Self { self.style = Some(css.into()); self } /// Put these classes on ``, beside the host shell's own. #[must_use] pub fn classed(mut self, class: impl Into) -> Self { self.body_class = Some(class.into()); self } /// Set this attribute on the root element. /// /// Adds rather than replaces, for [`Field::consults`]' reason: a document /// saying two things about its root is saying two things, and a builder /// that took the last call would make the pair unwritable. A name given /// twice is a description arguing with itself, and the renderer takes the /// first. #[must_use] pub fn rooted(mut self, name: impl Into, value: impl Into) -> Self { self.root.push((name.into(), value.into())); self } } /// Whether a renderer with a markup document will write this root attribute. /// /// A description names the attribute and a host writes it, so the name reaches /// markup as a name rather than as a value and cannot be escaped the way a /// value is. `data-theme` is the shape the measured consumer wants; `x /// onload=alert(1)` is the shape this refuses. /// /// ASCII letters, digits and `-`, starting with a letter. Deliberately narrower /// than what HTML permits: every attribute anybody has wanted here fits, and /// the cost of being wrong is a script tag rather than a missing class. /// /// Here rather than in the webview because the terminal and egui hosts answer /// the same question if they ever grow a document, and a second copy is a /// second answer. #[must_use] pub fn writable_root_attr(name: &str) -> bool { let mut characters = name.chars(); characters.next().is_some_and(|c| c.is_ascii_alphabetic()) && characters.all(|c| c.is_ascii_alphanumeric() || c == '-') } /// How a screen is found, shared and indexed. /// /// Not presentation, which is why it is here and not in `makeover-layout`: a /// terminal ignores every field, the same way it ignores [`Slot::id`]. It is an /// address-and-identity fact, and that is the line that put [`Action`] in this /// crate rather than in the vocabulary. /// /// Measured before it was added. Every `og:*` value in the MNW server's 37 /// templates is one of four things interpolated from the entity the screen is /// about: a title, a summary sentence, an image URL, or the screen's own /// address. None of them needed knowledge only a handler has, which is what /// made this the screen's to say rather than the host's. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Discovery { /// Whether a crawler should index this screen. /// /// Defaults to indexable, because most screens are and a default that hides /// pages is a default that hides the bug. The six screens saying otherwise /// are purchased-content pages, and this field is why that is a fact the /// type carries rather than a line in a template that a conversion can drop /// in silence. pub indexable: bool, /// The sentence a link preview shows. [`Screen::title`] is the title. pub summary: Option, /// The image a link preview shows, as an absolute URL. pub image: Option, /// What kind of thing this screen is about. pub kind: SocialKind, /// The canonical address, when the screen answers at more than one. pub canonical: Option, /// The syndication feed this screen offers, if it offers one. /// /// Singular, because no measured screen offers two: MNW's project blog, a /// user's page and a project page each publish exactly one. It widens to a /// `Vec` on the day a screen actually offers a second, which is the rule /// every other member here arrived under. /// /// Typed rather than a MIME string. `application/rss+xml` written out at /// each of the three sites is three chances to write `application/rss` and /// have a reader skip it, and the spelling is the same on every host, so it /// belongs to [`FeedKind::media_type`] rather than to whoever is describing /// the screen. /// /// # What a renderer with no autodiscovery does /// /// Ignores it, the way it already ignores [`image`](Self::image). A feed is /// a fact a browser acts on -- it is what `` says -- /// and a terminal has nothing to hand it to. Said here rather than left to /// each renderer's author to guess, because a guess is how two hosts end up /// disagreeing about what a description means. pub feed: Option, } /// A syndication feed a screen offers. /// /// Three members and no more: what kind of document it is, what it is called, /// and where it is. That is the whole of what a browser's autodiscovery reads, /// and anything else here would be describing the feed's contents rather than /// its existence. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Feed { /// What kind of feed document it is. pub kind: FeedKind, /// What it is called, which is what a reader's subscribe list shows. /// /// Not [`Screen::title`], and not derived from it. A page titled "Max /// Johnson" offers a feed called "Max Johnson's posts", and a subscribe /// list holding a dozen entries called after their pages is a list nobody /// can read. pub title: String, /// Where the feed document is. pub href: String, } impl Feed { /// A feed, by kind, name and address. pub fn new(kind: FeedKind, title: impl Into, href: impl Into) -> Self { Self { kind, title: title.into(), href: href.into(), } } } /// What kind of syndication document a feed is. /// /// The three formats a browser and every reader understand. `#[non_exhaustive]` /// for [`SocialKind`]'s reason: a fourth arriving should not be a lockstep /// event across every renderer that spells one. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[non_exhaustive] pub enum FeedKind { /// RSS 2.0. The default, and what all three measured screens publish. #[default] Rss, /// Atom. Atom, /// JSON Feed. JsonFeed, } impl FeedKind { /// What this is spelled as in a `type` attribute. /// /// Named here rather than agreed between each renderer and each host, for /// [`SocialKind::as_str`]'s reason: that is how one screen ends up /// `application/rss+xml` and the next `application/rss`. #[must_use] pub const fn media_type(self) -> &'static str { match self { Self::Rss => "application/rss+xml", Self::Atom => "application/atom+xml", Self::JsonFeed => "application/feed+json", } } } impl Default for Discovery { /// Indexable, and nothing else claimed. /// /// Written out rather than derived, and the reason is the one field that /// matters: `bool::default()` is `false`, so a derived impl would deindex /// every screen that never mentioned the subject, silently, and the failure /// would show up as traffic rather than as a test. fn default() -> Self { Self { indexable: true, summary: None, image: None, kind: SocialKind::Website, canonical: None, feed: None, } } } /// What kind of thing a screen is about. /// /// The six the server actually emits, and no more. Naming a seventh before a /// screen has one is how a description becomes a framework, which is the /// argument [`Arrangement`](layout::Arrangement) is held to two screens by. /// /// `#[non_exhaustive]`, because a seventh arriving should not be a lockstep /// event across every renderer that spells one. The match below stays /// exhaustive: within this crate the attribute does not apply, and a wildcard /// here would only hide a member added without a spelling. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[non_exhaustive] pub enum SocialKind { /// A page. The default, and four of the server's screens. #[default] Website, /// Something written, with an author and a date. Article, /// A person or an account. Profile, /// Something for sale. Product, /// A video. Video, /// A piece of music. Song, } impl SocialKind { /// What this is spelled as in `og:type`. /// /// Named here rather than agreed between each renderer and each host, which /// is how one screen ends up `video.other` and the next `video`. #[must_use] pub const fn as_str(self) -> &'static str { match self { Self::Website => "website", Self::Article => "article", Self::Profile => "profile", Self::Product => "product", Self::Video => "video.other", Self::Song => "music.song", } } } impl Screen { /// An empty screen with the given arrangement. pub fn new(title: impl Into, arrangement: layout::Arrangement) -> Self { Self { title: title.into(), arrangement, slots: Vec::new(), notices: Vec::new(), discovery: Discovery::default(), place: None, selection: None, opens_at: None, measure: layout::Measure::default(), document: Document::default(), } } /// Say what this screen is about the document it is drawn into. /// /// See [`document`](Self::document). Replaces rather than adds, because a /// [`Document`] is one statement built with its own builders: /// `screen.documented(Document::default().classed("admin-page"))`. #[must_use] pub fn documented(mut self, document: Document) -> Self { self.document = document; self } /// Say which of the app's places this screen is. /// /// See [`place`](Self::place). The key is a /// [`Place::key`](crate::chrome::Place::key), not a label and not an /// address. #[must_use] pub fn at_place(mut self, key: impl Into) -> Self { self.place = Some(key.into()); self } /// How wide this screen's content runs, chaining. /// /// See [`measure`](Self::measure). [`Measure::Wide`](layout::Measure::Wide) /// is the default and does not need saying. #[must_use] pub const fn measured(mut self, measure: layout::Measure) -> Self { self.measure = measure; self } /// This screen holds a set of ticks under this name, chaining. /// /// The rows that join it say so with [`Row::ticking`], and the control that /// acts on it with [`Act::over`]. See [`selection`](Self::selection). #[must_use] pub fn selecting(mut self, name: impl Into) -> Self { self.selection = Some(name.into()); self } /// The caret starts in this question, chaining. /// /// The name is a [`Field::name`], which is what the value is submitted /// under, and not the label. See [`opens_at`](Self::opens_at). #[must_use] pub fn opening_at(mut self, name: impl Into) -> Self { self.opens_at = Some(name.into()); self } /// Whether a crawler should index this screen, chaining. #[must_use] pub fn indexed(mut self, indexable: bool) -> Self { self.discovery.indexable = indexable; self } /// The sentence a link preview shows, chaining. #[must_use] pub fn summarised(mut self, text: impl Into) -> Self { self.discovery.summary = Some(text.into()); self } /// The image a link preview shows, chaining. An absolute URL. #[must_use] pub fn illustrated(mut self, url: impl Into) -> Self { self.discovery.image = Some(url.into()); self } /// What kind of thing this screen is about, chaining. #[must_use] pub fn about(mut self, kind: SocialKind) -> Self { self.discovery.kind = kind; self } /// The address this screen should be known by, chaining. #[must_use] pub fn canonical_at(mut self, url: impl Into) -> Self { self.discovery.canonical = Some(url.into()); self } /// The feed this screen offers, chaining. /// /// See [`Discovery::feed`]. `screen.syndicating(Feed::new(FeedKind::Rss, /// "Project updates", "/p/thing/feed.xml"))`. #[must_use] pub fn syndicating(mut self, feed: Feed) -> Self { self.discovery.feed = Some(feed); self } /// A list that chooses what the detail beside it shows. pub fn list_detail(title: impl Into, tabbed: bool) -> Self { Self::new(title, layout::Arrangement::list_detail(tabbed)) } /// Navigation down the side, content filling the rest. pub fn sidebar_content(title: impl Into) -> Self { Self::new(title, layout::Arrangement::sidebar_content()) } /// One region, filling the document. /// /// What a screen with nothing beside anything says. pub fn single(title: impl Into) -> Self { Self::new(title, layout::Arrangement::Single) } /// Add a region, chaining. #[must_use] pub fn with(mut self, slot: Slot) -> Self { self.slots.push(slot); self } /// Raise a message on this screen, chaining. /// /// # Panics /// /// If the node is not a [`Node::Notice`]. The field is typed as a [`Node`] /// so a renderer walks one kind of thing, and this is the constructor that /// keeps that from meaning anything can go in it. #[must_use] pub fn saying(mut self, notice: Node) -> Self { assert!( matches!(notice, Node::Notice { .. }), "Screen::saying takes a Node::Notice" ); self.notices.push(notice); self } /// What this screen offers under a control name. /// /// The described half of a [`Reveal`], asked of the whole screen. See /// [`Slot::holds`], and [`Node::holds`] for what the walk reaches. #[must_use] pub fn holds(&self, name: &str) -> Option<&str> { self.slots.iter().find_map(|slot| slot.holds(name)) } /// The slot under this address, at any depth. #[must_use] pub fn slot(&self, id: &str) -> Option<&Slot> { self.slots.iter().find_map(|slot| slot.find(id)) } /// Whether any row on this screen is part of a live selection. /// /// [`Row::chosen`] is a fact on a row rather than on the screen -- /// unlike [`selection`](Self::selection), which names the staged set -- so /// the answer is a walk. Every region, nested ones included, for /// [`slot`](Self::slot)'s reason: a table inside a pane inside the shell is /// where every real app puts its list. /// /// Walked rather than cached. A screen is built fresh per answer, and a /// cached fact about its rows is one more thing that can disagree with them. #[must_use] pub fn chooses(&self) -> bool { self.slots.iter().any(Slot::chooses) } /// Whether this screen carries the thing an anchor names. /// /// One answer for all three renderers rather than three walks that can /// disagree, which is the same argument [`replace`](Self::replace) makes: /// deciding what a description means is this crate's, and a host writing /// it means every host picks its own answer. /// /// A renderer asks this to know whether it can draw an /// [`Outcome::Anchored`](crate::Outcome::Anchored) *at* something or has to /// fall back to drawing it over everything. `false` is a description bug -- /// a route answered with an anchor naming what is not on the screen -- and /// every renderer degrades rather than refusing, the way a missing region /// does. #[must_use] pub fn anchors(&self, anchor: &crate::Anchor) -> bool { match anchor { crate::Anchor::Region(id) => self.slot(id).is_some(), // Set at all is the whole of it, matching how `Act::over` reads its // own name. See `Anchor::Selection`. // // Either kind of selection, since `1894e95d`: a screen naming a // staged set has one, and so does a screen whose rows carry a live // one. The second is what audiofiles' file list has, and reading // only the first is what made this answer `false` there while the // reader was looking at eleven chosen rows. crate::Anchor::Selection => self.selection.is_some() || self.chooses(), crate::Anchor::Control(id) => { self.notices.iter().any(|notice| notice.names(id)) || self.slots.iter().any(|slot| slot.names(id)) } } } /// Every call this screen's regions are waiting on, in draw order. /// /// What a host performs after putting a screen up: each answer comes back as /// a [`Response::Fragment`] naming the region, and [`replace`](Self::replace) /// clears the feed as it lands, so asking again after applying one is /// answered with what is still outstanding rather than with the same list. /// /// Empty for every screen that has all of its content, which is nearly all /// of them. #[must_use] pub fn feeds(&self) -> Vec<&Action> { let mut out = Vec::new(); for slot in &self.slots { slot.feeds_into(&mut out); } out } /// Every region on this screen that asks a question of its own, at any /// depth, in draw order. /// /// What a renderer walks when a question moves: the regions holding that /// question are the ones whose [`Slot::consults`] the keystroke set off, /// and [`Slot::questions`] says which those are. /// /// Empty for nearly every screen, which is every screen written before /// [`Slot::consults`] existed, so the ordinary keystroke pays a walk and no /// more. #[must_use] pub fn consulting(&self) -> Vec<&Slot> { let mut out = Vec::new(); for slot in &self.slots { slot.consulting_into(&mut out); } out } /// Every call this screen's regions re-ask on a cadence, in draw order. /// /// [`feeds`](Self::feeds)'s counterpart, and the two never return the same /// call: a feed arrives once and a refresh never stops. What a host does /// with these is ask again on whatever interval its renderer picked, and /// keep doing it for as long as the screen is up. /// /// Empty for every screen with no [`Slot::live`] region, which is nearly /// all of them. A live region that names no call is not here either — there /// is nothing to ask — and the host re-reads it by redrawing. #[must_use] pub fn refreshes(&self) -> Vec<&Action> { let mut out = Vec::new(); for slot in &self.slots { slot.refreshes_into(&mut out); } out } /// Whether anything on this screen changes without the user. /// /// What a retained host asks to decide whether to keep drawing. True for a /// live region whether or not it names a call, which is the difference from /// [`refreshes`](Self::refreshes): the audiofiles sync panel reads state the /// host already holds, so its cadence is a repaint and there is no request /// to make. #[must_use] pub fn is_live(&self) -> bool { self.slots.iter().any(Slot::live_within) } /// Every kind of time-derived readout on this screen, at any depth. /// /// What a renderer reads to pick a cadence: the description says a readout /// is derived from now and which way it runs, and how often to redraw /// follows the granularity the renderer chose to show it at. A screen of /// running stopwatches wants a second and a screen of last-modified stamps /// does not, and this is the difference said in the one place a host can /// act on it. /// /// A set rather than a count, because the cadence question is per kind: /// every [`Clock::Since`] on the screen redraws together, which is /// many-readouts-one-tick falling out of the vocabulary rather than being /// arranged by hand. /// /// Empty for nearly every screen, which is the cheap answer a host asks for /// on each draw. [`is_live`](Self::is_live) is the region-level fact beside /// it, and the two are independent: a still region can hold a stopwatch, /// and a live region usually holds none. #[must_use] pub fn clocks(&self) -> BTreeSet { let mut found = BTreeSet::new(); for node in self.notices.iter().chain( self.slots .iter() .flat_map(|slot| slot.body.iter().map(|ranked| &ranked.node)), ) { node.clocks_into(&mut found); } found } /// Apply a fragment: put `node` in the region under `region`, replacing /// whatever was there. Returns whether the region was found. /// /// This is what a host holding a `Screen` does with /// [`Outcome::Fragment`](crate::Outcome::Fragment). A webview host needs /// none of it -- `quasi-http` turns the same outcome into an `hx-retarget` /// header and the browser performs the swap against a document it already /// has -- but a host that retains the description rather than the markup /// has nothing between the fragment and the tree. /// /// It lives here and not in a host because applying a fragment is surgery /// on this crate's own type. A host writing it means every retained-screen /// host writes it separately and each picks its own answer for the three /// decisions below, which is the thing this crate's no-host-imports rule /// exists to prevent. /// /// **A region that is not there answers `false`, not a panic.** The caller /// is the one that can act on it: a host can fall back to a redraw, and a /// test can assert it. What is worth avoiding is the silent no-op, because /// a miss means a route naming a slot that no longer exists, and that is a /// description bug rather than a rendering one. /// /// **It replaces rather than appends.** `Outcome::Fragment` is one region's /// new contents, which is the whole reason it can be smaller than a screen. /// /// **The region becomes [`Ready`](layout::Readiness::Ready).** A fragment /// arriving is the content arriving, so a slot marked /// [`Pending`](layout::Readiness::Pending) while it was in flight stops /// being pending here, and a [`Slot::fed_by`] naming the call that just /// answered is cleared with it. Emptiness is a different axis and rides on the node: /// a [`Node::StandIn`] carries its own state, and replacing with one is a /// region that is ready and has nothing to show. pub fn replace(&mut self, region: &str, node: Node) -> bool { let Some(slot) = self.slots.iter_mut().find_map(|slot| slot.find_mut(region)) else { return false; }; slot.body.clear(); slot.body.push(Ranked::new(node)); slot.readiness = layout::Readiness::Ready; // The call that fed it has answered, so the region stops naming one. A // retained-screen host redraws from this tree, and a region still // pointing at its feed would ask again on the next paint. // // A live region keeps it. There the call is the cadence rather than an // arrival, so clearing it would make the region live exactly once and // then go still, and [`feeds`](Self::feeds) already refuses to return // it — the repeat is [`refreshes`](Self::refreshes)' and is paced. if !slot.live { slot.fed_by = None; } true } /// Mark a region as waiting on work that has been handed off, saying so. /// /// [`replace`](Self::replace)'s opposite number, and what a host holding a /// `Screen` does with [`Outcome::Started`](crate::Outcome::Started). It /// lives here for the same reason: applying an outcome to a retained /// description is surgery on this crate's own type, and a host writing it /// means every retained-screen host writes it separately. /// /// **A region that is not there answers `false`.** Same contract, same /// reason: a route naming a slot that is gone is a description bug, and the /// caller is the party that can say so. /// /// **[`readiness`](Slot::readiness) becomes /// [`Pending`](layout::Readiness::Pending)**, which is what every renderer /// draws its wait from. The message goes in as a /// [`Node::pending`](Node::pending) stand-in, replacing what was there — /// so a host that draws the body under a pending region shows the sentence, /// and one that draws its own wait and returns early loses nothing it /// wanted. /// /// **[`fed_by`](Slot::fed_by) is left exactly as it is.** That call is how /// the finish gets reported, so clearing it here would be marking a region /// as waiting and removing the thing it waits on in the same breath. A /// region with no call keeps having none, and stays pending until something /// else tells it otherwise; see [`Outcome::Started`](crate::Outcome::Started) /// on why that is a description bug this cannot catch. pub fn started(&mut self, region: &str, message: impl Into) -> bool { let Some(slot) = self.slots.iter_mut().find_map(|slot| slot.find_mut(region)) else { return false; }; slot.body.clear(); slot.body.push(Ranked::new(Node::pending(message))); slot.readiness = layout::Readiness::Pending; true } } #[cfg(test)] mod tests { use super::{Cell, Column, Node, Row, Table}; /// The whole reason a cell names its column. /// /// `git_repos` builds a visibility cell only for an owner, by pushing onto /// a `Vec` whose length has to agree with a column list built under a /// second, separate conditional. Named, the two cannot disagree: the row /// answers the columns the table has. #[test] fn a_conditional_cell_does_not_shift_the_columns_after_it() { let columns = || { [ Column::new("Name"), Column::new("Visibility"), Column::new("Description"), ] }; let row = |is_owner: bool| { let mut cells = Row::default() .at("Name", Cell::new("quasi")) .at("Description", Cell::new("the app stack")); if is_owner { cells = cells.at("Visibility", Cell::new("public")); } cells }; let owner = Table::new(columns()).row(row(true)); let stranger = Table::new(columns()).row(row(false)); let read = |table: &Table| { table.rows[0] .cells .iter() .map(Cell::text) .collect::>() }; assert_eq!(read(&owner), ["quasi", "public", "the app stack"]); // The description stays in its own column rather than sliding left. assert_eq!(read(&stranger), ["quasi", "", "the app stack"]); } /// A column nothing named is empty rather than absent. #[test] fn a_column_nothing_named_is_empty() { let table = Table::new([Column::new("Name"), Column::new("Size")]) .row(Row::default().at("Name", Cell::new("kick.wav"))); assert_eq!(table.rows[0].cells.len(), 2); assert_eq!(table.rows[0].cells[0].text(), "kick.wav"); assert_eq!(table.rows[0].cells[1].text(), ""); } /// Naming a column the table does not have is a bug, and it is loud. /// /// The one failure mode naming introduces: the cell is dropped, so a typo /// is an empty column rather than a compile error. Nothing above this can /// catch it, because the row and the column list are written apart, so the /// check lives where both are in hand. #[test] #[should_panic(expected = "a cell named a column this table does not have")] fn naming_a_column_the_table_does_not_have_is_caught() { let _ = Table::new([Column::new("Name")]).row(Row::default().at("Nmae", Cell::new("kick.wav"))); } /// Two columns with one name cannot be addressed apart. #[test] #[should_panic(expected = "two columns share a name")] fn two_columns_sharing_a_name_is_caught() { let _ = Table::new([Column::new("Actions"), Column::new("Actions")]) .row(Row::default().at("Actions", Cell::new("edit"))); } /// A row built by position still works and is untouched by resolution. #[test] fn a_positional_row_is_left_alone() { let table = Table::new([Column::new("Name"), Column::new("Size")]) .row(Row::cells(["kick.wav", "2.1 MB"])); assert_eq!(table.rows[0].cells.len(), 2); assert_eq!(table.rows[0].cells[1].text(), "2.1 MB"); } /// The table is the node, so nothing reaches for the variant by hand. #[test] fn a_table_becomes_its_node() { let node = Node::from(Table::new([Column::new("Name")]).row(Row::cells(["kick.wav"]))); match node { Node::Table { columns, rows, .. } => { assert_eq!(columns.len(), 1); assert_eq!(rows.len(), 1); } other => panic!("expected a table, got {other:?}"), } } use super::*; fn frames(count: usize) -> Vec { (0..count) .map(|n| Node::Image(Image::new(format!("/frame-{n}.png"), format!("frame {n}")))) .collect() } /// `drums` shut, `drums.kick` and `drums.snare` under it, `genre` after. fn outline() -> Vec { vec![ Row::new("drums").disclosing(false), Row::new("drums.kick").depth(layout::Nesting::at(1)), Row::new("drums.snare").depth(layout::Nesting::at(1)), Row::new("genre").disclosing(true), Row::new("genre.house").depth(layout::Nesting::at(1)), ] } #[test] fn a_row_is_flat_and_a_leaf_until_it_says_otherwise() { // Every row described before these members existed, unchanged. let row = Row::new("kick.wav"); assert_eq!(row.depth, layout::Nesting::top()); assert_eq!(row.open, None); assert_eq!(Row::cells(["kick.wav"]).depth, layout::Nesting::top()); assert_eq!(Row::cells(["kick.wav"]).open, None); } #[test] fn a_shut_branch_folds_what_is_under_it_and_nothing_else() { assert_eq!( folded(&outline()), [false, true, true, false, false], "the two under the shut `drums` go, the open `genre`'s child stays" ); } #[test] fn a_shut_branch_takes_the_open_branches_inside_it() { let rows = vec![ Row::new("drums").disclosing(false), Row::new("drums.kick") .depth(layout::Nesting::at(1)) .disclosing(true), Row::new("drums.kick.hard").depth(layout::Nesting::at(2)), Row::new("genre"), ]; assert_eq!(folded(&rows), [false, true, true, false]); } #[test] fn a_list_with_no_disclosure_folds_nothing() { // The graceful-degradation case, from the other side: depth alone is an // indent and never hides a row. let rows: Vec = (0..4u8) .map(|n| Row::new("t").depth(layout::Nesting::at(n))) .collect(); assert_eq!(folded(&rows), [false; 4]); } #[test] fn a_branch_ends_at_the_next_row_no_deeper_than_it() { // Including a sibling at its own depth, which is the boundary case a // greater-than would get wrong. let rows = vec![ Row::new("a") .depth(layout::Nesting::at(1)) .disclosing(false), Row::new("a.one").depth(layout::Nesting::at(2)), Row::new("b").depth(layout::Nesting::at(1)), Row::new("root").depth(layout::Nesting::at(0)), ]; assert_eq!(folded(&rows), [false, true, false, false]); } #[test] fn a_table_row_says_the_hierarchy_the_same_way_a_list_row_does() { let rows = vec![ Row::cells(["drums"]).disclosing(false), Row::cells(["drums.kick"]).depth(layout::Nesting::at(1)), Row::cells(["genre"]), ]; assert_eq!(folded(&rows), [false, true, false]); } #[test] fn a_navigating_action_says_so_and_changes_nothing_else() { // `00ee7af5`. The mark is a member beside the others rather than a kind // of destination, so where the call goes is untouched by it. let action = Action::get("/p/slow-reader") .carrying("from", "discover") .navigating(); assert!(action.navigates); assert_eq!(action.route(), Some("/p/slow-reader")); assert_eq!(action.method, Method::Get); assert_eq!(action.carried.get("from"), Some("discover")); assert!(action.replaces.is_none()); assert!(!action.elsewhere); } #[test] fn an_action_does_not_navigate_unless_it_says_so() { // Off by default in every constructor, so nothing described before the // member existed says anything new. assert!(!Action::get("/p/slow-reader").navigates); assert!(!Action::post("/p/slow-reader").navigates); assert!(!Action::local().navigates); assert!(!Action::external("https://example.com").navigates); assert!(!Action::default().navigates); } #[test] fn an_interval_states_both_names_and_holds_both_values() { let f = Field::interval("bpm_min", "bpm_max", "BPM") .value("90") .upper_value("130"); assert_eq!(f.kind, layout::FieldKind::Interval); assert_eq!(f.name, "bpm_min"); assert_eq!(f.upper_name.as_deref(), Some("bpm_max")); assert_eq!(f.value.as_deref(), Some("90")); assert_eq!(f.upper_value.as_deref(), Some("130")); // The extent stays optional, unlike a range's: an interval's bounds are // a rule on each end rather than the control. assert_eq!(f.min, None); assert_eq!(f.max, None); } #[test] fn a_refused_interval_is_re_offered_under_both_names() { // The half option (b) would not have paid. Without a second value a // refusal hands back the low end and silently drops the high one, so // the user retypes half of what they already answered. let mut params = crate::Params::new(); params.insert("bpm_min".to_owned(), "90".to_owned()); params.insert("bpm_max".to_owned(), "130".to_owned()); let f = Field::interval("bpm_min", "bpm_max", "BPM").refilled(¶ms); assert_eq!(f.value.as_deref(), Some("90")); assert_eq!(f.upper_value.as_deref(), Some("130")); } #[test] fn an_open_end_comes_back_open() { // "Over 120 BPM" is an answer rather than a half-filled form, so an // absent end stays absent instead of being filled with a bound. let mut params = crate::Params::new(); params.insert("bpm_min".to_owned(), "120".to_owned()); let f = Field::interval("bpm_min", "bpm_max", "BPM").refilled(¶ms); assert_eq!(f.value.as_deref(), Some("120")); assert_eq!(f.upper_value, None); } #[test] fn a_field_with_one_name_reads_only_that_one() { // Every other kind is untouched: `refilled` returns after the lower // half when there is no second name to read. let mut params = crate::Params::new(); params.insert("title".to_owned(), "Kick".to_owned()); let f = Field::new(layout::FieldKind::Text, "title", "Title").refilled(¶ms); assert_eq!(f.value.as_deref(), Some("Kick")); assert_eq!(f.upper_value, None); } #[test] fn a_consult_with_no_floor_asks_about_anything_including_nothing() { let consult = Consult::new(Action::get("/api/validate/username")); assert_eq!(consult.at_least, 0); assert!(consult.asks_about("")); assert!(consult.asks_about("m")); } /// The pages a strip offers are the description's, and so is each one's /// address: a renderer cannot build page 5's out of prev and next without /// knowing the address grammar. #[test] fn a_pager_offers_the_pages_the_host_windowed_and_marks_the_one_being_read() { let rest = Rest::page(100, 50) .of(400) .back(Action::get("/feed?page=2")) .forward(Action::get("/feed?page=4")) .jumping(Jump::new(2, Action::get("/feed?page=2"))) .jumping(Jump::new(3, Action::get("/feed?page=3")).here()) .jumping(Jump::new(4, Action::get("/feed?page=4"))); // Five pages out of eight, which is the window MNW already computes. // Nothing here windows anything: a renderer choosing its own would give // a different answer per host for one list. assert_eq!(rest.jumps.len(), 3); assert_eq!(rest.as_layout().page(), Some(3)); assert_eq!(rest.as_layout().pages_total(), Some(8)); // The page is carried rather than implied by position, because a window // around the reader does not start at one. Which one the reader is on is // carried too rather than compared against the paging: a strip is a // loop, and a residual holds one compiled body per loop. assert!(!rest.jumps[0].here); assert!(rest.jumps[1].here); assert!(!rest.jumps[2].here); } /// Empty jumps is prev/next paging, which is every site that existed before /// the member did. #[test] fn a_pager_offers_no_pages_until_it_is_given_some() { assert!(Rest::page(0, 50).of(400).jumps.is_empty()); assert!(Rest::more(50, Action::get("/more")).jumps.is_empty()); } /// A screen may say what is true of the document it is drawn into, and /// every host without one drops it whole. #[test] fn a_screen_says_nothing_about_its_document_until_it_does() { let plain = Screen::new("A", layout::Arrangement::Single); assert_eq!(plain.document, Document::default()); assert!(plain.document.body_class.is_none()); assert!(plain.document.root.is_empty()); let said = plain.documented( Document::default() .classed("admin-page") .rooted("data-theme", "slate") .rooted("dir", "rtl"), ); assert_eq!(said.document.body_class.as_deref(), Some("admin-page")); // Adds rather than replaces: a document saying two things about its // root is saying two things. assert_eq!(said.document.root.len(), 2); } /// The name reaches markup as a name rather than as a value, so a gate is /// what protects it and escaping is not. #[test] fn a_root_attribute_name_is_letters_digits_and_dashes_from_a_letter() { assert!(writable_root_attr("data-theme")); assert!(writable_root_attr("dir")); assert!(writable_root_attr("x1-2")); assert!(!writable_root_attr("")); assert!(!writable_root_attr("1data")); assert!(!writable_root_attr("-theme")); // The shapes that are the reason for the gate. assert!(!writable_root_attr("x\" onload=alert(1) y")); assert!(!writable_root_attr("data theme")); assert!(!writable_root_attr("data_theme")); } /// A control chosen in one gesture has nothing to wait out, and the wait /// stays the description's rather than becoming something a kind implies. #[test] fn a_question_about_a_value_chosen_in_one_gesture_waits_for_nothing() { let at_once = Consult::at_once(Action::get("/mail/list")); assert_eq!(at_once.after, std::time::Duration::ZERO); // Everything else is what `new` gives it: a floor of nothing, and the // value travelling under the field's own name. assert_eq!(at_once.at_least, 0); assert!(at_once.sends.is_empty()); assert_eq!( Consult::new(Action::get("/mail/list")).after, Consult::SETTLES ); } /// The question a field owns and the questions it merely asks are separate /// members, because MNW's discover box has both: a list of its own, and a /// results route that lands in a region. #[test] fn a_field_owns_one_list_and_may_still_ask_other_questions() { let field = Field::new(layout::FieldKind::Text, "q", "Search") .suggesting(Consult::new(Action::get("/discover/suggestions")).at_least(2)) .consulting(Consult::new(Action::get("/discover/results")).sending(["mode"])); let owned = field.suggests.as_ref().expect("a list of its own"); assert_eq!(owned.action.destination.as_str(), "/discover/suggestions"); assert_eq!(owned.at_least, 2); assert_eq!(field.consults.len(), 1); // One list, so a second call is a description changing its mind rather // than asking twice. `consults` adds, for the opposite reason. let field = field.suggests(Action::get("/other")); assert_eq!( field .suggests .expect("the later one") .action .destination .as_str(), "/other" ); } /// A list row and a table row say the same facts the same way. /// /// A table row gained `current` and the plural `menu` on 2026-09-02 and a /// list row did not, although both carried the same fact under the same /// name. So a screen holding both had to write one in the chain and the /// other by assignment. The 2026-09-05 collapse ended the drift by ending /// the second type; this test is what holds the parity it left behind. /// #[test] fn a_list_row_and_a_table_row_say_row_ness_alike() { let acts = || { [ Act::new("Rename", Action::post("/rename")), Act::new("Delete", Action::post("/delete")), ] }; let row = Row::new("kick.wav").current(true).menu(acts()); let cells = Row::cells(["kick.wav"]).current(true).menu(acts()); assert!(row.current); assert!(cells.current); assert_eq!(row.menu.len(), 2); assert_eq!(cells.menu.len(), 2); // The plural extends rather than replacing, so it composes with the // singular. let row = row.offers(Act::new("Reveal", Action::local())); assert_eq!(row.menu.len(), 3); // And neither is set until it is said. let plain = Row::new("kick.wav"); assert!(!plain.current); assert!(plain.menu.is_empty()); } /// Every fact a field carries is reachable without leaving the chain. /// /// Five members had no builder and were set by assigning the public field: /// `placeholder`, `max_length`, `min`, `max` and `extended`. That is /// the missing-builder defect on the vocabulary's largest struct, and the /// measured cost was 66 sites across the three shape trees breaking out of /// a builder chain to write one of them. It also made those five unsayable /// in a declared description, which resolves an attribute to a builder's /// name and so cannot reach a member that has none. /// #[test] fn every_fact_a_field_carries_is_reachable_from_the_chain() { let field = Field::new(layout::FieldKind::Text, "promo", "Promo code") .placeholder("e.g. TRIAL14") .limited_to(8) .extended(); assert_eq!(field.placeholder.as_deref(), Some("e.g. TRIAL14")); assert_eq!(field.max_length, Some(8)); assert!(field.extended); // A placeholder is not a hint: one disappears when the reader types and // the other does not, so writing one must not write the other. assert!(field.hint.is_none()); // The extent, both ends at once, which is the ordinary spelling. let dial = Field::new(layout::FieldKind::Number, "price", "Price").within("0", "9999"); assert_eq!(dial.min.as_deref(), Some("0")); assert_eq!(dial.max.as_deref(), Some("9999")); // And one end alone, because an open end is a real answer rather than a // missing one. `Field::interval`'s own doc says so. let floor = Field::new(layout::FieldKind::Number, "n", "How many").at_least("1"); assert_eq!(floor.min.as_deref(), Some("1")); assert!(floor.max.is_none()); let ceiling = Field::new(layout::FieldKind::Number, "n", "How many").at_most("10"); assert!(ceiling.min.is_none()); assert_eq!(ceiling.max.as_deref(), Some("10")); // Nothing is set until it is said, so a field written the short way // still measures exactly as it did before these existed. let plain = Field::new(layout::FieldKind::Text, "title", "Title"); assert!(plain.placeholder.is_none()); assert!(plain.max_length.is_none()); assert!(plain.min.is_none()); assert!(plain.max.is_none()); assert!(!plain.extended); } /// A field asks nothing about its own value until it says so, and owns no /// list until it says so either. #[test] fn a_field_owns_no_list_until_it_says_it_does() { let field = Field::new(layout::FieldKind::Text, "title", "Title"); assert!(field.suggests.is_none()); assert!(field.consults.is_empty()); } #[test] fn a_control_asks_for_nothing_until_it_says_it_does() { let act = Act::new("Delete", Action::post("/items/delete")); assert!(act.asks.is_empty()); } #[test] fn a_control_deposits_nothing_until_it_names_a_field_and_a_value() { // The member has to be absent by default or every renderer starts // writing into a box on a press that never did before. let act = Act::new("Delete", Action::post("/items/delete")); assert!(act.fills.is_none()); } #[test] fn a_picker_card_names_the_box_it_writes_to_and_what_lands_there() { // MNW's media picker, measured 2026-08-19: the card reads as a file // name and deposits a markdown reference, and the two are different // strings, which is why a destination on its own is not enough. let act = Act::new("kick.png", Action::local()).filling("body", "![](media/kick.png)"); let fill = act.fills.as_ref().expect("a destination"); assert_eq!(fill.field, "body"); assert_eq!(fill.value, "![](media/kick.png)"); // Nothing about a caret, in either direction. `d52884b0` stands. assert_eq!(act.label, "kick.png"); } #[test] fn a_deposit_replaces_rather_than_accumulating() { // Unlike `asking`. One press deposits one value, and a control writing // into two boxes is a description doing two things at once. let act = Act::new("Insert", Action::local()) .filling("body", "first") .filling("body", "second"); assert_eq!(act.fills.expect("a destination").value, "second"); } #[test] fn a_deposit_does_not_cross_into_the_description_layer() { // Where a value lands is quasi's, the same as an address and a // confirmation. `layout::Act` claims to be what a renderer needs to // *draw* the control and nothing more. let act = Act::new("Insert", Action::local()).filling("body", "![](x.png)"); let drawn = act.as_layout(); assert_eq!(drawn.label, "Insert"); assert_eq!(drawn.tone, layout::Tone::Neutral); } #[test] fn a_verb_that_needs_a_value_carries_the_question_and_the_set() { // MNW's bulk bar, measured 2026-08-18: "Set Price" and "Add Tag" each // reveal one box and apply it to whatever is ticked. Both halves are on // the control, so a renderer never has to pair a form with a verb by // where they sit on the screen. let act = Act::new("Set Price", Action::post("/items/price")) .over("chosen") .asking( Field::new(layout::FieldKind::Number, "price", "New price ($)") .hint("Enter 0 to make items free."), ); assert_eq!(act.over.as_deref(), Some("chosen")); assert_eq!(act.asks.len(), 1); assert_eq!(act.asks[0].name, "price"); } #[test] fn a_box_a_verb_asked_for_writes_nothing_of_its_own() { // The value is answered by the press. A write of the box's own would // send it twice. let asked = Field::new(layout::FieldKind::Text, "tag", "Tag slug") .writes(Action::post("/items/tag")) .consulting(Consult::new(Action::get("/tags/known"))) .as_asked(); assert!(asked.writes.is_none()); // A question about the value survives: asking whether a slug is taken // is not a write of it. assert_eq!(asked.consults.len(), 1); } #[test] fn a_field_asks_nothing_until_it_says_it_does() { let field = Field::new(layout::FieldKind::Text, "q", "Search"); assert!(field.consults.is_empty()); } #[test] fn a_box_can_ask_two_routes_at_two_rates() { // MNW's discover search, measured 2026-08-18: `#search-input` re-reads // the results after 150ms and a hand-written `fetch` asks for // suggestions on its own schedule. Two questions about one value, and // only one of them was sayable. let field = Field::new(layout::FieldKind::Text, "q", "Search") .consulting( Consult::new(Action::get("/discover/suggestions")) .after(std::time::Duration::from_millis(200)) .at_least(2), ) .consulting( Consult::new(Action::get("/discover/results")) .after(std::time::Duration::from_millis(150)) .at_least(2) .sending(["mode", "sort", "tags"]), ); assert_eq!(field.consults.len(), 2); assert_eq!( field.consults[0].after, std::time::Duration::from_millis(200) ); assert!(field.consults[0].sends.is_empty()); assert_eq!(field.consults[1].sends, ["mode", "sort", "tags"]); } #[test] fn consulting_adds_rather_than_replaces() { // The builder reading that makes two questions writable at all. A // builder taking the last call would make the shape unsayable rather // than merely awkward. let field = Field::new(layout::FieldKind::Text, "q", "Search") .consults(Action::get("/one")) .consults(Action::get("/two")); assert_eq!(field.consults.len(), 2); } #[test] fn a_question_carries_nothing_beside_its_own_value_by_default() { // The common field, and the reason `sends` is a list rather than a // required argument: a validate route asks about the box and nothing // else. let consult = Consult::new(Action::get("/api/validate/username")); assert!(consult.sends.is_empty()); } #[test] fn sending_replaces_the_set_rather_than_growing_it() { let consult = Consult::new(Action::get("/discover/results")) .sending(["mode"]) .sending(["sort", "tags"]); assert_eq!(consult.sends, ["sort", "tags"]); } #[test] fn a_floor_counts_characters_and_not_bytes() { // The difference the renderers are spared: "el" and "él" are the same // question, and a byte count refuses one of them. let consult = Consult::new(Action::get("/discover/tag-suggest")).at_least(2); assert!(!consult.asks_about("e")); assert!(consult.asks_about("el")); assert!(!consult.asks_about("é")); assert!(consult.asks_about("él")); } #[test] fn a_region_is_still_until_it_says_it_is_live() { // The default has to be the old behaviour: a screen written before this // field existed says the same thing after it arrives. let slot = Slot::new("summary", RegionKind::Pane); assert!(!slot.live); assert!(slot.live().live); } #[test] fn a_live_call_is_a_refresh_and_not_a_feed() { // The split that keeps a host from having to work out which kind of // call it is holding. One walk answers "ask now", the other "keep // asking", and no action is in both. let screen = Screen::sidebar_content("Admin") .with(Slot::new("payouts", RegionKind::Pane).fed_by(Action::get("/dashboard/payouts"))) .with( Slot::new("queue", RegionKind::Pane) .fed_by(Action::get("/admin/queue")) .live(), ); let feeds: Vec<_> = screen .feeds() .iter() .map(|call| call.destination.as_str().to_owned()) .collect(); let refreshes: Vec<_> = screen .refreshes() .iter() .map(|call| call.destination.as_str().to_owned()) .collect(); assert_eq!(feeds, ["/dashboard/payouts"]); assert_eq!(refreshes, ["/admin/queue"]); } #[test] fn a_live_region_keeps_its_call_when_the_answer_lands() { // The half `replace` had to learn. A feed is cleared as it answers so a // retained host does not ask twice; a cadence cleared on its first // answer would tick once and stop. let mut screen = Screen::sidebar_content("Admin").with( Slot::new("queue", RegionKind::Pane) .fed_by(Action::get("/admin/queue")) .live(), ); assert!(screen.replace("queue", Node::text("4 waiting"))); assert!(screen.feeds().is_empty(), "a live call is never a feed"); assert_eq!( screen.refreshes().len(), 1, "the cadence survives an answer" ); } #[test] fn a_still_region_still_drops_its_call_when_the_answer_lands() { let mut screen = Screen::sidebar_content("Payments") .with(Slot::new("payouts", RegionKind::Pane).fed_by(Action::get("/dashboard/payouts"))); assert!(screen.replace("payouts", Node::text("$12.00"))); assert!(screen.feeds().is_empty()); assert!(screen.refreshes().is_empty()); } #[test] fn a_screen_is_live_when_a_region_inside_it_is() { // The predicate a retained host reads to decide whether to keep // drawing. It has to see through nesting, because the live thing is // usually a panel of something rather than a top-level region. let still = Screen::sidebar_content("Sync").with(Slot::new("body", RegionKind::Pane)); assert!(!still.is_live()); let nested = Screen::sidebar_content("Sync").with( Slot::new("body", RegionKind::Pane) .with(Node::Region(Slot::new("sync", RegionKind::Pane).live())), ); assert!(nested.is_live()); } #[test] fn a_screen_carries_no_clock_until_a_readout_derives_itself_from_one() { let still = Screen::sidebar_content("Tasks") .with(Slot::new("body", RegionKind::Pane).with(Node::text("Write the brief"))); assert!(still.clocks().is_empty()); } #[test] fn a_readout_in_a_row_is_found_as_readily_as_one_in_a_region() { // The walk that matters. goingson puts the elapsed time on a task row // beside its title, so a search that stopped at the region's own blocks // would answer that a screen full of running timers needs no clock. let started = std::time::SystemTime::UNIX_EPOCH; let screen = Screen::sidebar_content("Tasks").with( Slot::new("body", RegionKind::Pane).with(Node::list([ Row::new("Write the brief").part(layout::RowPart::Meta, Node::since(started)) ])), ); assert_eq!(screen.clocks(), BTreeSet::from([Clock::Since])); } #[test] fn each_kind_is_reported_once_however_many_readouts_say_it() { // Many readouts, one tick: the renderer asks which kinds it is holding // and picks a cadence per kind, rather than a timer per readout. let at = std::time::SystemTime::UNIX_EPOCH; let screen = Screen::sidebar_content("Tasks").with( Slot::new("body", RegionKind::Pane) .with(Node::since(at)) .with(Node::since(at)) .with(Node::Region( Slot::new("footer", RegionKind::Pane).with(Node::age(at)), )), ); assert_eq!(screen.clocks(), BTreeSet::from([Clock::Since, Clock::Age])); // Finest first, which is what a renderer taking the minimum wants. assert_eq!(screen.clocks().into_iter().next(), Some(Clock::Since)); } #[test] fn a_time_derived_node_says_which_way_it_runs_and_from_when() { let at = std::time::SystemTime::UNIX_EPOCH; assert_eq!(Node::since(at).clock(), Some((Clock::Since, at))); assert_eq!(Node::until(at).clock(), Some((Clock::Until, at))); assert_eq!(Node::age(at).clock(), Some((Clock::Age, at))); assert_eq!(Node::text("12:04").clock(), None); } #[test] fn a_live_region_with_no_call_asks_for_nothing() { // Liveness and a route are separate halves. The audiofiles sync panel // reads state the host already holds, so its cadence is a repaint and // there is nothing here for a host to perform. let screen = Screen::sidebar_content("Sync").with( Slot::new("sync", RegionKind::Pane) .live() .with(Node::text("Authenticating")), ); assert!(screen.refreshes().is_empty()); assert!(screen.feeds().is_empty()); } #[test] fn a_region_shows_everything_until_it_says_otherwise() { // The default has to be the old behaviour, or every description written // before this field existed changes meaning when it arrives. let pane = Slot::new("content", RegionKind::Pane).extend(frames(3)); assert_eq!(pane.showing(), layout::Showing::All); assert_eq!(pane.current(), None); } #[test] fn a_carousel_with_no_stated_frame_is_on_its_first() { // `Showing::One` says exactly one is up, so there is no honest reading // of a missing index other than the first. A renderer never has to // decide this for itself, which is the point of the method. let mut carousel = Slot::widget("shots", "carousel").extend(frames(3)); carousel = carousel.showing_one(0); assert_eq!(carousel.current(), Some(0)); } #[test] fn a_frame_past_the_end_clamps_rather_than_vanishing() { // An out-of-range index is an app bug either way. Clamping reports it as // a carousel stuck on its last frame, which is findable; drawing nothing // reports it as a region that disappeared, which is not. let carousel = Slot::widget("shots", "carousel") .extend(frames(3)) .showing_one(9); assert_eq!(carousel.current(), Some(2)); // And an empty body has no frame to clamp to. assert_eq!( Slot::widget("shots", "carousel").showing_one(0).current(), None ); } #[test] fn a_closed_disclosure_is_the_one_selective_region_showing_nothing() { let closed = Slot::widget("details", "disclosure") .extend(frames(1)) .showing_at_most_one(None); let open = Slot::widget("details", "disclosure") .extend(frames(1)) .showing_at_most_one(Some(0)); assert_eq!(closed.current(), None); assert_eq!(open.current(), Some(0)); // Closed and `Showing::All` answer the same here on purpose: they differ // in the chrome around the body, not in what a renderer does with it. assert!(closed.showing().selective()); } #[test] fn labels_are_all_or_nothing() { // A strip with a hole in it is worse than the prev/next row it would // have replaced, so a half-labelled body gets the row. let tabs = Slot::new("detail", RegionKind::TabGroup) .frame( "Overview", Node::Region(Slot::new("overview", RegionKind::Pane)), ) .frame("Files", Node::Region(Slot::new("files", RegionKind::Pane))); assert_eq!(tabs.labels(), ["Overview", "Files"]); let half = tabs .clone() .with(Node::Region(Slot::new("history", RegionKind::Pane))); assert!(half.labels().is_empty()); } #[test] fn a_carousels_frames_carry_no_label_and_that_is_the_switch() { // Which idiom a renderer draws falls out of this rather than out of the // widget's name. A frame has a caption; only a region has a tab name. let carousel = Slot::widget("shots", "carousel") .extend(frames(3)) .showing_one(1); assert!(carousel.labels().is_empty()); assert_eq!(carousel.current(), Some(1)); } #[test] fn a_region_member_never_drops_unless_it_was_asked_to() { // The whole of what makes `Ranked` additive. Every screen in the tree // was written before it existed and every one of them still says the // same thing. let pane = Slot::new("main", RegionKind::Pane) .with(Node::text("kept")) .extend([Node::text("also kept")]); for placed in pane.body.iter() { assert_eq!(placed.priority, layout::Priority::Essential); for cutoff in CUTOFFS { assert!(placed.kept_at(cutoff)); } } } #[test] fn a_tab_strip_and_the_band_beside_it_are_one_row() { // goingson's bug, said in the description instead of in a stylesheet. // The toolbar is not in the pane and is not out of flow; it is a member // of the tab strip's row, and it says what it is worth when that row // runs out of space. let view = Slot::new("work-view", RegionKind::TabGroup).across( Run::new(layout::Fallback::Menu).beside( Node::Region(Slot::new("work-toolbar", RegionKind::Band)), layout::Priority::Secondary, ), ); let run = view.run.as_ref().expect("the row was declared"); assert_eq!(run.fallback, layout::Fallback::Menu); assert_eq!(run.members.len(), 1); // The strip itself is not a member. It is what the tab group already // puts in the row, generated from its children's labels, and a member // standing for it would be a second source for the same fact. assert!(view.body.is_empty()); } #[test] fn a_row_member_is_still_a_region_a_fragment_can_be_aimed_at() { // The property that makes the toolbar's move out of the pane free. A // region that stopped being findable would stop updating, which is a // worse bug than the overlap it was moved to fix. let view = Slot::new("work-view", RegionKind::TabGroup).across( Run::new(layout::Fallback::Menu).beside( Node::Region( Slot::new("work-toolbar", RegionKind::Band).fed_by(Action::get("/toolbar")), ), layout::Priority::Secondary, ), ); assert!(view.find("work-toolbar").is_some()); let mut screen = Screen::sidebar_content("Work").with(view); assert_eq!(screen.feeds().len(), 1); assert!(screen.replace("work-toolbar", Node::text("filters"))); } #[test] fn what_a_tight_row_keeps_depends_on_what_it_said_it_would_do() { let members = |fallback| { Slot::new("head", RegionKind::TabGroup) .across( Run::new(fallback) .beside(Node::text("search"), layout::Priority::Secondary) .beside(Node::text("count"), layout::Priority::Optional), ) .run .expect("declared") }; // Wrap and Stack rearrange, so every member survives however tight the // row is and the rank goes unread. for keeping in [layout::Fallback::Wrap, layout::Fallback::Stack] { let run = members(keeping); assert!(run.keeps_every_member()); assert_eq!(run.kept_at(layout::Priority::Essential).len(), 2); } // Shed and Menu take members out of the row, by rank and never by // position, which is the whole reason the rank is on the member. for shedding in [layout::Fallback::Shed, layout::Fallback::Menu] { let run = members(shedding); assert!(!run.keeps_every_member()); assert_eq!(run.kept_at(layout::Priority::Optional).len(), 2); assert_eq!(run.kept_at(layout::Priority::Secondary).len(), 1); assert!(run.kept_at(layout::Priority::Essential).is_empty()); } } #[test] fn a_region_that_never_declared_a_row_has_no_row_to_put_anything_in() { // Rule 2, held by the type instead of by a check. This was a // `#[should_panic]` test while `beside` was a method on the region: it // had to look for a row at runtime and refuse when there was none, // which made a correct call and an incorrect one identical to a // compiler. There is nothing left to panic on. `beside` is on `Run`, // `Run::new` takes the fallback, and a region that never declared a // row offers no method that would put a member in one. // // What a running program can still observe is the half below: silence // stays silence, and both ways of declaring a row arrive with the // answer the panic used to stand in for. let quiet = Slot::new("head", RegionKind::Band); assert!(quiet.run.is_none()); let empty = Slot::new("head", RegionKind::Band).across(layout::Fallback::Shed); let run = empty.run.expect("declared"); assert_eq!(run.fallback, layout::Fallback::Shed); assert!(run.members.is_empty()); let filled = Slot::new("head", RegionKind::Band).across( Run::new(layout::Fallback::Menu) .beside(Node::text("search"), layout::Priority::Secondary), ); let run = filled.run.expect("declared"); assert_eq!(run.fallback, layout::Fallback::Menu); assert_eq!(run.members.len(), 1); } #[test] fn declaring_the_row_twice_is_a_second_row_rather_than_a_correction() { // The row is one value, so handing the region another one replaces it // whole. Correcting a fallback means correcting it on the `Run`, where // the members it belongs to are, rather than reaching past them. let head = Slot::new("head", RegionKind::TabGroup) .across( Run::new(layout::Fallback::Shed) .beside(Node::text("search"), layout::Priority::Secondary), ) .across(layout::Fallback::Menu); let run = head.run.expect("declared"); assert_eq!(run.fallback, layout::Fallback::Menu); assert!(run.members.is_empty()); } #[test] fn a_member_inserted_above_the_cut_does_not_change_what_drops() { // `makeover-tui`'s table states this for columns and tests it; this is // the same property for a region's members, and it is the property the // whole rank exists to buy. Positional narrowing -- goingson's // `nth-child(n+5)` -- fails it, which is the bug `Priority` replaced. let before = Slot::new("bar", RegionKind::Band) .with(Node::text("title")) .with_ranked(Node::text("filter"), layout::Priority::Optional); let after = Slot::new("bar", RegionKind::Band) .with(Node::text("title")) .with(Node::text("inserted")) .with_ranked(Node::text("filter"), layout::Priority::Optional); let dropped = |slot: &Slot| -> Vec { slot.body .iter() .filter(|placed| !placed.kept_at(layout::Priority::Secondary)) .map(|placed| format!("{:?}", placed.node)) .collect() }; assert_eq!(dropped(&before), dropped(&after)); assert_eq!(dropped(&before).len(), 1); } #[test] fn the_cutoffs_run_weakest_first() { // A renderer walks these in order and stops at the first that fits, so // the order is the whole meaning of the constant. A tier added // upstream and appended here rather than placed would narrow to it // last, whatever it said. assert_eq!( CUTOFFS, [ layout::Priority::Optional, layout::Priority::Secondary, layout::Priority::Essential, ] ); assert!(CUTOFFS.is_sorted()); } #[test] fn started_marks_a_region_waiting_and_leaves_the_call_that_reports_the_finish() { // `dc2f2b46`. `replace`'s opposite number, and the pair is the whole // cycle: the work is handed off, the region goes pending, and the live // call that was already declared is what eventually puts content back. let mut screen = Screen::list_detail("Import & Export", false).with( Slot::new("backups", RegionKind::Pane) .with(Node::text("3 backups")) .fed_by(Action::get("/backups").awaiting()) .live(), ); // A live region is asked again on a cadence rather than once, so it is // `refreshes` and not `feeds` that names its call. assert!(screen.feeds().is_empty()); assert_eq!(screen.refreshes().len(), 1); assert!(screen.started("backups", "Creating backup…")); assert_eq!(screen.slots[0].readiness, layout::Readiness::Pending); assert_eq!( screen.slots[0].body.iter().next().expect("one member").node, Node::pending("Creating backup…"), "the sentence stands where the content was" ); // The call is untouched. Clearing it here would mark the region as // waiting and remove the thing it waits on in one breath. assert_eq!(screen.refreshes().len(), 1); // And the finish is an ordinary fragment, which takes it back out. assert!(screen.replace("backups", Node::text("4 backups"))); assert_eq!(screen.slots[0].readiness, layout::Readiness::Ready); assert_eq!( screen.refreshes().len(), 1, "a live region keeps its call after an answer lands" ); // A region that is not there is the description bug a fragment naming // one is, and answers the same way rather than panicking. assert!(!screen.started("gone", "…")); } #[test] fn a_region_fed_by_a_call_is_pending_until_the_answer_lands() { // `d8d6f380`. The two facts move together: a region that says where its // content is coming from does not have it, and a region that has been // filled is no longer asking. let mut screen = Screen::list_detail("Payments", false).with( Slot::new("payouts", RegionKind::Pane) .fed_by(Action::get("/dashboard/payouts").awaiting()), ); assert_eq!(screen.feeds().len(), 1); assert_eq!( screen.slots[0].readiness, layout::Readiness::Pending, "a fed region has not arrived" ); assert!(screen.replace("payouts", Node::text("paid out"))); assert_eq!(screen.slots[0].readiness, layout::Readiness::Ready); assert!( screen.feeds().is_empty(), "a filled region asks for itself again" ); } #[test] fn an_upload_carries_all_four_axes_and_only_two_of_them_are_the_layers() { // f7261a5a. What it takes and how many are the description's; where the // bytes go and how far along it is are the action's, and both already // existed. The test is that reading the field as the layer's own type // carries the first two across the owned/borrowed seam. let field = Field::upload( "media", "Media", [ Accepted::family(layout::Family::Image), Accepted::media_type("text/csv"), Accepted::suffix(".tar.gz"), ], ) .many() .writes(Action::post("/media").awaiting_amount(41_943_040)); assert!(field.multiple); // A suffix names no family and a csv is not media, so what earns the // preview here is the one entry that says so. assert!(field.accepts_media()); assert!(!Field::upload("build", "Build", [Accepted::suffix(".zip")]).accepts_media()); // The destination and the size ride on the action, unchanged by any of // this. let action = field.writes.clone().expect("the field writes on its own"); assert_eq!( action.awaiting.and_then(|mark| mark.amount), Some(41_943_040) ); field.with_layout(|borrowed| { assert_eq!(borrowed.kind, layout::FieldKind::File); assert!(borrowed.multiple); assert_eq!( borrowed.accept, [ layout::Accepted::Family(layout::Family::Image), layout::Accepted::Type("text/csv"), layout::Accepted::Suffix(".tar.gz"), ] ); assert!(borrowed.accepts_media()); }); } #[test] fn a_field_that_is_not_an_upload_takes_no_files_and_says_so() { let text = Field::new(layout::FieldKind::Text, "title", "Title"); assert!(!text.kind.takes_files()); assert!(text.accept.is_empty()); assert!(!text.multiple); // An upload with an empty list is a field that takes any file, which is // a different sentence from a field that takes none. let any = Field::upload("file", "File", []); assert!(any.kind.takes_files()); assert!(any.accept.is_empty()); } #[test] fn the_wait_is_measured_only_where_something_measured_it() { // The ruling on `5fa96a82`: an amount is a fact about the payload, and // a call with nothing countable about it says nothing rather than // guessing. let stripe = Action::post("/checkout").awaiting(); let upload = Action::post("/media").awaiting_amount(41_943_040); assert!(stripe.awaits() && upload.awaits()); assert_eq!(stripe.awaiting.and_then(|mark| mark.amount), None); assert_eq!( upload.awaiting.and_then(|mark| mark.amount), Some(41_943_040) ); assert!(!Action::post("/save").awaits()); } #[test] fn a_nested_region_is_fed_too() { // The walk descends the way `find_mut` does, or a region inside a pane // never asks for itself and shows its stand-in forever. let inner = Slot::new("inner", RegionKind::Pane).fed_by(Action::get("/slow").awaiting()); let screen = Screen::list_detail("Screen", false) .with(Slot::new("outer", RegionKind::Pane).with(Node::Region(inner))); let feeds = screen.feeds(); assert_eq!(feeds.len(), 1); assert_eq!(feeds[0].route(), Some("/slow")); } #[test] fn a_tab_strips_panels_are_not_fed_because_the_strip_asks() { // `dfbc88ce`. A labelled child's `fed_by` is the tab's address, so a // host asking for every one of them fetches five frames for a reader // looking at one. Measured on MNW's library page, which is the screen // that found this. let screen = Screen::list_detail("Library", false).with( Slot::new("tab-content", RegionKind::TabGroup) .frame( "Purchases", Node::Region( Slot::new("purchases", RegionKind::Pane) .fed_by(Action::get("/library/tabs/purchases")), ), ) .frame( "Feed", Node::Region( Slot::new("feed", RegionKind::Pane) .fed_by(Action::get("/library/tabs/feed")), ), ) .showing_one(0), ); assert!(screen.feeds().is_empty(), "{:?}", screen.feeds()); } #[test] fn a_carousels_frames_were_never_fed_and_still_are_not() { // The unlabelled half of the same region. Nothing here changed; the // assertion exists so the tab rule cannot be widened into one that // silences an ordinary nested feed. let screen = Screen::list_detail("Project", false).with( Slot::new("detail", RegionKind::TabGroup) .with(Node::Region(Slot::new("one", RegionKind::Pane))) .with(Node::Region( Slot::new("two", RegionKind::Pane).fed_by(Action::get("/slow")), )) .showing_one(0), ); let feeds = screen.feeds(); assert_eq!(feeds.len(), 1, "an unlabelled child still asks for itself"); assert_eq!(feeds[0].route(), Some("/slow")); } #[test] fn a_region_inside_a_panel_that_arrived_still_asks_for_itself() { // The walk descends past the panel, because a panel that has arrived can // hold something genuinely slow and that thing is nobody's tab. let panel = Slot::new("purchases", RegionKind::Pane) .fed_by(Action::get("/library/tabs/purchases")) .with(Node::Region( Slot::new("payouts", RegionKind::Pane).fed_by(Action::get("/slow")), )); let screen = Screen::list_detail("Library", false).with( Slot::new("tab-content", RegionKind::TabGroup) .frame("Purchases", Node::Region(panel)) .frame("Feed", Node::Region(Slot::new("feed", RegionKind::Pane))) .showing_one(0), ); let feeds = screen.feeds(); assert_eq!(feeds.len(), 1); assert_eq!(feeds[0].route(), Some("/slow")); } #[test] fn a_panel_names_what_asks_for_it_only_while_it_is_empty() { // `asked_for` is what a retained-screen host reads when a tab is // pressed. A panel already read is not asked for again, which is what // going back to a tab means. let empty = Slot::new("feed", RegionKind::Pane).fed_by(Action::get("/library/tabs/feed")); assert_eq!( empty.asked_for().and_then(Action::route), Some("/library/tabs/feed") ); let read = empty.clone().with(Node::text("what your creators posted")); assert!(read.asked_for().is_none()); } #[test] fn a_local_destination_is_no_route_and_no_address() { // The three answers a renderer asks for, and the reason the third one // is empty rather than absent: `as_str` is for rendering, and there is // nothing to render. A renderer reading it for a local action has // skipped a branch, which is what the empty string makes visible // instead of an address that half-works. let action = Action::local(); assert_eq!(action.destination.route(), None); assert_eq!(action.destination.as_str(), ""); assert!(action.destination.is_local()); // Not external. The two are the whole of "not a route this app answers" // and they are opposite kinds of not: one leaves and is still a // request, the other asks nothing at all. assert!(!action.destination.is_external()); assert!(!Destination::External("https://example.invalid".into()).is_local()); assert!(!Destination::Route("/x".into()).is_local()); } #[test] fn a_local_action_still_carries_what_the_behaviour_acts_on() { // Which suggestion was picked is a value, not an address, so it rides // where every other value does. Losing it here would leave the mark // saying something happens and nothing saying to what. let action = Action::local().with("choice", "ada"); assert_eq!(action.params.get("choice"), Some("ada")); assert_eq!(action.destination, Destination::Local); } #[test] fn only_the_hybrid_renderer_is_obliged_to_read_the_mark() { use crate::Renderer; assert!(Renderer::Hybrid.reads_locality()); assert!(!Renderer::Client.reads_locality()); } /// The four shapes, and the one convention they share: a control holding /// an empty string is holding nothing, which is what an unticked box is on /// every host. #[test] fn a_condition_reads_an_empty_value_as_nothing_held() { let ticked = Reveal::ticked("pwyw"); assert!(ticked.satisfied_by(Some("on"))); assert!(!ticked.satisfied_by(Some(""))); assert!(!ticked.satisfied_by(None)); let unticked = Reveal::unticked("pwyw"); assert!(unticked.satisfied_by(None)); assert!(unticked.satisfied_by(Some(""))); assert!(!unticked.satisfied_by(Some("on"))); let custom = Reveal::holding("license", "custom"); assert!(custom.satisfied_by(Some("custom"))); assert!(!custom.satisfied_by(Some("cc-by"))); assert!(!custom.satisfied_by(None)); let zoned = Reveal::holding_one_of("tz_kind", ["floating", "zoned", "utc"]); assert!(zoned.satisfied_by(Some("zoned"))); assert!(!zoned.satisfied_by(Some("none"))); assert!(!zoned.satisfied_by(None)); } /// A region that says nothing about what reveals it is always out, which is /// what every region did before the member existed. #[test] fn a_region_with_no_condition_is_out_whatever_is_held() { let plain = Slot::group("body"); assert!(plain.revealed(None)); assert!(plain.revealed(Some("anything"))); assert_eq!(plain.watches(), None); let conditional = Slot::group("pwyw-settings").revealed_by(Reveal::ticked("pwyw")); assert_eq!(conditional.watches(), Some("pwyw")); assert!(!conditional.revealed(None)); } /// The condition is on the region and names the control, so a renderer /// asking "does this section apply" reads one member. Nothing is added to /// the control, which is the half the ruling rejected. #[test] fn the_condition_is_the_regions_and_the_control_is_untouched() { let control = Field::new(layout::FieldKind::Checkbox, "pwyw", "Pay what you want"); let form = Slot::group("pricing") .with(Node::Field(Box::new(control.clone()))) .with(Node::Region( Slot::group("pwyw-settings") .revealed_by(Reveal::ticked("pwyw")) .with(Node::section("Suggested price")), )); let section = form.find("pwyw-settings").expect("the section"); assert_eq!(section.watches(), Some("pwyw")); // The field is what it was: nothing points back at the region it // reveals, which is the back-pointer the ruling turned down. assert_eq!( control, Field::new(layout::FieldKind::Checkbox, "pwyw", "Pay what you want") ); } /// The described half of the condition: what the box was handed to the /// reader holding, wherever on the screen it sits. #[test] fn a_screen_answers_what_a_named_control_is_offered_holding() { let screen = Screen::new("Settings", layout::Arrangement::sidebar_content()) .with(Slot::group("licensing").with(Node::Form { marks: crate::stage::Marks::none(), action: Action::post("/settings"), submit: "Save".into(), fields: vec![ Field::select("license", "Licence", vec![Choice::new("custom", "Custom")]) .value("custom"), ], })) .with( Slot::group("recurrence").with(Node::Region( Slot::group("rule").with(Node::Field(Box::new( Field::new(layout::FieldKind::Text, "rrule", "Repeats") .value("FREQ=WEEKLY"), ))), )), ); assert_eq!(screen.holds("license"), Some("custom")); // Nested, because a form inside a region is where most fields are. assert_eq!(screen.holds("rrule"), Some("FREQ=WEEKLY")); // A control the screen does not carry holds nothing, which is not the // same as holding an empty value and is why this is an `Option`. assert_eq!(screen.holds("pwyw"), None); } /// A field in a row of a list is still a control a region can watch. The /// walk is exhaustive over the containers rather than over the two nodes /// that hold a field directly. #[test] fn the_walk_reaches_a_field_inside_a_row() { let row = Row::new("Repeat").part( layout::RowPart::Meta, Node::Field(Box::new( Field::new(layout::FieldKind::Checkbox, "repeats", "Repeat").value("on"), )), ); let screen = Screen::new("Task", layout::Arrangement::sidebar_content()).with( Slot::group("body").with(Node::Table { marks: crate::stage::Marks::none(), columns: Vec::new(), rows: vec![row], more: None, }), ); assert_eq!(screen.holds("repeats"), Some("on")); } /// The eight sites the member was measured against, each said as one /// region with one condition. Six MNW toggles and two goingson ones, and /// every one of them is a `window.` function today. #[test] fn the_eight_measured_sites_are_describable() { // MNW, static/actions-tabs.js and actions-partials.js. let pwyw = Slot::group("pwyw-settings").revealed_by(Reveal::ticked("pwyw")); let license = Slot::group("dash-custom-license") .revealed_by(Reveal::holding("license_kind", "custom")); let keys = Slot::group("license-keys-section").revealed_by(Reveal::ticked("license_keys")); let trial = Slot::group("trial-details").revealed_by(Reveal::ticked("trial")); let promo = Slot::group("promo-fields").revealed_by(Reveal::holding("promo_type", "percent")); let offset = Slot::group("offset-input").revealed_by(Reveal::holding_one_of( "placement_position", ["before", "after"], )); assert!(pwyw.revealed(Some("on")) && !pwyw.revealed(None)); assert!(license.revealed(Some("custom")) && !license.revealed(Some("cc-by"))); assert!(keys.revealed(Some("on")) && !keys.revealed(None)); assert!(trial.revealed(Some("on")) && !trial.revealed(None)); assert!(promo.revealed(Some("percent")) && !promo.revealed(Some("fixed"))); assert!(offset.revealed(Some("after")) && !offset.revealed(Some("inline"))); // goingson: the zone picker, out on three of the four kinds, and the // recurrence detail, out while the rule is anything at all. let zone = Slot::group("tz-config").revealed_by(Reveal::holding_one_of( "tz_kind", ["floating", "zoned", "utc"], )); // `edit_fields` offers four patterns and grows a second form under any // of them, which is the same shape as the zone picker: the detail is // out on the patterns and away on "none". let recurrence = Slot::group("recurrence-detail").revealed_by(Reveal::holding_one_of( "recurrence", ["daily", "weekly", "monthly", "yearly"], )); assert!(zone.revealed(Some("zoned"))); assert!(!zone.revealed(Some("none"))); assert!(recurrence.revealed(Some("weekly"))); assert!(!recurrence.revealed(Some("none"))); assert!(!recurrence.revealed(None)); } /// Found by the consumer `079a011e` was ruled for: a form's questions are /// a flat list, so a single conditional question inside one has nowhere to /// put the fact a region would carry. #[test] fn one_question_inside_a_form_can_say_what_reveals_it() { // goingson's `initTzKindConfig`: the box is out on one of the kinds. let zone = Field::new(layout::FieldKind::Text, "timezone", "Anchored to") .revealed_by(Reveal::holding("tz_kind", "local")); assert_eq!(zone.watches(), Some("tz_kind")); assert!(zone.revealed(Some("local"))); assert!(!zone.revealed(Some("relative"))); assert!(!zone.revealed(None)); // The ordinary question says nothing and is always asked, which is // every other field in the tree. let title = Field::new(layout::FieldKind::Text, "title", "Title"); assert_eq!(title.watches(), None); assert!(title.revealed(None)); // A slot of a repeating question does not carry the condition: the // question does, and a renderer that has reached the slots has already // answered it once for the whole group. let repeating = Field::new(layout::FieldKind::Number, "reminder", "Reminder") .repeating(Repeat::answered(["300"])) .revealed_by(Reveal::ticked("remind")); assert_eq!(repeating.watches(), Some("remind")); assert_eq!(repeating.instance(0).watches(), None); } /// The wire naming, picked once here so the three renderers and whatever /// reads the submission back cannot disagree. #[test] fn a_slot_submits_under_its_question_and_its_index() { assert_eq!(Repeat::at("reminder", 0), "reminder[0]"); assert_eq!(Repeat::at("reminder", 11), "reminder[11]"); assert_eq!( Repeat::instance_of("reminder[2]"), Some(("reminder", 2usize)) ); // An ordinary field name is not a slot, which is what makes it safe to // ask this of any name a host is holding. assert_eq!(Repeat::instance_of("reminder"), None); // Nor is anything this did not write. assert_eq!(Repeat::instance_of("reminder[]"), None); assert_eq!(Repeat::instance_of("reminder[ 1]"), None); assert_eq!(Repeat::instance_of("reminder[+1]"), None); assert_eq!(Repeat::instance_of("reminder[2"), None); assert_eq!(Repeat::instance_of("reminder[two]"), None); } /// One slot is an ordinary field, so every renderer draws it with what it /// already does to a field. The value and the error are the slot's; the /// question's own error is not any slot's. #[test] fn a_slot_is_the_question_under_its_own_name() { let mut field = Field::new(layout::FieldKind::Number, "reminder", "Reminder") .repeating(Repeat::answered(["300", "900"]).wrong(1, "Must be positive")); field.error = Some("At most eight".into()); field.min = Some("0".into()); let second = field.instance(1); assert_eq!(second.name, "reminder[1]"); assert_eq!(second.label, "Reminder 2"); assert_eq!(second.value.as_deref(), Some("900")); assert_eq!(second.error.as_deref(), Some("Must be positive")); // Carried through: the question is what repeats, so everything it said // about itself holds for every slot. assert_eq!(second.kind, layout::FieldKind::Number); assert_eq!(second.min.as_deref(), Some("0")); // And a slot cannot recurse into slots of its own. assert!(second.repeats.is_none()); assert_eq!(field.instance(0).error, None); // A slot past the end is an empty box under the right name, which is // exactly what a slot the reader has just added is. let added = field.instance(2); assert_eq!(added.name, "reminder[2]"); assert_eq!(added.value, None); assert_eq!(added.error, None); } /// The floor, the ceiling, and how many slots stand before anyone touches /// anything. Every renderer asks these rather than doing the arithmetic, so /// the three of them offer the same controls. #[test] fn the_floor_and_the_ceiling_say_which_controls_are_offered() { let capped = Repeat::answered(["300", "900"]).most(2); assert_eq!(capped.standing(), 2); assert!(!capped.more(2)); assert!(capped.more(1)); assert!(capped.fewer(1)); let floored = Repeat::answered(["ana"]).least(1); assert!(!floored.fewer(1)); assert!(floored.fewer(2)); assert!(floored.more(9)); // Zero answers is a real state: the add control alone. let none = Repeat::new(); assert_eq!(none.standing(), 0); assert!(none.more(0)); assert!(!none.fewer(0)); // A question that must be answered twice opens with two boxes rather // than with none and a refusal on submit. assert_eq!(Repeat::new().least(2).standing(), 2); } /// A refusal naming a slot the form did not offer is worth seeing on the /// screen rather than being dropped. #[test] fn an_error_reaches_a_slot_past_the_ones_described() { let repeat = Repeat::answered(["300"]).wrong(2, "Must be positive"); assert_eq!(repeat.instances.len(), 3); assert_eq!(repeat.holds(0), Some("300")); assert_eq!(repeat.holds(1), None); assert_eq!(repeat.amiss(2), Some("Must be positive")); } /// The consumer, said in the vocabulary: goingson `8fdb814c`'s event form, /// against `Event.reminder_offsets_seconds` and the cap /// `sanitize_reminder_offsets` enforces. #[test] fn the_reminders_question_is_describable() { let held: Vec = vec![300, 900, 3600]; let field = Field::new(layout::FieldKind::Number, "reminder", "Reminder").repeating( Repeat::answered(held.iter().map(i64::to_string)) .most(8) .adding("Add reminder"), ); assert_eq!(field.slots(), 3); let names: Vec = (0..field.slots()) .map(|at| field.instance(at).name) .collect(); assert_eq!(names, ["reminder[0]", "reminder[1]", "reminder[2]"]); // Which is one submit carrying three values under one question, not // three submits and not one control taking a set. assert!(!field.multiple); } /// An ordinary field answers the repeating questions the way it always did, /// so nothing that loops over slots has to branch first. #[test] fn a_field_that_repeats_nothing_stands_in_one_slot() { let field = Field::new(layout::FieldKind::Text, "title", "Title"); assert!(field.repeats.is_none()); assert_eq!(field.slots(), 1); } #[test] fn a_part_of_a_slot_submits_under_the_slot_and_its_own_name() { assert_eq!(Repeat::part_at("file", 0, "size"), "file[0].size"); assert_eq!(Repeat::part_at("file", 11, "name"), "file[11].name"); } #[test] fn a_parts_wire_name_comes_back_apart_and_a_bare_slots_does_not() { assert_eq!(Repeat::part_of("file[0].size"), Some(("file", 0, "size"))); // The two readers never both answer, which is what makes it safe to // ask either of any name a host is holding. assert_eq!(Repeat::part_of("file[0]"), None); assert_eq!(Repeat::instance_of("file[0].size"), None); assert_eq!(Repeat::part_of("file"), None); // Refused rather than parsed loosely, for `instance_of`'s reason. assert_eq!(Repeat::part_of("file[0]."), None); assert_eq!(Repeat::part_of("file[0].a.b"), None); assert_eq!(Repeat::part_of("file[x].size"), None); } #[test] fn a_grouped_slot_draws_one_field_per_question_and_an_ordinary_one_draws_itself() { let ordinary = Field::new(layout::FieldKind::Text, "reminder", "Reminder") .repeating(Repeat::answered(["300", "900"])); assert_eq!(ordinary.instance_fields(0).len(), 1); assert_eq!(ordinary.instance_fields(0)[0].name, "reminder[0]"); let grouped = Field::new(layout::FieldKind::Text, "file", "File").repeating( Repeat::new() .of([Question::new("name", "Name"), Question::new("size", "Size")]) .instances_of([Instance::grouped([ Answer::new("track.wav"), Answer::new("4.2 MB"), ])]), ); let slots = grouped.instance_fields(0); assert_eq!(slots.len(), 2); assert_eq!(slots[0].name, "file[0].name"); assert_eq!(slots[0].label, "Name"); assert_eq!(slots[0].value.as_deref(), Some("track.wav")); assert_eq!(slots[1].name, "file[0].size"); assert_eq!(slots[1].value.as_deref(), Some("4.2 MB")); } #[test] fn a_slot_the_reader_just_added_is_the_questions_with_nothing_in_them() { // What the blank a renderer offers is built from: the questions live on // the repeat, so a slot past the end still knows what it is asking. let grouped = Field::new(layout::FieldKind::Text, "file", "File").repeating( Repeat::new().of([Question::new("name", "Name"), Question::new("size", "Size")]), ); let slots = grouped.instance_fields(3); assert_eq!(slots.len(), 2); assert_eq!(slots[0].name, "file[3].name"); assert_eq!(slots[0].label, "Name"); assert_eq!(slots[0].value, None); assert_eq!(slots[0].error, None); } #[test] fn a_slots_own_message_and_one_of_its_questions_are_different_facts() { let slot = Instance::grouped([Answer::new("track.wav").wrong("Already uploaded")]) .getting(Progress::Failed); assert_eq!(slot.part(0).error.as_deref(), Some("Already uploaded")); // The slot's own error stays free for what is wrong with the slot // rather than with one of its questions. assert_eq!(slot.error, None); assert!(slot.progress.failed()); assert!(!slot.progress.busy()); } #[test] fn a_slot_nothing_is_happening_to_is_the_default() { assert_eq!(Instance::blank().progress, Progress::Idle); assert_eq!(Instance::new("300").progress, Progress::Idle); assert!(!Progress::Idle.busy()); assert!(Progress::Working(None).busy()); assert!(Progress::Working(Some(Meter::new(1, 4))).busy()); assert!(!Progress::Done.busy()); } #[test] fn a_question_whose_slots_come_from_elsewhere_offers_no_control_of_its_own() { let ordinary = Repeat::new(); assert_eq!(ordinary.add.label(), Some("Add")); assert_eq!(ordinary.add.from(), None); let named = Repeat::new().adding("Add reminder"); assert_eq!(named.add.label(), Some("Add reminder")); // The queue: the picker above the table makes the slots, so there is // no blank a reader could fill and every renderer draws no control. let queue = Repeat::new().added_by("version-files"); assert_eq!(queue.add.label(), None); assert_eq!(queue.add.from(), Some("version-files")); } #[test] fn a_slot_named_by_what_is_in_it_is_not_numbered() { let field = Field::new(layout::FieldKind::Text, "version-file", "File").repeating( Repeat::new().instances_of([ Instance::new("macOS (arm)").called("track-arm.dmg"), Instance::new("Linux (x86_64)"), ]), ); // What is in it, for a queue whose slots differ by their file. assert_eq!(field.instance(0).label, "track-arm.dmg"); // And the ordinal still, for a slot that differs only by position. assert_eq!(field.instance(1).label, "File 2"); // A slot past the end is a blank the reader just made, and it has no // name of its own yet. assert_eq!(field.instance(2).label, "File 3"); } #[test] fn an_owned_option_carries_its_second_line_across_the_conversion() { // `5e21dcfc`. The conversion is what keeps the mirror from drifting, so // the member is asserted through it rather than on the struct: a field // added over there and forgotten here compiles until something reads // it, and this is the something. let tier = Choice::new("24", "Small Files") .detailing("$24/mo. Fits audio, plugins, binaries.") .unless("Sold out while the founder window is open."); let borrowed = tier.as_layout(); assert_eq!(borrowed.value, "24"); assert_eq!(borrowed.label, "Small Files"); assert_eq!( borrowed.detail, Some("$24/mo. Fits audio, plugins, binaries.") ); assert_eq!( borrowed.unavailable, Some("Sold out while the founder window is open.") ); assert!(!borrowed.available()); // An ordinary option says neither, which is nearly all of them. let free = Choice::plain("free"); let plain = free.as_layout(); assert_eq!(plain.detail, None); assert!(plain.available()); } #[test] fn a_regions_questions_are_the_ones_inside_it_at_any_depth() { // `cb62a9dc`. The one walk all three renderers read, so "the dials // inside this panel" cannot mean three things. let calculator = Slot::group("calculator") .across(Run::new(layout::Fallback::Shed).beside( Node::field(Field::new(layout::FieldKind::Text, "tier", "Tier")), layout::Priority::Essential, )) .with(Node::field(Field::new( layout::FieldKind::Number, "item_price", "Price", ))) .with(Node::Form { marks: crate::stage::Marks::none(), action: Action::post("/save"), submit: "Save".into(), fields: vec![Field::new(layout::FieldKind::Number, "sales", "Sales")], }) .with(Node::Region(Slot::group("other").with(Node::field( Field::new(layout::FieldKind::Number, "other_pct", "Their cut"), )))) .with(Node::text("You keep $9.00")); let names: Vec<&str> = calculator .questions() .iter() .map(|field| field.name.as_str()) .collect(); // The run first, because that is draw order, then the body outside in. assert_eq!(names, ["tier", "item_price", "sales", "other_pct"]); } #[test] fn only_the_regions_that_ask_are_walked_for() { let screen = Screen::sidebar_content("Pricing") .with(Slot::group("notes").with(Node::text("Nothing to ask"))) .with( Slot::group("calculator") .with(Node::Region(Slot::group("panel").consulting(Consult::new( Action::get("/nested").replacing("x"), )))) .consulting(Consult::new( Action::get("/pricing/compare").replacing("results"), )), ); let asking: Vec<&str> = screen .consulting() .iter() .map(|slot| slot.id.as_str()) .collect(); // Outside in, and a region that asks nothing is not here at all. assert_eq!(asking, ["calculator", "panel"]); } }