max / makeover-layout
- Co-Authored-By
- Claude Opus 5 (1M context) <noreply@anthropic.com>
2 files changed,
+454 insertions,
-3 deletions
| @@ -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 |
| @@ -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