Skip to main content

max / makeover-layout

13.1 KB · 316 lines History Blame Raw
1 use crate::Nesting;
2
3 // Names this module's prose links to, resolved for rustdoc.
4 #[allow(unused_imports)]
5 use crate::{Awaiting, Choice, Column, Field, FieldKind};
6
7 /// A named dimension a set can be narrowed by.
8 ///
9 /// One word for six things that were six mechanisms. MNW's discover page filters
10 /// by free text, a flat any-of over item types, a tree of tags, a numeric range
11 /// over price, a nested one-of over AI tier, and a browse position in the tag
12 /// tree held separately from the tag selection — and the last two being separate
13 /// is the whole reason a filter row there needs a tick box *and* a chevron. The
14 /// panel is a mixed bag of hand-written controls because nothing named the thing
15 /// they all are.
16 ///
17 /// Deliberately wider than that one page. audiofiles' library browser and
18 /// goingson's filters are the same shape, and a word that only fitted discover
19 /// would be discover's markup with a neutral name on it.
20 ///
21 /// # What it does not say
22 ///
23 /// **What picking a value calls.** This crate names no address, so a facet is
24 /// paired with routes the way a column's [`sortable`](Column::sortable) flag is
25 /// paired with what reordering calls.
26 ///
27 /// **How a tree is drawn.** Indented rows, a column of panes, a breadcrumb and a
28 /// list: all four are honest renderings of the same described facet, and a
29 /// terminal will not pick the same one a browser does. [`FacetValue::depth`] is
30 /// what a renderer needs to draw any of them; the choice is not described.
31 ///
32 /// **Which values to show.** A tag tree has thousands of nodes and a panel shows
33 /// a handful. Deciding which handful is the app's — it is the same question as
34 /// which rows go in a table, and no table member answers it either.
35 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
36 #[non_exhaustive]
37 pub struct Facet<'a> {
38 /// What the dimension is called, as the user reads it.
39 pub name: &'a str,
40 /// How many of its values may be in force, and in what shape.
41 pub mode: Selecting,
42 /// The values on offer, in the order they are drawn.
43 ///
44 /// A [`Selecting::Text`] facet has none: the value is whatever was typed,
45 /// and a description that listed the possible strings would be listing the
46 /// corpus. A [`Selecting::Range`] facet has none either, for the reason
47 /// [`FieldKind::Range`] takes bounds rather than options — the ends are the
48 /// question and the values between them are not enumerable.
49 pub values: &'a [FacetValue<'a>],
50 }
51
52 impl<'a> Facet<'a> {
53 /// A dimension with values to pick from.
54 #[must_use]
55 pub const fn new(name: &'a str, mode: Selecting, values: &'a [FacetValue<'a>]) -> Self {
56 Self { name, mode, values }
57 }
58
59 /// Whether the facet is narrowing the set right now.
60 ///
61 /// The question a renderer asks to decide whether to offer a way out of it,
62 /// and the reason it is derived rather than carried: a facet with nothing
63 /// standing is unengaged by construction, so a member saying so could
64 /// disagree with the values beside it. [`Standing::Inherited`] does not
65 /// count — something further up is what is doing the narrowing, and clearing
66 /// a child that was never picked clears nothing.
67 ///
68 /// Always false for [`Selecting::Text`] and [`Selecting::Range`], which
69 /// carry no values. A host that wants a clear affordance on those knows
70 /// whether its own box is empty; the description does not hold the typed
71 /// string.
72 #[must_use]
73 pub fn engaged(&self) -> bool {
74 self.values.iter().any(|value| value.standing.is_picked())
75 }
76
77 /// The deepest value in the facet, or zero when it is flat.
78 ///
79 /// What an indenting renderer needs to reserve a gutter before it draws the
80 /// first row, which is "First paint is final paint" applied to a tree: a
81 /// gutter widened as deeper values arrive is the reflow that rule forbids.
82 #[must_use]
83 pub fn reach(&self) -> u8 {
84 self.values
85 .iter()
86 .map(|value| value.depth.level)
87 .max()
88 .unwrap_or(0)
89 }
90 }
91
92 /// How many of a [`Facet`]'s values may be in force, and in what shape.
93 ///
94 /// Five, and the fifth is what made this an enum rather than a bool. `one-of`,
95 /// `any-of`, a range and free text are the four a form vocabulary already has in
96 /// [`FieldKind`]; a tree's selection is none of them, and describing tags as
97 /// any-of was what forced browsing to be a second mechanism beside filtering.
98 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99 #[non_exhaustive]
100 pub enum Selecting {
101 /// Exactly one value, and picking another replaces it.
102 ///
103 /// MNW's AI tier, whose three options are nested ranges rather than
104 /// independent values, so two of them at once means nothing.
105 OneOf,
106 /// Any number of values, each independent of the others.
107 AnyOf,
108 /// A low end, a high end, or both.
109 ///
110 /// Carries no values for [`FieldKind::Range`]'s reason: the ends are the
111 /// question.
112 Range,
113 /// Whatever the user types.
114 Text,
115 /// A position in a tree, edited by taking branches in and pruning branches
116 /// out.
117 ///
118 /// The one mode that is not reducible to the others, and the one gesture
119 /// that replaced two. Picking a value narrows the set to it *and* reveals
120 /// its children, so browsing a tree and filtering by it stop being separate
121 /// mechanisms with separate state. What a selection then is: a set of
122 /// branches taken and a set pruned, resolved nearest-ancestor-first, so
123 /// `music` in and `music/synths` out is sayable and no flat mode can say it.
124 ///
125 /// Resolution happens in the app, and what reaches a renderer is the
126 /// [`Standing`] each drawn value ended up with. A renderer walking ancestors
127 /// itself would be a renderer that can disagree with the results beside it.
128 Subtree,
129 }
130
131 impl Selecting {
132 /// Whether the mode picks from values the description lists.
133 ///
134 /// False for [`Text`](Self::Text) and [`Range`](Self::Range), which are the
135 /// two whose answer is not one of a set. A renderer asks this before it
136 /// looks at [`Facet::values`], the way it asks
137 /// [`FieldKind::offers_options`] before it looks at [`Field::options`].
138 #[must_use]
139 pub const fn offers_values(self) -> bool {
140 matches!(self, Self::OneOf | Self::AnyOf | Self::Subtree)
141 }
142
143 /// Whether a value can be pruned as well as picked.
144 ///
145 /// [`Subtree`](Self::Subtree) alone. Excluding a value from a flat facet is
146 /// the same fact as not picking it, so an exclude affordance there would be
147 /// a second control for a state the first one already holds.
148 #[must_use]
149 pub const fn prunes(self) -> bool {
150 matches!(self, Self::Subtree)
151 }
152
153 /// Whether picking a second value keeps the first.
154 #[must_use]
155 pub const fn accumulates(self) -> bool {
156 matches!(self, Self::AnyOf | Self::Subtree)
157 }
158 }
159
160 /// One value a [`Facet`] offers, as it currently stands.
161 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162 #[non_exhaustive]
163 pub struct FacetValue<'a> {
164 /// What identifies it, and what a host keys its route on.
165 ///
166 /// [`Choice::value`]'s split, and a tree is why it is not optional: two
167 /// leaves under different parents are legitimately both called "Ambient",
168 /// and the path is the only thing telling them apart. It is also what
169 /// nearest-ancestor-wins resolves over, so an app that carried only labels
170 /// could not compute the [`standing`](Self::standing) it hands back here.
171 pub value: &'a str,
172 /// What it is called, as the user reads it.
173 ///
174 /// The leaf's own name rather than its path: a facet drawn as an indented
175 /// tree repeats every ancestor on every line otherwise, and one drawn as a
176 /// breadcrumb has the ancestors already.
177 pub label: &'a str,
178 /// How many members of the set carry it.
179 ///
180 /// Optional, and settled that way rather than made mandatory: a count is a
181 /// measured fact the app may not have. Counting a tag subtree under an
182 /// active text search is a second query, and an app that will not pay for it
183 /// should be able to describe the facet anyway rather than write a zero that
184 /// reads as "none of them". That is [`Awaiting::amount`]'s rule in a second
185 /// place — state a number when it was measured, and nothing when it was not.
186 pub count: Option<u64>,
187 /// Whether it is narrowing the set, and how it came to be.
188 pub standing: Standing,
189 /// How far down the tree it sits, counting from zero at the root.
190 ///
191 /// Always zero for a flat facet, which is what makes an indenting renderer
192 /// one code path rather than two. A renderer that draws no tree at all still
193 /// reads this, since a value's depth is what distinguishes two same-named
194 /// leaves under different parents.
195 pub depth: Nesting,
196 /// Whether taking it reveals values under it.
197 ///
198 /// Distinct from having a nonzero [`depth`](Self::depth): a leaf deep in the
199 /// tree branches no further, and a root with children does. Both facts are
200 /// needed and neither implies the other, which is why the pair is two
201 /// members rather than one count.
202 pub branching: bool,
203 }
204
205 impl<'a> FacetValue<'a> {
206 /// An unpicked value at the root of the facet.
207 #[must_use]
208 pub const fn new(value: &'a str, label: &'a str) -> Self {
209 Self {
210 value,
211 label,
212 count: None,
213 standing: Standing::Open,
214 depth: Nesting::top(),
215 branching: false,
216 }
217 }
218
219 /// A value whose identifier is also what the user reads.
220 ///
221 /// [`Choice::plain`]'s convenience, and it is the flat case: a type or a tier
222 /// is its own name, and only a tree needs a path that is not one.
223 #[must_use]
224 pub const fn of(value: &'a str) -> Self {
225 Self::new(value, value)
226 }
227
228 /// How many members carry it, when that was measured.
229 #[must_use]
230 pub const fn counted(mut self, count: u64) -> Self {
231 self.count = Some(count);
232 self
233 }
234
235 /// How it stands in the current selection.
236 #[must_use]
237 pub const fn standing(mut self, standing: Standing) -> Self {
238 self.standing = standing;
239 self
240 }
241
242 /// Where it sits in the tree, and whether anything hangs off it.
243 #[must_use]
244 pub const fn at(mut self, depth: Nesting, branching: bool) -> Self {
245 self.depth = depth;
246 self.branching = branching;
247 self
248 }
249 }
250
251 /// Whether a [`FacetValue`] is narrowing the set, and how it came to be.
252 ///
253 /// Four rather than a bool, and the two extra members are what a tree costs. A
254 /// pruned branch and an untaken one are not the same state — one was decided
255 /// against and the other was never reached — and a child under a taken parent is
256 /// in force without anybody having picked it. A renderer given a bool either
257 /// marks every descendant of a taken branch, which reads as forty deliberate
258 /// choices, or marks none of them, which reads as unfiltered.
259 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
260 #[non_exhaustive]
261 pub enum Standing {
262 /// Not picked, and nothing above it is either.
263 #[default]
264 Open,
265 /// Picked here. The set is narrowed to it and whatever hangs off it.
266 Taken,
267 /// In force because something above it was taken.
268 Inherited,
269 /// Pruned out, though something above it was taken.
270 ///
271 /// The state that only [`Selecting::Subtree`] can reach, and the reason
272 /// exclusion is drawn as a visible affordance beside each label rather than
273 /// as a modifier on the ordinary one: a gesture a terminal cannot express is
274 /// a gesture half the renderers would have to leave out, and an affordance
275 /// nothing teaches is one users do not find.
276 Pruned,
277 }
278
279 impl Standing {
280 /// Whether the user decided this value, either way.
281 ///
282 /// True for [`Taken`](Self::Taken) and [`Pruned`](Self::Pruned) — both are
283 /// choices, and both are things a "clear this" affordance has to clear.
284 /// [`Inherited`](Self::Inherited) is not: clearing it clears nothing,
285 /// because the decision is further up.
286 #[must_use]
287 pub const fn is_picked(self) -> bool {
288 matches!(self, Self::Taken | Self::Pruned)
289 }
290
291 /// Whether the value narrows the set in.
292 ///
293 /// [`Taken`](Self::Taken) and [`Inherited`](Self::Inherited): one was picked
294 /// and one came down from above, and to the set they mean the same thing.
295 /// The pair is named here so a renderer colouring in-force values does not
296 /// have to know which is which.
297 #[must_use]
298 pub const fn in_force(self) -> bool {
299 matches!(self, Self::Taken | Self::Inherited)
300 }
301
302 /// The content intent the value takes.
303 ///
304 /// [`Pruned`](Self::Pruned) reads back a step, which is the three-tone rule
305 /// above rather than a new decision: a pruned branch is still a live control
306 /// — pressing it takes the prune off — so it may not wear `content-muted`,
307 /// and it is not the thing itself either.
308 #[must_use]
309 pub const fn intent(self) -> &'static str {
310 match self {
311 Self::Taken | Self::Inherited | Self::Open => "content",
312 Self::Pruned => "content-secondary",
313 }
314 }
315 }
316