max7219_display/led_matrix/
scroll.rs1use crate::{
6 Result,
7 led_matrix::{buffer::MatrixBuffer, fonts::LedFont},
8};
9
10#[derive(Clone, Copy)]
12pub struct ScrollConfig {
13 pub step_delay_ns: u32,
15 pub pixels_per_step: u8,
17 pub loop_text: bool,
19 pub loop_padding: u8,
21}
22
23impl Default for ScrollConfig {
24 fn default() -> Self {
25 Self {
26 step_delay_ns: 100_000_000, pixels_per_step: 1,
28 loop_text: true,
29 loop_padding: 16, }
31 }
32}
33
34pub struct ScrollingText<'a> {
36 text: &'a str,
37 font: &'a LedFont,
38 config: ScrollConfig,
39 text_width: usize,
40 pub(crate) current_offset: i32,
41}
42
43impl<'a> ScrollingText<'a> {
44 pub fn new(text: &'a str, font: &'a LedFont, config: ScrollConfig) -> Self {
46 let mut scroller = Self {
47 text,
48 font,
49 config,
50 text_width: 0,
51 current_offset: 0,
52 };
53 scroller.calculate_text_width();
54 scroller
55 }
56
57 pub fn new_default(text: &'a str, font: &'a LedFont) -> Self {
59 Self::new(text, font, ScrollConfig::default())
60 }
61
62 fn calculate_text_width(&mut self) {
64 self.text_width = self.text.chars().count() * 8;
65
66 if self.config.loop_text {
68 self.text_width += self.config.loop_padding as usize;
69 }
70 }
71
72 pub fn get_frame(&self) -> Result<MatrixBuffer> {
75 let mut buffer = MatrixBuffer::new();
76
77 for row in 0..8 {
78 let mut row_data = 0u8;
79 for col in 0..8 {
80 if self.pixel_on(col, row) {
81 row_data |= 1 << (7 - col);
82 }
83 }
84 buffer.set_row(row as u8, row_data)?;
85 }
86
87 Ok(buffer)
88 }
89 fn pixel_on(&self, source_col: usize, row: usize) -> bool {
91 let actual_col = self.current_offset as isize + source_col as isize;
93
94 if actual_col < 0 {
96 return false;
97 }
98
99 let col = actual_col as usize;
100
101 if col >= self.text_width && !self.config.loop_text {
103 return false;
104 }
105
106 let final_col = if self.config.loop_text && col >= self.text_width {
108 col % self.text_width
109 } else {
110 col
111 };
112
113 let text_pixels = self.text.chars().count() * 8;
115 if final_col >= text_pixels {
116 return false;
117 }
118
119 let char_index = final_col / 8;
120 let bit_index = final_col % 8;
121
122 let ch = self.text.chars().nth(char_index).unwrap_or('?');
124 let bitmap = self.font.get_char(ch);
125 let row_data = bitmap[row];
126
127 (row_data >> (7 - bit_index)) & 1 != 0
129 }
130
131 pub fn step(&mut self) -> bool {
133 self.current_offset += self.config.pixels_per_step as i32;
134
135 if self.config.loop_text {
136 if self.current_offset >= self.text_width as i32 {
138 self.current_offset = 0;
139 }
140 true } else {
142 self.current_offset < (self.text_width as i32 + 8)
144 }
145 }
146
147 pub fn reset(&mut self) {
149 self.current_offset = -(8i32); }
151
152 pub fn offset(&self) -> i32 {
154 self.current_offset
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[rustfmt::skip]
164 const TEST_FONT_DATA: &[([u8; 8], char)] = &[
165 (
166 [
167 0b00111100,
168 0b01100110,
169 0b01101110,
170 0b01110110,
171 0b01100110,
172 0b01100110,
173 0b00111100,
174 0b00000000,
175 ],
176 '0',
177 ),
178 (
179 [
180 0b00011000,
181 0b00111000,
182 0b00011000,
183 0b00011000,
184 0b00011000,
185 0b00011000,
186 0b01111110,
187 0b00000000,
188 ],
189 '1',
190 ),
191 (
192 [
193 0b00000000,
194 0b00000000,
195 0b00000000,
196 0b00000000,
197 0b00000000,
198 0b00000000,
199 0b00000000,
200 0b00000000,
201 ],
202 ' ',
203 ),
204 ];
205
206 const TEST_FONT: LedFont = LedFont::new(TEST_FONT_DATA);
207 #[test]
208 fn test_scroll_config_default() {
209 let config = ScrollConfig::default();
210 assert_eq!(config.step_delay_ns, 100_000_000);
211 assert_eq!(config.pixels_per_step, 1);
212 assert!(config.loop_text);
213 assert_eq!(config.loop_padding, 16);
214 }
215
216 #[test]
217 fn test_scrolling_text_new() {
218 let text = "01";
219 let config = ScrollConfig::default();
220 let scroller = ScrollingText::new(text, &TEST_FONT, config);
221
222 assert_eq!(scroller.text, text);
223 assert_eq!(scroller.text_width, 32);
224 assert_eq!(scroller.current_offset, 0);
225 }
226
227 #[test]
228 fn test_calculate_text_width() {
229 let text = "01";
230 let config = ScrollConfig {
231 loop_text: true,
232 loop_padding: 8,
233 ..Default::default()
234 };
235 let scroller = ScrollingText::new(text, &TEST_FONT, config);
236 assert_eq!(scroller.text_width, 24);
237 }
238
239 #[test]
240 fn test_reset() {
241 let mut scroller = ScrollingText::new_default("01", &TEST_FONT);
242 scroller.current_offset = 10;
243 scroller.reset();
244 assert_eq!(scroller.current_offset, -8);
245 }
246
247 #[test]
248 fn test_offset() {
249 let mut scroller = ScrollingText::new_default("01", &TEST_FONT);
250 assert_eq!(scroller.offset(), 0);
251 scroller.current_offset = 5;
252 assert_eq!(scroller.offset(), 5);
253 }
254
255 #[test]
256 fn test_step_looping() {
257 let mut scroller = ScrollingText::new_default("01", &TEST_FONT);
258 scroller.current_offset = 30;
259
260 let should_continue = scroller.step();
261 assert_eq!(scroller.current_offset, 31);
262 assert!(should_continue);
263
264 let should_continue = scroller.step();
265 assert_eq!(scroller.current_offset, 0);
266 assert!(should_continue);
267 }
268
269 #[test]
270 fn test_step_non_looping() {
271 let config = ScrollConfig {
272 loop_text: false,
273 ..Default::default()
274 };
275 let mut scroller = ScrollingText::new("01", &TEST_FONT, config);
276
277 scroller.current_offset = 22;
279 assert!(scroller.step());
280 assert_eq!(scroller.current_offset, 23);
281
282 assert!(!scroller.step()); assert_eq!(scroller.current_offset, 24);
284
285 assert!(!scroller.step()); assert_eq!(scroller.current_offset, 25);
288 }
289
290 #[test]
291 fn test_pixel_on_basic() {
292 let scroller = ScrollingText::new_default("0", &TEST_FONT);
293
294 assert!(!scroller.pixel_on(0, 0));
295 assert!(!scroller.pixel_on(1, 0));
296 assert!(scroller.pixel_on(2, 0));
297 assert!(scroller.pixel_on(3, 0));
298 assert!(scroller.pixel_on(4, 0));
299 assert!(scroller.pixel_on(5, 0));
300 assert!(!scroller.pixel_on(6, 0));
301 assert!(!scroller.pixel_on(7, 0));
302 }
303
304 #[test]
305 fn test_pixel_on_negative_offset() {
306 let mut scroller = ScrollingText::new_default("0", &TEST_FONT);
307 scroller.current_offset = -4;
308
309 assert!(!scroller.pixel_on(0, 0));
312 assert!(!scroller.pixel_on(1, 0));
313 assert!(!scroller.pixel_on(2, 0));
314 assert!(!scroller.pixel_on(3, 0));
315
316 assert!(!scroller.pixel_on(4, 0)); assert!(!scroller.pixel_on(5, 0)); assert!(scroller.pixel_on(6, 0)); }
329
330 #[test]
331 fn test_pixel_on_multiple_characters() {
332 let scroller = ScrollingText::new_default("01", &TEST_FONT);
333
334 assert!(!scroller.pixel_on(0, 0));
335 assert!(!scroller.pixel_on(1, 0));
336 assert!(scroller.pixel_on(2, 0));
337
338 assert!(!scroller.pixel_on(8, 0));
339 assert!(!scroller.pixel_on(9, 0));
340 assert!(!scroller.pixel_on(10, 0));
341 assert!(scroller.pixel_on(11, 0));
342 assert!(scroller.pixel_on(12, 0));
343 }
344
345 #[test]
346 fn test_pixel_on_looping() {
347 let config = ScrollConfig {
348 loop_text: true,
349 loop_padding: 0,
350 ..Default::default()
351 };
352 let mut scroller = ScrollingText::new("01", &TEST_FONT, config);
353
354 scroller.current_offset = scroller.text_width as i32;
355
356 assert!(!scroller.pixel_on(0, 0));
357 assert!(!scroller.pixel_on(1, 0));
358 assert!(scroller.pixel_on(2, 0));
359 }
360
361 #[test]
362 fn test_pixel_on_padding() {
363 let scroller = ScrollingText::new_default("0", &TEST_FONT);
364
365 for col in 8..24 {
366 assert!(
367 !scroller.pixel_on(col, 0),
368 "Pixel {col} should be off in padding",
369 );
370 }
371 }
372
373 #[test]
374 fn test_get_frame() {
375 let scroller = ScrollingText::new_default("0", &TEST_FONT);
376 let frame = scroller.get_frame().expect("Should get frame successfully");
377
378 let expected_rows = [
404 0b00111100, 0b01100110, 0b01101110, 0b01110110, 0b01100110, 0b01100110, 0b00111100, 0b00000000, ];
413
414 for (row_index, &expected_row) in expected_rows.iter().enumerate() {
415 let actual_row = frame
416 .get_row(row_index as u8)
417 .expect("Should get row successfully");
418 assert_eq!(actual_row, expected_row, "Row {row_index} mismatch");
419 }
420 }
421}