Skip to content

Move SDL objects to TLS, allowing multiple windows to co-exist. #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

## [Unreleased] - ReleaseDate

### Changed

- [#48] Allowed for multiple window to co-exist in the same application.

## [0.6.0] - 2023-11-26

### Changed
Expand Down
138 changes: 138 additions & 0 deletions examples/multiple-windows.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//! # Example: Input Handling
//!
//! This example allows you to move a red circle to the location of a click on the simulator
//! screen, or move the circle using the arrow keys. Although input handling is not a part of the
//! embedded-graphics API, the simulator can be used to emulate input controls in order to
//! represent more complex UI systems such as touch screens.

extern crate embedded_graphics;
extern crate embedded_graphics_simulator;

use embedded_graphics::{
pixelcolor::Rgb888,
prelude::*,
primitives::{Circle, PrimitiveStyle},
};
use embedded_graphics_simulator::{
sdl2::Keycode, OutputSettings, SimulatorDisplay, SimulatorEvent, Window,
};
use sdl2::event::WindowEvent;

const BACKGROUND_COLOR: Rgb888 = Rgb888::BLACK;
const FOREGROUND_COLOR: Rgb888 = Rgb888::RED;
const KEYBOARD_DELTA: i32 = 20;

fn move_circle(
display: &mut SimulatorDisplay<Rgb888>,
old_center: Point,
new_center: Point,
) -> Result<(), core::convert::Infallible> {
// Clear old circle
Circle::with_center(old_center, 200)
.into_styled(PrimitiveStyle::with_fill(BACKGROUND_COLOR))
.draw(display)?;

// Draw circle at new location
Circle::with_center(new_center, 200)
.into_styled(PrimitiveStyle::with_fill(FOREGROUND_COLOR))
.draw(display)?;

Ok(())
}

enum LoopResult {
Continue,
AddNewWindow,
RemoveWindow,
Quit,
}

fn handle_loop(
window: &mut Window,
display: &mut SimulatorDisplay<Rgb888>,
position: &mut Point,
) -> Result<LoopResult, core::convert::Infallible> {
window.update(display);

let events = window.events();
for event in events {
match event {
SimulatorEvent::Quit => return Ok(LoopResult::Quit),
SimulatorEvent::KeyDown { keycode, .. } => {
let delta = match keycode {
Keycode::Left => Point::new(-KEYBOARD_DELTA, 0),
Keycode::Right => Point::new(KEYBOARD_DELTA, 0),
Keycode::Up => Point::new(0, -KEYBOARD_DELTA),
Keycode::Down => Point::new(0, KEYBOARD_DELTA),
Keycode::N => return Ok(LoopResult::AddNewWindow),
Keycode::Escape => return Ok(LoopResult::RemoveWindow),
_ => Point::zero(),
};
let new_position = *position + delta;
move_circle(display, *position, new_position)?;
*position = new_position;
}
SimulatorEvent::MouseButtonUp { point, .. } => {
move_circle(display, *position, point)?;
*position = point;
}
SimulatorEvent::WindowEvent(WindowEvent::Close) => return Ok(LoopResult::RemoveWindow),
_ => {}
}
}
Ok(LoopResult::Continue)
}

fn main() -> Result<(), core::convert::Infallible> {
let mut windows = vec![(
SimulatorDisplay::new(Size::new(800, 480)),
Window::new(
"Click to move circle (press N for new window)",
&OutputSettings::default(),
),
Point::new(200, 200),
)];

for (display, _, position) in windows.iter_mut() {
Circle::with_center(*position, 200)
.into_styled(PrimitiveStyle::with_fill(FOREGROUND_COLOR))
.draw(display)?;
}

'running: loop {
// When no windows are opened, exit the application.
if windows.is_empty() {
break;
}

for (i, (display, window, position)) in windows.iter_mut().enumerate() {
match handle_loop(window, display, position)? {
LoopResult::Continue => {}
LoopResult::AddNewWindow => {
let mut display = SimulatorDisplay::new(Size::new(800, 480));
let window = Window::new(
"Click to move circle (press N for new window)",
&OutputSettings::default(),
);
let position = Point::new(200, 200);

Circle::with_center(position, 200)
.into_styled(PrimitiveStyle::with_fill(FOREGROUND_COLOR))
.draw(&mut display)?;

windows.push((display, window, position));
break;
}
LoopResult::RemoveWindow => {
windows.swap_remove(i);
break;
}
LoopResult::Quit => {
break 'running;
}
}
}
}

Ok(())
}
193 changes: 117 additions & 76 deletions src/window/sdl_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,32 @@ use sdl2::{

use crate::{OutputImage, OutputSettings, SimulatorDisplay};

struct SdlContext {
video_subsystem: sdl2::VideoSubsystem,
event_pump: EventPump,
}

const MAXIMUM_GLOBAL_EVENT_PUMP_SIZE: usize = 32;

thread_local! {
/// A global event pump that captures all events that are unhandled.
/// This will start dropping events if it's full, to avoid memory leaks.
static GLOBAL_EVENT_PUMP: std::cell::RefCell<Vec<Event>> = {
std::cell::RefCell::new(Vec::with_capacity(MAXIMUM_GLOBAL_EVENT_PUMP_SIZE))
};

static SDL_CONTEXT: std::cell::RefCell<SdlContext> = {
let sdl_context = sdl2::init().unwrap();
let video_subsystem = sdl_context.video().unwrap();
let event_pump = sdl_context.event_pump().unwrap();

std::cell::RefCell::new(SdlContext {
video_subsystem,
event_pump,
})
};
}

/// A derivation of [`sdl2::event::Event`] mapped to embedded-graphics coordinates
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum SimulatorEvent {
Expand Down Expand Up @@ -63,11 +89,12 @@ pub enum SimulatorEvent {
},
/// An exit event
Quit,
/// A window event, containing the SDL2 window event
WindowEvent(sdl2::event::WindowEvent),
}

pub struct SdlWindow {
canvas: Canvas<sdl2::video::Window>,
event_pump: EventPump,
window_texture: SdlWindowTexture,
size: Size,
}
Expand All @@ -81,19 +108,18 @@ impl SdlWindow {
where
C: PixelColor + Into<Rgb888>,
{
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()
.build()
.unwrap();
let window = SDL_CONTEXT.with(|ctx| {
ctx.borrow_mut()
.video_subsystem
.window(title, size.width, size.height)
.position_centered()
.build()
.unwrap()
});

let canvas = window.into_canvas().build().unwrap();
let event_pump = sdl_context.event_pump().unwrap();

let window_texture = SdlWindowTextureBuilder {
texture_creator: canvas.texture_creator(),
Expand All @@ -107,7 +133,6 @@ impl SdlWindow {

Self {
canvas,
event_pump,
window_texture,
size,
}
Expand All @@ -132,76 +157,92 @@ impl SdlWindow {
}

/// Handle events
/// Return an iterator of all captured SimulatorEvent
/// Return an iterator of all captured SimulatorEvent for this window.
/// If an event is not targeted to this window, keep it in the global
/// event pump.
pub fn events(
&mut self,
output_settings: &OutputSettings,
) -> impl Iterator<Item = SimulatorEvent> + '_ {
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,
..
} => {
if let Some(valid_keycode) = keycode {
Some(SimulatorEvent::KeyDown {
keycode: valid_keycode,
keymod,
repeat,
})
} else {
None
}
}
Event::KeyUp {
keycode,
keymod,
repeat,
..
} => {
if let Some(valid_keycode) = keycode {
Some(SimulatorEvent::KeyUp {
keycode: valid_keycode,
keymod,
repeat,
})
} else {
None
}
}
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,
let window_id = self.canvas.window().id();

// Pump the global pump, adding new events to it, and filters only the
// events that are for this window (or global).
let events = SDL_CONTEXT.with(|ctx| {
let mut bindings = ctx.borrow_mut();
let new_events = bindings.event_pump.poll_iter();

GLOBAL_EVENT_PUMP.with(|pump| {
let (events, remaining): (_, Vec<Event>) = pump
.borrow_mut()
.drain(..)
.into_iter()
.chain(new_events.into_iter())
.partition(|e| {
e.get_window_id() == Some(window_id) || e.get_window_id().is_none()
});

// Limit the size of the global event pump to avoid memory leaks.
*pump.borrow_mut() =
remaining[..remaining.len().min(MAXIMUM_GLOBAL_EVENT_PUMP_SIZE)].to_vec();
events
})
});

let output_settings = output_settings.clone();
// TODO: Handling of ESC key should be left to the application. They might
// want to handle it differently.
events.into_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(|keycode| SimulatorEvent::KeyDown {
keycode,
keymod,
repeat,
}),
Event::KeyUp {
keycode,
keymod,
repeat,
..
} => keycode.map(|keycode| SimulatorEvent::KeyUp {
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 })
}
Event::Window { win_event, .. } => Some(SimulatorEvent::WindowEvent(win_event)),
_ => None,
})
}
}

Expand Down