diff --git a/CHANGELOG.md b/CHANGELOG.md index ad1dec0..1deb867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/examples/multiple-windows.rs b/examples/multiple-windows.rs new file mode 100644 index 0000000..d4947aa --- /dev/null +++ b/examples/multiple-windows.rs @@ -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, + 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, + position: &mut Point, +) -> Result { + 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(()) +} diff --git a/src/window/sdl_window.rs b/src/window/sdl_window.rs index ed7e1c4..fbc32ba 100644 --- a/src/window/sdl_window.rs +++ b/src/window/sdl_window.rs @@ -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> = { + std::cell::RefCell::new(Vec::with_capacity(MAXIMUM_GLOBAL_EVENT_PUMP_SIZE)) + }; + + static SDL_CONTEXT: std::cell::RefCell = { + 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 { @@ -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, - event_pump: EventPump, window_texture: SdlWindowTexture, size: Size, } @@ -81,19 +108,18 @@ impl SdlWindow { where C: PixelColor + Into, { - 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(), @@ -107,7 +133,6 @@ impl SdlWindow { Self { canvas, - event_pump, window_texture, size, } @@ -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 + '_ { - 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) = 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, + }) } }