Skip to main content

ravenrs_sheet/engine/
outline.rs

1//! Outline groups (Data ▸ Group) of a sheet's rows or columns: the runs of lines at or above
2//! each outline level, as the grid draws them (brackets, − / + buttons, level buttons) and the
3//! Show / Hide Detail commands act on them. Summary rows sit below their detail and summary
4//! columns right of it, or above / left of it when the sheet's outline settings say so
5//! ([`Sheet::summary_below`], [`Sheet::summary_right`]).
6
7use std::collections::BTreeMap;
8
9use crate::model::{LineInfo, Sheet};
10
11/// The deepest outline level XLSX allows.
12pub const MAX_LEVEL: u8 = 7;
13
14/// One group: lines `start..=end` at outline level `level` or deeper, with its summary line
15/// just after `end`, or just before `start` when `summary_before` is set.
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct OutlineGroup {
18    pub level: u8,
19    pub start: u32,
20    pub end: u32,
21    /// Every detail line is hidden: the button shows +.
22    pub collapsed: bool,
23    /// The summary line is above (rows) or left of (columns) the detail.
24    pub summary_before: bool,
25}
26
27impl OutlineGroup {
28    /// The summary line, where the − / + button sits.
29    pub fn summary(&self) -> u32 {
30        if self.summary_before { self.start.saturating_sub(1) } else { self.end.saturating_add(1) }
31    }
32    /// Whether the summary line exists: a group that starts at the first line (summary before)
33    /// or ends at the last one (summary after) has none.
34    pub fn has_summary(&self) -> bool {
35        !self.contains(self.summary())
36    }
37    pub fn contains(&self, line: u32) -> bool {
38        line >= self.start && line <= self.end
39    }
40}
41
42/// Whether the sheet's summary rows (`rows`) or summary columns sit before their detail.
43pub fn summary_before(sh: &Sheet, rows: bool) -> bool {
44    if rows { !sh.summary_below } else { !sh.summary_right }
45}
46
47/// The deepest outline level used by these lines (0 = no outline).
48pub fn max_level(lines: &BTreeMap<u32, LineInfo>) -> u8 {
49    lines.values().map(|l| l.outline.min(MAX_LEVEL)).max().unwrap_or(0)
50}
51
52/// Every group, with its summary line after the detail; see [`groups_with`].
53pub fn groups(lines: &BTreeMap<u32, LineInfo>) -> Vec<OutlineGroup> {
54    groups_with(lines, false)
55}
56
57/// Every group, ordered by where it ends (inner groups before the group around them), with its
58/// summary line before the detail when `summary_before` is set.
59pub fn groups_with(lines: &BTreeMap<u32, LineInfo>, summary_before: bool) -> Vec<OutlineGroup> {
60    let mut out = Vec::new();
61    // Open group per level (index level - 1): start line and whether all its lines are hidden.
62    let mut open: [Option<(u32, bool)>; MAX_LEVEL as usize] = [None; MAX_LEVEL as usize];
63    let mut prev: Option<u32> = None;
64    let close_from = |open: &mut [Option<(u32, bool)>; MAX_LEVEL as usize], from: u8, end: u32, out: &mut Vec<OutlineGroup>| {
65        for level in (from..=MAX_LEVEL).rev() {
66            if let Some(slot) = open.get_mut(level as usize - 1)
67                && let Some((start, hidden)) = slot.take()
68            {
69                out.push(OutlineGroup { level, start, end, collapsed: hidden, summary_before });
70            }
71        }
72    };
73    for (&i, info) in lines.iter().filter(|(_, l)| l.outline > 0) {
74        let level = info.outline.min(MAX_LEVEL);
75        match prev {
76            // A gap ends every group.
77            Some(p) if p.checked_add(1) != Some(i) => close_from(&mut open, 1, p, &mut out),
78            Some(p) if level < MAX_LEVEL => close_from(&mut open, level + 1, p, &mut out),
79            _ => {}
80        }
81        for slot in open.iter_mut().take(level as usize) {
82            match slot {
83                Some((_, hidden)) => *hidden &= info.hidden,
84                None => *slot = Some((i, info.hidden)),
85            }
86        }
87        prev = Some(i);
88    }
89    if let Some(p) = prev {
90        close_from(&mut open, 1, p, &mut out);
91    }
92    out
93}
94
95/// The group at `level` that holds `line`.
96pub fn group_at(lines: &BTreeMap<u32, LineInfo>, line: u32, level: u8) -> Option<OutlineGroup> {
97    groups(lines).into_iter().find(|g| g.level == level && g.contains(line))
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    fn lines(spec: &[(u32, u8, bool)]) -> BTreeMap<u32, LineInfo> {
105        spec.iter().map(|&(i, outline, hidden)| (i, LineInfo { outline, hidden, ..Default::default() })).collect()
106    }
107
108    #[test]
109    fn no_outline_has_no_groups() {
110        let l = lines(&[(3, 0, true)]);
111        assert_eq!(max_level(&l), 0);
112        assert!(groups(&l).is_empty());
113    }
114
115    #[test]
116    fn nested_and_separate_groups() {
117        // Rows 5..8 at level 1 with 6..7 at level 2; rows 12..13 at level 1 after a gap.
118        let l = lines(&[(4, 1, false), (5, 2, false), (6, 2, false), (7, 1, false), (11, 1, true), (12, 1, true)]);
119        assert_eq!(max_level(&l), 2);
120        let g = groups(&l);
121        assert_eq!(
122            g,
123            vec![
124                OutlineGroup { level: 2, start: 5, end: 6, collapsed: false, summary_before: false },
125                OutlineGroup { level: 1, start: 4, end: 7, collapsed: false, summary_before: false },
126                OutlineGroup { level: 1, start: 11, end: 12, collapsed: true, summary_before: false },
127            ]
128        );
129        assert_eq!(g[1].summary(), 8);
130        assert_eq!(group_at(&l, 6, 1).map(|g| g.start), Some(4));
131        assert_eq!(group_at(&l, 4, 2), None);
132    }
133
134    #[test]
135    fn adjacent_groups_at_one_level_split_by_a_shallower_line() {
136        // Two level-2 groups inside one level-1 group, separated by a level-1 line.
137        let l = lines(&[(0, 2, true), (1, 2, true), (2, 1, false), (3, 2, false)]);
138        let g = groups(&l);
139        assert_eq!(g.iter().filter(|g| g.level == 2).count(), 2);
140        assert_eq!(g.iter().find(|g| g.level == 2 && g.start == 0).map(|g| g.collapsed), Some(true));
141        assert_eq!(g.iter().find(|g| g.level == 1).map(|g| (g.start, g.end, g.collapsed)), Some((0, 3, false)));
142    }
143
144    #[test]
145    fn deepest_level_and_last_line_do_not_overflow() {
146        let l = lines(&[(u32::MAX - 1, 9, false), (u32::MAX, 7, false)]);
147        let g = groups(&l);
148        assert_eq!(g.len(), 7);
149        assert!(g.iter().all(|g| g.start == u32::MAX - 1 && g.end == u32::MAX));
150        assert_eq!(g[0].summary(), u32::MAX);
151        assert!(!g[0].has_summary());
152    }
153
154    #[test]
155    fn summary_before_the_detail() {
156        // Rows 5..8 at level 1 with 6..7 at level 2: the summaries are rows 4 and 5.
157        let l = lines(&[(4, 1, false), (5, 2, false), (6, 2, false), (7, 1, false), (0, 1, false)]);
158        let g = groups_with(&l, true);
159        let at = |level: u8, start: u32| g.iter().find(|g| g.level == level && g.start == start).copied();
160        assert_eq!(at(1, 4).map(|g| (g.summary(), g.has_summary())), Some((3, true)));
161        assert_eq!(at(2, 5).map(|g| (g.summary(), g.has_summary())), Some((4, true)));
162        // A group on the first line has no line before it.
163        assert_eq!(at(1, 0).map(|g| (g.summary(), g.has_summary())), Some((0, false)));
164        let mut sh = Sheet::new("S");
165        assert!(!summary_before(&sh, true) && !summary_before(&sh, false));
166        sh.summary_below = false;
167        assert!(summary_before(&sh, true) && !summary_before(&sh, false));
168    }
169}