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}