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 | |
| 6 | use 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)] |
| 12 | pub 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 | |
| 20 | impl 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 | } |