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