git.lucas.co / cce-notes
notes editor over the vault (Obsidian-compatible)
git clone https://git.lucas.co/cce-notes.git

src/panel.rs (5.9K)

  1 //! A scrollable list of rows for the side panes: section headers, note
  2 //! titles and indented line snippets, each optionally clickable. The
  3 //! backlinks and outline pane on the right and the search pane on the left
  4 //! all draw through this, so they hover, scroll and click alike.
  5 
  6 use cce_ui::scene::layout::Rect;
  7 use cce_ui::scene::paint::{PaintCtx, TextAttrs};
  8 use cce_ui::widget::{Bounds, MouseScrollDelta, ScrollMotion};
  9 
 10 use crate::reading::srgb_u8;
 11 
 12 pub const ROW_H: f32 = 22.0;
 13 const PAD: f32 = 6.0;
 14 const INDENT: f32 = 14.0;
 15 
 16 /// What a click on a row does.
 17 #[derive(Clone, Debug, PartialEq)]
 18 pub enum Action {
 19     /// Open a note, optionally at a 0-based source line.
 20     Open { path: String, line: Option<usize> },
 21     /// Bring a 0-based line of the open note into view.
 22     Line(usize),
 23 }
 24 
 25 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
 26 pub enum Kind {
 27     /// A section label ("Linked mentions (3)").
 28     Header,
 29     /// A note's name.
 30     Title,
 31     /// A line of text under a title, or an outline heading.
 32     Line,
 33     /// An explanatory line ("No backlinks").
 34     Note,
 35 }
 36 
 37 #[derive(Clone, Debug, PartialEq)]
 38 pub struct Item {
 39     pub depth: usize,
 40     pub text: String,
 41     pub kind: Kind,
 42     pub action: Option<Action>,
 43 }
 44 
 45 impl Item {
 46     pub fn header(text: impl Into<String>) -> Item {
 47         Item { depth: 0, text: text.into(), kind: Kind::Header, action: None }
 48     }
 49     pub fn note(text: impl Into<String>) -> Item {
 50         Item { depth: 0, text: text.into(), kind: Kind::Note, action: None }
 51     }
 52     pub fn title(text: impl Into<String>, action: Action) -> Item {
 53         Item { depth: 0, text: text.into(), kind: Kind::Title, action: Some(action) }
 54     }
 55     pub fn line(depth: usize, text: impl Into<String>, action: Action) -> Item {
 56         Item { depth, text: text.into(), kind: Kind::Line, action: Some(action) }
 57     }
 58 }
 59 
 60 #[derive(Default)]
 61 pub struct Panel {
 62     pub items: Vec<Item>,
 63     pub scroll: f32,
 64     motion: ScrollMotion,
 65     pub hover: Option<usize>,
 66 }
 67 
 68 impl Panel {
 69     /// Replace the rows. The scroll stays when the rows are an update of
 70     /// the same content (a refresh), and resets when `reset` says the
 71     /// content is for something else (another note, another query).
 72     pub fn set(&mut self, items: Vec<Item>, reset: bool) {
 73         self.items = items;
 74         self.hover = None;
 75         if reset {
 76             self.scroll = 0.0;
 77             self.motion = ScrollMotion::new();
 78         }
 79     }
 80 
 81     fn max_scroll(&self, area: Rect) -> f32 {
 82         (self.items.len() as f32 * ROW_H + 2.0 * PAD - area.height).max(0.0)
 83     }
 84 
 85     pub fn item_at(&self, area: Rect, x: f32, y: f32) -> Option<usize> {
 86         if x < area.x || x > area.x + area.width || y < area.y || y > area.y + area.height {
 87             return None;
 88         }
 89         let i = ((y - area.y - PAD + self.scroll) / ROW_H).floor();
 90         if i < 0.0 {
 91             return None;
 92         }
 93         let i = i as usize;
 94         (i < self.items.len() && self.items[i].action.is_some()).then_some(i)
 95     }
 96 
 97     /// Update the hovered row; true when it changed.
 98     pub fn hover_at(&mut self, area: Rect, x: f32, y: f32) -> bool {
 99         let h = self.item_at(area, x, y);
100         let changed = h != self.hover;
101         self.hover = h;
102         changed
103     }
104 
105     pub fn click(&self, area: Rect, x: f32, y: f32) -> Option<Action> {
106         self.item_at(area, x, y).and_then(|i| self.items[i].action.clone())
107     }
108 
109     pub fn wheel(&mut self, delta: &MouseScrollDelta, area: Rect) -> bool {
110         let max = self.max_scroll(area);
111         self.motion.reconcile(0.0, self.scroll);
112         let moved = self.motion.apply(delta, (ROW_H, ROW_H), Bounds::max(0.0), Bounds::max(max));
113         self.scroll = self.motion.y.pos();
114         moved
115     }
116 
117     /// Advance a wheel glide; true while it moves.
118     pub fn tick(&mut self, dt: f32, area: Rect) -> bool {
119         if !self.motion.is_animating() {
120             return false;
121         }
122         let max = self.max_scroll(area);
123         self.motion.tick(dt, Bounds::max(0.0), Bounds::max(max));
124         self.scroll = self.motion.y.pos();
125         true
126     }
127 
128     pub fn clamp(&mut self, area: Rect) {
129         let max = self.max_scroll(area);
130         if self.scroll > max {
131             self.scroll = max;
132             self.motion.y.jump_to(max);
133         }
134     }
135 
136     pub fn paint(&self, pc: &mut PaintCtx, area: Rect) {
137         let (family, size) = cce_ui::layout::tree_font_parsed();
138         let small = (size * 0.9).round();
139         let body = "sans-serif".to_string();
140         pc.clip(area, |pc| {
141             for (i, item) in self.items.iter().enumerate() {
142                 let y = area.y + PAD + i as f32 * ROW_H - self.scroll;
143                 if y + ROW_H < area.y || y > area.y + area.height {
144                     continue;
145                 }
146                 let r = Rect { x: area.x + 4.0, y, width: area.width - 8.0, height: ROW_H };
147                 if self.hover == Some(i) {
148                     pc.rounded_rect(r, 4.0, (true, true, true, true), [1.0, 1.0, 1.0, 0.05]);
149                 }
150                 let x = r.x + 6.0 + item.depth as f32 * INDENT;
151                 let bounds = Some([x, r.y.max(area.y), r.x + r.width - 4.0, (r.y + r.height).min(area.y + area.height)]);
152                 let (font, fsize, color, attrs) = match item.kind {
153                     Kind::Header => (family.clone(), small, cce_ui::colors::TEXT_DIM, TextAttrs::default()),
154                     Kind::Title => (family.clone(), size, cce_ui::colors::TEXT_FG, TextAttrs::default()),
155                     Kind::Line => (body.clone(), small, cce_ui::colors::TEXT_FG, TextAttrs::default()),
156                     Kind::Note => (body.clone(), small, cce_ui::colors::TEXT_DIM, TextAttrs { italic: true, weight: None, ..Default::default() }),
157                 };
158                 let ty = cce_ui::layout::align_text_y(r.y, r.height, fsize, 0.0);
159                 pc.text_attrs(item.text.clone(), x, ty, fsize, srgb_u8(color), Some(font), bounds, attrs);
160             }
161         });
162     }
163 }