simulate_lcd/lib.rs
1// * Simulate LCD: A Simple LCD Screen Simulator *
2
3// Copyright 2023 Simon Varey - github.com/simonvarey
4
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at
8
9// http://www.apache.org/licenses/LICENSE-2.0
10
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17#![warn(missing_docs)]
18//#![warn(rustdoc::missing_doc_code_examples)]
19#![doc = include_str!("../README.md")]
20
21// Imports
22
23use std::{
24 error::Error,
25 fmt::{Display, Formatter, Result as FmtResult},
26};
27
28use sdl2::{
29 pixels::Color,
30 rect::Rect,
31 render::Canvas,
32 video::{Window, WindowBuildError},
33 IntegerOrSdlError, Sdl,
34};
35
36// Constants
37
38/// A [`sdl2::pixels::Color`] object representing the 'on' color of green backlight LCD screens.
39pub const LCD_DARK_GREEN: Color = Color::RGB(69, 75, 59);
40
41/// A [`sdl2::pixels::Color`] object representing the 'off' color of green backlight LCD screens.
42pub const LCD_LIGHT_GREEN: Color = Color::RGB(158, 171, 136);
43
44// Error
45
46/// Errors that can arise from the creation and use of [`LcdScreens`].
47///
48/// [`LcdScreens`]: crate::LcdScreen
49#[derive(Debug)]
50pub enum LcdError {
51 /// Indicates that an error occurred when attempting to initalize the SDL video subsystem. This error
52 /// is a simple wrapper around the underlying SDL error. Please consult the [`sdl2`] documentation for
53 /// more details.
54 Video(String),
55 /// Indicates that an error occurred when attempting to build the OS window for the [`LcdScreen`]. This
56 /// error is a simple wrapper around the [underlying SDL error](https://rust-sdl2.github.io/rust-sdl2/sdl2/video/enum.WindowBuildError.html).
57 /// Please consult the [`sdl2`] documentation for more details.
58 WindowBuild(WindowBuildError),
59 /// Indicates that an error occurred when attempting to build the canvas for the [`LcdScreen`]. This
60 /// error is a simple wrapper around the [underlying SDL error](https://rust-sdl2.github.io/rust-sdl2/sdl2/enum.IntegerOrSdlError.html).
61 /// Please consult the [`sdl2`] documentation for more details.
62 CanvasBuild(IntegerOrSdlError),
63 /// Indicates that an error occurred when attempting to fill a dot on the [`LcdScreen`]. This
64 /// error is a simple wrapper around the underlying SDL error. Please consult the [`sdl2`] documentation for
65 /// more details.
66 Fill(String),
67 /// Indicates that the [`LcdScreen`] is too wide to be displayed. The maximum width of a screen is [`i32::MAX`]
68 /// pixels. As the width of the screen is set by the number of rows of dots it has multiplied by the
69 /// pixel width of each dot, one or both of those values must be reduced.
70 ///
71 /// [`i32::MAX`]: std::i32::MAX
72 WindowWidth {
73 /// the total width in pixels of the undisplayed screen
74 width: u32,
75 /// the number of rows of dots of the undisplayed screen
76 row: usize,
77 /// the pixel width of the dots of the undisplayed screen
78 dot_width: u32,
79 },
80 /// Indicates that the [`LcdScreen`] is too high to be displayed. The maximum height of a screen is [`i32::MAX`]
81 /// pixels. As the height of the screen is set by the number of columns of dots it has multiplied by the
82 /// height of each dot, one or both of those values must be reduced.
83 ///
84 /// [`i32::MAX`]: std::i32::MAX
85 WindowHeight {
86 /// the total height in pixels of the undisplayed screen
87 height: u32,
88 /// the number of columns of dots of the undisplayed screen
89 col: usize,
90 /// the pixel height of the dots of the undisplayed screen
91 dot_height: u32,
92 },
93}
94
95impl Display for LcdError {
96 fn fmt(&self, fmtr: &mut Formatter<'_>) -> FmtResult {
97 match self {
98 LcdError::Video(err) => write!(fmtr, "Error initalizing video subsystem: {err}"),
99 LcdError::WindowBuild(err) => write!(fmtr, "Error building window: {err}"),
100 LcdError::CanvasBuild(err) => write!(fmtr, "Error building canvas: {err}"),
101 LcdError::Fill(err) => write!(fmtr, "Error filling dot: {err}"),
102 LcdError::WindowWidth { width, row, dot_width }
103 => write!(fmtr, "{width} pixels is too large for a window width. Window width cannot be larger than {}. Reduce either the number of dot rows {row} or the width {dot_width} of dots.", i32::MAX),
104 LcdError::WindowHeight { height, col, dot_height }
105 => write!(fmtr, "{height} pixels is too large for a window height. Window height cannot be larger than {}. Reduce either the number of dot columns {col} or the height {dot_height} of dots.", i32::MAX),
106 }
107 }
108}
109
110impl Error for LcdError {}
111
112impl From<WindowBuildError> for LcdError {
113 fn from(err: WindowBuildError) -> Self {
114 Self::WindowBuild(err)
115 }
116}
117
118impl From<IntegerOrSdlError> for LcdError {
119 fn from(err: IntegerOrSdlError) -> Self {
120 Self::CanvasBuild(err)
121 }
122}
123
124// Bitmap
125
126/// This is an alias for a C-by-R row-major array-of-arrays of booleans. Arrays of this form can be
127/// written to an [`LcdScreen`] using the [`draw_bitmap`] method. This alias can be used as a convenience
128/// to generate the bitmaps you want to draw to the LCD screen.
129///
130/// [`draw_bitmap`]: crate::LcdScreen::draw_bitmap
131pub type Bitmap<const C: usize, const R: usize> = [[bool; C]; R];
132
133// LCD Dot
134
135#[derive(Debug)]
136struct LcdDot {
137 rect: Rect,
138 on: bool,
139}
140
141impl LcdDot {
142 fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
143 assert!((1..=(i32::MAX as u32)).contains(&width), "INTERNAL ERROR: the width of a TIDot must be > 0 and <= i32::MAX. If you are seeing this error then RusTI-BASIC has a bug.");
144 assert!((1..=(i32::MAX as u32)).contains(&height), "INTERNAL ERROR: the height of a TIDot must be > 0 and <= i32::MAX. If you are seeing this error then RusTI-BASIC has a bug.");
145
146 Self {
147 rect: Rect::new(
148 x * width as i32, // Note: as this has been checked to be positive, this is a true cast
149 y * height as i32, // Note: as this has been checked to be positive, this is a true cast
150 width,
151 height,
152 ),
153 on: false,
154 }
155 }
156}
157
158// * LCD Screen *
159
160///
161/// A simulated LCD dot-matrix screen.
162///
163/// The screen has `R` rows and `C` columns of dots. *Note*: The number of rows and columns of dots for
164/// the screen is specified as a const parameter on the type of the screen, rather than as an argument to
165/// the constructor function [`new`].
166///
167/// # Parameters
168///
169/// * `R` - The number of rows of dots of the screen
170/// * `C` - The number of columns of dots of the screen
171///
172/// # Examples
173///
174/// ```
175/// use std::{thread::sleep, time::Duration};
176///
177/// use rand::{thread_rng, Rng};
178/// use sdl2::{event::Event, keyboard::Keycode};
179/// use simulate_lcd::{Bitmap, LcdScreen, LCD_DARK_GREEN, LCD_LIGHT_GREEN};
180///
181/// const NANOS_PER_SEC: u64 = 1_000_000_000;
182///
183/// fn main() {
184/// let sdl_context = sdl2::init().unwrap();
185/// let mut screen = LcdScreen::<64, 96>::new(
186/// &sdl_context,
187/// "LCD Example: Random",
188/// LCD_DARK_GREEN,
189/// LCD_LIGHT_GREEN,
190/// 10,
191/// 10,
192/// )
193/// .unwrap();
194///
195/// let mut event_pump = sdl_context.event_pump().unwrap();
196/// 'running: loop {
197/// for event in event_pump.poll_iter() {
198/// match event {
199/// Event::Quit { .. }
200/// | Event::KeyDown {
201/// keycode: Some(Keycode::Escape),
202/// ..
203/// } => break 'running,
204/// _ => {}
205/// }
206/// }
207/// let mut rng = thread_rng();
208///
209/// let random_bits: Vec<[bool; 96]> = (0..64).map(|_| rng.gen()).collect();
210///
211/// screen.draw_bitmap(&random_bits.try_into().unwrap()).unwrap();
212///
213/// sleep(Duration::from_nanos(NANOS_PER_SEC / 60));
214/// }
215/// }
216/// ```
217///
218/// [`new`]: crate::LcdScreen::new
219pub struct LcdScreen<const R: usize, const C: usize> {
220 dots: Box<[[LcdDot; C]; R]>,
221 canvas: Canvas<Window>,
222 on_color: Color,
223 off_color: Color,
224}
225
226impl<const R: usize, const C: usize> LcdScreen<R, C> {
227 /// Creates a simulated LCD screen.
228 ///
229 /// *Note*: The number of rows and columns of dots for a screen is specified as a const parameter
230 /// on the type of the screen, rather than as an argument to this function.
231 ///
232 /// # Arguments
233 ///
234 /// * `sdl_context` - An [`Sdl`] context object
235 /// * `title` - The title of the window containing the screen
236 /// * `on_color` - A [`Color`] object representing the color of a dot when it is 'on'
237 /// * `off_color` - A [`Color`] object representing the color of a dot when it is 'off'
238 /// * `dot_width` - The width of a dot on the screen in pixels
239 /// * `dot_height` - The height of a dot on the screen in pixels
240 ///
241 /// # Examples
242 ///
243 /// ```
244 /// # use simulate_lcd::{LcdScreen, LCD_DARK_GREEN, LCD_LIGHT_GREEN};
245 /// # let sdl_context = sdl2::init().unwrap();
246 /// let mut screen = LcdScreen::<64, 96>::new(
247 /// &sdl_context,
248 /// "LCD Example: Blank",
249 /// LCD_DARK_GREEN,
250 /// LCD_LIGHT_GREEN,
251 /// 10,
252 /// 10,
253 /// )
254 /// .unwrap();
255 /// # std::thread::sleep(std::time::Duration::from_secs(1));
256 /// ```
257 ///
258 /// # Errors
259 ///
260 /// - [`LcdError::Video`] when there is an error initializing the SDL video subsystem
261 /// - [`LcdError::WindowBuild`] when there is an error building the window
262 /// - [`LcdError::CanvasBuild`] when there is an error building the window canvas
263 /// - [`LcdError::WindowWidth`] when the total window width, in pixels, would exceed [`i32::MAX`]
264 /// - [`LcdError::WindowHeight`] when the total window width, in pixels, would exceed [`i32::MAX`]
265 ///
266 /// [`Sdl`]: sdl2::Sdl
267 /// [`Color`]: sdl2::pixels::Color
268 /// [`i32::MAX`]: std::i32::MAX
269 ///
270 pub fn new(
271 sdl_context: &Sdl,
272 title: &str,
273 on_color: Color,
274 off_color: Color,
275 dot_width: u32,
276 dot_height: u32,
277 ) -> Result<LcdScreen<R, C>, LcdError> {
278 // Note: usize can be truly cast to u32.
279 let window_width = (C as u32) * dot_width;
280 let window_height = (R as u32) * dot_height;
281
282 // Note: if window_width/window_height are between 1 and i32::MAX then both R/C and
283 // dot_width/dot_height must be between 1 and i32::MAX. Also, i32::MAX can be truly cast to u32.
284 if !(1..=(i32::MAX as u32)).contains(&window_width) {
285 Err(LcdError::WindowWidth {
286 width: window_width,
287 row: R,
288 dot_width,
289 })?
290 };
291 if !(1..=(i32::MAX as u32)).contains(&window_height) {
292 Err(LcdError::WindowHeight {
293 height: window_height,
294 col: C,
295 dot_height,
296 })?
297 };
298
299 // Set up window
300
301 let video_subsystem = sdl_context.video().map_err(LcdError::Video)?;
302
303 let window = video_subsystem
304 .window(title, window_width, window_height)
305 .position_centered()
306 .build()?; //TODO: provide more options than just centered
307
308 let mut canvas = window.into_canvas().build()?;
309
310 canvas.set_draw_color(off_color);
311 canvas.clear();
312 canvas.present();
313
314 // Create screen
315
316 //Note: R and C can be truly cast to i32 as they have been proved to be less than i32::MAX
317 let dots_vec: Vec<[LcdDot; C]> = (0..R as i32)
318 .map(|y| {
319 let row_vec: Vec<LcdDot> = (0..C as i32)
320 .map(|x| LcdDot::new(x, y, dot_width, dot_height))
321 .collect();
322 row_vec.try_into().unwrap() // Note: every row_vec must be C in length, so this cannot fail
323 })
324 .collect();
325
326 // Note: dots_vec must be R in length, so this cannot fail
327 Ok(Self {
328 dots: dots_vec.try_into().unwrap(),
329 canvas,
330 on_color,
331 off_color,
332 })
333 }
334
335 /// Draws a bitmap to a simulated LCD screen.
336 ///
337 /// # Arguments
338 ///
339 /// * `bm` - A [`Bitmap`], or something that can be converted into a bitmap, to write to the LCD screen
340 ///
341 /// # Examples
342 ///
343 /// ```
344 /// # use simulate_lcd::{LcdScreen, LCD_DARK_GREEN, LCD_LIGHT_GREEN};
345 /// # let sdl_context = sdl2::init().unwrap();
346 /// let mut screen = LcdScreen::<2, 2>::new(
347 /// &sdl_context,
348 /// "LCD Example: Checkerboard",
349 /// LCD_DARK_GREEN,
350 /// LCD_LIGHT_GREEN,
351 /// 100,
352 /// 100,
353 /// )
354 /// .unwrap();
355 /// # std::thread::sleep(std::time::Duration::from_secs(1));
356 ///
357 /// screen.draw_bitmap(&[[true, false], [false, true]]);
358 /// # std::thread::sleep(std::time::Duration::from_secs(1));
359 /// ```
360 ///
361 /// # Errors
362 ///
363 /// - [`LcdError::Fill`] when there is an error filling one of the dots with the relevant color
364 ///
365 pub fn draw_bitmap<'a, BM: Into<&'a Bitmap<C, R>>>(&mut self, bm: BM) -> Result<(), LcdError> {
366 let bm_array: &[[bool; C]; R] = bm.into();
367 for (row_dots, row_bm) in self.dots.iter_mut().zip(bm_array) {
368 for (dot, bit) in row_dots.iter_mut().zip(row_bm) {
369 if dot.on != *bit {
370 dot.on = *bit;
371 self.canvas.set_draw_color(if dot.on {
372 self.on_color
373 } else {
374 self.off_color
375 });
376 self.canvas.fill_rect(dot.rect).map_err(LcdError::Fill)?;
377 }
378 }
379 }
380 self.canvas.present();
381 Ok(())
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 //use sdl2::{event::Event, keyboard::Keycode};
389
390 #[test]
391 fn test_success() {
392 let sdl_context = sdl2::init().unwrap();
393 let _screen = LcdScreen::<10, 10>::new(
394 &sdl_context,
395 "LCD Test: Success",
396 LCD_DARK_GREEN,
397 LCD_LIGHT_GREEN,
398 10,
399 10,
400 )
401 .unwrap();
402 }
403}