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)]
41pub struct Log<'a> {
42 messages: &'a [Line],
43 offset: usize,
44}
45
46impl<'a> Log<'a> {
47 #[must_use]
49 pub const fn new(messages: &'a [Line]) -> Self {
50 Self {
51 messages,
52 offset: 0,
53 }
54 }
55
56 #[must_use]
58 pub const fn offset(mut self, offset: usize) -> Self {
59 self.offset = offset;
60 self
61 }
62}
63
64impl<B: Backend> Widget<B> for Log<'_> {
65 fn render(self, area: Rect, term: &mut Terminal<B>) {
66 let visible_height = area.height_usize();
67 if area.width() == 0 || visible_height == 0 {
68 return;
69 }
70
71 let Some(bottom) = self
74 .messages
75 .len()
76 .checked_sub(self.offset.saturating_add(1))
77 else {
78 return;
79 };
80 let top = bottom.saturating_sub(visible_height - 1);
81
82 for (row, message) in self.messages[top..=bottom].iter().enumerate() {
83 let y = area.top() + row as u16;
84 let row_area = Rect::new(area.left(), y, area.width(), 1);
85 PrintLine::new(message).render(row_area, term);
86 }
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use retroglyph_core::Headless;
93
94 use super::*;
95
96 fn lines(texts: &[&str]) -> Vec<Line> {
97 texts.iter().map(|t| Line::raw(*t)).collect()
98 }
99
100 #[test]
101 fn shows_the_most_recent_messages_oldest_at_top() {
102 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).render(area, &mut term);
108
109 assert_eq!(term.grid().get(0, 0).glyph(), 'c'); assert_eq!(term.grid().get(0, 1).glyph(), 'd'); }
112
113 #[test]
114 fn offset_scrolls_back_through_history() {
115 let area = Rect::new(0, 0, 20, 2);
116 let messages = lines(&["alpha", "bravo", "charlie", "delta"]);
117
118 let mut term = Terminal::new(Headless::new(20, 2));
119 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'); }
124
125 #[test]
126 fn offset_past_the_start_shows_fewer_lines_without_panicking() {
127 let area = Rect::new(0, 0, 20, 2);
128 let messages = lines(&["alpha", "bravo"]);
129
130 let mut term = Terminal::new(Headless::new(20, 2));
131 Log::new(&messages).offset(5).render(area, &mut term); assert_eq!(term.grid().get(0, 0).glyph(), ' ');
135 assert_eq!(term.grid().get(0, 1).glyph(), ' ');
136 }
137
138 #[test]
139 fn fewer_messages_than_visible_rows_leaves_the_rest_untouched() {
140 let area = Rect::new(0, 0, 20, 4);
141 let messages = lines(&["only"]);
142
143 let mut term = Terminal::new(Headless::new(20, 4));
144 Log::new(&messages).render(area, &mut term);
145
146 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(), ' '); }
150
151 #[test]
152 fn clips_long_lines_to_area_width() {
153 let area = Rect::new(0, 0, 5, 1);
154 let messages = lines(&["a much longer message than fits"]);
155
156 let mut term = Terminal::new(Headless::new(5, 1));
157 Log::new(&messages).render(area, &mut term);
158
159 assert_eq!(term.grid().get(4, 0).glyph(), 'c');
161 }
162}