Skip to main content

max / makeover-layout

0.30.0: a field whose value is markdown, and a dimension a set narrows by Two members batched into one release, on 0.11.0's precedent: pre-1.0 a minor is breaking, the cascade below this crate is seven repos, and paying it twice in a week for two unrelated words is what the batching avoids. FieldKind::Rich (task f8ad0b32). The editing counterpart of prose already carried as markdown source, and renderable everywhere for the same reason the carrying is: editing markdown is editing text, so no host has to refuse. It buys a renderer permission to offer a preview or a syntax pass, and buys a host reading the value back the knowledge of what it holds; a renderer with neither draws a textarea. It says nothing about when the value is saved -- autosave is a clock. Measured against four MNW section editors that are one shape written four times. FieldKind::multiline arrives with it, since two kinds now answer yes and a matches! per renderer is the pair drifting apart. Facet, Selecting, FacetValue and Standing (task 34950ade). MNW's discover page filters six ways through six mechanisms, and its filter rows carry a tick box AND a chevron only because a tag's selection and its browse position were held separately. One word covers all six. Selecting::Subtree is what made it an enum rather than a bool: a tree's selection is branches taken and branches pruned, resolved nearest-ancestor-first, which no flat mode can say -- and one gesture doing browsing and filtering together is what lets the second mechanism go. Standing has four members rather than a bool for the same reason: a value in force because an ancestor is, is not a value somebody picked, and a renderer given a bool either marks every descendant of a taken branch or marks none. Two things the task left open, settled here. Counts are optional, on Awaiting::amount's rule -- state a number when it was measured, and nothing when it was not, because a written zero reads as "none of them". And the type lives here with the router carrying the owned half, which is Column's split. Deliberately wider than discover: audiofiles' library browser and goingson's filters are the same shape. Also corrects the header, which labelled the Awaiting release 0.28.3. It shipped as 0.29.1, and the row-group releases landed on top of it in between.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-18 20:25 UTC
Signed with PGP, not checked
Commit: 6d778c51a9cb9eb3d140037d3a37e8831bb3d97f
Parent: 3b0753f
2 files changed, +454 insertions, -3 deletions
M Cargo.toml +1 -1
@@ -1,6 +1,6 @@
1 1 [package]
2 2 name = "makeover-layout"
3 - version = "0.29.1"
3 + version = "0.30.0"
4 4 edition = "2024"
5 5 # One copy of this vocabulary per dependency graph, enforced by cargo rather
6 6 # than by remembering. Two versions of a description layer in one build means
M src/lib.rs +453 -2
@@ -323,7 +323,7 @@
323 323 //! empty box and the instruction lived on a disabled button elsewhere. The
324 324 //! renderers moved, not the description.
325 325 //!
326 - //! 0.28.3 adds [`Awaiting`], which is the sentence [`Readiness`] could say about
326 + //! 0.29.1 adds [`Awaiting`], which is the sentence [`Readiness`] could say about
327 327 //! a region and could not say about a control. A described screen could state
328 328 //! that a list was on its way and could not state that the button just pressed
329 329 //! is doing the thing it was pressed for, so every renderer's in-flight
@@ -346,10 +346,38 @@
346 346 //!
347 347 //! A patch release for a new member, which is the 0.27.5 precedent rather than a
348 348 //! new rule: nothing existing changed shape, so every consumer already asking
349 - //! for 0.28 keeps resolving and the suite below this crate does not have to move
349 + //! for 0.29 keeps resolving and the suite below this crate does not have to move
350 350 //! for a type only quasi reads. The minor releases above were minor because they
351 351 //! also narrowed something.
352 352 //!
353 + //! 0.30.0 is two members batched into one release, which is 0.11.0's precedent
354 + //! and its reasoning: pre-1.0 a minor is breaking, the cascade below this crate
355 + //! is seven repos, and paying that twice in a week for two unrelated words is
356 + //! the tax the batching exists to avoid.
357 + //!
358 + //! - [`FieldKind::Rich`], a field whose value is markdown source. The editing
359 + //! counterpart of prose already carried as markdown, and it is renderable
360 + //! everywhere for the reason the carrying is: editing markdown is editing
361 + //! text. It buys a renderer permission to offer a preview or a syntax pass and
362 + //! buys a host reading the value back the knowledge of what it holds; a
363 + //! renderer with neither draws a textarea. It says nothing about when the
364 + //! value is saved, because autosave is a clock. Measured against four MNW
365 + //! section editors that are one shape written four times.
366 + //! [`FieldKind::multiline`] arrives with it, since the pair is now two members
367 + //! every renderer has to ask about.
368 + //! - [`Facet`], [`Selecting`], [`FacetValue`] and [`Standing`]: a named
369 + //! dimension a set is narrowed by. MNW's discover page filters six ways
370 + //! through six mechanisms, and its filter rows carry a tick box *and* a
371 + //! chevron only because a tag's selection and a tag's browse position were
372 + //! held separately. One word covers all six, and [`Selecting::Subtree`] is the
373 + //! member that made it an enum rather than a bool: a tree's selection is
374 + //! branches taken and branches pruned, which no flat mode can express, and
375 + //! making one gesture do browsing and filtering together is what lets the
376 + //! second mechanism go. [`Standing`] has four members rather than a bool for
377 + //! the tree's sake — a value in force because an ancestor is, is not a value
378 + //! somebody picked. Deliberately wider than that one page: audiofiles' library
379 + //! browser and goingson's filters are the same shape.
380 + //!
353 381 //! # Reach, focus and the focus ring
354 382 //!
355 383 //! Three terms, and no others, for what 0.19.0 moved out of the description.
@@ -3210,6 +3238,36 @@
3210 3238 DateTime,
3211 3239 /// Several lines of text.
3212 3240 Textarea,
3241 + /// Several lines of text the user writes markdown in.
3242 + ///
3243 + /// The editing counterpart of prose a description carries as markdown
3244 + /// source, and the reason it can exist at all is the same one that lets the
3245 + /// source be carried: editing markdown is editing text, so a terminal, an
3246 + /// immediate-mode host and a webview all have an honest answer, and none of
3247 + /// them has to refuse. A kind that meant "rich text" in the WYSIWYG sense
3248 + /// would have been a document model, and two of the three hosts would have
3249 + /// had to draw something they cannot.
3250 + ///
3251 + /// What the mark buys over [`Textarea`](Self::Textarea) is that a renderer
3252 + /// may offer the affordances markdown has and plain text does not — a
3253 + /// preview, a syntax pass, a monospaced face for the source — and that a
3254 + /// host reading the value back knows what it is holding. A renderer with
3255 + /// none of that draws a textarea, which is why this is additive rather than
3256 + /// a second control.
3257 + ///
3258 + /// It says nothing about **when** the value is saved. Autosave is a clock,
3259 + /// clocks are not described here, and the four MNW editors this was measured
3260 + /// against each keep their own.
3261 + ///
3262 + /// Sanitising stays where it already is for markdown that is only displayed:
3263 + /// with the renderer, at the point markup is produced. Being described is
3264 + /// not a safety property, and a host with its own sanitiser and its own
3265 + /// content-security posture still owns both.
3266 + ///
3267 + /// Added 0.30.0, `f8ad0b32`, from four hand-written MNW section editors —
3268 + /// `project-sections.js`, `blog-editor.js`, `partial-item-text-editor.js`
3269 + /// and `wizard-item-sections.js` — which are one shape written four times.
3270 + Rich,
3213 3271 /// One of a fixed set, offered behind a control that shows one at a time.
3214 3272 Select,
3215 3273 /// One of a fixed set, with every option on screen at once.
@@ -3325,6 +3383,18 @@
3325 3383 pub const fn offers_options(self) -> bool {
3326 3384 matches!(self, Self::Select | Self::Radio)
3327 3385 }
3386 +
3387 + /// Whether the value runs to more than one line.
3388 + ///
3389 + /// Named once here for [`temporal`](Self::temporal)'s reason: two kinds
3390 + /// answer yes, every renderer has to ask it before it can size anything,
3391 + /// and a `matches!` per renderer is the pair drifting apart one member at a
3392 + /// time. What a host does with the markdown, if anything, it reads from the
3393 + /// kind itself; this is only whether one line is enough.
3394 + #[must_use]
3395 + pub const fn multiline(self) -> bool {
3396 + matches!(self, Self::Textarea | Self::Rich)
3397 + }
3328 3398 }
3329 3399
3330 3400 /// One option offered by a field [`FieldKind::offers_options`] accepts.
@@ -3973,10 +4043,390 @@
3973 4043 }
3974 4044 }
3975 4045
4046 + /// A named dimension a set can be narrowed by.
4047 + ///
4048 + /// One word for six things that were six mechanisms. MNW's discover page filters
4049 + /// by free text, a flat any-of over item types, a tree of tags, a numeric range
4050 + /// over price, a nested one-of over AI tier, and a browse position in the tag
4051 + /// tree held separately from the tag selection — and the last two being separate
4052 + /// is the whole reason a filter row there needs a tick box *and* a chevron. The
4053 + /// panel is a mixed bag of hand-written controls because nothing named the thing
4054 + /// they all are.
4055 + ///
4056 + /// Deliberately wider than that one page. audiofiles' library browser and
4057 + /// goingson's filters are the same shape, and a word that only fitted discover
4058 + /// would be discover's markup with a neutral name on it.
4059 + ///
4060 + /// # What it does not say
4061 + ///
4062 + /// **What picking a value calls.** This crate names no address, so a facet is
4063 + /// paired with routes the way a column's [`sortable`](Column::sortable) flag is
4064 + /// paired with what reordering calls.
4065 + ///
4066 + /// **How a tree is drawn.** Indented rows, a column of panes, a breadcrumb and a
4067 + /// list: all four are honest renderings of the same described facet, and a
4068 + /// terminal will not pick the same one a browser does. [`FacetValue::depth`] is
4069 + /// what a renderer needs to draw any of them; the choice is not described.
4070 + ///
4071 + /// **Which values to show.** A tag tree has thousands of nodes and a panel shows
4072 + /// a handful. Deciding which handful is the app's — it is the same question as
4073 + /// which rows go in a table, and no table member answers it either.
4074 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4075 + #[non_exhaustive]
4076 + pub struct Facet<'a> {
4077 + /// What the dimension is called, as the user reads it.
4078 + pub name: &'a str,
4079 + /// How many of its values may be in force, and in what shape.
4080 + pub mode: Selecting,
4081 + /// The values on offer, in the order they are drawn.
4082 + ///
4083 + /// A [`Selecting::Text`] facet has none: the value is whatever was typed,
4084 + /// and a description that listed the possible strings would be listing the
4085 + /// corpus. A [`Selecting::Range`] facet has none either, for the reason
4086 + /// [`FieldKind::Range`] takes bounds rather than options — the ends are the
4087 + /// question and the values between them are not enumerable.
4088 + pub values: &'a [FacetValue<'a>],
4089 + }
4090 +
4091 + impl<'a> Facet<'a> {
4092 + /// A dimension with values to pick from.
4093 + #[must_use]
4094 + pub const fn new(name: &'a str, mode: Selecting, values: &'a [FacetValue<'a>]) -> Self {
4095 + Self { name, mode, values }
4096 + }
4097 +
4098 + /// Whether the facet is narrowing the set right now.
4099 + ///
4100 + /// The question a renderer asks to decide whether to offer a way out of it,
4101 + /// and the reason it is derived rather than carried: a facet with nothing
4102 + /// standing is unengaged by construction, so a member saying so could
4103 + /// disagree with the values beside it. [`Standing::Inherited`] does not
4104 + /// count — something further up is what is doing the narrowing, and clearing
4105 + /// a child that was never picked clears nothing.
4106 + ///
4107 + /// Always false for [`Selecting::Text`] and [`Selecting::Range`], which
4108 + /// carry no values. A host that wants a clear affordance on those knows
4109 + /// whether its own box is empty; the description does not hold the typed
4110 + /// string.
4111 + #[must_use]
4112 + pub fn engaged(&self) -> bool {
4113 + self.values.iter().any(|value| value.standing.is_picked())
4114 + }
4115 +
4116 + /// The deepest value in the facet, or zero when it is flat.
4117 + ///
4118 + /// What an indenting renderer needs to reserve a gutter before it draws the
4119 + /// first row, which is "First paint is final paint" applied to a tree: a
4120 + /// gutter widened as deeper values arrive is the reflow that rule forbids.
4121 + #[must_use]
4122 + pub fn reach(&self) -> u8 {
4123 + self.values
4124 + .iter()
4125 + .map(|value| value.depth)
4126 + .max()
4127 + .unwrap_or(0)
4128 + }
4129 + }
4130 +
4131 + /// How many of a [`Facet`]'s values may be in force, and in what shape.
4132 + ///
4133 + /// Five, and the fifth is what made this an enum rather than a bool. `one-of`,
4134 + /// `any-of`, a range and free text are the four a form vocabulary already has in
4135 + /// [`FieldKind`]; a tree's selection is none of them, and describing tags as
4136 + /// any-of was what forced browsing to be a second mechanism beside filtering.
4137 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4138 + #[non_exhaustive]
4139 + pub enum Selecting {
4140 + /// Exactly one value, and picking another replaces it.
4141 + ///
4142 + /// MNW's AI tier, whose three options are nested ranges rather than
4143 + /// independent values, so two of them at once means nothing.
4144 + OneOf,
4145 + /// Any number of values, each independent of the others.
4146 + AnyOf,
4147 + /// A low end, a high end, or both.
4148 + ///
4149 + /// Carries no values for [`FieldKind::Range`]'s reason: the ends are the
4150 + /// question.
4151 + Range,
4152 + /// Whatever the user types.
4153 + Text,
4154 + /// A position in a tree, edited by taking branches in and pruning branches
4155 + /// out.
4156 + ///
4157 + /// The one mode that is not reducible to the others, and the one gesture
4158 + /// that replaced two. Picking a value narrows the set to it *and* reveals
4159 + /// its children, so browsing a tree and filtering by it stop being separate
4160 + /// mechanisms with separate state. What a selection then is: a set of
4161 + /// branches taken and a set pruned, resolved nearest-ancestor-first, so
4162 + /// `music` in and `music/synths` out is sayable and no flat mode can say it.
4163 + ///
4164 + /// Resolution happens in the app, and what reaches a renderer is the
4165 + /// [`Standing`] each drawn value ended up with. A renderer walking ancestors
4166 + /// itself would be a renderer that can disagree with the results beside it.
4167 + Subtree,
4168 + }
4169 +
4170 + impl Selecting {
4171 + /// Whether the mode picks from values the description lists.
4172 + ///
4173 + /// False for [`Text`](Self::Text) and [`Range`](Self::Range), which are the
4174 + /// two whose answer is not one of a set. A renderer asks this before it
4175 + /// looks at [`Facet::values`], the way it asks
4176 + /// [`FieldKind::offers_options`] before it looks at [`Field::options`].
4177 + #[must_use]
4178 + pub const fn offers_values(self) -> bool {
4179 + matches!(self, Self::OneOf | Self::AnyOf | Self::Subtree)
4180 + }
4181 +
4182 + /// Whether a value can be pruned as well as picked.
4183 + ///
4184 + /// [`Subtree`](Self::Subtree) alone. Excluding a value from a flat facet is
4185 + /// the same fact as not picking it, so an exclude affordance there would be
4186 + /// a second control for a state the first one already holds.
4187 + #[must_use]
4188 + pub const fn prunes(self) -> bool {
4189 + matches!(self, Self::Subtree)
4190 + }
4191 +
4192 + /// Whether picking a second value keeps the first.
4193 + #[must_use]
4194 + pub const fn accumulates(self) -> bool {
4195 + matches!(self, Self::AnyOf | Self::Subtree)
4196 + }
4197 + }
4198 +
4199 + /// One value a [`Facet`] offers, as it currently stands.
4200 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
4201 + #[non_exhaustive]
4202 + pub struct FacetValue<'a> {
4203 + /// What it is called, as the user reads it.
4204 + pub label: &'a str,
4205 + /// How many members of the set carry it.
4206 + ///
4207 + /// Optional, and settled that way rather than made mandatory: a count is a
4208 + /// measured fact the app may not have. Counting a tag subtree under an
4209 + /// active text search is a second query, and an app that will not pay for it
4210 + /// should be able to describe the facet anyway rather than write a zero that
4211 + /// reads as "none of them". That is [`Awaiting::amount`]'s rule in a second
4212 + /// place — state a number when it was measured, and nothing when it was not.
4213 + pub count: Option<u64>,
4214 + /// Whether it is narrowing the set, and how it came to be.
4215 + pub standing: Standing,
4216 + /// How far down the tree it sits, counting from zero at the root.
4217 + ///
4218 + /// Always zero for a flat facet, which is what makes an indenting renderer
4219 + /// one code path rather than two. A renderer that draws no tree at all still
4220 + /// reads this, since a value's depth is what distinguishes two same-named
4221 + /// leaves under different parents.
4222 + pub depth: u8,
4223 + /// Whether taking it reveals values under it.
4224 + ///
4225 + /// Distinct from having a nonzero [`depth`](Self::depth): a leaf deep in the
4226 + /// tree branches no further, and a root with children does. Both facts are
4227 + /// needed and neither implies the other, which is why the pair is two
4228 + /// members rather than one count.
4229 + pub branching: bool,
4230 + }
4231 +
4232 + impl<'a> FacetValue<'a> {
4233 + /// An unpicked value at the root of the facet.
4234 + #[must_use]
4235 + pub const fn new(label: &'a str) -> Self {
4236 + Self {
4237 + label,
4238 + count: None,
4239 + standing: Standing::Open,
4240 + depth: 0,
4241 + branching: false,
4242 + }
4243 + }
4244 +
4245 + /// How many members carry it, when that was measured.
4246 + #[must_use]
4247 + pub const fn counted(mut self, count: u64) -> Self {
4248 + self.count = Some(count);
4249 + self
4250 + }
4251 +
4252 + /// How it stands in the current selection.
4253 + #[must_use]
4254 + pub const fn standing(mut self, standing: Standing) -> Self {
4255 + self.standing = standing;
4256 + self
4257 + }
4258 +
4259 + /// Where it sits in the tree, and whether anything hangs off it.
4260 + #[must_use]
4261 + pub const fn at(mut self, depth: u8, branching: bool) -> Self {
4262 + self.depth = depth;
4263 + self.branching = branching;
4264 + self
4265 + }
4266 + }
4267 +
4268 + /// Whether a [`FacetValue`] is narrowing the set, and how it came to be.
4269 + ///
4270 + /// Four rather than a bool, and the two extra members are what a tree costs. A
4271 + /// pruned branch and an untaken one are not the same state — one was decided
4272 + /// against and the other was never reached — and a child under a taken parent is
4273 + /// in force without anybody having picked it. A renderer given a bool either
4274 + /// marks every descendant of a taken branch, which reads as forty deliberate
4275 + /// choices, or marks none of them, which reads as unfiltered.
4276 + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
4277 + #[non_exhaustive]
4278 + pub enum Standing {
4279 + /// Not picked, and nothing above it is either.
4280 + #[default]
4281 + Open,
4282 + /// Picked here. The set is narrowed to it and whatever hangs off it.
4283 + Taken,
4284 + /// In force because something above it was taken.
4285 + Inherited,
4286 + /// Pruned out, though something above it was taken.
4287 + ///
4288 + /// The state that only [`Selecting::Subtree`] can reach, and the reason
4289 + /// exclusion is drawn as a visible affordance beside each label rather than
4290 + /// as a modifier on the ordinary one: a gesture a terminal cannot express is
4291 + /// a gesture half the renderers would have to leave out, and an affordance
4292 + /// nothing teaches is one users do not find.
4293 + Pruned,
4294 + }
4295 +
4296 + impl Standing {
4297 + /// Whether the user decided this value, either way.
4298 + ///
4299 + /// True for [`Taken`](Self::Taken) and [`Pruned`](Self::Pruned) — both are
4300 + /// choices, and both are things a "clear this" affordance has to clear.
4301 + /// [`Inherited`](Self::Inherited) is not: clearing it clears nothing,
4302 + /// because the decision is further up.
4303 + #[must_use]
4304 + pub const fn is_picked(self) -> bool {
4305 + matches!(self, Self::Taken | Self::Pruned)
4306 + }
4307 +
4308 + /// Whether the value narrows the set in.
4309 + ///
4310 + /// [`Taken`](Self::Taken) and [`Inherited`](Self::Inherited): one was picked
4311 + /// and one came down from above, and to the set they mean the same thing.
4312 + /// The pair is named here so a renderer colouring in-force values does not
4313 + /// have to know which is which.
4314 + #[must_use]
4315 + pub const fn in_force(self) -> bool {
4316 + matches!(self, Self::Taken | Self::Inherited)
4317 + }
4318 +
4319 + /// The content intent the value takes.
4320 + ///
4321 + /// [`Pruned`](Self::Pruned) reads back a step, which is the three-tone rule
4322 + /// above rather than a new decision: a pruned branch is still a live control
4323 + /// — pressing it takes the prune off — so it may not wear `content-muted`,
4324 + /// and it is not the thing itself either.
4325 + #[must_use]
4326 + pub const fn intent(self) -> &'static str {
4327 + match self {
4328 + Self::Taken | Self::Inherited | Self::Open => "content",
4329 + Self::Pruned => "content-secondary",
4330 + }
4331 + }
4332 + }
4333 +
3976 4334 #[cfg(test)]
3977 4335 mod tests {
3978 4336 use super::*;
3979 4337
4338 + #[test]
4339 + fn a_markdown_field_is_multiline_and_offers_nothing() {
4340 + // The editing counterpart of markdown prose is still text: every host
4341 + // can draw it, which is the whole reason the kind was addable.
4342 + assert!(FieldKind::Rich.multiline());
4343 + assert!(FieldKind::Textarea.multiline());
4344 + assert!(!FieldKind::Text.multiline());
4345 + // It is not a chooser and not a moment.
4346 + assert!(!FieldKind::Rich.offers_options());
4347 + assert!(!FieldKind::Rich.temporal());
4348 + assert!(FieldKind::Rich.visible());
4349 + }
4350 +
4351 + #[test]
4352 + fn only_a_subtree_prunes_and_only_the_listing_modes_offer_values() {
4353 + // Excluding a value from a flat facet is the same fact as not picking
4354 + // it, so the affordance exists in exactly one mode.
4355 + assert!(Selecting::Subtree.prunes());
4356 + for mode in [
4357 + Selecting::OneOf,
4358 + Selecting::AnyOf,
4359 + Selecting::Range,
4360 + Selecting::Text,
4361 + ] {
4362 + assert!(!mode.prunes(), "{mode:?}");
4363 + }
4364 + // Text and Range answer with something that is not one of a set.
4365 + assert!(!Selecting::Text.offers_values());
4366 + assert!(!Selecting::Range.offers_values());
4367 + assert!(Selecting::OneOf.offers_values());
4368 + assert!(Selecting::AnyOf.accumulates());
4369 + assert!(!Selecting::OneOf.accumulates());
4370 + }
4371 +
4372 + #[test]
4373 + fn an_inherited_value_is_in_force_without_having_been_picked() {
4374 + // The distinction a bool cannot hold, and the reason Standing has four
4375 + // members: a child under a taken parent narrows the set, and clearing
4376 + // it clears nothing.
4377 + assert!(Standing::Inherited.in_force());
4378 + assert!(!Standing::Inherited.is_picked());
4379 + assert!(Standing::Taken.in_force());
4380 + assert!(Standing::Taken.is_picked());
4381 + // A prune is a decision that takes the value out.
4382 + assert!(Standing::Pruned.is_picked());
4383 + assert!(!Standing::Pruned.in_force());
4384 + assert!(!Standing::Open.is_picked());
4385 + assert!(!Standing::Open.in_force());
4386 + // A pruned branch still answers a press, so it may not read as inert.
4387 + assert_ne!(Standing::Pruned.intent(), "content-muted");
4388 + }
4389 +
4390 + #[test]
4391 + fn a_facet_is_engaged_by_a_decision_and_not_by_an_inherited_value() {
4392 + let inherited = [
4393 + FacetValue::new("music")
4394 + .standing(Standing::Taken)
4395 + .at(0, true),
4396 + FacetValue::new("synths")
4397 + .standing(Standing::Inherited)
4398 + .at(1, false),
4399 + ];
4400 + let facet = Facet::new("Tag", Selecting::Subtree, &inherited);
4401 + assert!(facet.engaged());
4402 + // The gutter an indenting renderer reserves before its first paint.
4403 + assert_eq!(facet.reach(), 1);
4404 +
4405 + let untouched = [
4406 + FacetValue::new("music").at(0, true),
4407 + FacetValue::new("synths")
4408 + .standing(Standing::Inherited)
4409 + .at(1, false),
4410 + ];
4411 + // Inherited alone is something further up doing the narrowing, and
4412 + // there is nothing further up here.
4413 + assert!(!Facet::new("Tag", Selecting::Subtree, &untouched).engaged());
4414 +
4415 + // A text facet lists nothing, so it is flat and never reads as engaged
4416 + // from its values: the typed string is not held here.
4417 + let typed = Facet::new("Search", Selecting::Text, &[]);
4418 + assert!(!typed.engaged());
4419 + assert_eq!(typed.reach(), 0);
4420 + }
4421 +
4422 + #[test]
4423 + fn a_count_is_absent_rather_than_zero_when_it_was_not_measured() {
4424 + // Awaiting::amount's rule in a second place: a written zero reads as
4425 + // "none of them", which is a different claim from "not counted".
4426 + assert_eq!(FacetValue::new("Ambient").count, None);
4427 + assert_eq!(FacetValue::new("Ambient").counted(0).count, Some(0));
4428 + }
4429 +
3980 4430 #[test]
3981 4431 fn the_four_readiness_states_are_one_axis_and_only_one_shows_content() {
3982 4432 // Mutually exclusive is the test for one enum against several fields: a
@@ -4440,6 +4890,7 @@
4440 4890 FieldKind::Tel,
4441 4891 FieldKind::Range,
4442 4892 FieldKind::Textarea,
4893 + FieldKind::Rich,
4443 4894 FieldKind::Select,
4444 4895 FieldKind::Radio,
4445 4896 FieldKind::Checkbox,
@@ -4871,6 +5322,7 @@
4871 5322 FieldKind::Text,
4872 5323 FieldKind::Number,
Lines truncated