1use super::line::Line;
2use super::soft_wrap::{soft_wrap_lines_with_map, truncate_line};
3
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
6pub struct Cursor {
7 pub row: usize,
8 pub col: usize,
9 pub is_visible: bool,
10}
11
12impl Cursor {
13 pub fn hidden() -> Self {
15 Self::default()
16 }
17
18 pub fn visible(row: usize, col: usize) -> Self {
20 Self { row, col, is_visible: true }
21 }
22
23 pub fn shift_col(self, delta: usize) -> Self {
26 if self.is_visible { Self { col: self.col + delta, ..self } } else { self }
27 }
28}
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum Overflow {
33 Wrap,
35 Truncate,
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct FitOptions {
42 pub overflow_x: Overflow,
43 pub fill_x: bool,
47}
48
49impl FitOptions {
50 pub fn wrap() -> Self {
52 Self { overflow_x: Overflow::Wrap, fill_x: false }
53 }
54
55 pub fn truncate() -> Self {
57 Self { overflow_x: Overflow::Truncate, fill_x: false }
58 }
59
60 pub fn with_fill(mut self) -> Self {
62 self.fill_x = true;
63 self
64 }
65}
66
67#[derive(Debug, Clone)]
76pub struct FramePart {
77 pub frame: Frame,
78 pub width: u16,
79}
80
81impl FramePart {
82 pub fn new(frame: Frame, width: u16) -> Self {
86 Self { frame, width }
87 }
88
89 pub fn fit(frame: Frame, width: u16, options: FitOptions) -> Self {
93 Self { frame: frame.fit(width, options), width }
94 }
95
96 pub fn wrap(frame: Frame, width: u16) -> Self {
100 Self::fit(frame, width, FitOptions::wrap().with_fill())
101 }
102
103 pub fn truncate(frame: Frame, width: u16) -> Self {
107 Self::fit(frame, width, FitOptions::truncate().with_fill())
108 }
109}
110
111#[doc = include_str!("../docs/frame.md")]
112#[derive(Debug, Clone, PartialEq, Eq)]
113pub struct Frame {
114 lines: Vec<Line>,
115 cursor: Cursor,
116}
117
118impl Frame {
119 pub fn new(lines: Vec<Line>) -> Self {
120 Self { lines, cursor: Cursor::hidden() }
121 }
122
123 pub fn empty() -> Self {
125 Self { lines: Vec::new(), cursor: Cursor::hidden() }
126 }
127
128 pub fn lines(&self) -> &[Line] {
129 &self.lines
130 }
131
132 pub fn cursor(&self) -> Cursor {
133 self.cursor
134 }
135
136 pub fn with_cursor(mut self, cursor: Cursor) -> Self {
138 self.cursor = cursor;
139 self
140 }
141
142 pub fn into_lines(self) -> Vec<Line> {
143 self.lines
144 }
145
146 pub fn into_parts(self) -> (Vec<Line>, Cursor) {
147 (self.lines, self.cursor)
148 }
149
150 pub fn clamp_cursor(mut self) -> Self {
151 if self.cursor.row >= self.lines.len() {
152 self.cursor.row = self.lines.len().saturating_sub(1);
153 }
154 self
155 }
156
157 pub fn fit(self, width: u16, options: FitOptions) -> Self {
176 if width == 0 {
177 return Self { lines: self.lines, cursor: Cursor::hidden() };
178 }
179
180 match options.overflow_x {
181 Overflow::Wrap => self.fit_wrap(width, options.fill_x),
182 Overflow::Truncate => self.fit_truncate(width, options.fill_x),
183 }
184 }
185
186 pub fn indent(self, cols: u16) -> Self {
193 if cols == 0 {
194 return self;
195 }
196 let prefix = " ".repeat(usize::from(cols));
197 let lines = self.lines.into_iter().map(|line| line.prepend(prefix.clone())).collect();
198 Self { lines, cursor: self.cursor.shift_col(usize::from(cols)) }
199 }
200
201 pub fn vstack(frames: impl IntoIterator<Item = Frame>) -> Self {
206 let mut all_lines: Vec<Line> = Vec::new();
207 let mut cursor = Cursor::hidden();
208 for frame in frames {
209 let row_offset = all_lines.len();
210 if !cursor.is_visible && frame.cursor.is_visible {
211 cursor = Cursor { row: frame.cursor.row + row_offset, col: frame.cursor.col, is_visible: true };
212 }
213 all_lines.extend(frame.lines);
214 }
215 Self { lines: all_lines, cursor }
216 }
217
218 pub fn hstack(parts: impl IntoIterator<Item = FramePart>) -> Self {
228 let parts: Vec<FramePart> = parts.into_iter().collect();
229 if parts.is_empty() {
230 return Self::empty();
231 }
232
233 let max_rows = parts.iter().map(|p| p.frame.lines.len()).max().unwrap_or(0);
234
235 let mut cursor = Cursor::hidden();
236 let mut col_offset: usize = 0;
237 for part in &parts {
238 if !cursor.is_visible && part.frame.cursor.is_visible {
239 cursor =
240 Cursor { row: part.frame.cursor.row, col: part.frame.cursor.col + col_offset, is_visible: true };
241 }
242 col_offset += usize::from(part.width);
243 }
244
245 let mut merged: Vec<Line> = Vec::with_capacity(max_rows);
246 for row_idx in 0..max_rows {
247 let mut row = Line::default();
248 for part in &parts {
249 let slot_width = usize::from(part.width);
250 let Some(line) = part.frame.lines.get(row_idx) else {
251 row.push_text(" ".repeat(slot_width));
252 continue;
253 };
254 if line.fill().is_some() {
255 let mut materialized = line.clone();
256 materialized.extend_bg_to_width(slot_width);
257 row.append_line(&materialized);
258 } else {
259 row.append_line(line);
260 let line_width = line.display_width();
261 if line_width < slot_width {
262 row.push_text(" ".repeat(slot_width - line_width));
263 }
264 }
265 }
266 merged.push(row);
267 }
268
269 Self { lines: merged, cursor }
270 }
271
272 pub fn map_lines<T: FnMut(Line) -> Line>(self, f: T) -> Self {
276 let lines = self.lines.into_iter().map(f).collect();
277 Self { lines, cursor: self.cursor }
278 }
279
280 pub fn prefix(self, head: &Line, tail: &Line) -> Self {
289 let shift = head.display_width();
290 debug_assert_eq!(shift, tail.display_width(), "Frame::prefix: head and tail must have equal display width");
291 let lines: Vec<Line> = self
292 .lines
293 .into_iter()
294 .enumerate()
295 .map(|(i, line)| {
296 let prefix_src = if i == 0 { head } else { tail };
297 let row_fill = line.fill();
298 let mut prefixed = Line::default();
299 prefixed.append_line(prefix_src);
300 prefixed.append_line(&line);
301 prefixed.set_fill(row_fill);
302 prefixed
303 })
304 .collect();
305
306 Self { lines, cursor: self.cursor.shift_col(shift) }
307 }
308
309 pub fn pad_height(self, target: u16, width: u16) -> Self {
312 let target_usize = usize::from(target);
313 let mut lines = self.lines;
314 if lines.len() < target_usize {
315 let blank = Line::new(" ".repeat(usize::from(width)));
316 lines.resize(target_usize, blank);
317 }
318 Self { lines, cursor: self.cursor }
319 }
320
321 pub fn truncate_height(self, target: u16) -> Self {
324 let target_usize = usize::from(target);
325 let mut lines = self.lines;
326 if lines.len() > target_usize {
327 lines.truncate(target_usize);
328 }
329 let cursor =
330 if self.cursor.is_visible && self.cursor.row >= target_usize { Cursor::hidden() } else { self.cursor };
331 Self { lines, cursor }
332 }
333
334 pub fn fit_height(self, target: u16, width: u16) -> Self {
338 self.truncate_height(target).pad_height(target, width)
339 }
340
341 pub fn wrap_each(self, inner_width: u16, left: &Line, right: &Line) -> Self {
348 let inner_width_usize = usize::from(inner_width);
349 let left_width = left.display_width();
350 let lines: Vec<Line> = self
351 .lines
352 .into_iter()
353 .map(|mut line| {
354 line.extend_bg_to_width(inner_width_usize);
355 let mut wrapped = Line::default();
356 wrapped.append_line(left);
357 wrapped.append_line(&line);
358 wrapped.append_line(right);
359 wrapped
360 })
361 .collect();
362 Self { lines, cursor: self.cursor.shift_col(left_width) }
363 }
364
365 pub fn splice(self, after_row: usize, other: Frame) -> Self {
379 let inserted_count = other.lines.len();
380 if inserted_count == 0 {
381 return self;
382 }
383
384 let split_at = (after_row + 1).min(self.lines.len());
385 let mut lines = self.lines;
386 let tail: Vec<Line> = lines.drain(split_at..).collect();
387 lines.extend(other.lines);
388 lines.extend(tail);
389
390 let cursor = if self.cursor.is_visible {
391 if self.cursor.row > after_row {
392 Cursor { row: self.cursor.row + inserted_count, ..self.cursor }
393 } else {
394 self.cursor
395 }
396 } else if other.cursor.is_visible {
397 Cursor { row: other.cursor.row + split_at, col: other.cursor.col, is_visible: true }
398 } else {
399 Cursor::hidden()
400 };
401
402 Self { lines, cursor }
403 }
404
405 pub fn scroll(self, offset: usize, height: usize) -> Self {
410 let end = (offset + height).min(self.lines.len());
411 let visible: Vec<Line> = self.lines.into_iter().skip(offset).take(height).collect();
412
413 let cursor = if self.cursor.is_visible && self.cursor.row >= offset && self.cursor.row < end {
414 Cursor { row: self.cursor.row - offset, col: self.cursor.col, is_visible: true }
415 } else {
416 Cursor::hidden()
417 };
418
419 Self { lines: visible, cursor }
420 }
421
422 fn fit_wrap(self, width: u16, fill_x: bool) -> Self {
423 let (mut wrapped_lines, logical_to_visual) = soft_wrap_lines_with_map(&self.lines, width);
424
425 let cursor = if self.cursor.is_visible {
426 let mut visual_row = logical_to_visual
427 .get(self.cursor.row)
428 .copied()
429 .unwrap_or_else(|| wrapped_lines.len().saturating_sub(1));
430 let mut visual_col = self.cursor.col;
431 let width_usize = usize::from(width);
432 visual_row += visual_col / width_usize;
433 visual_col %= width_usize;
434 if visual_row >= wrapped_lines.len() {
435 visual_row = wrapped_lines.len().saturating_sub(1);
436 }
437 Cursor { row: visual_row, col: visual_col, is_visible: true }
438 } else {
439 Cursor::hidden()
440 };
441
442 apply_fill_metadata(&mut wrapped_lines, fill_x);
443 Self { lines: wrapped_lines, cursor }
444 }
445
446 fn fit_truncate(self, width: u16, fill_x: bool) -> Self {
447 let width_usize = usize::from(width);
448 let mut lines: Vec<Line> = self.lines.iter().map(|line| truncate_line(line, width_usize)).collect();
449
450 apply_fill_metadata(&mut lines, fill_x);
451
452 let cursor = if self.cursor.is_visible {
453 let max_col = width_usize.saturating_sub(1);
454 Cursor { row: self.cursor.row, col: self.cursor.col.min(max_col), is_visible: true }
455 } else {
456 Cursor::hidden()
457 };
458
459 Self { lines, cursor }
460 }
461}
462
463fn apply_fill_metadata(lines: &mut [Line], fill_x: bool) {
464 if !fill_x {
465 return;
466 }
467 for line in lines {
468 line.set_fill(line.infer_fill_color());
469 }
470}
471
472#[cfg(test)]
473mod tests {
474 use super::*;
475
476 #[test]
477 fn cursor_hidden_returns_invisible_cursor_at_origin() {
478 let cursor = Cursor::hidden();
479 assert_eq!(cursor.row, 0);
480 assert_eq!(cursor.col, 0);
481 assert!(!cursor.is_visible);
482 }
483
484 #[test]
485 fn cursor_visible_returns_visible_cursor_at_position() {
486 let cursor = Cursor::visible(5, 10);
487 assert_eq!(cursor.row, 5);
488 assert_eq!(cursor.col, 10);
489 assert!(cursor.is_visible);
490 }
491
492 #[test]
493 fn clamp_cursor_clamps_out_of_bounds_row() {
494 let frame = Frame::new(vec![Line::new("a")]).with_cursor(Cursor::visible(10, 100));
495 let frame = frame.clamp_cursor();
496 assert_eq!(frame.cursor().row, 0);
497 assert_eq!(frame.cursor().col, 100);
498 }
499
500 #[test]
501 fn with_cursor_replaces_cursor_without_cloning_lines() {
502 let frame = Frame::new(vec![Line::new("hello")]);
503 let new_cursor = Cursor::visible(0, 3);
504 let frame = frame.with_cursor(new_cursor);
505 assert_eq!(frame.cursor(), new_cursor);
506 assert_eq!(frame.lines()[0].plain_text(), "hello");
507 }
508
509 #[test]
510 fn fit_wrap_breaks_long_line_into_multiple_rows() {
511 let frame = Frame::new(vec![Line::new("abcdef")]);
512 let frame = frame.fit(3, FitOptions::wrap());
513 assert_eq!(frame.lines().len(), 2);
514 assert_eq!(frame.lines()[0].plain_text(), "abc");
515 assert_eq!(frame.lines()[1].plain_text(), "def");
516 }
517
518 #[test]
519 fn fit_wrap_remaps_cursor_on_wrapped_row() {
520 let frame = Frame::new(vec![Line::new("abcdef")]).with_cursor(Cursor::visible(0, 5));
521 let frame = frame.fit(3, FitOptions::wrap());
522 assert_eq!(frame.cursor().row, 1);
524 assert_eq!(frame.cursor().col, 2);
525 assert!(frame.cursor().is_visible);
526 }
527
528 #[test]
529 fn fit_wrap_remaps_cursor_across_logical_rows() {
530 let frame = Frame::new(vec![Line::new("abcdef"), Line::new("xy")]).with_cursor(Cursor::visible(1, 1));
531 let frame = frame.fit(3, FitOptions::wrap());
532 assert_eq!(frame.cursor().row, 2);
535 assert_eq!(frame.cursor().col, 1);
536 }
537
538 #[test]
539 fn fit_wrap_hides_cursor_when_logical_row_is_invisible() {
540 let frame = Frame::new(vec![Line::new("abcdef")]);
541 let frame = frame.fit(3, FitOptions::wrap());
542 assert!(!frame.cursor().is_visible);
543 }
544
545 #[test]
546 fn fit_wrap_with_fill_marks_each_row_with_fill_metadata_only() {
547 use crate::style::Style;
548 use crossterm::style::Color;
549 let frame = Frame::new(vec![Line::with_style("abcdef", Style::default().bg_color(Color::Blue))]);
553 let frame = frame.fit(4, FitOptions::wrap().with_fill());
554 assert_eq!(frame.lines().len(), 2);
555 assert_eq!(frame.lines()[0].plain_text(), "abcd");
556 assert_eq!(frame.lines()[1].plain_text(), "ef");
557 for line in frame.lines() {
558 assert_eq!(line.fill(), Some(Color::Blue), "fill metadata should be set");
559 }
560 }
561
562 #[test]
563 fn fit_wrap_zero_width_returns_lines_unchanged_and_hides_cursor() {
564 let frame = Frame::new(vec![Line::new("abc")]).with_cursor(Cursor::visible(0, 1));
565 let frame = frame.fit(0, FitOptions::wrap());
566 assert_eq!(frame.lines().len(), 1);
567 assert_eq!(frame.lines()[0].plain_text(), "abc");
568 assert!(!frame.cursor().is_visible);
569 }
570
571 #[test]
572 fn fit_truncate_cuts_each_row_to_width() {
573 let frame = Frame::new(vec![Line::new("abcdef"), Line::new("xyz")]);
574 let frame = frame.fit(3, FitOptions::truncate());
575 assert_eq!(frame.lines().len(), 2);
576 assert_eq!(frame.lines()[0].plain_text(), "abc");
577 assert_eq!(frame.lines()[1].plain_text(), "xyz");
578 }
579
580 #[test]
581 fn fit_truncate_clamps_cursor_col_within_width() {
582 let frame = Frame::new(vec![Line::new("abcdef")]).with_cursor(Cursor::visible(0, 10));
583 let frame = frame.fit(3, FitOptions::truncate());
584 assert_eq!(frame.cursor().row, 0);
585 assert_eq!(frame.cursor().col, 2); assert!(frame.cursor().is_visible);
587 }
588
589 #[test]
590 fn fit_truncate_preserves_in_range_cursor() {
591 let frame = Frame::new(vec![Line::new("abcdef")]).with_cursor(Cursor::visible(0, 1));
592 let frame = frame.fit(5, FitOptions::truncate());
593 assert_eq!(frame.cursor().col, 1);
594 }
595
596 #[test]
597 fn fit_truncate_preserves_row_count() {
598 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]);
599 let frame = frame.fit(2, FitOptions::truncate());
600 assert_eq!(frame.lines().len(), 3);
601 }
602
603 #[test]
604 fn fit_truncate_with_fill_marks_rows_with_fill_metadata_only() {
605 use crate::style::Style;
606 use crossterm::style::Color;
607 let frame = Frame::new(vec![Line::with_style("ab", Style::default().bg_color(Color::Red))]);
608 let frame = frame.fit(5, FitOptions::truncate().with_fill());
609 assert_eq!(frame.lines()[0].plain_text(), "ab");
611 assert_eq!(frame.lines()[0].fill(), Some(Color::Red));
612 }
613
614 #[test]
615 fn indent_prepends_spaces_to_each_line() {
616 let frame = Frame::new(vec![Line::new("a"), Line::new("b")]);
617 let frame = frame.indent(2);
618 assert_eq!(frame.lines()[0].plain_text(), " a");
619 assert_eq!(frame.lines()[1].plain_text(), " b");
620 }
621
622 #[test]
623 fn indent_shifts_cursor_col() {
624 let frame = Frame::new(vec![Line::new("hi")]).with_cursor(Cursor::visible(0, 1));
625 let frame = frame.indent(3);
626 assert_eq!(frame.cursor().row, 0);
627 assert_eq!(frame.cursor().col, 4);
628 assert!(frame.cursor().is_visible);
629 }
630
631 #[test]
632 fn indent_zero_is_noop() {
633 let frame = Frame::new(vec![Line::new("hi")]).with_cursor(Cursor::visible(0, 1));
634 let original_text = frame.lines()[0].plain_text();
635 let original_cursor = frame.cursor();
636 let frame = frame.indent(0);
637 assert_eq!(frame.lines()[0].plain_text(), original_text);
638 assert_eq!(frame.cursor(), original_cursor);
639 }
640
641 #[test]
642 fn indent_carries_background_through_prefix() {
643 use crate::style::Style;
644 use crossterm::style::Color;
645 let frame = Frame::new(vec![Line::with_style("hi", Style::default().bg_color(Color::Blue))]);
646 let frame = frame.indent(2);
647 let line = &frame.lines()[0];
648 assert_eq!(line.spans()[0].style().bg, Some(Color::Blue));
650 assert_eq!(line.plain_text(), " hi");
651 }
652
653 #[test]
654 fn indent_does_not_make_hidden_cursor_visible() {
655 let frame = Frame::new(vec![Line::new("hi")]);
656 let frame = frame.indent(2);
657 assert!(!frame.cursor().is_visible);
658 }
659
660 #[test]
661 fn vstack_empty_input_produces_empty_frame() {
662 let frame = Frame::vstack(std::iter::empty());
663 assert!(frame.lines().is_empty());
664 assert!(!frame.cursor().is_visible);
665 }
666
667 #[test]
668 fn vstack_concatenates_in_order() {
669 let a = Frame::new(vec![Line::new("a1"), Line::new("a2")]);
670 let b = Frame::new(vec![Line::new("b1")]);
671 let frame = Frame::vstack([a, b]);
672 assert_eq!(frame.lines().len(), 3);
673 assert_eq!(frame.lines()[0].plain_text(), "a1");
674 assert_eq!(frame.lines()[1].plain_text(), "a2");
675 assert_eq!(frame.lines()[2].plain_text(), "b1");
676 }
677
678 #[test]
679 fn vstack_offsets_cursor_by_preceding_line_count() {
680 let a = Frame::new(vec![Line::new("a1"), Line::new("a2")]);
681 let b = Frame::new(vec![Line::new("b1")]).with_cursor(Cursor::visible(0, 0));
682 let frame = Frame::vstack([a, b]);
683 assert_eq!(frame.cursor().row, 2);
684 assert_eq!(frame.cursor().col, 0);
685 assert!(frame.cursor().is_visible);
686 }
687
688 #[test]
689 fn vstack_first_visible_cursor_wins() {
690 let a = Frame::new(vec![Line::new("a")]).with_cursor(Cursor::visible(0, 1));
691 let b = Frame::new(vec![Line::new("b")]).with_cursor(Cursor::visible(0, 5));
692 let frame = Frame::vstack([a, b]);
693 assert_eq!(frame.cursor().row, 0);
694 assert_eq!(frame.cursor().col, 1);
695 }
696
697 #[test]
698 fn vstack_no_visible_cursor_returns_hidden_cursor() {
699 let a = Frame::new(vec![Line::new("a")]);
700 let b = Frame::new(vec![Line::new("b")]);
701 let frame = Frame::vstack([a, b]);
702 assert!(!frame.cursor().is_visible);
703 }
704
705 #[test]
706 fn hstack_empty_input_produces_empty_frame() {
707 let frame = Frame::hstack(std::iter::empty());
708 assert!(frame.lines().is_empty());
709 assert!(!frame.cursor().is_visible);
710 }
711
712 #[test]
713 fn hstack_merges_equal_height_parts_row_by_row() {
714 let left = Frame::new(vec![Line::new("aa"), Line::new("bb")]);
715 let right = Frame::new(vec![Line::new("XX"), Line::new("YY")]);
716 let frame = Frame::hstack([FramePart::new(left, 2), FramePart::new(right, 2)]);
717 assert_eq!(frame.lines().len(), 2);
718 assert_eq!(frame.lines()[0].plain_text(), "aaXX");
719 assert_eq!(frame.lines()[1].plain_text(), "bbYY");
720 }
721
722 #[test]
723 fn hstack_pads_shorter_part_with_blank_rows() {
724 let left = Frame::new(vec![Line::new("aa"), Line::new("bb")]);
725 let right = Frame::new(vec![Line::new("XX")]);
726 let frame = Frame::hstack([FramePart::new(left, 2), FramePart::new(right, 2)]);
727 assert_eq!(frame.lines().len(), 2);
728 assert_eq!(frame.lines()[0].plain_text(), "aaXX");
729 assert_eq!(frame.lines()[1].plain_text(), "bb ");
730 }
731
732 #[test]
733 fn hstack_left_visible_cursor_unchanged_col() {
734 let left = Frame::new(vec![Line::new("aa")]).with_cursor(Cursor::visible(0, 1));
735 let right = Frame::new(vec![Line::new("XX")]);
736 let frame = Frame::hstack([FramePart::new(left, 2), FramePart::new(right, 2)]);
737 assert_eq!(frame.cursor().row, 0);
738 assert_eq!(frame.cursor().col, 1);
739 assert!(frame.cursor().is_visible);
740 }
741
742 #[test]
743 fn hstack_right_visible_cursor_offset_by_left_width() {
744 let left = Frame::new(vec![Line::new("aaa")]);
745 let right = Frame::new(vec![Line::new("XX")]).with_cursor(Cursor::visible(0, 1));
746 let frame = Frame::hstack([FramePart::new(left, 3), FramePart::new(right, 2)]);
747 assert_eq!(frame.cursor().row, 0);
748 assert_eq!(frame.cursor().col, 1 + 3);
749 assert!(frame.cursor().is_visible);
750 }
751
752 #[test]
753 fn hstack_first_visible_cursor_wins_when_both_present() {
754 let left = Frame::new(vec![Line::new("aa")]).with_cursor(Cursor::visible(0, 0));
755 let right = Frame::new(vec![Line::new("XX")]).with_cursor(Cursor::visible(0, 1));
756 let frame = Frame::hstack([FramePart::new(left, 2), FramePart::new(right, 2)]);
757 assert_eq!(frame.cursor().col, 0);
758 }
759
760 #[test]
761 fn hstack_no_visible_cursor_returns_hidden_cursor() {
762 let left = Frame::new(vec![Line::new("aa")]);
763 let right = Frame::new(vec![Line::new("XX")]);
764 let frame = Frame::hstack([FramePart::new(left, 2), FramePart::new(right, 2)]);
765 assert!(!frame.cursor().is_visible);
766 }
767
768 #[test]
769 fn hstack_materializes_fill_to_each_part_slot_width() {
770 use crate::style::Style;
771 use crossterm::style::Color;
772
773 let left =
774 Frame::new(vec![Line::with_style("hi", Style::default().bg_color(Color::Red)).with_fill(Color::Red)]);
775 let right = Frame::new(vec![Line::new("XX")]);
776 let frame = Frame::hstack([FramePart::new(left, 5), FramePart::new(right, 2)]);
777 assert_eq!(frame.lines()[0].plain_text(), "hi XX");
779 assert_eq!(frame.lines()[0].fill(), None);
781 }
782
783 #[test]
784 fn fit_wrap_with_fill_propagates_metadata_to_wrapped_rows() {
785 use crate::style::Style;
786 use crossterm::style::Color;
787
788 let line = Line::with_style("abcdefgh", Style::default().bg_color(Color::Blue));
789 let frame = Frame::new(vec![line]).fit(3, FitOptions::wrap().with_fill());
790 assert_eq!(frame.lines().len(), 3);
791 for row in frame.lines() {
792 assert_eq!(row.fill(), Some(Color::Blue), "every wrapped row should carry fill metadata");
793 }
794 }
795
796 #[test]
797 fn hstack_three_parts_offsets_cursor_by_cumulative_widths() {
798 let left = Frame::new(vec![Line::new("aa")]);
799 let mid = Frame::new(vec![Line::new("|")]);
800 let right = Frame::new(vec![Line::new("XX")]).with_cursor(Cursor::visible(0, 1));
801 let frame = Frame::hstack([FramePart::new(left, 2), FramePart::new(mid, 1), FramePart::new(right, 2)]);
802 assert_eq!(frame.lines()[0].plain_text(), "aa|XX");
803 assert_eq!(frame.cursor().col, 1 + 2 + 1);
804 }
805
806 #[test]
807 fn map_lines_applies_function_to_each_line() {
808 let frame = Frame::new(vec![Line::new("a"), Line::new("b")]);
809 let frame = frame.map_lines(|mut line| {
810 line.push_text("!");
811 line
812 });
813 assert_eq!(frame.lines()[0].plain_text(), "a!");
814 assert_eq!(frame.lines()[1].plain_text(), "b!");
815 }
816
817 #[test]
818 fn map_lines_preserves_cursor() {
819 let frame = Frame::new(vec![Line::new("a"), Line::new("b")]).with_cursor(Cursor::visible(1, 0));
820 let frame = frame.map_lines(|line| line);
821 assert_eq!(frame.cursor(), Cursor::visible(1, 0));
822 }
823
824 #[test]
825 fn map_lines_preserves_row_count() {
826 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]);
827 let frame = frame.map_lines(|line| line);
828 assert_eq!(frame.lines().len(), 3);
829 }
830
831 #[test]
832 fn prefix_uses_head_on_first_row_and_tail_on_rest() {
833 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]);
834 let frame = frame.prefix(&Line::new("> "), &Line::new(" "));
835 assert_eq!(frame.lines()[0].plain_text(), "> a");
836 assert_eq!(frame.lines()[1].plain_text(), " b");
837 assert_eq!(frame.lines()[2].plain_text(), " c");
838 }
839
840 #[test]
841 fn prefix_shifts_cursor_col_by_gutter_width() {
842 let frame = Frame::new(vec![Line::new("hi")]).with_cursor(Cursor::visible(0, 1));
843 let frame = frame.prefix(&Line::new("> "), &Line::new(" "));
844 assert_eq!(frame.cursor().row, 0);
845 assert_eq!(frame.cursor().col, 1 + 2);
846 assert!(frame.cursor().is_visible);
847 }
848
849 #[test]
850 fn prefix_does_not_make_hidden_cursor_visible() {
851 let frame = Frame::new(vec![Line::new("a")]);
852 let frame = frame.prefix(&Line::new("> "), &Line::new(" "));
853 assert!(!frame.cursor().is_visible);
854 }
855
856 #[test]
857 fn prefix_preserves_row_fill_metadata() {
858 use crossterm::style::Color;
859 let line = Line::new("hi").with_fill(Color::Blue);
860 let frame = Frame::new(vec![line]);
861 let frame = frame.prefix(&Line::new("> "), &Line::new(" "));
862 assert_eq!(frame.lines()[0].fill(), Some(Color::Blue), "row-fill metadata should pass through prefix");
863 }
864
865 #[test]
866 fn prefix_carries_styled_head_into_output() {
867 use crate::style::Style;
868 use crossterm::style::Color;
869 let head = Line::with_style("├─ ", Style::fg(Color::Yellow));
870 let tail = Line::with_style(" ", Style::fg(Color::Yellow));
871 let frame = Frame::new(vec![Line::new("a"), Line::new("b")]).prefix(&head, &tail);
872 assert_eq!(frame.lines()[0].spans()[0].style().fg, Some(Color::Yellow));
873 assert_eq!(frame.lines()[1].spans()[0].style().fg, Some(Color::Yellow));
874 }
875
876 #[test]
877 fn prefix_empty_frame_returns_empty() {
878 let frame = Frame::empty().prefix(&Line::new("> "), &Line::new(" "));
879 assert!(frame.lines().is_empty());
880 assert!(!frame.cursor().is_visible);
881 }
882
883 #[test]
884 fn pad_height_appends_blank_rows_to_reach_target() {
885 let frame = Frame::new(vec![Line::new("a")]);
886 let frame = frame.pad_height(3, 4);
887 assert_eq!(frame.lines().len(), 3);
888 assert_eq!(frame.lines()[0].plain_text(), "a");
889 assert_eq!(frame.lines()[1].plain_text(), " ");
890 assert_eq!(frame.lines()[2].plain_text(), " ");
891 }
892
893 #[test]
894 fn pad_height_no_op_if_already_at_or_above_target() {
895 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]);
896 let frame = frame.pad_height(2, 4);
897 assert_eq!(frame.lines().len(), 3);
898 }
899
900 #[test]
901 fn pad_height_preserves_cursor() {
902 let frame = Frame::new(vec![Line::new("a")]).with_cursor(Cursor::visible(0, 1));
903 let frame = frame.pad_height(3, 4);
904 assert_eq!(frame.cursor(), Cursor::visible(0, 1));
905 }
906
907 #[test]
908 fn truncate_height_drops_excess_rows() {
909 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c"), Line::new("d")]);
910 let frame = frame.truncate_height(2);
911 assert_eq!(frame.lines().len(), 2);
912 assert_eq!(frame.lines()[0].plain_text(), "a");
913 assert_eq!(frame.lines()[1].plain_text(), "b");
914 }
915
916 #[test]
917 fn truncate_height_hides_cursor_when_row_falls_outside() {
918 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]).with_cursor(Cursor::visible(2, 0));
919 let frame = frame.truncate_height(2);
920 assert!(!frame.cursor().is_visible);
921 }
922
923 #[test]
924 fn truncate_height_preserves_cursor_when_in_range() {
925 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]).with_cursor(Cursor::visible(1, 0));
926 let frame = frame.truncate_height(2);
927 assert_eq!(frame.cursor(), Cursor::visible(1, 0));
928 }
929
930 #[test]
931 fn truncate_height_no_op_if_already_below_target() {
932 let frame = Frame::new(vec![Line::new("a")]);
933 let frame = frame.truncate_height(5);
934 assert_eq!(frame.lines().len(), 1);
935 }
936
937 #[test]
938 fn fit_height_truncates_taller_frames() {
939 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c"), Line::new("d")]);
940 let frame = frame.fit_height(2, 4);
941 assert_eq!(frame.lines().len(), 2);
942 }
943
944 #[test]
945 fn fit_height_pads_shorter_frames() {
946 let frame = Frame::new(vec![Line::new("a")]);
947 let frame = frame.fit_height(3, 4);
948 assert_eq!(frame.lines().len(), 3);
949 assert_eq!(frame.lines()[1].plain_text(), " ");
950 }
951
952 #[test]
953 fn wrap_each_adds_left_and_right_to_each_row() {
954 let frame = Frame::new(vec![Line::new("a"), Line::new("bb")]);
955 let frame = frame.wrap_each(3, &Line::new("│ "), &Line::new(" │"));
956 assert_eq!(frame.lines()[0].plain_text(), "│ a │");
957 assert_eq!(frame.lines()[1].plain_text(), "│ bb │");
958 }
959
960 #[test]
961 fn wrap_each_shifts_cursor_col_by_left_width() {
962 let frame = Frame::new(vec![Line::new("hi")]).with_cursor(Cursor::visible(0, 1));
963 let frame = frame.wrap_each(4, &Line::new("│ "), &Line::new(" │"));
964 assert_eq!(frame.cursor().row, 0);
965 assert_eq!(frame.cursor().col, 1 + 2);
966 assert!(frame.cursor().is_visible);
967 }
968
969 #[test]
970 fn wrap_each_materializes_fill_before_right_edge() {
971 use crate::style::Style;
972 use crossterm::style::Color;
973 let line = Line::with_style("hi", Style::default().bg_color(Color::Blue)).with_fill(Color::Blue);
974 let frame = Frame::new(vec![line]);
975 let frame = frame.wrap_each(5, &Line::new("│ "), &Line::new(" │"));
976 assert_eq!(frame.lines()[0].plain_text(), "│ hi │");
978 }
979
980 #[test]
981 fn wrap_each_does_not_make_hidden_cursor_visible() {
982 let frame = Frame::new(vec![Line::new("a")]);
983 let frame = frame.wrap_each(3, &Line::new("│ "), &Line::new(" │"));
984 assert!(!frame.cursor().is_visible);
985 }
986
987 #[test]
988 fn frame_part_fit_wraps_inner_to_slot_width() {
989 let inner = Frame::new(vec![Line::new("abcdefgh")]);
990 let part = FramePart::fit(inner, 3, FitOptions::wrap());
991 assert_eq!(part.width, 3);
992 assert_eq!(part.frame.lines().len(), 3);
993 assert_eq!(part.frame.lines()[0].plain_text(), "abc");
994 }
995
996 #[test]
997 fn frame_part_wrap_marks_rows_with_fill_metadata_when_bg_present() {
998 use crate::style::Style;
999 use crossterm::style::Color;
1000 let inner = Frame::new(vec![Line::with_style("abcdefgh", Style::default().bg_color(Color::Red))]);
1001 let part = FramePart::wrap(inner, 3);
1002 for line in part.frame.lines() {
1003 assert_eq!(line.fill(), Some(Color::Red), "wrap should mark each wrapped row with fill metadata");
1004 }
1005 }
1006
1007 #[test]
1008 fn frame_part_truncate_clips_inner_to_slot_width() {
1009 let inner = Frame::new(vec![Line::new("abcdefgh"), Line::new("xy")]);
1010 let part = FramePart::truncate(inner, 3);
1011 assert_eq!(part.width, 3);
1012 assert_eq!(part.frame.lines().len(), 2);
1013 assert_eq!(part.frame.lines()[0].plain_text(), "abc");
1014 assert_eq!(part.frame.lines()[1].plain_text(), "xy");
1015 }
1016
1017 #[test]
1018 fn frame_part_truncate_marks_rows_with_fill_metadata_when_bg_present() {
1019 use crate::style::Style;
1020 use crossterm::style::Color;
1021 let inner = Frame::new(vec![Line::with_style("abc", Style::default().bg_color(Color::Green))]);
1022 let part = FramePart::truncate(inner, 5);
1023 assert_eq!(part.frame.lines()[0].fill(), Some(Color::Green));
1024 }
1025
1026 #[test]
1027 fn frame_part_wrap_then_hstack_composes_full_width_per_row() {
1028 let left = Frame::new(vec![Line::new("abcdefgh")]);
1029 let right = Frame::new(vec![Line::new("XX"), Line::new("YY"), Line::new("ZZ")]);
1030 let frame = Frame::hstack([FramePart::wrap(left, 3), FramePart::wrap(right, 2)]);
1031 assert_eq!(frame.lines().len(), 3);
1032 for line in frame.lines() {
1033 assert_eq!(line.display_width(), 5, "every composed row should be exactly slot_left + slot_right wide");
1034 }
1035 assert_eq!(frame.lines()[0].plain_text(), "abcXX");
1036 assert_eq!(frame.lines()[1].plain_text(), "defYY");
1037 assert_eq!(frame.lines()[2].plain_text(), "gh ZZ");
1038 }
1039
1040 #[test]
1041 fn splice_inserts_after_row() {
1042 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]);
1043 let other = Frame::new(vec![Line::new("X"), Line::new("Y")]);
1044 let frame = frame.splice(1, other);
1045 assert_eq!(frame.lines().len(), 5);
1046 assert_eq!(frame.lines()[0].plain_text(), "a");
1047 assert_eq!(frame.lines()[1].plain_text(), "b");
1048 assert_eq!(frame.lines()[2].plain_text(), "X");
1049 assert_eq!(frame.lines()[3].plain_text(), "Y");
1050 assert_eq!(frame.lines()[4].plain_text(), "c");
1051 }
1052
1053 #[test]
1054 fn splice_at_end_appends() {
1055 let frame = Frame::new(vec![Line::new("a"), Line::new("b")]);
1056 let other = Frame::new(vec![Line::new("X")]);
1057 let frame = frame.splice(1, other);
1058 assert_eq!(frame.lines().len(), 3);
1059 assert_eq!(frame.lines()[2].plain_text(), "X");
1060 }
1061
1062 #[test]
1063 fn splice_beyond_end_appends() {
1064 let frame = Frame::new(vec![Line::new("a")]);
1065 let other = Frame::new(vec![Line::new("X")]);
1066 let frame = frame.splice(100, other);
1067 assert_eq!(frame.lines().len(), 2);
1068 assert_eq!(frame.lines()[1].plain_text(), "X");
1069 }
1070
1071 #[test]
1072 fn splice_empty_other_is_noop() {
1073 let frame = Frame::new(vec![Line::new("a"), Line::new("b")]).with_cursor(Cursor::visible(1, 0));
1074 let other = Frame::empty();
1075 let frame = frame.splice(0, other);
1076 assert_eq!(frame.lines().len(), 2);
1077 assert_eq!(frame.cursor(), Cursor::visible(1, 0));
1078 }
1079
1080 #[test]
1081 fn splice_shifts_self_cursor_down() {
1082 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]).with_cursor(Cursor::visible(2, 3));
1083 let other = Frame::new(vec![Line::new("X"), Line::new("Y"), Line::new("Z")]);
1084 let frame = frame.splice(1, other);
1085 assert_eq!(frame.cursor(), Cursor::visible(5, 3));
1086 }
1087
1088 #[test]
1089 fn splice_preserves_self_cursor_before_insertion() {
1090 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]).with_cursor(Cursor::visible(0, 1));
1091 let other = Frame::new(vec![Line::new("X")]);
1092 let frame = frame.splice(1, other);
1093 assert_eq!(frame.cursor(), Cursor::visible(0, 1));
1094 }
1095
1096 #[test]
1097 fn splice_does_not_shift_self_cursor_on_insertion_row() {
1098 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]).with_cursor(Cursor::visible(1, 0));
1099 let other = Frame::new(vec![Line::new("X")]);
1100 let frame = frame.splice(1, other);
1101 assert_eq!(frame.cursor(), Cursor::visible(1, 0));
1102 }
1103
1104 #[test]
1105 fn splice_adopts_other_cursor() {
1106 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]);
1107 let other = Frame::new(vec![Line::new("X"), Line::new("Y")]).with_cursor(Cursor::visible(1, 5));
1108 let frame = frame.splice(1, other);
1109 assert_eq!(frame.cursor(), Cursor::visible(3, 5));
1110 }
1111
1112 #[test]
1113 fn splice_self_cursor_wins() {
1114 let frame = Frame::new(vec![Line::new("a"), Line::new("b")]).with_cursor(Cursor::visible(0, 0));
1115 let other = Frame::new(vec![Line::new("X")]).with_cursor(Cursor::visible(0, 5));
1116 let frame = frame.splice(0, other);
1117 assert_eq!(frame.cursor(), Cursor::visible(0, 0));
1118 }
1119
1120 #[test]
1121 fn scroll_clips_to_viewport() {
1122 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c"), Line::new("d"), Line::new("e")]);
1123 let frame = frame.scroll(1, 3);
1124 assert_eq!(frame.lines().len(), 3);
1125 assert_eq!(frame.lines()[0].plain_text(), "b");
1126 assert_eq!(frame.lines()[1].plain_text(), "c");
1127 assert_eq!(frame.lines()[2].plain_text(), "d");
1128 }
1129
1130 #[test]
1131 fn scroll_adjusts_cursor() {
1132 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c"), Line::new("d"), Line::new("e")])
1133 .with_cursor(Cursor::visible(3, 2));
1134 let frame = frame.scroll(1, 4);
1135 assert_eq!(frame.cursor(), Cursor::visible(2, 2));
1136 }
1137
1138 #[test]
1139 fn scroll_hides_cursor_above_viewport() {
1140 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]).with_cursor(Cursor::visible(0, 0));
1141 let frame = frame.scroll(2, 1);
1142 assert!(!frame.cursor().is_visible);
1143 }
1144
1145 #[test]
1146 fn scroll_hides_cursor_below_viewport() {
1147 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c"), Line::new("d")])
1148 .with_cursor(Cursor::visible(3, 0));
1149 let frame = frame.scroll(0, 2);
1150 assert!(!frame.cursor().is_visible);
1151 }
1152
1153 #[test]
1154 fn scroll_zero_offset_is_truncate() {
1155 let frame = Frame::new(vec![Line::new("a"), Line::new("b"), Line::new("c")]);
1156 let frame = frame.scroll(0, 2);
1157 assert_eq!(frame.lines().len(), 2);
1158 assert_eq!(frame.lines()[0].plain_text(), "a");
1159 assert_eq!(frame.lines()[1].plain_text(), "b");
1160 }
1161}