retroglyph_widgets/widget/
log.rs1use retroglyph_core::Rect;
3use retroglyph_core::text::Line;
4
5use super::{PrintLine, Widget};
6use crate::Surface;
7
8#[derive(Clone, Copy, Debug)]
43pub struct Log<'a> {
44 messages: &'a [Line],
45 offset: usize,
46}
47
48impl<'a> Log<'a> {
49 #[must_use]
51 pub const fn new(messages: &'a [Line]) -> Self {
52 Self {
53 messages,
54 offset: 0,
55 }
56 }
57
58 #[must_use]
60 pub const fn offset(mut self, offset: usize) -> Self {
61 self.offset = offset;
62 self
63 }
64}
65
66impl Widget for Log<'_> {
67 fn render(&self, area: Rect, surface: &mut Surface<'_>) {
68 let visible_height = area.height_usize();
69 if area.width() == 0 || visible_height == 0 {
70 return;
71 }
72
73 let Some(bottom) = self
76 .messages
77 .len()
78 .checked_sub(self.offset.saturating_add(1))
79 else {
80 return;
81 };
82 let top = bottom.saturating_sub(visible_height - 1);
83
84 for (row, message) in self.messages[top..=bottom].iter().enumerate() {
85 #[allow(clippy::cast_possible_truncation)]
88 let y = area.top() + row as u16;
89 let row_area = Rect::new(area.left(), y, area.width(), 1);
90 PrintLine::new(message).render(row_area, surface);
91 }
92 }
93}
94
95#[cfg(test)]
96mod tests {
97 use retroglyph_core::{Grid, Pos};
98
99 use super::*;
100
101 fn lines(texts: &[&str]) -> Vec<Line> {
102 texts.iter().map(|t| Line::raw(*t)).collect()
103 }
104
105 #[test]
106 fn shows_the_most_recent_messages_oldest_at_top() {
107 let area = Rect::new(0, 0, 20, 2);
109 let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
110
111 let mut grid = Grid::new(20, 2);
112 Log::new(&messages).render(area, &mut Surface::new(&mut grid, area, 0));
113
114 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'c'); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'd'); }
117
118 #[test]
119 fn offset_scrolls_back_through_history() {
120 let area = Rect::new(0, 0, 20, 2);
121 let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
122
123 let mut grid = Grid::new(20, 2);
124 Log::new(&messages)
125 .offset(1)
126 .render(area, &mut Surface::new(&mut grid, area, 0)); assert_eq!(grid[Pos::new(0, 0)].glyph(), 'b'); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'c'); }
131
132 #[test]
133 fn offset_past_the_start_shows_fewer_lines_without_panicking() {
134 let area = Rect::new(0, 0, 20, 2);
135 let messages = lines(&["alpha", "bravo"]);
136
137 let mut grid = Grid::new(20, 2);
138 Log::new(&messages)
139 .offset(5)
140 .render(area, &mut Surface::new(&mut grid, area, 0)); assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
144 assert_eq!(grid[Pos::new(0, 1)].glyph(), ' ');
145 }
146
147 #[test]
148 fn fewer_messages_than_visible_rows_leaves_the_rest_untouched() {
149 let area = Rect::new(0, 0, 20, 4);
150 let messages = lines(&["only"]);
151
152 let mut grid = Grid::new(20, 4);
153 Log::new(&messages).render(area, &mut Surface::new(&mut grid, area, 0));
154
155 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'o'); assert_eq!(grid[Pos::new(0, 1)].glyph(), ' '); assert_eq!(grid[Pos::new(0, 2)].glyph(), ' '); }
159
160 #[test]
161 fn clips_long_lines_to_area_width() {
162 let area = Rect::new(0, 0, 5, 1);
163 let messages = lines(&["a much longer message than fits"]);
164
165 let mut grid = Grid::new(5, 1);
166 Log::new(&messages).render(area, &mut Surface::new(&mut grid, area, 0));
167
168 assert_eq!(grid[Pos::new(4, 0)].glyph(), 'c');
170 }
171}