use crate::Nesting; // Names this module's prose links to, resolved for rustdoc. #[allow(unused_imports)] use crate::{Awaiting, Choice, Column, Field, FieldKind}; /// A named dimension a set can be narrowed by. /// /// One word for six things that were six mechanisms. MNW's discover page filters /// by free text, a flat any-of over item types, a tree of tags, a numeric range /// over price, a nested one-of over AI tier, and a browse position in the tag /// tree held separately from the tag selection — and the last two being separate /// is the whole reason a filter row there needs a tick box *and* a chevron. The /// panel is a mixed bag of hand-written controls because nothing named the thing /// they all are. /// /// Deliberately wider than that one page. audiofiles' library browser and /// goingson's filters are the same shape, and a word that only fitted discover /// would be discover's markup with a neutral name on it. /// /// # What it does not say /// /// **What picking a value calls.** This crate names no address, so a facet is /// paired with routes the way a column's [`sortable`](Column::sortable) flag is /// paired with what reordering calls. /// /// **How a tree is drawn.** Indented rows, a column of panes, a breadcrumb and a /// list: all four are honest renderings of the same described facet, and a /// terminal will not pick the same one a browser does. [`FacetValue::depth`] is /// what a renderer needs to draw any of them; the choice is not described. /// /// **Which values to show.** A tag tree has thousands of nodes and a panel shows /// a handful. Deciding which handful is the app's — it is the same question as /// which rows go in a table, and no table member answers it either. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub struct Facet<'a> { /// What the dimension is called, as the user reads it. pub name: &'a str, /// How many of its values may be in force, and in what shape. pub mode: Selecting, /// The values on offer, in the order they are drawn. /// /// A [`Selecting::Text`] facet has none: the value is whatever was typed, /// and a description that listed the possible strings would be listing the /// corpus. A [`Selecting::Range`] facet has none either, for the reason /// [`FieldKind::Range`] takes bounds rather than options — the ends are the /// question and the values between them are not enumerable. pub values: &'a [FacetValue<'a>], } impl<'a> Facet<'a> { /// A dimension with values to pick from. #[must_use] pub const fn new(name: &'a str, mode: Selecting, values: &'a [FacetValue<'a>]) -> Self { Self { name, mode, values } } /// Whether the facet is narrowing the set right now. /// /// The question a renderer asks to decide whether to offer a way out of it, /// and the reason it is derived rather than carried: a facet with nothing /// standing is unengaged by construction, so a member saying so could /// disagree with the values beside it. [`Standing::Inherited`] does not /// count — something further up is what is doing the narrowing, and clearing /// a child that was never picked clears nothing. /// /// Always false for [`Selecting::Text`] and [`Selecting::Range`], which /// carry no values. A host that wants a clear affordance on those knows /// whether its own box is empty; the description does not hold the typed /// string. #[must_use] pub fn engaged(&self) -> bool { self.values.iter().any(|value| value.standing.is_picked()) } /// The deepest value in the facet, or zero when it is flat. /// /// What an indenting renderer needs to reserve a gutter before it draws the /// first row, which is "First paint is final paint" applied to a tree: a /// gutter widened as deeper values arrive is the reflow that rule forbids. #[must_use] pub fn reach(&self) -> u8 { self.values .iter() .map(|value| value.depth.level) .max() .unwrap_or(0) } } /// How many of a [`Facet`]'s values may be in force, and in what shape. /// /// Five, and the fifth is what made this an enum rather than a bool. `one-of`, /// `any-of`, a range and free text are the four a form vocabulary already has in /// [`FieldKind`]; a tree's selection is none of them, and describing tags as /// any-of was what forced browsing to be a second mechanism beside filtering. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub enum Selecting { /// Exactly one value, and picking another replaces it. /// /// MNW's AI tier, whose three options are nested ranges rather than /// independent values, so two of them at once means nothing. OneOf, /// Any number of values, each independent of the others. AnyOf, /// A low end, a high end, or both. /// /// Carries no values for [`FieldKind::Range`]'s reason: the ends are the /// question. Range, /// Whatever the user types. Text, /// A position in a tree, edited by taking branches in and pruning branches /// out. /// /// The one mode that is not reducible to the others, and the one gesture /// that replaced two. Picking a value narrows the set to it *and* reveals /// its children, so browsing a tree and filtering by it stop being separate /// mechanisms with separate state. What a selection then is: a set of /// branches taken and a set pruned, resolved nearest-ancestor-first, so /// `music` in and `music/synths` out is sayable and no flat mode can say it. /// /// Resolution happens in the app, and what reaches a renderer is the /// [`Standing`] each drawn value ended up with. A renderer walking ancestors /// itself would be a renderer that can disagree with the results beside it. Subtree, } impl Selecting { /// Whether the mode picks from values the description lists. /// /// False for [`Text`](Self::Text) and [`Range`](Self::Range), which are the /// two whose answer is not one of a set. A renderer asks this before it /// looks at [`Facet::values`], the way it asks /// [`FieldKind::offers_options`] before it looks at [`Field::options`]. #[must_use] pub const fn offers_values(self) -> bool { matches!(self, Self::OneOf | Self::AnyOf | Self::Subtree) } /// Whether a value can be pruned as well as picked. /// /// [`Subtree`](Self::Subtree) alone. Excluding a value from a flat facet is /// the same fact as not picking it, so an exclude affordance there would be /// a second control for a state the first one already holds. #[must_use] pub const fn prunes(self) -> bool { matches!(self, Self::Subtree) } /// Whether picking a second value keeps the first. #[must_use] pub const fn accumulates(self) -> bool { matches!(self, Self::AnyOf | Self::Subtree) } } /// One value a [`Facet`] offers, as it currently stands. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[non_exhaustive] pub struct FacetValue<'a> { /// What identifies it, and what a host keys its route on. /// /// [`Choice::value`]'s split, and a tree is why it is not optional: two /// leaves under different parents are legitimately both called "Ambient", /// and the path is the only thing telling them apart. It is also what /// nearest-ancestor-wins resolves over, so an app that carried only labels /// could not compute the [`standing`](Self::standing) it hands back here. pub value: &'a str, /// What it is called, as the user reads it. /// /// The leaf's own name rather than its path: a facet drawn as an indented /// tree repeats every ancestor on every line otherwise, and one drawn as a /// breadcrumb has the ancestors already. pub label: &'a str, /// How many members of the set carry it. /// /// Optional, and settled that way rather than made mandatory: a count is a /// measured fact the app may not have. Counting a tag subtree under an /// active text search is a second query, and an app that will not pay for it /// should be able to describe the facet anyway rather than write a zero that /// reads as "none of them". That is [`Awaiting::amount`]'s rule in a second /// place — state a number when it was measured, and nothing when it was not. pub count: Option, /// Whether it is narrowing the set, and how it came to be. pub standing: Standing, /// How far down the tree it sits, counting from zero at the root. /// /// Always zero for a flat facet, which is what makes an indenting renderer /// one code path rather than two. A renderer that draws no tree at all still /// reads this, since a value's depth is what distinguishes two same-named /// leaves under different parents. pub depth: Nesting, /// Whether taking it reveals values under it. /// /// Distinct from having a nonzero [`depth`](Self::depth): a leaf deep in the /// tree branches no further, and a root with children does. Both facts are /// needed and neither implies the other, which is why the pair is two /// members rather than one count. pub branching: bool, } impl<'a> FacetValue<'a> { /// An unpicked value at the root of the facet. #[must_use] pub const fn new(value: &'a str, label: &'a str) -> Self { Self { value, label, count: None, standing: Standing::Open, depth: Nesting::top(), branching: false, } } /// A value whose identifier is also what the user reads. /// /// [`Choice::plain`]'s convenience, and it is the flat case: a type or a tier /// is its own name, and only a tree needs a path that is not one. #[must_use] pub const fn of(value: &'a str) -> Self { Self::new(value, value) } /// How many members carry it, when that was measured. #[must_use] pub const fn counted(mut self, count: u64) -> Self { self.count = Some(count); self } /// How it stands in the current selection. #[must_use] pub const fn standing(mut self, standing: Standing) -> Self { self.standing = standing; self } /// Where it sits in the tree, and whether anything hangs off it. #[must_use] pub const fn at(mut self, depth: Nesting, branching: bool) -> Self { self.depth = depth; self.branching = branching; self } } /// Whether a [`FacetValue`] is narrowing the set, and how it came to be. /// /// Four rather than a bool, and the two extra members are what a tree costs. A /// pruned branch and an untaken one are not the same state — one was decided /// against and the other was never reached — and a child under a taken parent is /// in force without anybody having picked it. A renderer given a bool either /// marks every descendant of a taken branch, which reads as forty deliberate /// choices, or marks none of them, which reads as unfiltered. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] #[non_exhaustive] pub enum Standing { /// Not picked, and nothing above it is either. #[default] Open, /// Picked here. The set is narrowed to it and whatever hangs off it. Taken, /// In force because something above it was taken. Inherited, /// Pruned out, though something above it was taken. /// /// The state that only [`Selecting::Subtree`] can reach, and the reason /// exclusion is drawn as a visible affordance beside each label rather than /// as a modifier on the ordinary one: a gesture a terminal cannot express is /// a gesture half the renderers would have to leave out, and an affordance /// nothing teaches is one users do not find. Pruned, } impl Standing { /// Whether the user decided this value, either way. /// /// True for [`Taken`](Self::Taken) and [`Pruned`](Self::Pruned) — both are /// choices, and both are things a "clear this" affordance has to clear. /// [`Inherited`](Self::Inherited) is not: clearing it clears nothing, /// because the decision is further up. #[must_use] pub const fn is_picked(self) -> bool { matches!(self, Self::Taken | Self::Pruned) } /// Whether the value narrows the set in. /// /// [`Taken`](Self::Taken) and [`Inherited`](Self::Inherited): one was picked /// and one came down from above, and to the set they mean the same thing. /// The pair is named here so a renderer colouring in-force values does not /// have to know which is which. #[must_use] pub const fn in_force(self) -> bool { matches!(self, Self::Taken | Self::Inherited) } /// The content intent the value takes. /// /// [`Pruned`](Self::Pruned) reads back a step, which is the three-tone rule /// above rather than a new decision: a pruned branch is still a live control /// — pressing it takes the prune off — so it may not wear `content-muted`, /// and it is not the thing itself either. #[must_use] pub const fn intent(self) -> &'static str { match self { Self::Taken | Self::Inherited | Self::Open => "content", Self::Pruned => "content-secondary", } } }