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}