retroglyph_widgets/widget/
log.rs1use retroglyph_core::text::Line;
3use retroglyph_core::{Backend, Rect, Terminal};
4
5use super::{PrintLine, Widget};
6
7#[derive(Clone, Copy, Debug)]
29pub struct Log<'a> {
30 messages: &'a [Line],
31 offset: usize,
32}
33
34impl<'a> Log<'a> {
35 #[must_use]
37 pub const fn new(messages: &'a [Line]) -> Self {
38 Self {
39 messages,
40 offset: 0,
41 }
42 }
43
44 #[must_use]
46 pub const fn offset(mut self, offset: usize) -> Self {
47 self.offset = offset;
48 self
49 }
50}
51
52impl<B: Backend> Widget<B> for Log<'_> {
53 fn render(self, area: Rect, term: &mut Terminal<B>) {
54 let visible_height = area.height_usize();
55 if area.width() == 0 || visible_height == 0 {
56 return;
57 }
58
59 let Some(bottom) = self
62 .messages
63 .len()
64 .checked_sub(self.offset.saturating_add(1))
65 else {
66 return;
67 };
68 let top = bottom.saturating_sub(visible_height - 1);
69
70 for (row, message) in self.messages[top..=bottom].iter().enumerate() {
71 let y = area.top() + row as u16;
72 let row_area = Rect::new(area.left(), y, area.width(), 1);
73 PrintLine::new(message).render(row_area, term);
74 }
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use retroglyph_core::Headless;
81
82 use super::*;
83
84 fn lines(texts: &[&str]) -> Vec<Line> {
85 texts.iter().map(|t| Line::raw(*t)).collect()
86 }
87
88 #[test]
89 fn shows_the_most_recent_messages_oldest_at_top() {
90 let area = Rect::new(0, 0, 20, 2);
92 let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
93
94 let mut term = Terminal::new(Headless::new(20, 2));
95 Log::new(&messages).render(area, &mut term);
96
97 assert_eq!(term.grid().get(0, 0).glyph(), 'c'); assert_eq!(term.grid().get(0, 1).glyph(), 'd'); }
100
101 #[test]
102 fn offset_scrolls_back_through_history() {
103 let area = Rect::new(0, 0, 20, 2);
104 let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
105
106 let mut term = Terminal::new(Headless::new(20, 2));
107 Log::new(&messages).offset(1).render(area, &mut term); assert_eq!(term.grid().get(0, 0).glyph(), 'b'); assert_eq!(term.grid().get(0, 1).glyph(), 'c'); }
112
113 #[test]
114 fn offset_past_the_start_shows_fewer_lines_without_panicking() {
115 let area = Rect::new(0, 0, 20, 2);
116 let messages = lines(&["alpha", "bravo"]);
117
118 let mut term = Terminal::new(Headless::new(20, 2));
119 Log::new(&messages).offset(5).render(area, &mut term); assert_eq!(term.grid().get(0, 0).glyph(), ' ');
123 assert_eq!(term.grid().get(0, 1).glyph(), ' ');
124 }
125
126 #[test]
127 fn fewer_messages_than_visible_rows_leaves_the_rest_untouched() {
128 let area = Rect::new(0, 0, 20, 4);
129 let messages = lines(&["only"]);
130
131 let mut term = Terminal::new(Headless::new(20, 4));
132 Log::new(&messages).render(area, &mut term);
133
134 assert_eq!(term.grid().get(0, 0).glyph(), 'o'); assert_eq!(term.grid().get(0, 1).glyph(), ' '); assert_eq!(term.grid().get(0, 2).glyph(), ' '); }
138
139 #[test]
140 fn clips_long_lines_to_area_width() {
141 let area = Rect::new(0, 0, 5, 1);
142 let messages = lines(&["a much longer message than fits"]);
143
144 let mut term = Terminal::new(Headless::new(5, 1));
145 Log::new(&messages).render(area, &mut term);
146
147 assert_eq!(term.grid().get(4, 0).glyph(), 'c');
149 }
150}