1#![forbid(unsafe_code)]
2
3use crate::rope::Rope;
10use crate::wrap::{WrapMode, WrapOptions, display_width, wrap_with_options};
11use std::ops::Range;
12
13#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
15pub struct Viewport {
16 pub width: usize,
18 pub height: usize,
20}
21
22impl Viewport {
23 #[must_use]
25 pub const fn new(width: usize, height: usize) -> Self {
26 Self { width, height }
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
32pub struct ViewLine {
33 pub text: String,
35 pub source_line: usize,
37 pub is_wrap: bool,
39 pub width: usize,
41}
42
43#[derive(Debug, Clone)]
45pub struct TextView {
46 text: Rope,
47 wrap: WrapMode,
48 width: usize,
49 lines: Vec<ViewLine>,
50 max_width: usize,
51 source_line_count: usize,
52}
53
54impl TextView {
55 #[must_use]
57 pub fn new(text: impl Into<Rope>, width: usize, wrap: WrapMode) -> Self {
58 let mut view = Self {
59 text: text.into(),
60 wrap,
61 width,
62 lines: Vec::new(),
63 max_width: 0,
64 source_line_count: 0,
65 };
66 view.rebuild();
67 view
68 }
69
70 pub fn set_text(&mut self, text: impl Into<Rope>) {
72 self.text = text.into();
73 self.rebuild();
74 }
75
76 pub fn set_wrap(&mut self, wrap: WrapMode) {
78 if self.wrap != wrap {
79 self.wrap = wrap;
80 self.rebuild();
81 }
82 }
83
84 pub fn set_width(&mut self, width: usize) {
86 if self.width != width {
87 self.width = width;
88 self.rebuild();
89 }
90 }
91
92 #[must_use]
94 pub const fn wrap_mode(&self) -> WrapMode {
95 self.wrap
96 }
97
98 #[must_use]
100 pub const fn width(&self) -> usize {
101 self.width
102 }
103
104 #[must_use]
106 pub const fn source_line_count(&self) -> usize {
107 self.source_line_count
108 }
109
110 #[must_use]
112 pub fn virtual_line_count(&self) -> usize {
113 self.lines.len()
114 }
115
116 #[must_use]
118 pub const fn max_width(&self) -> usize {
119 self.max_width
120 }
121
122 #[must_use]
124 pub fn lines(&self) -> &[ViewLine] {
125 &self.lines
126 }
127
128 #[must_use]
130 pub fn source_to_virtual(&self, source_line: usize) -> Option<usize> {
131 self.lines
132 .iter()
133 .position(|line| line.source_line == source_line)
134 }
135
136 #[must_use]
138 pub fn virtual_to_source(&self, virtual_line: usize) -> Option<usize> {
139 self.lines.get(virtual_line).map(|line| line.source_line)
140 }
141
142 #[must_use]
144 pub fn clamp_scroll(&self, scroll_y: usize, viewport_height: usize) -> usize {
145 let total = self.lines.len();
146 if total == 0 {
147 return 0;
148 }
149 if viewport_height == 0 {
150 return scroll_y.min(total);
151 }
152 let max_scroll = total.saturating_sub(viewport_height);
153 scroll_y.min(max_scroll)
154 }
155
156 #[must_use]
158 pub fn max_scroll(&self, viewport_height: usize) -> usize {
159 let total = self.lines.len();
160 if total == 0 {
161 return 0;
162 }
163 if viewport_height == 0 {
164 return total;
165 }
166 total.saturating_sub(viewport_height)
167 }
168
169 #[must_use]
171 pub fn visible_range(&self, scroll_y: usize, viewport_height: usize) -> Range<usize> {
172 let total = self.lines.len();
173 if total == 0 || viewport_height == 0 {
174 return 0..0;
175 }
176 let scroll = self.clamp_scroll(scroll_y, viewport_height);
177 let end = (scroll + viewport_height).min(total);
178 scroll..end
179 }
180
181 #[must_use]
183 pub fn visible_lines(&self, scroll_y: usize, viewport_height: usize) -> &[ViewLine] {
184 let range = self.visible_range(scroll_y, viewport_height);
185 &self.lines[range]
186 }
187
188 #[must_use]
191 pub fn scroll_to_line(&self, source_line: usize, viewport_height: usize) -> Option<usize> {
192 let virtual_line = self.source_to_virtual(source_line)?;
193 Some(self.clamp_scroll(virtual_line, viewport_height))
194 }
195
196 #[must_use]
198 pub fn scroll_to_top(&self) -> usize {
199 0
200 }
201
202 #[must_use]
204 pub fn scroll_to_bottom(&self, viewport_height: usize) -> usize {
205 self.max_scroll(viewport_height)
206 }
207
208 #[must_use]
210 pub fn scroll_by_lines(&self, scroll_y: usize, delta: isize, viewport_height: usize) -> usize {
211 let next = (scroll_y as i64) + (delta as i64);
212 let next = if next < 0 { 0 } else { next as usize };
213 self.clamp_scroll(next, viewport_height)
214 }
215
216 #[must_use]
218 pub fn scroll_by_pages(&self, scroll_y: usize, pages: isize, viewport_height: usize) -> usize {
219 if viewport_height == 0 {
220 return self.clamp_scroll(scroll_y, viewport_height);
221 }
222 let delta = (viewport_height as i64) * (pages as i64);
223 let next = (scroll_y as i64) + delta;
224 let next = if next < 0 { 0 } else { next as usize };
225 self.clamp_scroll(next, viewport_height)
226 }
227
228 fn rebuild(&mut self) {
229 self.lines.clear();
230 self.max_width = 0;
231
232 let preserve_indent = self.wrap == WrapMode::Char;
233 let options = WrapOptions::new(self.width)
234 .mode(self.wrap)
235 .preserve_indent(preserve_indent);
236
237 let mut source_lines = 0;
238
239 for (source_line, line) in self.text.lines().enumerate() {
240 source_lines += 1;
241 let mut line_text = line.to_string();
242 if line_text.ends_with('\n') {
243 line_text.pop();
244 }
245
246 let wrapped = wrap_with_options(&line_text, &options);
247 if wrapped.is_empty() {
248 let width = 0;
249 self.lines.push(ViewLine {
250 text: String::new(),
251 source_line,
252 is_wrap: false,
253 width,
254 });
255 self.max_width = self.max_width.max(width);
256 continue;
257 }
258
259 for (idx, part) in wrapped.into_iter().enumerate() {
260 let width = display_width(&part);
261 self.max_width = self.max_width.max(width);
262 self.lines.push(ViewLine {
263 text: part,
264 source_line,
265 is_wrap: idx > 0,
266 width,
267 });
268 }
269 }
270
271 self.source_line_count = source_lines;
272 }
273}
274
275#[cfg(test)]
276mod tests {
277 use super::{TextView, Viewport};
278 use crate::wrap::WrapMode;
279
280 #[test]
281 fn view_basic_counts() {
282 let view = TextView::new("a\nbb", 10, WrapMode::None);
283 assert_eq!(view.source_line_count(), 2);
284 assert_eq!(view.virtual_line_count(), 2);
285 assert_eq!(view.max_width(), 2);
286 }
287
288 #[test]
289 fn view_wraps_word() {
290 let view = TextView::new("hello world", 5, WrapMode::Word);
291 let lines: Vec<&str> = view.lines().iter().map(|l| l.text.as_str()).collect();
292 assert_eq!(lines, vec!["hello", "world"]);
293 }
294
295 #[test]
296 fn view_wraps_cjk_by_cells() {
297 let view = TextView::new("你好世界", 4, WrapMode::Char);
298 let lines: Vec<&str> = view.lines().iter().map(|l| l.text.as_str()).collect();
299 assert_eq!(lines, vec!["你好", "世界"]);
300 }
301
302 #[test]
303 fn visible_range_clamps_scroll() {
304 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
305 let range = view.visible_range(5, 2);
306 assert_eq!(range, 1..3);
307 }
308
309 #[test]
310 fn scroll_to_line_clamps() {
311 let view = TextView::new("a\nb\nc\nd", 10, WrapMode::None);
312 let scroll = view.scroll_to_line(3, 2).expect("line 3 exists");
313 assert_eq!(scroll, 2);
314 }
315
316 #[test]
317 fn scroll_by_pages_moves_in_viewport_steps() {
318 let view = TextView::new("1\n2\n3\n4\n5", 10, WrapMode::None);
319 let scroll = view.scroll_by_pages(0, 1, 2);
320 assert_eq!(scroll, 2);
321 let back = view.scroll_by_pages(scroll, -1, 2);
322 assert_eq!(back, 0);
323 }
324
325 #[test]
326 fn scroll_to_bottom_respects_viewport() {
327 let view = TextView::new("a\nb\nc\nd", 10, WrapMode::None);
328 let bottom = view.scroll_to_bottom(2);
329 assert_eq!(bottom, 2);
330 let top = view.scroll_to_top();
331 assert_eq!(top, 0);
332 }
333
334 #[test]
335 fn visible_lines_returns_slice() {
336 let view = TextView::new("a\nb\nc\nd", 10, WrapMode::None);
337 let visible = view.visible_lines(1, 2);
338 let texts: Vec<&str> = visible.iter().map(|l| l.text.as_str()).collect();
339 assert_eq!(texts, vec!["b", "c"]);
340 }
341
342 #[test]
343 fn viewport_struct_is_copyable() {
344 let viewport = Viewport::new(80, 24);
345 let copy = viewport;
346 assert_eq!(copy.width, 80);
347 assert_eq!(copy.height, 24);
348 }
349
350 #[test]
353 fn empty_text_view() {
354 let view = TextView::new("", 10, WrapMode::None);
355 assert_eq!(view.source_line_count(), 1); assert_eq!(view.virtual_line_count(), 1);
357 assert_eq!(view.max_width(), 0);
358 }
359
360 #[test]
361 fn empty_text_scroll() {
362 let view = TextView::new("", 10, WrapMode::None);
363 assert_eq!(view.max_scroll(5), 0);
364 assert_eq!(view.clamp_scroll(100, 5), 0);
365 assert_eq!(view.visible_range(0, 5), 0..1);
366 }
367
368 #[test]
371 fn source_to_virtual_no_wrap() {
372 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
373 assert_eq!(view.source_to_virtual(0), Some(0));
374 assert_eq!(view.source_to_virtual(1), Some(1));
375 assert_eq!(view.source_to_virtual(2), Some(2));
376 assert_eq!(view.source_to_virtual(3), None);
377 }
378
379 #[test]
380 fn virtual_to_source_no_wrap() {
381 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
382 assert_eq!(view.virtual_to_source(0), Some(0));
383 assert_eq!(view.virtual_to_source(1), Some(1));
384 assert_eq!(view.virtual_to_source(2), Some(2));
385 assert_eq!(view.virtual_to_source(3), None);
386 }
387
388 #[test]
389 fn source_to_virtual_with_wrap() {
390 let view = TextView::new("abcde\nxy", 3, WrapMode::Char);
392 assert_eq!(view.source_to_virtual(0), Some(0)); assert_eq!(view.source_to_virtual(1), Some(2)); }
395
396 #[test]
397 fn virtual_to_source_with_wrap() {
398 let view = TextView::new("abcde\nxy", 3, WrapMode::Char);
399 assert_eq!(view.virtual_to_source(0), Some(0)); assert_eq!(view.virtual_to_source(1), Some(0)); assert_eq!(view.virtual_to_source(2), Some(1)); }
403
404 #[test]
407 fn is_wrap_flag_set_correctly() {
408 let view = TextView::new("abcde", 3, WrapMode::Char);
409 let lines = view.lines();
410 assert!(!lines[0].is_wrap); assert!(lines[1].is_wrap); }
413
414 #[test]
417 fn set_text_recomputes() {
418 let mut view = TextView::new("abc", 10, WrapMode::None);
419 assert_eq!(view.source_line_count(), 1);
420 view.set_text("a\nb\nc");
421 assert_eq!(view.source_line_count(), 3);
422 assert_eq!(view.virtual_line_count(), 3);
423 }
424
425 #[test]
426 fn set_wrap_recomputes() {
427 let mut view = TextView::new("hello world", 5, WrapMode::None);
428 let before = view.virtual_line_count();
429 view.set_wrap(WrapMode::Word);
430 let after = view.virtual_line_count();
431 assert!(after >= before);
433 }
434
435 #[test]
436 fn set_wrap_same_mode_is_noop() {
437 let mut view = TextView::new("hello", 10, WrapMode::None);
438 let count1 = view.virtual_line_count();
439 view.set_wrap(WrapMode::None); assert_eq!(view.virtual_line_count(), count1);
441 }
442
443 #[test]
444 fn set_width_recomputes() {
445 let mut view = TextView::new("abcdef", 3, WrapMode::Char);
446 let count_narrow = view.virtual_line_count();
447 view.set_width(100);
448 let count_wide = view.virtual_line_count();
449 assert!(count_narrow > count_wide);
450 }
451
452 #[test]
453 fn set_width_same_is_noop() {
454 let mut view = TextView::new("abc", 10, WrapMode::None);
455 let count = view.virtual_line_count();
456 view.set_width(10);
457 assert_eq!(view.virtual_line_count(), count);
458 }
459
460 #[test]
463 fn wrap_mode_accessor() {
464 let view = TextView::new("abc", 10, WrapMode::Word);
465 assert_eq!(view.wrap_mode(), WrapMode::Word);
466 }
467
468 #[test]
469 fn width_accessor() {
470 let view = TextView::new("abc", 42, WrapMode::None);
471 assert_eq!(view.width(), 42);
472 }
473
474 #[test]
477 fn max_width_across_lines() {
478 let view = TextView::new("ab\nabcde\nxy", 100, WrapMode::None);
479 assert_eq!(view.max_width(), 5); }
481
482 #[test]
483 fn max_width_with_wide_chars() {
484 let view = TextView::new("\u{4E16}\u{754C}", 100, WrapMode::None); assert_eq!(view.max_width(), 4);
486 }
487
488 #[test]
491 fn clamp_scroll_within_bounds() {
492 let view = TextView::new("a\nb\nc\nd", 10, WrapMode::None); assert_eq!(view.clamp_scroll(0, 2), 0);
494 assert_eq!(view.clamp_scroll(1, 2), 1);
495 assert_eq!(view.clamp_scroll(2, 2), 2); assert_eq!(view.clamp_scroll(3, 2), 2); assert_eq!(view.clamp_scroll(100, 2), 2); }
499
500 #[test]
501 fn clamp_scroll_viewport_larger_than_content() {
502 let view = TextView::new("a\nb", 10, WrapMode::None); assert_eq!(view.clamp_scroll(0, 10), 0);
505 assert_eq!(view.clamp_scroll(5, 10), 0);
506 }
507
508 #[test]
509 fn clamp_scroll_zero_viewport() {
510 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
511 let result = view.clamp_scroll(1, 0);
513 assert_eq!(result, 1);
514 }
515
516 #[test]
519 fn max_scroll_basic() {
520 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None); assert_eq!(view.max_scroll(3), 2); assert_eq!(view.max_scroll(5), 0); assert_eq!(view.max_scroll(10), 0); }
525
526 #[test]
527 fn max_scroll_zero_viewport() {
528 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
529 assert_eq!(view.max_scroll(0), 3); }
531
532 #[test]
535 fn visible_range_basic() {
536 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
537 assert_eq!(view.visible_range(0, 3), 0..3);
538 assert_eq!(view.visible_range(1, 3), 1..4);
539 assert_eq!(view.visible_range(2, 3), 2..5);
540 }
541
542 #[test]
543 fn visible_range_zero_viewport() {
544 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
545 assert_eq!(view.visible_range(0, 0), 0..0);
546 }
547
548 #[test]
549 fn visible_lines_content() {
550 let view = TextView::new("alpha\nbeta\ngamma\ndelta", 10, WrapMode::None);
551 let visible = view.visible_lines(1, 2);
552 assert_eq!(visible.len(), 2);
553 assert_eq!(visible[0].text, "beta");
554 assert_eq!(visible[1].text, "gamma");
555 }
556
557 #[test]
560 fn scroll_to_line_basic() {
561 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
562 assert_eq!(view.scroll_to_line(0, 3), Some(0));
563 assert_eq!(view.scroll_to_line(2, 3), Some(2));
564 assert_eq!(view.scroll_to_line(4, 3), Some(2)); }
566
567 #[test]
568 fn scroll_to_line_nonexistent() {
569 let view = TextView::new("a\nb", 10, WrapMode::None);
570 assert_eq!(view.scroll_to_line(5, 2), None);
571 }
572
573 #[test]
576 fn scroll_by_lines_positive() {
577 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
578 assert_eq!(view.scroll_by_lines(0, 2, 3), 2);
579 assert_eq!(view.scroll_by_lines(0, 100, 3), 2); }
581
582 #[test]
583 fn scroll_by_lines_negative() {
584 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
585 assert_eq!(view.scroll_by_lines(2, -1, 3), 1);
586 assert_eq!(view.scroll_by_lines(2, -100, 3), 0); }
588
589 #[test]
592 fn scroll_by_pages_forward() {
593 let text = (0..10)
595 .map(|i| format!("line{i}"))
596 .collect::<Vec<_>>()
597 .join("\n");
598 let view = TextView::new(text.as_str(), 100, WrapMode::None);
599 assert_eq!(view.scroll_by_pages(0, 1, 3), 3);
600 assert_eq!(view.scroll_by_pages(0, 2, 3), 6);
601 }
602
603 #[test]
604 fn scroll_by_pages_backward() {
605 let text = (0..10)
606 .map(|i| format!("line{i}"))
607 .collect::<Vec<_>>()
608 .join("\n");
609 let view = TextView::new(text.as_str(), 100, WrapMode::None);
610 assert_eq!(view.scroll_by_pages(6, -1, 3), 3);
611 assert_eq!(view.scroll_by_pages(6, -3, 3), 0); }
613
614 #[test]
615 fn scroll_by_pages_zero_viewport() {
616 let view = TextView::new("a\nb\nc", 10, WrapMode::None);
617 let result = view.scroll_by_pages(0, 1, 0);
619 assert_eq!(result, 0);
620 }
621
622 #[test]
625 fn scroll_to_top_and_bottom() {
626 let view = TextView::new("a\nb\nc\nd\ne", 10, WrapMode::None);
627 assert_eq!(view.scroll_to_top(), 0);
628 assert_eq!(view.scroll_to_bottom(3), 2);
629 assert_eq!(view.scroll_to_bottom(5), 0);
630 assert_eq!(view.scroll_to_bottom(1), 4);
631 }
632
633 #[test]
636 fn trailing_newline_text() {
637 let view = TextView::new("a\nb\n", 10, WrapMode::None);
638 assert_eq!(view.source_line_count(), 3);
639 let lines = view.lines();
641 assert_eq!(lines.last().unwrap().text, "");
642 }
643
644 #[test]
647 fn only_newlines() {
648 let view = TextView::new("\n\n\n", 10, WrapMode::None);
649 assert_eq!(view.source_line_count(), 4); assert_eq!(view.virtual_line_count(), 4);
651 for line in view.lines() {
652 assert_eq!(line.text, "");
653 }
654 }
655
656 #[test]
659 fn view_line_source_line_tracking() {
660 let view = TextView::new("ab\ncd\nef", 10, WrapMode::None);
661 for (i, line) in view.lines().iter().enumerate() {
662 assert_eq!(line.source_line, i);
663 assert!(!line.is_wrap);
664 }
665 }
666
667 #[test]
668 fn view_line_width_tracking() {
669 let view = TextView::new("ab\nabcde\n\u{4E16}", 10, WrapMode::None);
670 assert_eq!(view.lines()[0].width, 2);
671 assert_eq!(view.lines()[1].width, 5);
672 assert_eq!(view.lines()[2].width, 2); }
674
675 #[test]
678 fn viewport_default() {
679 let v = Viewport::default();
680 assert_eq!(v.width, 0);
681 assert_eq!(v.height, 0);
682 }
683
684 #[test]
685 fn viewport_equality() {
686 assert_eq!(Viewport::new(80, 24), Viewport::new(80, 24));
687 assert_ne!(Viewport::new(80, 24), Viewport::new(120, 24));
688 }
689}