From c198fae8c1a7ee741b90cbf2c96d55242a01c1d2 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Thu, 5 Jun 2025 04:02:50 +0200 Subject: [PATCH 1/2] Use shared self reference for Window::events --- CHANGELOG.md | 1 + src/output_settings.rs | 2 +- src/window/mod.rs | 12 ++-- src/window/sdl_window.rs | 146 +++++++++++++++++++++++---------------- 4 files changed, 96 insertions(+), 65 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c1fb33..5f8ccb0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ ### Changed - **(breaking)** [#65](https://github.com/embedded-graphics/simulator/pull/65) Bump Minimum Supported Rust Version (MSRV) to latest stable. +- [#66](https://github.com/embedded-graphics/simulator/pull/66) Changed `Window::events` to take `&self` instead of `&mut self`. ## [0.7.0] - 2024-09-10 diff --git a/src/output_settings.rs b/src/output_settings.rs index cf8381e..ed83eb1 100644 --- a/src/output_settings.rs +++ b/src/output_settings.rs @@ -2,7 +2,7 @@ use crate::{display::SimulatorDisplay, theme::BinaryColorTheme}; use embedded_graphics::prelude::*; /// Output settings. -#[derive(Debug, PartialEq, Eq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub struct OutputSettings { /// Pixel scale. pub scale: u32, diff --git a/src/window/mod.rs b/src/window/mod.rs index 3e14113..b01e6bb 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -17,7 +17,7 @@ use crate::{ mod sdl_window; #[cfg(feature = "with-sdl")] -pub use sdl_window::{SdlWindow, SimulatorEvent}; +pub use sdl_window::{SdlWindow, SimulatorEvent, SimulatorEventsIter}; /// Simulator window #[allow(dead_code)] @@ -160,15 +160,17 @@ impl Window { } } - /// Returns an iterator of all captured SimulatorEvents. + /// Returns an iterator of all captured simulator events. /// /// # Panics /// - /// Panics if called before [`update`](Self::update) is called at least once. + /// Panics if called before [`update`](Self::update) is called at least + /// once. Also panics if multiple instances of the iterator are used at the + /// same time. #[cfg(feature = "with-sdl")] - pub fn events(&mut self) -> impl Iterator + '_ { + pub fn events(&self) -> SimulatorEventsIter<'_> { self.sdl_window - .as_mut() + .as_ref() .unwrap() .events(&self.output_settings) } diff --git a/src/window/sdl_window.rs b/src/window/sdl_window.rs index 94e03d8..efb267d 100644 --- a/src/window/sdl_window.rs +++ b/src/window/sdl_window.rs @@ -1,3 +1,5 @@ +use std::cell::{RefCell, RefMut}; + use embedded_graphics::{ pixelcolor::Rgb888, prelude::{PixelColor, Point, Size}, @@ -65,9 +67,87 @@ pub enum SimulatorEvent { Quit, } +/// Iterator over simulator events. +/// +/// See [`Window::events`](crate::Window::events) and +/// [`MultiWindow::events`](crate::MultiWindow::events) for more details. +pub struct SimulatorEventsIter<'a> { + event_pump: RefMut<'a, EventPump>, + output_settings: OutputSettings, +} + +impl Iterator for SimulatorEventsIter<'_> { + type Item = SimulatorEvent; + + fn next(&mut self) -> Option { + while let Some(event) = self.event_pump.poll_event() { + match event { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), + .. + } => return Some(SimulatorEvent::Quit), + Event::KeyDown { + keycode, + keymod, + repeat, + .. + } => { + return keycode.map(|valid_keycode| SimulatorEvent::KeyDown { + keycode: valid_keycode, + keymod, + repeat, + }) + } + Event::KeyUp { + keycode, + keymod, + repeat, + .. + } => { + return keycode.map(|valid_keycode| SimulatorEvent::KeyUp { + keycode: valid_keycode, + keymod, + repeat, + }) + } + Event::MouseButtonUp { + x, y, mouse_btn, .. + } => { + let point = self.output_settings.output_to_display(Point::new(x, y)); + return Some(SimulatorEvent::MouseButtonUp { point, mouse_btn }); + } + Event::MouseButtonDown { + x, y, mouse_btn, .. + } => { + let point = self.output_settings.output_to_display(Point::new(x, y)); + return Some(SimulatorEvent::MouseButtonDown { point, mouse_btn }); + } + Event::MouseMotion { x, y, .. } => { + let point = self.output_settings.output_to_display(Point::new(x, y)); + return Some(SimulatorEvent::MouseMove { point }); + } + Event::MouseWheel { + x, y, direction, .. + } => { + return Some(SimulatorEvent::MouseWheel { + scroll_delta: Point::new(x, y), + direction, + }) + } + _ => { + // ignore other events and check next event + } + } + } + + None + } +} + pub struct SdlWindow { canvas: Canvas, - event_pump: EventPump, + event_pump: RefCell, window_texture: SdlWindowTexture, size: Size, } @@ -107,7 +187,7 @@ impl SdlWindow { Self { canvas, - event_pump, + event_pump: RefCell::new(event_pump), window_texture, size, } @@ -133,63 +213,11 @@ impl SdlWindow { /// Handle events /// Return an iterator of all captured SimulatorEvent - pub fn events( - &mut self, - output_settings: &OutputSettings, - ) -> impl Iterator + '_ { - let output_settings = output_settings.clone(); - self.event_pump - .poll_iter() - .filter_map(move |event| match event { - Event::Quit { .. } - | Event::KeyDown { - keycode: Some(Keycode::Escape), - .. - } => Some(SimulatorEvent::Quit), - Event::KeyDown { - keycode, - keymod, - repeat, - .. - } => keycode.map(|valid_keycode| SimulatorEvent::KeyDown { - keycode: valid_keycode, - keymod, - repeat, - }), - Event::KeyUp { - keycode, - keymod, - repeat, - .. - } => keycode.map(|valid_keycode| SimulatorEvent::KeyUp { - keycode: valid_keycode, - keymod, - repeat, - }), - Event::MouseButtonUp { - x, y, mouse_btn, .. - } => { - let point = output_settings.output_to_display(Point::new(x, y)); - Some(SimulatorEvent::MouseButtonUp { point, mouse_btn }) - } - Event::MouseButtonDown { - x, y, mouse_btn, .. - } => { - let point = output_settings.output_to_display(Point::new(x, y)); - Some(SimulatorEvent::MouseButtonDown { point, mouse_btn }) - } - Event::MouseWheel { - x, y, direction, .. - } => Some(SimulatorEvent::MouseWheel { - scroll_delta: Point::new(x, y), - direction, - }), - Event::MouseMotion { x, y, .. } => { - let point = output_settings.output_to_display(Point::new(x, y)); - Some(SimulatorEvent::MouseMove { point }) - } - _ => None, - }) + pub fn events(&self, output_settings: &OutputSettings) -> SimulatorEventsIter<'_> { + SimulatorEventsIter { + event_pump: self.event_pump.borrow_mut(), + output_settings: output_settings.clone(), + } } } From 31c9e8a5ecb23f9ca59ddb77d83807dca0687025 Mon Sep 17 00:00:00 2001 From: Ralf Fuest Date: Wed, 4 Jun 2025 19:48:38 +0200 Subject: [PATCH 2/2] Add support for multiple displays per window --- CHANGELOG.md | 4 + examples/multiple-displays.rs | 163 +++++++++++++++++ src/display.rs | 63 +++++-- src/lib.rs | 6 +- src/output_image.rs | 326 +++++++++++++++++++++++++++++----- src/output_settings.rs | 28 +-- src/window/mod.rs | 58 ++++-- src/window/multi_window.rs | 137 ++++++++++++++ src/window/sdl_window.rs | 15 +- 9 files changed, 691 insertions(+), 109 deletions(-) create mode 100644 examples/multiple-displays.rs create mode 100644 src/window/multi_window.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f8ccb0..588a751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,14 @@ - [#63](https://github.com/embedded-graphics/simulator/pull/63) Added support for custom binary color themes (`BinaryColorTheme::Custom`). - [#62](https://github.com/embedded-graphics/simulator/pull/62) Added an SDL based audio example (sdl-audio.rs). +- [#66](https://github.com/embedded-graphics/simulator/pull/66) Added `MultiWindow` to show multiple displays in one window. +- [#66](https://github.com/embedded-graphics/simulator/pull/66) Added `SimulatorDisplay::output_size`. ### Changed - **(breaking)** [#65](https://github.com/embedded-graphics/simulator/pull/65) Bump Minimum Supported Rust Version (MSRV) to latest stable. +- **(breaking)** [#66](https://github.com/embedded-graphics/simulator/pull/66) `OutputSettings::max_fps` has been removed, use `Window::set_max_fps` or `MultiWindow::set_max_fps` instead. +- **(breaking)** [#66](https://github.com/embedded-graphics/simulator/pull/66) Renamed `OutputImage::update` to `OutputImage::draw_display` and added `position` parameter. - [#66](https://github.com/embedded-graphics/simulator/pull/66) Changed `Window::events` to take `&self` instead of `&mut self`. ## [0.7.0] - 2024-09-10 diff --git a/examples/multiple-displays.rs b/examples/multiple-displays.rs new file mode 100644 index 0000000..20d0547 --- /dev/null +++ b/examples/multiple-displays.rs @@ -0,0 +1,163 @@ +//! # Example: Multiple displays +//! +//! This example demonstrates how multiple displays can be displayed in a common window. + +extern crate embedded_graphics; +extern crate embedded_graphics_simulator; + +use embedded_graphics::{ + geometry::AnchorPoint, + mono_font::{ascii::FONT_10X20, MonoTextStyle}, + pixelcolor::{BinaryColor, Rgb565, Rgb888}, + prelude::*, + primitives::{Circle, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle, StrokeAlignment}, + text::{Alignment, Baseline, Text, TextStyle, TextStyleBuilder}, +}; +use embedded_graphics_simulator::{ + sdl2::MouseButton, BinaryColorTheme, MultiWindow, OutputSettings, OutputSettingsBuilder, + SimulatorDisplay, SimulatorEvent, +}; + +const OLED_TEXT: MonoTextStyle = MonoTextStyle::new(&FONT_10X20, BinaryColor::On); +const TFT_TEXT: MonoTextStyle = + MonoTextStyle::new(&FONT_10X20, Rgb565::CSS_LIGHT_SLATE_GRAY); +const CENTERED: TextStyle = TextStyleBuilder::new() + .alignment(Alignment::Center) + .baseline(Baseline::Middle) + .build(); + +/// Determines the position of a display. +fn display_offset(window_size: Size, display_size: Size, anchor_point: AnchorPoint) -> Point { + // Position displays in a rectangle that is 20px than the the window. + let layout_rect = Rectangle::new(Point::zero(), window_size).offset(-20); + + // Resize the rectangle to the display size to determine the offset from the + // top left corner of the window to the top left corner of the display. + layout_rect.resized(display_size, anchor_point).top_left +} + +fn main() -> Result<(), core::convert::Infallible> { + // Create three simulated monochrome 128x64 OLED displays. + + let mut oled_displays = Vec::new(); + for i in 0..3 { + let mut oled: SimulatorDisplay = SimulatorDisplay::new(Size::new(128, 64)); + + Text::with_text_style( + &format!("Display {i}"), + oled.bounding_box().center(), + OLED_TEXT, + CENTERED, + ) + .draw(&mut oled) + .unwrap(); + + oled_displays.push(oled); + } + + // Create a simulated color 320x240 TFT display. + + let mut tft: SimulatorDisplay = SimulatorDisplay::new(Size::new(320, 240)); + tft.clear(Rgb565::new(5, 10, 5)).unwrap(); + + Text::with_text_style( + &format!("Draw here"), + tft.bounding_box().center(), + TFT_TEXT, + CENTERED, + ) + .draw(&mut tft) + .unwrap(); + + // The simulated displays can now be added to common simulator window. + + let window_size = Size::new(1300, 500); + let mut window = MultiWindow::new("Multiple displays example", window_size); + window.clear(Rgb888::CSS_DIM_GRAY); + + let oled_settings = OutputSettingsBuilder::new() + .theme(BinaryColorTheme::OledBlue) + .scale(2) + .build(); + let oled_size = oled_displays[0].output_size(&oled_settings); + + for (oled, anchor) in oled_displays.iter().zip( + [ + AnchorPoint::TopLeft, + AnchorPoint::TopCenter, + AnchorPoint::TopRight, + ] + .into_iter(), + ) { + let offset = display_offset(window_size, oled_size, anchor); + window.add_display(&oled, offset, &oled_settings); + } + + let tft_settings = OutputSettings::default(); + let tft_size = tft.output_size(&tft_settings); + let tft_offset = display_offset(window_size, tft_size, AnchorPoint::BottomCenter); + + window.add_display(&tft, tft_offset, &tft_settings); + + let border_style = PrimitiveStyleBuilder::new() + .stroke_width(5) + .stroke_alignment(StrokeAlignment::Inside) + .build(); + + let mut mouse_down = false; + + 'running: loop { + // Call `update_display` for all display. Note that the window won't be + // updated until `window.flush` is called. + for oled in &oled_displays { + window.update_display(oled); + } + window.update_display(&tft); + window.flush(); + + for event in window.events() { + match event { + SimulatorEvent::MouseMove { point } => { + // Mouse events use the window coordinate system. + // `translate_mouse_position` can be used to translate the + // mouse position into the display coordinate system. + + for oled in &mut oled_displays { + let is_inside = window.translate_mouse_position(oled, point).is_some(); + + let style = PrimitiveStyleBuilder::from(&border_style) + .stroke_color(BinaryColor::from(is_inside)) + .build(); + + oled.bounding_box().into_styled(style).draw(oled).unwrap(); + } + + if mouse_down { + if let Some(point) = window.translate_mouse_position(&tft, point) { + Circle::with_center(point, 10) + .into_styled(PrimitiveStyle::with_fill(Rgb565::CSS_DODGER_BLUE)) + .draw(&mut tft) + .unwrap(); + } + } + } + SimulatorEvent::MouseButtonDown { + mouse_btn: MouseButton::Left, + .. + } => { + mouse_down = true; + } + SimulatorEvent::MouseButtonUp { + mouse_btn: MouseButton::Left, + .. + } => { + mouse_down = false; + } + SimulatorEvent::Quit => break 'running, + _ => {} + } + } + } + + Ok(()) +} diff --git a/src/display.rs b/src/display.rs index 71e52ea..7f17bfc 100644 --- a/src/display.rs +++ b/src/display.rs @@ -1,4 +1,10 @@ -use std::{convert::TryFrom, fs::File, io::BufReader, path::Path}; +use std::{ + convert::TryFrom, + fs::File, + io::BufReader, + path::Path, + sync::atomic::{AtomicUsize, Ordering}, +}; use embedded_graphics::{ pixelcolor::{raw::ToBytes, BinaryColor, Gray8, Rgb888}, @@ -7,14 +13,23 @@ use embedded_graphics::{ use crate::{output_image::OutputImage, output_settings::OutputSettings}; +static NEXT_ID: AtomicUsize = AtomicUsize::new(0); + /// Simulator display. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Clone, Eq, PartialOrd, Ord, Hash)] pub struct SimulatorDisplay { size: Size, pub(crate) pixels: Box<[C]>, + pub(crate) id: usize, } impl SimulatorDisplay { + fn new_common(size: Size, pixels: Box<[C]>) -> Self { + let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); + + Self { size, pixels, id } + } + /// Creates a new display filled with a color. /// /// This constructor can be used if `C` doesn't implement `From` or another @@ -23,7 +38,7 @@ impl SimulatorDisplay { let pixel_count = size.width as usize * size.height as usize; let pixels = vec![default_color; pixel_count].into_boxed_slice(); - SimulatorDisplay { size, pixels } + SimulatorDisplay::new_common(size, pixels) } /// Returns the color of the pixel at a point. @@ -75,14 +90,21 @@ impl SimulatorDisplay { .into_boxed_slice(); if pixels.iter().any(|p| *p == BinaryColor::On) { - Some(SimulatorDisplay { - pixels, - size: self.size, - }) + Some(SimulatorDisplay::new_common(self.size, pixels)) } else { None } } + + /// Calculates the rendered size of this display based on the output settings. + /// + /// This method takes into account the [`scale`](OutputSettings::scale) and + /// [`pixel_spacing`](OutputSettings::pixel_spacing) settings to determine + /// the size of this display in output pixels. + pub fn output_size(&self, output_settings: &OutputSettings) -> Size { + self.size * output_settings.scale + + self.size.saturating_sub(Size::new_equal(1)) * output_settings.pixel_spacing + } } impl SimulatorDisplay @@ -122,8 +144,8 @@ where /// // example: output_image.save_png("out.png")?; /// ``` pub fn to_rgb_output_image(&self, output_settings: &OutputSettings) -> OutputImage { - let mut output = OutputImage::new(self, output_settings); - output.update(self); + let mut output = OutputImage::new(self.output_size(output_settings)); + output.draw_display(self, Point::zero(), output_settings); output } @@ -152,8 +174,9 @@ where &self, output_settings: &OutputSettings, ) -> OutputImage { - let mut output = OutputImage::new(self, output_settings); - output.update(self); + let size = self.output_size(output_settings); + let mut output = OutputImage::new(size); + output.draw_display(self, Point::zero(), output_settings); output } @@ -226,10 +249,10 @@ where .map(|p| Rgb888::new(p[0], p[1], p[2]).into()) .collect(); - Ok(Self { - size: Size::new(image.width(), image.height()), + Ok(Self::new_common( + Size::new(image.width(), image.height()), pixels, - }) + )) } } @@ -257,6 +280,12 @@ impl OriginDimensions for SimulatorDisplay { } } +impl PartialEq for SimulatorDisplay { + fn eq(&self, other: &Self) -> bool { + self.size == other.size && self.pixels == other.pixels + } +} + #[cfg(test)] mod tests { use super::*; @@ -321,6 +350,7 @@ mod tests { .map(|c| BinaryColor::from(*c != 0)) .collect::>() .into_boxed_slice(), + id: 0, }; let expected = [ @@ -345,6 +375,7 @@ mod tests { .map(|c| Gray2::new(*c)) .collect::>() .into_boxed_slice(), + id: 0, }; let expected = [ @@ -370,6 +401,7 @@ mod tests { .map(|c| Gray4::new(*c)) .collect::>() .into_boxed_slice(), + id: 0, }; let expected = [ @@ -398,6 +430,7 @@ mod tests { .map(Gray8::new) .collect::>() .into_boxed_slice(), + id: 0, }; assert_eq!(&display.to_be_bytes(), &expected); @@ -412,6 +445,7 @@ mod tests { let display = SimulatorDisplay { size: Size::new(2, 1), pixels: expected.clone().into_boxed_slice(), + id: 0, }; assert_eq!(&display.to_be_bytes(), &[0x80, 0x00, 0x00, 0x01]); @@ -425,6 +459,7 @@ mod tests { let display = SimulatorDisplay { size: Size::new(2, 1), pixels: expected.clone().into_boxed_slice(), + id: 0, }; assert_eq!( diff --git a/src/lib.rs b/src/lib.rs index 01386db..9ec7e3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,9 +158,6 @@ mod output_settings; mod theme; mod window; -#[cfg(feature = "with-sdl")] -pub use window::SimulatorEvent; - /// Re-exported types from sdl2 crate. /// /// The types in this module are used in the [`SimulatorEvent`] enum and are re-exported from the @@ -180,3 +177,6 @@ pub use crate::{ theme::BinaryColorTheme, window::Window, }; + +#[cfg(feature = "with-sdl")] +pub use window::{MultiWindow, SimulatorEvent, SimulatorEventsIter}; diff --git a/src/output_image.rs b/src/output_image.rs index ee765de..033381c 100644 --- a/src/output_image.rs +++ b/src/output_image.rs @@ -2,7 +2,7 @@ use std::{convert::TryFrom, marker::PhantomData, path::Path}; use base64::Engine; use embedded_graphics::{ - pixelcolor::{raw::ToBytes, Gray8, Rgb888, RgbColor}, + pixelcolor::{raw::ToBytes, Gray8, Rgb888}, prelude::*, primitives::Rectangle, }; @@ -22,65 +22,75 @@ use crate::{display::SimulatorDisplay, output_settings::OutputSettings}; pub struct OutputImage { size: Size, pub(crate) data: Box<[u8]>, - pub(crate) output_settings: OutputSettings, + row_buffer: Vec, + color_type: PhantomData, } impl OutputImage where - C: PixelColor + From + ToBytes, - ::Bytes: AsRef<[u8]>, + C: PixelColor + OutputImageColor + From, + Self: DrawTarget, { /// Creates a new output image. - pub(crate) fn new( - display: &SimulatorDisplay, - output_settings: &OutputSettings, - ) -> Self - where - DisplayC: PixelColor + Into, - { - let size = output_settings.framebuffer_size(display); - - // Create an empty pixel buffer, filled with the background color. - let background_color = C::from(output_settings.theme.convert(Rgb888::BLACK)).to_be_bytes(); - let data = background_color - .as_ref() - .iter() - .copied() - .cycle() - .take(size.width as usize * size.height as usize * background_color.as_ref().len()) - .collect::>() - .into_boxed_slice(); + pub(crate) fn new(size: Size) -> Self { + let bytes_per_row = usize::try_from(size.width).unwrap() * C::BYTES_PER_PIXEL; + let bytes_total = usize::try_from(size.height).unwrap() * bytes_per_row; + + let data = vec![0; bytes_total].into_boxed_slice(); + let row_buffer = Vec::with_capacity(bytes_per_row); Self { size, data, - output_settings: output_settings.clone(), + row_buffer, color_type: PhantomData, } } - /// Updates the image from a [`SimulatorDisplay`]. - pub fn update(&mut self, display: &SimulatorDisplay) - where + /// Draws a display using the given position and output setting. + pub fn draw_display( + &mut self, + display: &SimulatorDisplay, + position: Point, + output_settings: &OutputSettings, + ) where DisplayC: PixelColor + Into, { - let pixel_pitch = (self.output_settings.scale + self.output_settings.pixel_spacing) as i32; - let pixel_size = Size::new(self.output_settings.scale, self.output_settings.scale); + let display_area = Rectangle::new(position, display.output_size(output_settings)); + self.fill_solid( + &display_area, + output_settings.theme.convert(Rgb888::BLACK).into(), + ) + .unwrap(); - for p in display.bounding_box().points() { - let raw_color = display.get_pixel(p).into(); - let themed_color = self.output_settings.theme.convert(raw_color); - let output_color = C::from(themed_color).to_be_bytes(); - let output_color = output_color.as_ref(); + if output_settings.scale == 1 { + display + .bounding_box() + .points() + .map(|p| { + let raw_color = display.get_pixel(p).into(); + let themed_color = output_settings.theme.convert(raw_color); + let output_color = C::from(themed_color); - for p in Rectangle::new(p * pixel_pitch, pixel_size).points() { - if let Ok((x, y)) = <(u32, u32)>::try_from(p) { - let start_index = (x + y * self.size.width) as usize * output_color.len(); + Pixel(p + position, output_color) + }) + .draw(self) + .unwrap(); + } else { + let pixel_pitch = (output_settings.scale + output_settings.pixel_spacing) as i32; + let pixel_size = Size::new(output_settings.scale, output_settings.scale); - self.data[start_index..start_index + output_color.len()] - .copy_from_slice(output_color) - } + for p in display.bounding_box().points() { + let raw_color = display.get_pixel(p).into(); + let themed_color = output_settings.theme.convert(raw_color); + let output_color = C::from(themed_color); + + self.fill_solid( + &Rectangle::new(p * pixel_pitch + position, pixel_size), + output_color, + ) + .unwrap(); } } } @@ -123,6 +133,112 @@ impl OutputImage { } } +impl DrawTarget for OutputImage { + type Color = Rgb888; + type Error = (); + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + for Pixel(p, color) in pixels { + if p.x >= 0 + && p.y >= 0 + && (p.x as u32) < self.size.width + && (p.y as u32) < self.size.height + { + let bytes = color.to_be_bytes(); + let (x, y) = (p.x as u32, p.y as u32); + + let start_index = (x + y * self.size.width) as usize * 3; + self.data[start_index..start_index + 3].copy_from_slice(bytes.as_ref()) + } + } + + Ok(()) + } + + fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { + let area = area.intersection(&self.bounding_box()); + + let bytes = color.to_be_bytes(); + let bytes = bytes.as_ref(); + + // For large areas it's more efficient to prepare a row buffer and copy + // the entire row at one. + // TODO: the bounds were chosen arbitrarily and might not be optimal + let large = area.size.width >= 16 && area.size.height >= 16; + + if large { + self.row_buffer.clear(); + for _ in 0..area.size.width { + self.row_buffer.extend_from_slice(bytes); + } + } + + let bytes_per_row = self.size.width as usize * bytes.len(); + let x_start = area.top_left.x as usize * bytes.len(); + let x_end = x_start + area.size.width as usize * bytes.len(); + + if large { + for y in area.rows() { + let start = bytes_per_row * y as usize + x_start; + self.data[start..start + self.row_buffer.len()].copy_from_slice(&self.row_buffer); + } + } else { + for y in area.rows() { + let row_start = bytes_per_row * y as usize; + for chunk in + self.data[row_start + x_start..row_start + x_end].chunks_exact_mut(bytes.len()) + { + chunk.copy_from_slice(bytes); + } + } + } + + Ok(()) + } +} + +impl DrawTarget for OutputImage { + type Color = Gray8; + type Error = (); + + fn draw_iter(&mut self, pixels: I) -> Result<(), Self::Error> + where + I: IntoIterator>, + { + for Pixel(p, color) in pixels { + if p.x >= 0 + && p.y >= 0 + && (p.x as u32) < self.size.width + && (p.y as u32) < self.size.height + { + let (x, y) = (p.x as u32, p.y as u32); + let index = (x + y * self.size.width) as usize; + self.data[index] = color.into_storage(); + } + } + + Ok(()) + } + + fn fill_solid(&mut self, area: &Rectangle, color: Self::Color) -> Result<(), Self::Error> { + let area = area.intersection(&self.bounding_box()); + + let bytes_per_row = self.size.width as usize; + let x_start = area.top_left.x as usize; + let x_end = x_start + area.size.width as usize; + + for y in area.rows() { + let row_start = bytes_per_row * y as usize; + self.data[row_start + x_start..row_start + x_end].fill(color.into_storage()); + } + + Ok(()) + } +} + impl OriginDimensions for OutputImage { fn size(&self) -> Size { self.size @@ -132,14 +248,144 @@ impl OriginDimensions for OutputImage { pub trait OutputImageColor { type ImageColor: image::Pixel + 'static; const IMAGE_COLOR_TYPE: image::ColorType; + const BYTES_PER_PIXEL: usize; } impl OutputImageColor for Gray8 { type ImageColor = Luma; const IMAGE_COLOR_TYPE: image::ColorType = image::ColorType::L8; + const BYTES_PER_PIXEL: usize = 1; } impl OutputImageColor for Rgb888 { type ImageColor = Rgb; const IMAGE_COLOR_TYPE: image::ColorType = image::ColorType::Rgb8; + const BYTES_PER_PIXEL: usize = 3; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rgb888_default_data() { + let image = OutputImage::::new(Size::new(6, 5)); + assert_eq!(image.data.as_ref(), &[0u8; 6 * 5 * 3]); + } + + #[test] + fn rgb888_draw_iter() { + let mut image = OutputImage::::new(Size::new(4, 6)); + + [ + Pixel(Point::new(0, 0), Rgb888::new(0xFF, 0x00, 0x00)), + Pixel(Point::new(3, 0), Rgb888::new(0x00, 0xFF, 0x00)), + Pixel(Point::new(0, 5), Rgb888::new(0x00, 0x00, 0xFF)), + Pixel(Point::new(3, 5), Rgb888::new(0x12, 0x34, 0x56)), + // out of bounds pixels should be ignored + Pixel(Point::new(-1, -1), Rgb888::new(0xFF, 0xFF, 0xFF)), + Pixel(Point::new(0, 10), Rgb888::new(0xFF, 0xFF, 0xFF)), + Pixel(Point::new(10, 0), Rgb888::new(0xFF, 0xFF, 0xFF)), + ] + .into_iter() + .draw(&mut image) + .unwrap(); + + assert_eq!( + image.data.as_ref(), + &[ + 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x12, 0x34, 0x56, // + ] + ); + } + + #[test] + fn rgb888_fill_solid() { + let mut image = OutputImage::::new(Size::new(4, 6)); + + image + .fill_solid( + &Rectangle::new(Point::new(2, 3), Size::new(10, 20)), + Rgb888::new(0x01, 0x02, 0x03), + ) + .unwrap(); + + assert_eq!( + image.data.as_ref(), + &[ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x01, 0x02, 0x03, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x01, 0x02, 0x03, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x01, 0x02, 0x03, // + ] + ); + } + + #[test] + fn gray8_default_data() { + let image = OutputImage::::new(Size::new(6, 5)); + assert_eq!(image.data.as_ref(), &[0u8; 6 * 5]); + } + + #[test] + fn gray8_draw_iter() { + let mut image = OutputImage::::new(Size::new(12, 6)); + + [ + Pixel(Point::new(0, 0), Gray8::new(0x01)), + Pixel(Point::new(11, 0), Gray8::new(0x02)), + Pixel(Point::new(0, 5), Gray8::new(0x03)), + Pixel(Point::new(11, 5), Gray8::new(0x04)), + // out of bounds pixels should be ignored + Pixel(Point::new(-1, -1), Gray8::new(0xFF)), + Pixel(Point::new(0, 10), Gray8::new(0xFF)), + Pixel(Point::new(12, 0), Gray8::new(0xFF)), + ] + .into_iter() + .draw(&mut image) + .unwrap(); + + assert_eq!( + image.data.as_ref(), + &[ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // + 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, // + ] + ); + } + + #[test] + fn gray8_fill_solid() { + let mut image = OutputImage::::new(Size::new(4, 6)); + + image + .fill_solid( + &Rectangle::new(Point::new(2, 3), Size::new(10, 20)), + Gray8::WHITE, + ) + .unwrap(); + + assert_eq!( + image.data.as_ref(), + &[ + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0x00, 0x00, // + 0x00, 0x00, 0xFF, 0xFF, // + 0x00, 0x00, 0xFF, 0xFF, // + 0x00, 0x00, 0xFF, 0xFF, // + ] + ); + } } diff --git a/src/output_settings.rs b/src/output_settings.rs index ed83eb1..c8766cf 100644 --- a/src/output_settings.rs +++ b/src/output_settings.rs @@ -1,4 +1,4 @@ -use crate::{display::SimulatorDisplay, theme::BinaryColorTheme}; +use crate::theme::BinaryColorTheme; use embedded_graphics::prelude::*; /// Output settings. @@ -10,23 +10,6 @@ pub struct OutputSettings { pub pixel_spacing: u32, /// Binary color theme. pub theme: BinaryColorTheme, - /// Maximum frames per second shown in the window. - pub max_fps: u32, -} - -impl OutputSettings { - /// Calculates the size of the framebuffer required to display the scaled display. - pub(crate) fn framebuffer_size(&self, display: &SimulatorDisplay) -> Size - where - C: PixelColor, - { - let width = display.size().width; - let height = display.size().height; - let output_width = width * self.scale + width.saturating_sub(1) * self.pixel_spacing; - let output_height = height * self.scale + height.saturating_sub(1) * self.pixel_spacing; - - Size::new(output_width, output_height) - } } #[cfg(feature = "with-sdl")] @@ -54,7 +37,6 @@ pub struct OutputSettingsBuilder { scale: Option, pixel_spacing: Option, theme: BinaryColorTheme, - max_fps: Option, } impl OutputSettingsBuilder { @@ -112,20 +94,12 @@ impl OutputSettingsBuilder { self } - /// Sets the FPS limit of the window. - pub fn max_fps(mut self, max_fps: u32) -> Self { - self.max_fps = Some(max_fps); - - self - } - /// Builds the output settings. pub fn build(self) -> OutputSettings { OutputSettings { scale: self.scale.unwrap_or(1), pixel_spacing: self.pixel_spacing.unwrap_or(0), theme: self.theme, - max_fps: self.max_fps.unwrap_or(60), } } } diff --git a/src/window/mod.rs b/src/window/mod.rs index b01e6bb..6d35aec 100644 --- a/src/window/mod.rs +++ b/src/window/mod.rs @@ -19,6 +19,38 @@ mod sdl_window; #[cfg(feature = "with-sdl")] pub use sdl_window::{SdlWindow, SimulatorEvent, SimulatorEventsIter}; +#[cfg(feature = "with-sdl")] +mod multi_window; + +#[cfg(feature = "with-sdl")] +pub use multi_window::MultiWindow; + +pub(crate) struct FpsLimiter { + max_fps: u32, + frame_start: Instant, +} + +impl FpsLimiter { + pub(crate) fn new() -> Self { + Self { + max_fps: 60, + frame_start: Instant::now(), + } + } + + fn desired_loop_duration(&self) -> Duration { + Duration::from_secs_f32(1.0 / self.max_fps as f32) + } + + fn sleep(&mut self) { + let sleep_duration = (self.frame_start + self.desired_loop_duration()) + .saturating_duration_since(Instant::now()); + thread::sleep(sleep_duration); + + self.frame_start = Instant::now(); + } +} + /// Simulator window #[allow(dead_code)] pub struct Window { @@ -27,8 +59,7 @@ pub struct Window { sdl_window: Option, title: String, output_settings: OutputSettings, - desired_loop_duration: Duration, - frame_start: Instant, + fps_limiter: FpsLimiter, } impl Window { @@ -40,8 +71,7 @@ impl Window { sdl_window: None, title: String::from(title), output_settings: output_settings.clone(), - desired_loop_duration: Duration::from_millis(1000 / output_settings.max_fps as u64), - frame_start: Instant::now(), + fps_limiter: FpsLimiter::new(), } } @@ -118,27 +148,24 @@ impl Window { #[cfg(feature = "with-sdl")] { + let size = display.output_size(&self.output_settings); + if self.framebuffer.is_none() { - self.framebuffer = Some(OutputImage::new(display, &self.output_settings)); + self.framebuffer = Some(OutputImage::new(size)); } if self.sdl_window.is_none() { - self.sdl_window = Some(SdlWindow::new(display, &self.title, &self.output_settings)); + self.sdl_window = Some(SdlWindow::new(&self.title, size)); } let framebuffer = self.framebuffer.as_mut().unwrap(); let sdl_window = self.sdl_window.as_mut().unwrap(); - framebuffer.update(display); + framebuffer.draw_display(display, Point::zero(), &self.output_settings); sdl_window.update(framebuffer); } - thread::sleep( - (self.frame_start + self.desired_loop_duration) - .saturating_duration_since(Instant::now()), - ); - - self.frame_start = Instant::now(); + self.fps_limiter.sleep(); } /// Shows a static display. @@ -174,4 +201,9 @@ impl Window { .unwrap() .events(&self.output_settings) } + + /// Sets the FPS limit of the window. + pub fn set_max_fps(&mut self, max_fps: u32) { + self.fps_limiter.max_fps = max_fps; + } } diff --git a/src/window/multi_window.rs b/src/window/multi_window.rs new file mode 100644 index 0000000..45b6b7f --- /dev/null +++ b/src/window/multi_window.rs @@ -0,0 +1,137 @@ +use std::collections::HashMap; + +use embedded_graphics::{pixelcolor::Rgb888, prelude::*}; + +use crate::{ + window::{sdl_window::SimulatorEventsIter, FpsLimiter, SdlWindow}, + OutputImage, OutputSettings, SimulatorDisplay, +}; + +/// Simulator window with support for multiple displays. +/// +/// Multiple [`SimulatorDisplay`]s can be added to the window by using the +/// [`add_display`](Self::add_display) method. To update the window two steps +/// are required, first [`update_display`](Self::update_display) needs be called +/// for all changed displays, then [`flush`](Self::flush) to redraw the window. +/// +/// To determine if the mouse pointer is over one of the displays the +/// [`translate_mouse_position`](Self::translate_mouse_position) can be used to +/// translate window coordinates into display coordinates. +pub struct MultiWindow { + sdl_window: SdlWindow, + framebuffer: OutputImage, + displays: HashMap, + fps_limiter: FpsLimiter, +} + +impl MultiWindow { + /// Creates a new window with support for multiple displays. + pub fn new(title: &str, size: Size) -> Self { + let mut sdl_window = SdlWindow::new(title, size); + + let framebuffer = OutputImage::new(size); + + sdl_window.update(&framebuffer); + + Self { + sdl_window: sdl_window, + framebuffer, + displays: HashMap::new(), + fps_limiter: FpsLimiter::new(), + } + } + + /// Adds a display to the window. + pub fn add_display( + &mut self, + display: &SimulatorDisplay, + offset: Point, + output_settings: &OutputSettings, + ) { + self.displays.insert( + display.id, + DisplaySettings { + offset, + output_settings: output_settings.clone(), + }, + ); + } + + /// Fills the internal framebuffer with the given color. + /// + /// This method can be used to set the background color for the regions of + /// the window that aren't covered by a display. + pub fn clear(&mut self, color: Rgb888) { + self.framebuffer.clear(color).unwrap(); + } + + /// Updates one display. + /// + /// This method only updates the internal framebuffer. Use + /// [`flush`](Self::flush) after all displays have been updated to finally + /// update the window. + pub fn update_display(&mut self, display: &SimulatorDisplay) + where + C: PixelColor + Into + From, + { + let display_settings = self + .displays + .get(&display.id) + .expect("update_display called for a display that hasn't been added with add_display"); + + self.framebuffer.draw_display( + display, + display_settings.offset, + &display_settings.output_settings, + ); + } + + /// Updates the window from the internal framebuffer. + pub fn flush(&mut self) { + self.sdl_window.update(&self.framebuffer); + + self.fps_limiter.sleep(); + } + + /// Returns an iterator of all captured simulator events. + /// + /// The coordinates in mouse events are in raw window coordinates, use + /// [`translate_mouse_position`](Self::translate_mouse_position) to + /// translate them into display coordinates. + /// + /// # Panics + /// + /// Panics if multiple instances of the iterator are used at the same time. + pub fn events(&self) -> SimulatorEventsIter<'_> { + self.sdl_window.events(&crate::OutputSettings::default()) + } + + /// Translate a mouse position into display coordinates. + /// + /// Returns the corresponding position in the display coordinate system if + /// the mouse is inside the display area, otherwise `None` is returned. + pub fn translate_mouse_position( + &self, + display: &SimulatorDisplay, + position: Point, + ) -> Option { + let display_settings = self.displays.get(&display.id).expect( + "translate_mouse_position called for a display that hasn't been added with add_display", + ); + + let delta = position - display_settings.offset; + let p = display_settings.output_settings.output_to_display(delta); + + display.bounding_box().contains(p).then_some(p) + } + + /// Sets the FPS limit of the window. + pub fn set_max_fps(&mut self, max_fps: u32) { + self.fps_limiter.max_fps = max_fps; + } +} + +struct DisplaySettings { + offset: Point, + output_settings: OutputSettings, +} diff --git a/src/window/sdl_window.rs b/src/window/sdl_window.rs index efb267d..4049776 100644 --- a/src/window/sdl_window.rs +++ b/src/window/sdl_window.rs @@ -2,7 +2,7 @@ use std::cell::{RefCell, RefMut}; use embedded_graphics::{ pixelcolor::Rgb888, - prelude::{PixelColor, Point, Size}, + prelude::{Point, Size}, }; use sdl2::{ event::Event, @@ -14,7 +14,7 @@ use sdl2::{ EventPump, }; -use crate::{OutputImage, OutputSettings, SimulatorDisplay}; +use crate::{OutputImage, OutputSettings}; /// A derivation of [`sdl2::event::Event`] mapped to embedded-graphics coordinates #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -153,19 +153,10 @@ pub struct SdlWindow { } impl SdlWindow { - pub fn new( - display: &SimulatorDisplay, - title: &str, - output_settings: &OutputSettings, - ) -> Self - where - C: PixelColor + Into, - { + pub fn new(title: &str, size: Size) -> Self { let sdl_context = sdl2::init().unwrap(); let video_subsystem = sdl_context.video().unwrap(); - let size = output_settings.framebuffer_size(display); - let window = video_subsystem .window(title, size.width, size.height) .position_centered()