1#[cfg(feature = "egc")]
3use alloc::vec::Vec;
4
5use retroglyph_core::grid::Rect;
6#[cfg(feature = "egc")]
7use retroglyph_core::layout::wrap;
8use retroglyph_core::text::Line;
9
10use super::{Measure, PrintLine, Widget};
11use crate::Surface;
12
13#[derive(Clone, Copy, Debug)]
57pub struct Log<'a> {
58 messages: &'a [Line],
59 offset: usize,
60 #[cfg(feature = "egc")]
61 wrap: bool,
62}
63
64impl<'a> Log<'a> {
65 #[must_use]
67 pub const fn new(messages: &'a [Line]) -> Self {
68 Self {
69 messages,
70 offset: 0,
71 #[cfg(feature = "egc")]
72 wrap: false,
73 }
74 }
75
76 #[must_use]
78 pub const fn offset(mut self, offset: usize) -> Self {
79 self.offset = offset;
80 self
81 }
82
83 #[cfg(feature = "egc")]
87 #[must_use]
88 pub const fn wrap(mut self, wrap: bool) -> Self {
89 self.wrap = wrap;
90 self
91 }
92}
93
94impl Measure for Log<'_> {
95 fn height_for(&self, width: u16) -> u16 {
99 #[cfg(feature = "egc")]
100 if self.wrap {
101 let total: usize = self.messages.iter().map(|m| wrap(m, width).len()).sum();
102 #[allow(clippy::cast_possible_truncation)]
103 let height = total.min(usize::from(u16::MAX)) as u16;
104 return height;
105 }
106 #[cfg(not(feature = "egc"))]
107 let _ = width;
108 #[allow(clippy::cast_possible_truncation)]
109 let height = self.messages.len().min(usize::from(u16::MAX)) as u16;
110 height
111 }
112}
113
114impl Widget for Log<'_> {
115 fn render(&self, surface: &mut Surface<'_>) {
116 let (width, height) = (surface.width(), surface.height());
117 let visible_height = usize::from(height);
118 if width == 0 || visible_height == 0 {
119 return;
120 }
121
122 let Some(bottom) = self
125 .messages
126 .len()
127 .checked_sub(self.offset.saturating_add(1))
128 else {
129 return;
130 };
131
132 #[cfg(feature = "egc")]
133 if self.wrap {
134 self.render_wrapped(surface, bottom, visible_height, width);
135 return;
136 }
137
138 let top = bottom.saturating_sub(visible_height - 1);
139
140 let area = surface.area();
143 for (row, message) in self.messages[top..=bottom].iter().enumerate() {
144 #[allow(clippy::cast_possible_truncation)]
147 let y = area.top() + row as u16;
148 let row_area = Rect::new(area.left(), y, width, 1);
149 PrintLine::new(message).render(&mut surface.scope(row_area));
150 }
151 }
152}
153
154#[cfg(feature = "egc")]
155impl Log<'_> {
156 fn render_wrapped(
163 &self,
164 surface: &mut Surface<'_>,
165 bottom: usize,
166 visible_height: usize,
167 width: u16,
168 ) {
169 let mut included: Vec<Vec<Line>> = Vec::new();
170 let mut used = 0usize;
171 for message in self.messages[..=bottom].iter().rev() {
172 let rows = wrap(message, width);
173 if used + rows.len() > visible_height {
174 break;
175 }
176 used += rows.len();
177 included.push(rows);
178 }
179
180 let area = surface.area();
181 let mut y = area.top();
182 for rows in included.into_iter().rev() {
183 for row in rows {
184 let row_area = Rect::new(area.left(), y, width, 1);
185 PrintLine::new(&row).render(&mut surface.scope(row_area));
186 y += 1;
187 }
188 }
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use alloc::vec::Vec;
195
196 use retroglyph_core::grid::{Grid, Pos};
197
198 use super::*;
199
200 fn lines(texts: &[&str]) -> Vec<Line> {
201 texts.iter().map(|t| Line::raw(*t)).collect()
202 }
203
204 #[test]
205 fn shows_the_most_recent_messages_oldest_at_top() {
206 let area = Rect::new(0, 0, 20, 2);
208 let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
209
210 let mut grid = Grid::new(20, 2);
211 Log::new(&messages).render(&mut Surface::new(&mut grid, area, 0));
212
213 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'c'); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'd'); }
216
217 #[test]
218 fn height_for_is_the_message_count() {
219 let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
220 assert_eq!(Log::new(&messages).height_for(80), 4);
221 }
222
223 #[test]
224 fn offset_scrolls_back_through_history() {
225 let area = Rect::new(0, 0, 20, 2);
226 let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
227
228 let mut grid = Grid::new(20, 2);
229 Log::new(&messages)
230 .offset(1)
231 .render(&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'); }
236
237 #[test]
238 fn offset_past_the_start_shows_fewer_lines_without_panicking() {
239 let area = Rect::new(0, 0, 20, 2);
240 let messages = lines(&["alpha", "bravo"]);
241
242 let mut grid = Grid::new(20, 2);
243 Log::new(&messages)
244 .offset(5)
245 .render(&mut Surface::new(&mut grid, area, 0)); assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
249 assert_eq!(grid[Pos::new(0, 1)].glyph(), ' ');
250 }
251
252 #[test]
253 fn fewer_messages_than_visible_rows_leaves_the_rest_untouched() {
254 let area = Rect::new(0, 0, 20, 4);
255 let messages = lines(&["only"]);
256
257 let mut grid = Grid::new(20, 4);
258 Log::new(&messages).render(&mut Surface::new(&mut grid, area, 0));
259
260 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(), ' '); }
264
265 #[test]
266 fn clips_long_lines_to_area_width() {
267 let area = Rect::new(0, 0, 5, 1);
268 let messages = lines(&["a much longer message than fits"]);
269
270 let mut grid = Grid::new(5, 1);
271 Log::new(&messages).render(&mut Surface::new(&mut grid, area, 0));
272
273 assert_eq!(grid[Pos::new(4, 0)].glyph(), 'c');
275 }
276
277 #[cfg(feature = "egc")]
278 #[test]
279 fn wrap_word_wraps_a_long_message_across_rows() {
280 let area = Rect::new(0, 0, 7, 3);
281 let messages = lines(&["hello world"]);
282
283 let mut grid = Grid::new(7, 3);
284 Log::new(&messages)
285 .wrap(true)
286 .render(&mut Surface::new(&mut grid, area, 0));
287
288 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h'); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'w'); assert_eq!(grid[Pos::new(0, 2)].glyph(), ' '); }
292
293 #[cfg(feature = "egc")]
294 #[test]
295 fn height_for_sums_wrapped_rows_when_wrap_is_set() {
296 let messages = lines(&["hello world", "hi"]);
297 assert_eq!(Log::new(&messages).wrap(true).height_for(7), 3); assert_eq!(Log::new(&messages).height_for(7), 2); }
300
301 #[cfg(feature = "egc")]
302 #[test]
303 fn wrap_keeps_offset_counting_whole_messages_not_rows() {
304 let area = Rect::new(0, 0, 7, 4);
307 let messages = lines(&["hello world", "hi"]);
308
309 let mut grid = Grid::new(7, 4);
310 Log::new(&messages)
311 .wrap(true)
312 .offset(1)
313 .render(&mut Surface::new(&mut grid, area, 0));
314
315 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h'); assert_eq!(grid[Pos::new(0, 1)].glyph(), 'w'); assert_eq!(grid[Pos::new(0, 2)].glyph(), ' '); }
319
320 #[cfg(feature = "egc")]
321 #[test]
322 fn wrap_leaves_out_a_message_that_would_only_partially_fit() {
323 let area = Rect::new(0, 0, 7, 2);
326 let messages = lines(&["hello world", "hi"]);
327
328 let mut grid = Grid::new(7, 2);
329 Log::new(&messages)
330 .wrap(true)
331 .render(&mut Surface::new(&mut grid, area, 0));
332
333 assert_eq!(grid[Pos::new(0, 0)].glyph(), 'h'); assert_eq!(grid[Pos::new(1, 0)].glyph(), 'i');
335 assert_eq!(grid[Pos::new(0, 1)].glyph(), ' '); }
337}