1use rich::{Segment, Style};
15
16use crate::component::{Component, Context, Flow, View};
17use crate::event::{Event, Key};
18use crate::keymap::{Keymap, keys};
19use crate::kit::ScrollState;
20
21pub fn viewport_keymap() -> Keymap {
24 let mut keymap = Keymap::new("viewport")
25 .bind("done", keys("enter"), "finish here")
26 .bind("quit", keys("q escape"), "quit");
27 keymap.extend(ScrollState::keymap("viewport"));
28 keymap
29}
30
31#[derive(Clone, Debug)]
33pub struct Viewport {
34 lines: Vec<Vec<Segment>>,
35 scroll: ScrollState,
36 height: Option<usize>,
39 keymap: Keymap,
40 sticky: usize,
42}
43
44impl Default for Viewport {
45 fn default() -> Self {
46 Viewport::new(Vec::new())
47 }
48}
49
50impl Viewport {
51 pub fn new(lines: Vec<Vec<Segment>>) -> Viewport {
52 Viewport {
53 scroll: ScrollState::new(lines.len()),
54 lines,
55 height: None,
56 keymap: viewport_keymap(),
57 sticky: 0,
58 }
59 }
60
61 #[must_use]
68 pub fn sticky(mut self, rows: usize) -> Viewport {
69 self.set_sticky(rows);
70 self
71 }
72
73 pub fn set_sticky(&mut self, rows: usize) {
76 self.sticky = rows;
77 self.scroll.set_len(self.lines.len() - self.stuck());
78 }
79
80 pub fn sticky_rows(&self) -> usize {
82 self.sticky
83 }
84
85 fn stuck(&self) -> usize {
87 self.sticky.min(self.lines.len())
88 }
89
90 fn pinned(&self, page: usize) -> usize {
93 self.stuck().min(page.saturating_sub(1))
94 }
95
96 fn unpinned(&self, page: usize) -> usize {
99 self.stuck() - self.pinned(page)
100 }
101
102 fn body(&self, page: usize) -> usize {
104 page - self.pinned(page)
105 }
106
107 fn scroll_for(&mut self, page: usize) -> (&mut ScrollState, usize) {
110 let (len, body) = (self.lines.len() - self.pinned(page), self.body(page));
111 self.scroll.set_len(len);
112 (&mut self.scroll, body)
113 }
114
115 #[must_use]
119 pub fn rebind(mut self, action: &str, keys: impl IntoIterator<Item = Key>) -> Viewport {
120 self.keymap.rebind(action, keys);
121 self
122 }
123
124 #[must_use]
126 pub fn with_height(mut self, height: usize) -> Viewport {
127 self.height = Some(height);
128 self
129 }
130
131 pub fn set_lines(&mut self, lines: Vec<Vec<Segment>>) {
133 self.lines = lines;
134 self.scroll.set_len(self.lines.len() - self.stuck());
135 }
136
137 pub fn lines(&self) -> &[Vec<Segment>] {
138 &self.lines
139 }
140
141 pub fn len(&self) -> usize {
142 self.lines.len()
143 }
144
145 pub fn is_empty(&self) -> bool {
146 self.lines.is_empty()
147 }
148
149 pub fn offset(&self) -> usize {
152 self.scroll.offset()
153 }
154
155 pub fn scroll_state(&self) -> &ScrollState {
157 &self.scroll
158 }
159
160 pub fn page(&self, available: usize) -> usize {
162 self.height.unwrap_or(available).max(1)
163 }
164
165 pub fn scroll(&mut self, delta: isize, page: usize) -> bool {
167 let (scroll, body) = self.scroll_for(page);
168 scroll.scroll(delta, body)
169 }
170
171 pub fn scroll_to(&mut self, line: usize, page: usize) -> bool {
174 let (scroll, body) = self.scroll_for(page);
175 scroll.scroll_to(line, body)
176 }
177
178 pub fn show(&mut self, line: usize, page: usize) -> bool {
181 let (unpinned, pinned) = (self.unpinned(page), self.pinned(page));
182 let first = (line + 1).saturating_sub(page);
186 let last = if line < unpinned {
187 line
188 } else if line < self.stuck() {
189 usize::MAX
190 } else {
191 line - pinned
192 };
193 let offset = self.offset();
194 if offset < first {
195 self.scroll_to(first, page)
196 } else if offset > last {
197 self.scroll_to(last, page)
198 } else {
199 false
200 }
201 }
202
203 pub fn act(&mut self, action: &str, page: usize) -> bool {
207 let (scroll, body) = self.scroll_for(page);
208 scroll.act(action, body)
209 }
210
211 pub fn handle_scroll(&mut self, event: &Event, page: usize) -> bool {
215 let (scroll, body) = self.scroll_for(page);
216 scroll.handle(event, body)
217 }
218
219 fn stays(&self, page: usize) -> bool {
222 self.offset() >= self.unpinned(page)
223 }
224
225 pub fn top(&self, page: usize) -> usize {
229 let offset = self.offset();
230 if self.stays(page) {
231 offset + self.pinned(page)
232 } else {
233 offset
234 }
235 }
236
237 pub fn visible(&self, page: usize) -> &[Vec<Segment>] {
240 let end = (self.offset() + page).min(self.lines.len());
241 &self.lines[self.top(page).min(end)..end]
242 }
243
244 pub fn rows(&self, page: usize) -> Vec<(usize, &[Segment])> {
247 let staying = if self.stays(page) {
248 self.unpinned(page)..self.stuck()
249 } else {
250 0..0
251 };
252 let end = (self.offset() + page).min(self.lines.len());
253 staying
254 .chain(self.top(page).min(end)..end)
255 .map(|index| (index, self.lines[index].as_slice()))
256 .collect()
257 }
258
259 pub fn status(&self, page: usize) -> String {
263 let total = self.lines.len();
264 if total <= page {
265 return format!("all {total} lines");
266 }
267 let first = match self.offset() {
268 0 => 0,
269 _ => self.top(page),
270 };
271 let last = (self.offset() + page).min(total);
272 format!("lines {}–{last} of {total}", first + 1)
273 }
274}
275
276impl Component for Viewport {
280 type Output = usize;
281
282 fn handle(&mut self, event: &Event, context: &Context<'_>) -> Flow<usize> {
283 let page = self.page(context.height.saturating_sub(1));
284 let Some(key) = event.key() else {
285 return if self.handle_scroll(event, page) {
287 Flow::Continue
288 } else {
289 Flow::Ignored
290 };
291 };
292 match self.keymap.action(key).map(str::to_string).as_deref() {
293 Some("done") => Flow::Done(self.offset()),
294 Some("quit") => Flow::Cancel,
295 Some(action) if self.act(action, page) => Flow::Continue,
296 _ => Flow::Ignored,
297 }
298 }
299
300 fn render(&self, context: &Context<'_>) -> View {
301 let page = self.page(context.height.saturating_sub(1));
302 let mut lines: Vec<Vec<Segment>> = self
303 .rows(page)
304 .into_iter()
305 .map(|(_, line)| line.to_vec())
306 .collect();
307 let status = format!("{} · ↑↓ PgUp PgDn · q quits", self.status(page));
308 lines.push(vec![Segment::new(
309 status,
310 Some(Style::parse("dim").expect("style")),
311 )]);
312 View::new(lines)
313 }
314
315 fn keymap(&self) -> Keymap {
316 self.keymap.clone()
317 }
318
319 fn default_value(&self) -> Option<usize> {
320 Some(self.offset())
321 }
322}
323
324#[cfg(test)]
325mod tests {
326 use super::*;
327 use crate::event::{Key, MouseKind};
328
329 fn lines(n: usize) -> Vec<Vec<Segment>> {
330 (1..=n)
331 .map(|i| vec![Segment::new(format!("line {i}"), None)])
332 .collect()
333 }
334
335 #[test]
336 fn scrolls_within_bounds() {
337 let mut viewport = Viewport::new(lines(10));
338 assert!(!viewport.scroll(-1, 4));
339 assert!(viewport.scroll(3, 4));
340 assert_eq!(viewport.offset(), 3);
341 assert!(viewport.scroll(100, 4));
342 assert_eq!(viewport.offset(), 6, "the last page is full");
343 assert_eq!(viewport.visible(4).len(), 4);
344 assert_eq!(viewport.status(4), "lines 7–10 of 10");
345 viewport.show(1, 4);
346 assert_eq!(viewport.offset(), 1);
347 viewport.show(8, 4);
348 assert_eq!(viewport.offset(), 5);
349 assert_eq!(Viewport::new(lines(3)).status(4), "all 3 lines");
350 }
351
352 #[test]
353 fn keys_and_wheel_move_it() {
354 let mut viewport = Viewport::new(lines(50));
355 let key = |name: &str| Event::Key(Key::parse(name).unwrap());
356 assert!(viewport.handle_scroll(&key("pagedown"), 10));
357 assert_eq!(viewport.offset(), 10);
358 assert!(viewport.handle_scroll(&key("j"), 10));
359 assert_eq!(viewport.offset(), 11);
360 assert!(viewport.handle_scroll(&key("G"), 10));
361 assert_eq!(viewport.offset(), 40);
362 assert!(viewport.handle_scroll(&key("home"), 10));
363 assert_eq!(viewport.offset(), 0);
364 assert!(!viewport.handle_scroll(&key("x"), 10));
365 let wheel = Event::Mouse(crate::event::Mouse {
366 kind: MouseKind::ScrollDown,
367 column: 0,
368 row: 0,
369 modifiers: Default::default(),
370 pixel: None,
371 });
372 assert!(viewport.handle_scroll(&wheel, 10));
373 assert_eq!(viewport.offset(), 3);
374 }
375
376 #[test]
379 fn rebound_keys_scroll_it() {
380 use crate::headless::{self, Script};
381
382 let viewport = Viewport::new(lines(50)).rebind("scroll-down", [Key::char('n')]);
383 let (outcome, _) = headless::run(viewport, Script::new().keys("n n j enter"), 20, 6);
384 assert_eq!(outcome.unwrap(), crate::Outcome::Done(2));
385 let viewport = Viewport::new(lines(50)).rebind("done", [Key::char('x')]);
386 let (outcome, _) = headless::run(viewport, Script::new().keys("end x"), 20, 6);
387 assert_eq!(outcome.unwrap(), crate::Outcome::Done(45));
388 }
389
390 #[test]
391 fn replacing_lines_keeps_a_valid_offset() {
392 let mut viewport = Viewport::new(lines(50));
393 viewport.scroll_to(40, 10);
394 viewport.set_lines(lines(5));
395 assert_eq!(viewport.offset(), 4);
396 assert_eq!(viewport.visible(10).len(), 1);
397 }
398}