Skip to main content

makeover_layout/
tag_facet.rs

1//! A tag tree as a [`Selecting::Subtree`] facet, from tagtree's three answers.
2//!
3//! [`Selecting::Subtree`] leaves resolution to the app, and tagtree (published
4//! as `mnw-tagtree`) is where the resolution lives: an outline for the rows in
5//! draw order, a selection for how each row stands, and a rollup for how many
6//! items sit under each. This is the mapping from those to [`FacetValue`]s, in
7//! one place rather than once per app. Behind the `tagtree` feature, so a
8//! consumer that draws no tag tree takes no dependency for it.
9//!
10//! Which rows to show stays the app's, as [`Facet`] says, and
11//! [`tagtree::Selection::reveals`] is the usual answer: roots, the children of
12//! what was picked, and the path down to every decision.
13
14use crate::{FacetValue, Nesting, Standing};
15
16impl From<tagtree::Standing> for Standing {
17    fn from(standing: tagtree::Standing) -> Self {
18        match standing {
19            tagtree::Standing::Open => Self::Open,
20            tagtree::Standing::Taken => Self::Taken,
21            tagtree::Standing::Inherited => Self::Inherited,
22            tagtree::Standing::Pruned => Self::Pruned,
23            tagtree::Standing::PrunedAbove => Self::PrunedAbove,
24        }
25    }
26}
27
28/// One facet value per outline row, standing from `selection` and counted from
29/// `counts` when the app measured them.
30///
31/// `rows` is whichever part of [`tagtree::TagIndex::outline`] the app chose to
32/// show, in the order it came: the outline is pre-order, which is draw order.
33/// A row's [`depth`](FacetValue::depth) counts from zero at a root, where
34/// tagtree counts segments from one. `branching` is the outline's own answer,
35/// whether anything hangs below the node in the index, rather than whether its
36/// children happen to be shown, so a reader is told a branch opens before
37/// pressing it.
38///
39/// ```
40/// use makeover_layout::{Standing, tag_facet::values};
41///
42/// let index = tagtree::TagIndex::new(vec!["audio.genre.ambient".into(), "video.essay".into()]);
43/// let mut selection = tagtree::Selection::new();
44/// selection.pick("audio");
45/// let outline = index.outline();
46/// let shown: Vec<_> = outline.into_iter().filter(|n| selection.reveals(n.path)).collect();
47/// let rollup = tagtree::Rollup::from_sets([(vec!["audio.genre.ambient"], 3)]);
48///
49/// let facet = values(&shown, &selection, Some(&rollup));
50/// let audio = &facet[0];
51/// assert_eq!((audio.value, audio.standing, audio.count), ("audio", Standing::Taken, Some(3)));
52/// assert_eq!(facet[1].value, "audio.genre");
53/// assert_eq!(facet[1].standing, Standing::Inherited);
54/// assert_eq!(facet[1].depth.level, 1);
55/// ```
56#[must_use]
57pub fn values<'a>(
58    rows: &[tagtree::OutlineNode<'a>],
59    selection: &tagtree::Selection,
60    counts: Option<&tagtree::Rollup>,
61) -> Vec<FacetValue<'a>> {
62    rows.iter()
63        .map(|row| {
64            let value = FacetValue::new(row.path, row.label)
65                .standing(selection.standing(row.path).into())
66                .at(
67                    Nesting::at(u8::try_from(row.depth.saturating_sub(1)).unwrap_or(u8::MAX)),
68                    row.branching,
69                );
70            match counts {
71                Some(rollup) => value.counted(rollup.total(row.path)),
72                None => value,
73            }
74        })
75        .collect()
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn every_tagtree_standing_maps_to_its_namesake() {
84        assert_eq!(Standing::from(tagtree::Standing::Open), Standing::Open);
85        assert_eq!(Standing::from(tagtree::Standing::Taken), Standing::Taken);
86        assert_eq!(
87            Standing::from(tagtree::Standing::Inherited),
88            Standing::Inherited
89        );
90        assert_eq!(Standing::from(tagtree::Standing::Pruned), Standing::Pruned);
91        assert_eq!(
92            Standing::from(tagtree::Standing::PrunedAbove),
93            Standing::PrunedAbove
94        );
95    }
96
97    #[test]
98    fn an_unmeasured_facet_carries_no_counts() {
99        let index = tagtree::TagIndex::new(vec!["a.b".into()]);
100        let outline = index.outline();
101        let facet = values(&outline, &tagtree::Selection::new(), None);
102        assert!(facet.iter().all(|v| v.count.is_none()));
103        assert!(facet[0].branching && !facet[1].branching);
104        assert_eq!(facet[0].label, "a");
105        assert_eq!(facet[1].label, "b");
106    }
107
108    #[test]
109    fn a_prune_reads_out_below_it() {
110        let index = tagtree::TagIndex::new(vec!["a.b.c".into()]);
111        let outline = index.outline();
112        let mut selection = tagtree::Selection::taking(["a"]);
113        selection.prune("a.b");
114        let facet = values(&outline, &selection, None);
115        let standings: Vec<Standing> = facet.iter().map(|v| v.standing).collect();
116        assert_eq!(
117            standings,
118            [Standing::Taken, Standing::Pruned, Standing::PrunedAbove]
119        );
120    }
121}