1use super::frame::{FitOptions, Frame};
2use super::line::Line;
3
4pub fn digit_count(value: usize) -> usize {
7 value.checked_ilog10().map_or(1, |d| d as usize + 1)
8}
9
10pub fn wrap_with_gutter(content: Line, total_width: u16, head: &Line, tail: &Line) -> Frame {
20 let gutter_width = u16::try_from(head.display_width()).unwrap_or(u16::MAX);
21 let inner_width = total_width.saturating_sub(gutter_width);
22 let inner_width_usize = usize::from(inner_width);
23 Frame::new(vec![content])
24 .fit(inner_width, FitOptions::wrap())
25 .map_lines(|mut line| {
26 line.extend_bg_to_width(inner_width_usize);
27 line
28 })
29 .prefix(head, tail)
30}
31
32#[cfg(test)]
33mod tests {
34 use super::*;
35 use crate::rendering::style::Style;
36 use crossterm::style::Color;
37
38 fn head_tail(width: usize) -> (Line, Line) {
39 let head = Line::with_style(format!("{:>width$}", 7, width = width), Style::default());
40 let tail = Line::new(" ".repeat(width));
41 (head, tail)
42 }
43
44 #[test]
45 fn short_content_produces_single_row_with_head() {
46 let (head, tail) = head_tail(2);
47 let content = Line::new("hi");
48 let frame = wrap_with_gutter(content, 10, &head, &tail);
49 let lines = frame.into_lines();
50 assert_eq!(lines.len(), 1);
51 assert!(lines[0].plain_text().starts_with(" 7"), "expected head on first row: {:?}", lines[0].plain_text());
52 }
53
54 #[test]
55 fn long_content_wraps_and_tail_prefixes_continuations() {
56 let (head, tail) = head_tail(2);
57 let long = "x".repeat(30);
58 let content = Line::new(long);
59 let frame = wrap_with_gutter(content, 10, &head, &tail);
60 let lines = frame.into_lines();
61 assert!(lines.len() >= 2, "expected wrap into multiple rows, got {}", lines.len());
62 assert!(lines[0].plain_text().starts_with(" 7"), "row 0 should start with head");
63 for (i, line) in lines.iter().enumerate().skip(1) {
64 assert!(
65 line.plain_text().starts_with(" "),
66 "row {i} should start with tail spaces, got {:?}",
67 line.plain_text()
68 );
69 }
70 }
71
72 #[test]
73 fn content_background_is_extended_but_does_not_cover_gutter() {
74 let head = Line::with_style("77 ", Style::default());
75 let tail = Line::new(" ");
76 let bg = Color::Rgb { r: 10, g: 20, b: 30 };
77 let content = Line::with_style("short", Style::default().bg_color(bg));
78 let frame = wrap_with_gutter(content, 20, &head, &tail);
79 let lines = frame.into_lines();
80 assert_eq!(lines.len(), 1);
81 let spans = lines[0].spans();
82 assert_eq!(spans[0].style().bg, None, "gutter span should not carry content bg");
83 let any_content_bg = spans.iter().skip(1).any(|s| s.style().bg == Some(bg));
84 assert!(any_content_bg, "content spans should retain their bg");
85 }
86
87 #[test]
88 #[should_panic(expected = "head and tail must have equal display width")]
89 fn head_tail_width_mismatch_panics_in_debug() {
90 let head = Line::new("12");
91 let tail = Line::new(" ");
92 let _ = wrap_with_gutter(Line::new("hi"), 10, &head, &tail);
93 }
94
95 #[test]
96 fn digit_count_works() {
97 assert_eq!(digit_count(0), 1);
98 assert_eq!(digit_count(1), 1);
99 assert_eq!(digit_count(9), 1);
100 assert_eq!(digit_count(10), 2);
101 assert_eq!(digit_count(99), 2);
102 assert_eq!(digit_count(100), 3);
103 assert_eq!(digit_count(999), 3);
104 }
105}