Oregami
Repositories/oxedyne/fe2o3

oxedyne/fe2o3/fe2o3_pearlite/src/contents.rs

4.3 KiB, 1 run

created by r1870400018:60978, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! The contents tree a reader's sidebar shows: the document outline nested by heading level, with each
2//! branch open or closed, the rows that are showing, and which heading the reader is currently in. It
3//! holds no pixels and no window, so the rules are the same whatever draws them; the native window
4//! paints it, and the web reader's contents rail (`pearl-reader/index.html`) follows the same rules.
5
6use oxedyne_fe2o3_austenite::emit::pearl::OutlineEntry;
7
8/// A document outline arranged as a collapsible tree. Each entry nests under the nearest earlier entry
9/// of a shallower level, so a level-3 heading directly after a level-1 heading is that heading's child.
10/// Every branch starts open.
11#[derive(Clone, Debug)]
12pub struct Contents {
13 entries: Vec<OutlineEntry>,
14 parent: Vec<Option<usize>>, // index of the enclosing entry
15 depth: Vec<usize>, // nesting depth, 0 at the root
16 kids: Vec<bool>, // does the entry hold at least one child?
17 open: Vec<bool>, // is the branch expanded?
18}
19
20impl Contents {
21 pub fn new(entries: Vec<OutlineEntry>) -> Self {
22 let n = entries.len();
23 let mut parent = Vec::with_capacity(n);
24 let mut depth = Vec::with_capacity(n);
25 let mut kids = vec![false; n];
26 // The open ancestors of the entry being placed, as (level, index).
27 let mut stack: Vec<(u8, usize)> = Vec::new();
28 for (i, e) in entries.iter().enumerate() {
29 while let Some(&(lvl, _)) = stack.last() {
30 if lvl >= e.level {
31 stack.pop();
32 } else {
33 break;
34 }
35 }
36 let p = stack.last().map(|&(_, idx)| idx);
37 if let Some(pi) = p {
38 kids[pi] = true;
39 }
40 parent.push(p);
41 depth.push(stack.len());
42 stack.push((e.level, i));
43 }
44 Self { entries, parent, depth, kids, open: vec![true; n] }
45 }
46
47 pub fn is_empty(&self) -> bool { self.entries.is_empty() }
48 pub fn len(&self) -> usize { self.entries.len() }
49 pub fn entries(&self) -> &[OutlineEntry] { &self.entries }
50 pub fn depth(&self, idx: usize) -> usize { self.depth.get(idx).copied().unwrap_or(0) }
51
52 /// Does the entry hold children, so that it can be opened and closed?
53 pub fn has_children(&self, idx: usize) -> bool { self.kids.get(idx).copied().unwrap_or(false) }
54
55 /// Is the entry's branch expanded? A leaf counts as open.
56 pub fn is_open(&self, idx: usize) -> bool { self.open.get(idx).copied().unwrap_or(true) }
57
58 /// Opens a closed branch or closes an open one. A leaf has nothing to fold, and the call reports
59 /// whether anything changed.
60 pub fn toggle(&mut self, idx: usize) -> bool {
61 if !self.has_children(idx) {
62 return false;
63 }
64 match self.open.get_mut(idx) {
65 Some(o) => { *o = !*o; true },
66 None => false,
67 }
68 }
69
70 /// Is every enclosing branch of the entry open, so that its row shows?
71 pub fn is_shown(&self, idx: usize) -> bool {
72 let mut at = self.parent.get(idx).copied().flatten();
73 while let Some(p) = at {
74 if !self.is_open(p) {
75 return false;
76 }
77 at = self.parent[p];
78 }
79 true
80 }
81
82 /// The entries whose rows show, in document order: everything not inside a closed branch.
83 pub fn visible(&self) -> Vec<usize> {
84 (0..self.entries.len()).filter(|&i| self.is_shown(i)).collect()
85 }
86
87 /// The row that stands for an entry: the entry itself when it shows, otherwise its nearest enclosing
88 /// entry that does. The current section inside a closed branch is marked on that branch's row.
89 pub fn shown_for(&self, idx: usize) -> usize {
90 let mut row = idx;
91 let mut at = self.parent.get(idx).copied().flatten();
92 while let Some(p) = at {
93 if !self.is_open(p) {
94 row = p;
95 }
96 at = self.parent[p];
97 }
98 row
99 }
100
101 /// The heading the reader is in: the last placed entry whose position is at or above `at`, with
102 /// `pos` mapping an entry's page (1-based) and y to the same scale as `at`. An entry the ledger never
103 /// fixed has no position and is never current. `None` when the view is above the first heading.
104 pub fn current<F>(&self, pos: F, at: f64) -> Option<usize>
105 where F: Fn(&OutlineEntry) -> Option<f64>
106 {
107 let mut best: Option<(usize, f64)> = None;
108 for (i, e) in self.entries.iter().enumerate() {
109 if let Some(y) = pos(e) {
110 if y <= at {
111 // Ties go to the later entry: two headings on one line, the deeper one is current.
112 let better = match best {
113 Some((_, by)) => y >= by,
114 None => true,
115 };
116 if better {
117 best = Some((i, y));
118 }
119 }
120 }
121 }
122 best.map(|(i, _)| i)
123 }
124}