Skip to content

Commit f228cfe

Browse files
committed
Add support for multiple displays per window
1 parent c198fae commit f228cfe

9 files changed

Lines changed: 692 additions & 108 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010

1111
- [#63](https://github.com/embedded-graphics/simulator/pull/63) Added support for custom binary color themes (`BinaryColorTheme::Custom`).
1212
- [#62](https://github.com/embedded-graphics/simulator/pull/62) Added an SDL based audio example (sdl-audio.rs).
13+
- [#66](https://github.com/embedded-graphics/simulator/pull/66) Added `MultiWindow` to show multiple displays in one window.
14+
- [#66](https://github.com/embedded-graphics/simulator/pull/66) Added `SimulatorDisplay::output_size`.
1315

1416
### Changed
1517

1618
- **(breaking)** [#65](https://github.com/embedded-graphics/simulator/pull/65) Bump Minimum Supported Rust Version (MSRV) to latest stable.
19+
- **(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.
1720
- [#66](https://github.com/embedded-graphics/simulator/pull/66) Changed `Window::events` to take `&self` instead of `&mut self`.
1821

1922
## [0.7.0] - 2024-09-10

examples/multiple-displays.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//! # Example: Multiple displays
2+
//!
3+
//! This example demonstrates how multiple displays can be displayed in a common window.
4+
5+
extern crate embedded_graphics;
6+
extern crate embedded_graphics_simulator;
7+
8+
use embedded_graphics::{
9+
geometry::AnchorPoint,
10+
mono_font::{ascii::FONT_10X20, MonoTextStyle},
11+
pixelcolor::{BinaryColor, Rgb565, Rgb888},
12+
prelude::*,
13+
primitives::{Circle, PrimitiveStyle, PrimitiveStyleBuilder, Rectangle, StrokeAlignment},
14+
text::{Alignment, Baseline, Text, TextStyle, TextStyleBuilder},
15+
};
16+
use embedded_graphics_simulator::{
17+
sdl2::MouseButton, BinaryColorTheme, MultiWindow, OutputSettings, OutputSettingsBuilder,
18+
SimulatorDisplay, SimulatorEvent,
19+
};
20+
21+
const OLED_TEXT: MonoTextStyle<BinaryColor> = MonoTextStyle::new(&FONT_10X20, BinaryColor::On);
22+
const TFT_TEXT: MonoTextStyle<Rgb565> =
23+
MonoTextStyle::new(&FONT_10X20, Rgb565::CSS_LIGHT_SLATE_GRAY);
24+
const CENTERED: TextStyle = TextStyleBuilder::new()
25+
.alignment(Alignment::Center)
26+
.baseline(Baseline::Middle)
27+
.build();
28+
29+
/// Determines the position of a display.
30+
fn display_offset(window_size: Size, display_size: Size, anchor_point: AnchorPoint) -> Point {
31+
// Position displays in a rectangle that is 20px than the the window.
32+
let layout_rect = Rectangle::new(Point::zero(), window_size).offset(-20);
33+
34+
// Resize the rectangle to the display size to determine the offset from the
35+
// top left corner of the window to the top left corner of the display.
36+
layout_rect.resized(display_size, anchor_point).top_left
37+
}
38+
39+
fn main() -> Result<(), core::convert::Infallible> {
40+
// Create three simulated monochrome 128x64 OLED displays.
41+
42+
let mut oled_displays = Vec::new();
43+
for i in 0..3 {
44+
let mut oled: SimulatorDisplay<BinaryColor> = SimulatorDisplay::new(Size::new(128, 64));
45+
46+
Text::with_text_style(
47+
&format!("Display {i}"),
48+
oled.bounding_box().center(),
49+
OLED_TEXT,
50+
CENTERED,
51+
)
52+
.draw(&mut oled)
53+
.unwrap();
54+
55+
oled_displays.push(oled);
56+
}
57+
58+
// Create a simulated color 320x240 TFT display.
59+
60+
let mut tft: SimulatorDisplay<Rgb565> = SimulatorDisplay::new(Size::new(320, 240));
61+
tft.clear(Rgb565::new(5, 10, 5)).unwrap();
62+
63+
Text::with_text_style(
64+
&format!("Draw here"),
65+
tft.bounding_box().center(),
66+
TFT_TEXT,
67+
CENTERED,
68+
)
69+
.draw(&mut tft)
70+
.unwrap();
71+
72+
// The simulated displays can now be added to common simulator window.
73+
74+
let window_size = Size::new(1300, 500);
75+
let mut window = MultiWindow::new("Multiple displays example", window_size);
76+
window.clear(Rgb888::CSS_DIM_GRAY);
77+
78+
let oled_settings = OutputSettingsBuilder::new()
79+
.theme(BinaryColorTheme::OledBlue)
80+
.scale(2)
81+
.build();
82+
let oled_size = oled_displays[0].output_size(&oled_settings);
83+
84+
for (oled, anchor) in oled_displays.iter().zip(
85+
[
86+
AnchorPoint::TopLeft,
87+
AnchorPoint::TopCenter,
88+
AnchorPoint::TopRight,
89+
]
90+
.into_iter(),
91+
) {
92+
let offset = display_offset(window_size, oled_size, anchor);
93+
window.add_display(&oled, offset, &oled_settings);
94+
}
95+
96+
let tft_settings = OutputSettings::default();
97+
let tft_size = tft.output_size(&tft_settings);
98+
let tft_offset = display_offset(window_size, tft_size, AnchorPoint::BottomCenter);
99+
100+
window.add_display(&tft, tft_offset, &tft_settings);
101+
102+
let border_style = PrimitiveStyleBuilder::new()
103+
.stroke_width(5)
104+
.stroke_alignment(StrokeAlignment::Inside)
105+
.build();
106+
107+
let mut mouse_down = false;
108+
109+
'running: loop {
110+
// Call `update_display` for all display. Note that the window won't be
111+
// updated until `window.flush` is called.
112+
for oled in &oled_displays {
113+
window.update_display(oled);
114+
}
115+
window.update_display(&tft);
116+
window.flush();
117+
118+
for event in window.events() {
119+
match event {
120+
SimulatorEvent::MouseMove { point } => {
121+
// Mouse events use the window coordinate system.
122+
// `translate_mouse_position` can be used to translate the
123+
// mouse position into the display coordinate system.
124+
125+
for oled in &mut oled_displays {
126+
let is_inside = window.translate_mouse_position(oled, point).is_some();
127+
128+
let style = PrimitiveStyleBuilder::from(&border_style)
129+
.stroke_color(BinaryColor::from(is_inside))
130+
.build();
131+
132+
oled.bounding_box().into_styled(style).draw(oled).unwrap();
133+
}
134+
135+
if mouse_down {
136+
if let Some(point) = window.translate_mouse_position(&tft, point) {
137+
Circle::with_center(point, 10)
138+
.into_styled(PrimitiveStyle::with_fill(Rgb565::CSS_DODGER_BLUE))
139+
.draw(&mut tft)
140+
.unwrap();
141+
}
142+
}
143+
}
144+
SimulatorEvent::MouseButtonDown {
145+
mouse_btn: MouseButton::Left,
146+
..
147+
} => {
148+
mouse_down = true;
149+
}
150+
SimulatorEvent::MouseButtonUp {
151+
mouse_btn: MouseButton::Left,
152+
..
153+
} => {
154+
mouse_down = false;
155+
}
156+
SimulatorEvent::Quit => break 'running,
157+
_ => {}
158+
}
159+
}
160+
}
161+
162+
Ok(())
163+
}

src/display.rs

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
use std::{convert::TryFrom, fs::File, io::BufReader, path::Path};
1+
use std::{
2+
convert::TryFrom,
3+
fs::File,
4+
io::BufReader,
5+
path::Path,
6+
sync::atomic::{AtomicUsize, Ordering},
7+
};
28

39
use embedded_graphics::{
410
pixelcolor::{raw::ToBytes, BinaryColor, Gray8, Rgb888},
@@ -7,14 +13,23 @@ use embedded_graphics::{
713

814
use crate::{output_image::OutputImage, output_settings::OutputSettings};
915

16+
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
17+
1018
/// Simulator display.
11-
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
19+
#[derive(Debug, Clone, Eq, PartialOrd, Ord, Hash)]
1220
pub struct SimulatorDisplay<C> {
1321
size: Size,
1422
pub(crate) pixels: Box<[C]>,
23+
pub(crate) id: usize,
1524
}
1625

1726
impl<C: PixelColor> SimulatorDisplay<C> {
27+
fn new_common(size: Size, pixels: Box<[C]>) -> Self {
28+
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
29+
30+
Self { size, pixels, id }
31+
}
32+
1833
/// Creates a new display filled with a color.
1934
///
2035
/// This constructor can be used if `C` doesn't implement `From<BinaryColor>` or another
@@ -23,7 +38,7 @@ impl<C: PixelColor> SimulatorDisplay<C> {
2338
let pixel_count = size.width as usize * size.height as usize;
2439
let pixels = vec![default_color; pixel_count].into_boxed_slice();
2540

26-
SimulatorDisplay { size, pixels }
41+
SimulatorDisplay::new_common(size, pixels)
2742
}
2843

2944
/// Returns the color of the pixel at a point.
@@ -75,14 +90,21 @@ impl<C: PixelColor> SimulatorDisplay<C> {
7590
.into_boxed_slice();
7691

7792
if pixels.iter().any(|p| *p == BinaryColor::On) {
78-
Some(SimulatorDisplay {
79-
pixels,
80-
size: self.size,
81-
})
93+
Some(SimulatorDisplay::new_common(self.size, pixels))
8294
} else {
8395
None
8496
}
8597
}
98+
99+
/// Calculates the rendered size of this display based on the output settings.
100+
///
101+
/// This method takes into account the [`scale`](OutputSettings::scale) and
102+
/// [`pixel_spacing`](OutputSettings::pixel_spacing) settings to determine
103+
/// the size of this display in output pixels.
104+
pub fn output_size(&self, output_settings: &OutputSettings) -> Size {
105+
self.size * output_settings.scale
106+
+ self.size.saturating_sub(Size::new_equal(1)) * output_settings.pixel_spacing
107+
}
86108
}
87109

88110
impl<C> SimulatorDisplay<C>
@@ -122,8 +144,8 @@ where
122144
/// // example: output_image.save_png("out.png")?;
123145
/// ```
124146
pub fn to_rgb_output_image(&self, output_settings: &OutputSettings) -> OutputImage<Rgb888> {
125-
let mut output = OutputImage::new(self, output_settings);
126-
output.update(self);
147+
let mut output = OutputImage::new(self.output_size(output_settings));
148+
output.update(self, Point::zero(), output_settings);
127149

128150
output
129151
}
@@ -152,8 +174,9 @@ where
152174
&self,
153175
output_settings: &OutputSettings,
154176
) -> OutputImage<Gray8> {
155-
let mut output = OutputImage::new(self, output_settings);
156-
output.update(self);
177+
let size = self.output_size(output_settings);
178+
let mut output = OutputImage::new(size);
179+
output.update(self, Point::zero(), output_settings);
157180

158181
output
159182
}
@@ -226,10 +249,10 @@ where
226249
.map(|p| Rgb888::new(p[0], p[1], p[2]).into())
227250
.collect();
228251

229-
Ok(Self {
230-
size: Size::new(image.width(), image.height()),
252+
Ok(Self::new_common(
253+
Size::new(image.width(), image.height()),
231254
pixels,
232-
})
255+
))
233256
}
234257
}
235258

@@ -257,6 +280,12 @@ impl<C> OriginDimensions for SimulatorDisplay<C> {
257280
}
258281
}
259282

283+
impl<C: PartialEq> PartialEq for SimulatorDisplay<C> {
284+
fn eq(&self, other: &Self) -> bool {
285+
self.size == other.size && self.pixels == other.pixels
286+
}
287+
}
288+
260289
#[cfg(test)]
261290
mod tests {
262291
use super::*;
@@ -321,6 +350,7 @@ mod tests {
321350
.map(|c| BinaryColor::from(*c != 0))
322351
.collect::<Vec<_>>()
323352
.into_boxed_slice(),
353+
id: 0,
324354
};
325355

326356
let expected = [
@@ -345,6 +375,7 @@ mod tests {
345375
.map(|c| Gray2::new(*c))
346376
.collect::<Vec<_>>()
347377
.into_boxed_slice(),
378+
id: 0,
348379
};
349380

350381
let expected = [
@@ -370,6 +401,7 @@ mod tests {
370401
.map(|c| Gray4::new(*c))
371402
.collect::<Vec<_>>()
372403
.into_boxed_slice(),
404+
id: 0,
373405
};
374406

375407
let expected = [
@@ -398,6 +430,7 @@ mod tests {
398430
.map(Gray8::new)
399431
.collect::<Vec<_>>()
400432
.into_boxed_slice(),
433+
id: 0,
401434
};
402435

403436
assert_eq!(&display.to_be_bytes(), &expected);
@@ -412,6 +445,7 @@ mod tests {
412445
let display = SimulatorDisplay {
413446
size: Size::new(2, 1),
414447
pixels: expected.clone().into_boxed_slice(),
448+
id: 0,
415449
};
416450

417451
assert_eq!(&display.to_be_bytes(), &[0x80, 0x00, 0x00, 0x01]);
@@ -425,6 +459,7 @@ mod tests {
425459
let display = SimulatorDisplay {
426460
size: Size::new(2, 1),
427461
pixels: expected.clone().into_boxed_slice(),
462+
id: 0,
428463
};
429464

430465
assert_eq!(

src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,6 @@ mod output_settings;
158158
mod theme;
159159
mod window;
160160

161-
#[cfg(feature = "with-sdl")]
162-
pub use window::SimulatorEvent;
163-
164161
/// Re-exported types from sdl2 crate.
165162
///
166163
/// The types in this module are used in the [`SimulatorEvent`] enum and are re-exported from the
@@ -180,3 +177,6 @@ pub use crate::{
180177
theme::BinaryColorTheme,
181178
window::Window,
182179
};
180+
181+
#[cfg(feature = "with-sdl")]
182+
pub use window::{MultiWindow, SimulatorEvent};

0 commit comments

Comments
 (0)