Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@

- [#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

Expand Down
163 changes: 163 additions & 0 deletions examples/multiple-displays.rs
Original file line number Diff line number Diff line change
@@ -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<BinaryColor> = MonoTextStyle::new(&FONT_10X20, BinaryColor::On);
const TFT_TEXT: MonoTextStyle<Rgb565> =
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<BinaryColor> = 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<Rgb565> = 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(())
}
63 changes: 49 additions & 14 deletions src/display.rs
Original file line number Diff line number Diff line change
@@ -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},
Expand All @@ -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<C> {
size: Size,
pub(crate) pixels: Box<[C]>,
pub(crate) id: usize,
}

impl<C: PixelColor> SimulatorDisplay<C> {
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<BinaryColor>` or another
Expand All @@ -23,7 +38,7 @@ impl<C: PixelColor> SimulatorDisplay<C> {
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.
Expand Down Expand Up @@ -75,14 +90,21 @@ impl<C: PixelColor> SimulatorDisplay<C> {
.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<C> SimulatorDisplay<C>
Expand Down Expand Up @@ -122,8 +144,8 @@ where
/// // example: output_image.save_png("out.png")?;
/// ```
pub fn to_rgb_output_image(&self, output_settings: &OutputSettings) -> OutputImage<Rgb888> {
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
}
Expand Down Expand Up @@ -152,8 +174,9 @@ where
&self,
output_settings: &OutputSettings,
) -> OutputImage<Gray8> {
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
}
Expand Down Expand Up @@ -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,
})
))
}
}

Expand Down Expand Up @@ -257,6 +280,12 @@ impl<C> OriginDimensions for SimulatorDisplay<C> {
}
}

impl<C: PartialEq> PartialEq for SimulatorDisplay<C> {
fn eq(&self, other: &Self) -> bool {
self.size == other.size && self.pixels == other.pixels
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -321,6 +350,7 @@ mod tests {
.map(|c| BinaryColor::from(*c != 0))
.collect::<Vec<_>>()
.into_boxed_slice(),
id: 0,
};

let expected = [
Expand All @@ -345,6 +375,7 @@ mod tests {
.map(|c| Gray2::new(*c))
.collect::<Vec<_>>()
.into_boxed_slice(),
id: 0,
};

let expected = [
Expand All @@ -370,6 +401,7 @@ mod tests {
.map(|c| Gray4::new(*c))
.collect::<Vec<_>>()
.into_boxed_slice(),
id: 0,
};

let expected = [
Expand Down Expand Up @@ -398,6 +430,7 @@ mod tests {
.map(Gray8::new)
.collect::<Vec<_>>()
.into_boxed_slice(),
id: 0,
};

assert_eq!(&display.to_be_bytes(), &expected);
Expand All @@ -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]);
Expand All @@ -425,6 +459,7 @@ mod tests {
let display = SimulatorDisplay {
size: Size::new(2, 1),
pixels: expected.clone().into_boxed_slice(),
id: 0,
};

assert_eq!(
Expand Down
6 changes: 3 additions & 3 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -180,3 +177,6 @@ pub use crate::{
theme::BinaryColorTheme,
window::Window,
};

#[cfg(feature = "with-sdl")]
pub use window::{MultiWindow, SimulatorEvent, SimulatorEventsIter};
Loading