Krolyxon
Commit 4654f72

Branches

Tags

Refs →

remagesrcstateoverlay.rs

237 lines · 6.5 KB · Rust

1//! Overlay state: the `:` palette, `/` search, which-key, help and the info panel.
2
3use std::path::PathBuf;
4
5use super::fuzzy;
6
7/// Rows shown at most in the palette and the image-mode search list.
8pub const LIST_ROWS: usize = 8;
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum InputKind {
12 Palette,
13 Search,
14}
15
16/// One palette row: an action (or a template like `goto N` to complete), what it does and
17/// the keys bound to it.
18#[derive(Clone, Debug, PartialEq)]
19pub struct PaletteItem {
20 pub action: String,
21 pub desc: String,
22 pub keys: String,
23 /// `action` has a placeholder argument (`goto N`): `<CR>` completes it instead of running.
24 pub template: bool,
25}
26
27impl PaletteItem {
28 /// What `<Tab>` puts on the line: the action, or a template's name and a space.
29 pub fn completion(&self) -> String {
30 if self.template {
31 let name = self.action.split_whitespace().next().unwrap_or("");
32 format!("{name} ")
33 } else {
34 self.action.clone()
35 }
36 }
37}
38
39/// An edit to the prompt line.
40#[derive(Clone, Debug, PartialEq, Eq)]
41pub enum Edit {
42 Insert(String),
43 Backspace,
44 DeleteWord,
45 Clear,
46 /// Towards worse matches (drawn above), or the next file in the grid.
47 Up,
48 Down,
49 Left,
50 Right,
51 /// Complete the selected palette item into the line.
52 Complete,
53}
54
55/// The prompt line and what it matches.
56#[derive(Debug)]
57pub struct Input {
58 pub kind: InputKind,
59 pub text: String,
60 pub items: Vec<PaletteItem>,
61 /// Matching palette items, or queue indices in image-mode search; best first.
62 pub matches: Vec<usize>,
63 pub selected: usize,
64 /// The selection was moved by hand, so `<CR>` takes it over the typed text.
65 pub moved: bool,
66 /// The file and filter before a search, for `<Esc>`.
67 pub origin: Option<PathBuf>,
68}
69
70impl Input {
71 pub fn new(kind: InputKind, items: Vec<PaletteItem>, origin: Option<PathBuf>) -> Self {
72 let mut i = Self {
73 kind,
74 text: String::new(),
75 items,
76 matches: Vec::new(),
77 selected: 0,
78 moved: false,
79 origin,
80 };
81 i.rank_items();
82 i
83 }
84
85 pub fn prefix(&self) -> &'static str {
86 match self.kind {
87 InputKind::Palette => ":",
88 InputKind::Search => "/",
89 }
90 }
91
92 /// Re-rank palette items against the text.
93 pub fn rank_items(&mut self) {
94 if self.kind != InputKind::Palette {
95 return;
96 }
97 let hay: Vec<String> = self
98 .items
99 .iter()
100 .map(|i| format!("{} {}", i.action, i.desc))
101 .collect();
102 self.matches = fuzzy::rank(&self.text, hay.iter().map(String::as_str));
103 self.selected = 0;
104 self.moved = false;
105 }
106
107 /// Apply a text edit. Returns whether the text changed.
108 pub fn edit_text(&mut self, e: &Edit) -> bool {
109 let before = self.text.len();
110 match e {
111 Edit::Insert(s) => self.text.extend(s.chars().filter(|c| !c.is_control())),
112 Edit::Backspace => {
113 self.text.pop();
114 }
115 Edit::DeleteWord => {
116 let t = self.text.trim_end();
117 let cut = t.rfind(char::is_whitespace).map_or(0, |i| i + 1);
118 self.text.truncate(cut);
119 }
120 Edit::Clear => self.text.clear(),
121 _ => return false,
122 }
123 self.text.len() != before || matches!(e, Edit::Insert(_))
124 }
125
126 /// Move the selection through the match list.
127 pub fn select(&mut self, up: bool) {
128 let n = self.matches.len();
129 if n == 0 {
130 return;
131 }
132 self.selected = if up {
133 (self.selected + 1).min(n - 1)
134 } else {
135 self.selected.saturating_sub(1)
136 };
137 self.moved = true;
138 }
139
140 pub fn selected_item(&self) -> Option<&PaletteItem> {
141 self.matches
142 .get(self.selected)
143 .and_then(|&i| self.items.get(i))
144 }
145
146 /// The first row of the visible window of matches, keeping the selection in view.
147 pub fn window_start(&self) -> usize {
148 self.selected.saturating_sub(LIST_ROWS - 1)
149 }
150}
151
152/// One which-key row.
153#[derive(Clone, Debug, PartialEq)]
154pub struct WhichRow {
155 pub key: String,
156 pub desc: String,
157 /// More keys follow this one.
158 pub prefix: bool,
159}
160
161#[derive(Clone, Debug, PartialEq)]
162pub enum HelpRow {
163 Header(String),
164 Binding { keys: String, desc: String },
165}
166
167#[derive(Debug)]
168pub struct Help {
169 pub rows: Vec<HelpRow>,
170 /// First row shown.
171 pub scroll: usize,
172}
173
174impl Help {
175 /// Scroll by `delta` rows, keeping `page` rows on screen.
176 pub fn scroll_by(&mut self, delta: isize, page: usize) {
177 let max = self.rows.len().saturating_sub(page);
178 self.scroll = (self.scroll as isize + delta).clamp(0, max as isize) as usize;
179 }
180}
181
182/// Metadata rows of a file for the info panel.
183#[derive(Clone, Debug, PartialEq)]
184pub struct Info {
185 pub path: PathBuf,
186 pub rows: Vec<(String, String)>,
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 fn items() -> Vec<PaletteItem> {
194 ["next", "next-mark", "goto N", "help"]
195 .iter()
196 .map(|a| PaletteItem {
197 action: a.to_string(),
198 desc: String::new(),
199 keys: String::new(),
200 template: a.contains('N'),
201 })
202 .collect()
203 }
204
205 #[test]
206 fn typing_ranks_and_selects() {
207 let mut i = Input::new(InputKind::Palette, items(), None);
208 assert_eq!(i.matches.len(), 4);
209 assert!(i.edit_text(&Edit::Insert("nxt".into())));
210 i.rank_items();
211 assert_eq!(i.selected_item().unwrap().action, "next");
212 i.select(true);
213 assert_eq!(i.selected_item().unwrap().action, "next-mark");
214 assert!(i.moved);
215 i.select(true);
216 assert_eq!(i.selected, 1, "clamped to the matches");
217 i.edit_text(&Edit::Insert(" more words".into()));
218 i.edit_text(&Edit::DeleteWord);
219 assert_eq!(i.text, "nxt more ");
220 i.edit_text(&Edit::Clear);
221 assert!(!i.edit_text(&Edit::Backspace));
222 assert_eq!(i.items[2].completion(), "goto ");
223 assert_eq!(i.items[1].completion(), "next-mark");
224 }
225
226 #[test]
227 fn help_scroll_clamps() {
228 let mut h = Help {
229 rows: vec![HelpRow::Header("X".into()); 10],
230 scroll: 0,
231 };
232 h.scroll_by(-3, 4);
233 assert_eq!(h.scroll, 0);
234 h.scroll_by(100, 4);
235 assert_eq!(h.scroll, 6);
236 }
237}