use sdl2::event::Event as SdlEvent;
use sdl2::event::WindowEvent;
use sdl2::pixels::PixelFormatEnum;
use sdl2::rect::Rect;
use sdl2::render::Canvas;
use sdl2::render::Texture;
use sdl2::render::TextureCreator;
use sdl2::surface::Surface;
use std::sync::Arc;
use std::sync::Mutex;
use std::sync::mpsc;
use std::time::Duration;
use std::time::Instant;
use crate::Image;
use crate::ImageData;
use crate::ImageInfo;
#[cfg(feature = "save")]
use crate::KeyCode;
use crate::KeyState;
use crate::KeyboardEvent;
#[cfg(feature = "save")]
use crate::KeyModifiers;
use crate::PixelFormat;
use crate::Rectangle;
use crate::WaitKeyError;
use crate::WindowOptions;
use crate::oneshot;
use crate::background_thread::BackgroundThread;
use crate::MouseState;
use crate::MouseMoveEvent;
use crate::MouseButtonState;
use crate::MouseButton;
use crate::MouseButtonEvent;
use crate::Event;
mod monochrome;
mod key_code;
mod key_location;
mod modifiers;
mod scan_code;
pub use super::EventHandler;
pub use super::EventHandlerContext;
const RESULT_TIMEOUT: Duration = Duration::from_millis(500);
pub struct Context {
command_tx: mpsc::SyncSender<ContextCommand>,
thread: Mutex<Option<BackgroundThread<Result<(), String>>>>,
}
pub struct Window {
id: u32,
command_tx: mpsc::SyncSender<ContextCommand>,
event_rx: mpsc::Receiver<Event>,
}
enum ContextCommand {
Stop(oneshot::Sender<()>),
CreateWindow(WindowOptions, mpsc::SyncSender<ContextCommand>, oneshot::Sender<Result<Window, String>>),
DestroyWindow(u32, oneshot::Sender<Result<(), String>>),
SetImage(u32, Box<[u8]>, ImageInfo, String, oneshot::Sender<Result<(), String>>),
GetImage(u32, oneshot::Sender<Result<Option<Image>, String>>),
AddEventHandler(u32, EventHandler, oneshot::Sender<Result<(), String>>),
}
struct ContextInner {
video: sdl2::VideoSubsystem,
events: sdl2::EventPump,
mono_palette: sdl2::pixels::Palette,
windows: Vec<WindowInner>,
command_rx: mpsc::Receiver<ContextCommand>,
event_handlers: Vec<(u32, EventHandler)>,
background_tasks: Vec<BackgroundThread<()>>,
stop: bool,
}
pub struct WindowInner {
id: u32,
canvas: Canvas<sdl2::video::Window>,
texture_creator: TextureCreator<sdl2::video::WindowContext>,
texture: Option<(Texture<'static>, Rect)>,
image: Option<Image>,
event_tx: mpsc::SyncSender<Event>,
preserve_aspect_ratio: bool,
}
impl Context {
pub fn new() -> Result<Self, String> {
let (result_tx, mut result_rx) = oneshot::channel();
let (command_tx, command_rx) = mpsc::sync_channel(10);
let thread = BackgroundThread::new(move || {
match ContextInner::new(command_rx) {
Err(e) => {
result_tx.send(Err(e));
Ok(())
},
Ok(mut context) => {
result_tx.send(Ok(()));
context.run()?;
context.join_background_tasks();
Ok(())
}
}
});
match result_rx.recv_timeout(Duration::from_millis(1500)) {
Err(e) => Err(format!("failed to receive ready notification from context thread: {}", e)),
Ok(Err(e)) => Err(e),
Ok(Ok(())) => Ok(Context {
command_tx,
thread: Mutex::new(Some(thread)),
})
}
}
pub fn make_window(&self, name: impl Into<String>) -> Result<Window, String> {
let options = WindowOptions { name: name.into(), ..Default::default() };
self.make_window_full(options)
}
pub fn make_window_full(&self, options: WindowOptions) -> Result<Window, String> {
let (result_tx, mut result_rx) = oneshot::channel();
self.command_tx.send(ContextCommand::CreateWindow(options, self.command_tx.clone(), result_tx))
.map_err(|e| format!("failed to send CreateWindow command to context thread: {}", e))?;
result_rx.recv_timeout(RESULT_TIMEOUT).map_err(|e| format!("failed to receive CreateWindow result from context thread: {}", e))?
}
#[allow(unused)]
pub fn stop(&self) -> Result<(), String> {
let (result_tx, mut result_rx) = oneshot::channel();
self.command_tx.send(ContextCommand::Stop(result_tx))
.map_err(|e| format!("failed to send Stop command to context thread: {}", e))?;
result_rx.recv_timeout(RESULT_TIMEOUT).map_err(|e| format!("failed to receive Stop result from context thread: {}", e))
}
#[allow(unused)]
pub fn join(&self) -> Result<(), String> {
let mut thread = self.thread.lock().unwrap();
if let Some(thread) = thread.take() {
thread.join().map_err(|e| format!("failed to join context thread: {:?}", e))?
} else {
Ok(())
}
}
}
impl Window {
pub fn set_image(&self, image: impl ImageData, name: impl Into<String>) -> Result<(), String> {
let info = image.info().map_err(|e| format!("failed to display image: {}", e))?;
let data = image.data();
let (result_tx, mut result_rx) = oneshot::channel();
self.command_tx.send(ContextCommand::SetImage(self.id, data, info, name.into(), result_tx)).unwrap();
result_rx.recv_timeout(RESULT_TIMEOUT)
.map_err(|e| format!("failed to receive SetImage result from context thread: {}", e))?
.map_err(|e| format!("failed to display image: {}", e))
}
pub fn get_image(&self) -> Result<Option<Image>, String> {
let (result_tx, mut result_rx) = oneshot::channel();
self.command_tx.send(ContextCommand::GetImage(self.id, result_tx)).unwrap();
result_rx.recv_timeout(RESULT_TIMEOUT)
.map_err(|e| format!("failed to receive GetImage result from context thread: {}", e))?
}
pub fn add_event_handler<Handler>(&self, handler: Handler) -> Result<(), String>
where
Handler: FnMut(&mut EventHandlerContext) + Send + 'static,
{
let (result_tx, mut result_rx) = oneshot::channel();
self.command_tx.send(ContextCommand::AddEventHandler(self.id, Box::new(handler), result_tx)).unwrap();
result_rx.recv_timeout(RESULT_TIMEOUT)
.map_err(|e| format!("failed to receive AddEventHandler result from context thread: {}", e))?
}
pub fn close(self) -> Result<(), String> {
self.close_impl()
}
pub fn events(&self) -> &mpsc::Receiver<Event> {
&self.event_rx
}
pub fn wait_key(&self, timeout: Duration) -> Result<Option<KeyboardEvent>, WaitKeyError> {
self.wait_key_deadline(Instant::now() + timeout)
}
pub fn wait_key_deadline(&self, deadline: Instant) -> Result<Option<KeyboardEvent>, WaitKeyError> {
loop {
let now = Instant::now();
if now >= deadline {
return Ok(None);
}
let event = match self.events().recv_timeout(deadline - now) {
Ok(Event::KeyboardEvent(x)) => x,
Ok(_) => continue,
Err(mpsc::RecvTimeoutError::Timeout) => return Ok(None),
Err(mpsc::RecvTimeoutError::Disconnected) => return Err(WaitKeyError::WindowClosed),
};
if event.state == KeyState::Down {
return Ok(Some(event))
}
}
}
fn close_impl(&self) -> Result<(), String> {
let (result_tx, mut result_rx) = oneshot::channel();
self.command_tx.send(ContextCommand::DestroyWindow(self.id, result_tx))
.map_err(|e| format!("failed to send DestroyWindow command to window: {}", e))?;
result_rx.recv_timeout(RESULT_TIMEOUT).map_err(|e| format!("failed to receive DestroyWindow result from context thread: {}", e))?
}
}
impl Drop for Window {
fn drop(&mut self) {
let _ = self.close_impl();
}
}
impl ContextInner {
fn new(command_rx: mpsc::Receiver<ContextCommand>) -> Result<Self, String> {
sdl2::hint::set("SDL_NO_SIGNAL_HANDLERS", "1");
let context = sdl2::init().map_err(|e| format!("failed to initialize SDL2: {}", e))?;
let video = context.video().map_err(|e| format!("failed to get SDL2 video subsystem: {}", e))?;
let events = context.event_pump().map_err(|e| format!("failed to get SDL2 event pump: {}", e))?;
let mono_palette = monochrome::mono_palette().map_err(|e| format!("failed to create monochrome palette: {}", e))?;
Ok(Self {
video,
events,
mono_palette,
windows: Vec::new(),
command_rx,
event_handlers: Vec::new(),
background_tasks: Vec::new(),
stop: false,
})
}
fn run(&mut self) -> Result<(), String> {
let delay = Duration::from_nanos(1_000_000_000 / 60);
let mut next_frame = Instant::now() + delay;
while !self.stop {
self.run_one()?;
let now = Instant::now();
if now < next_frame {
std::thread::sleep(next_frame - now);
next_frame += delay;
} else {
next_frame = now.max(next_frame + delay);
}
}
Ok(())
}
fn run_one(&mut self) -> Result<(), String> {
let mut focused_windows = Vec::new();
while let Some(event) = self.events.poll_event() {
if let SdlEvent::Window { window_id, win_event, .. } = event {
match win_event {
WindowEvent::Close => self.destroy_window(window_id)?,
WindowEvent::FocusGained => focused_windows.push(window_id),
_ => (),
}
continue;
}
if let Some((window_id, event)) = convert_event(event, &focused_windows) {
self.handle_event(window_id, event);
}
}
self.poll_commands();
for window in &mut self.windows {
window.draw()?;
}
self.clean_background_tasks();
Ok(())
}
fn handle_event(&mut self, window_id: u32, event: Event) {
if let Some(window) = self.windows.iter_mut().find(|x| x.id == window_id) {
#[cfg(feature = "save")] {
if let Event::KeyboardEvent(event) = &event {
let ctrl = event.modifiers.contains(KeyModifiers::CONTROL);
let shift = event.modifiers.contains(KeyModifiers::SHIFT);
let alt = event.modifiers.contains(KeyModifiers::ALT);
if event.state == KeyState::Down && event.key == KeyCode::Character("S".into()) && ctrl && !shift && !alt {
if let Some(work) = window.save_image() {
self.background_tasks.push(work);
}
}
}
}
for (_, handler) in self.event_handlers.iter_mut().filter(|(id, _)| *id == window_id) {
let mut context = EventHandlerContext::new(&mut self.background_tasks, &event, &window);
handler(&mut context);
if context.should_stop_propagation() {
break;
}
}
let _ = window.event_tx.try_send(event);
}
}
fn poll_commands(&mut self) {
while let Ok(command) = self.command_rx.try_recv() {
self.handle_command(command);
}
}
fn handle_command(&mut self, command: ContextCommand) {
match command {
ContextCommand::Stop(result_tx) => {
self.stop = true;
result_tx.send(());
},
ContextCommand::CreateWindow(options, command_tx, result_tx) => {
result_tx.send(self.make_window(options, command_tx));
},
ContextCommand::DestroyWindow(id, result_tx) => {
result_tx.send(self.destroy_window(id));
},
ContextCommand::SetImage(id, data, info, name, result_tx) => {
match self.windows.iter_mut().find(|x| x.id == id) {
None => result_tx.send(Err(format!("failed to find window with ID {}", id))),
Some(window) => result_tx.send(window.set_image(&self.mono_palette, data, info, name)),
}
},
ContextCommand::GetImage(id, result_tx) => {
match self.windows.iter_mut().find(|x| x.id == id) {
None => result_tx.send(Err(format!("failed to find window with ID {}", id))),
Some(window) => result_tx.send(Ok(window.image.clone())),
}
},
ContextCommand::AddEventHandler(id, handler, result_tx) => {
match self.windows.iter_mut().find(|x| x.id == id) {
None => result_tx.send(Err(format!("failed to find window with ID {}", id))),
Some(_) => {
self.event_handlers.push((id, handler));
result_tx.send(Ok(()))
}
}
},
}
}
fn make_window(&mut self, options: WindowOptions, command_tx: mpsc::SyncSender<ContextCommand>) -> Result<Window, String> {
let window = self.video.window(&options.name, options.size[0], options.size[1])
.borderless()
.resizable()
.build()
.map_err(|e| format!("failed to create window {:?}: {}", options.name, e))?;
let id = window.id();
let canvas = window.into_canvas().build().map_err(|e| format!("failed to create canvas for window {:?}: {}", options.name, e))?;
let texture_creator = canvas.texture_creator();
let (event_tx, event_rx) = mpsc::sync_channel(10);
let inner = WindowInner {
id,
canvas,
texture_creator,
texture: None,
image: None,
event_tx,
preserve_aspect_ratio: options.preserve_aspect_ratio,
};
self.windows.push(inner);
Ok(Window { id, command_tx, event_rx })
}
fn destroy_window(&mut self, id: u32) -> Result<(), String> {
self.event_handlers.retain(|(handler_window_id, _)| *handler_window_id != id);
let index = self.windows.iter().position(|x| x.id == id)
.ok_or_else(|| format!("failed to find window with ID {}", id))?;
let mut window = self.windows.remove(index);
window.close();
Ok(())
}
fn clean_background_tasks(&mut self) {
self.background_tasks.retain(|x| !x.is_done());
}
fn join_background_tasks(&mut self) {
while !self.background_tasks.is_empty() {
let _ = self.background_tasks.remove(self.background_tasks.len() - 1).join();
}
}
}
impl<'a> From<&'a Rect> for Rectangle {
fn from(other: &'a Rect) -> Self {
Self::from_xywh(other.x(), other.y(), other.width(), other.height())
}
}
impl From<Rect> for Rectangle {
fn from(other: Rect) -> Self {
(&other).into()
}
}
impl<'a> From<&'a Rectangle> for Rect {
fn from(other: &'a Rectangle) -> Self {
Self::new(other.x(), other.y(), other.width(), other.height())
}
}
impl From<Rectangle> for Rect {
fn from(other: Rectangle) -> Self {
(&other).into()
}
}
impl WindowInner {
pub fn image(&self) -> Option<&Image> {
self.image.as_ref()
}
pub fn size(&self) -> [u32; 2] {
let viewport = self.canvas.viewport();
[viewport.width(), viewport.height()]
}
pub fn image_area(&self) -> Option<Rectangle> {
let (_texture, image_size) = self.texture.as_ref()?;
if self.preserve_aspect_ratio {
let image_size = Rectangle::from(image_size);
let canvas_size = Rectangle::from(&self.canvas.viewport());
Some(compute_target_rect_with_aspect_ratio(&image_size, &canvas_size))
} else {
Some(self.canvas.viewport().into())
}
}
fn set_image(&mut self, mono_palette: &sdl2::pixels::Palette, mut data: Box<[u8]>, info: ImageInfo, name: String) -> Result<(), String> {
let pixel_format = match info.pixel_format {
PixelFormat::Mono8 => PixelFormatEnum::Index8,
PixelFormat::Rgb8 => PixelFormatEnum::RGB24,
PixelFormat::Rgba8 => PixelFormatEnum::RGBA32,
PixelFormat::Bgr8 => PixelFormatEnum::BGR24,
PixelFormat::Bgra8 => PixelFormatEnum::BGRA32,
};
let mut surface = Surface::from_data(&mut data, info.width as u32, info.height as u32, info.row_stride as u32, pixel_format)
.map_err(|e| format!("failed to create surface for pixel data: {}", e))?;
let image_size = surface.rect();
if info.pixel_format == PixelFormat::Mono8 {
surface.set_palette(mono_palette).map_err(|e| format!("failed to set monochrome palette on canvas: {}", e))?;
}
let texture = self.texture_creator.create_texture_from_surface(surface)
.map_err(|e| format!("failed to create texture from surface: {}", e))?;
let texture = unsafe { std::mem::transmute::<_, Texture<'static>>(texture) };
self.texture = Some((texture, image_size));
self.image = Some(Image { data: Arc::from(data), info, name });
Ok(())
}
fn draw(&mut self) -> Result<(), String> {
self.canvas.clear();
if let Some((texture, image_size)) = &self.texture {
let rect = if self.preserve_aspect_ratio {
compute_target_rect_with_aspect_ratio(&image_size.into(), &self.canvas.viewport().into()).into()
} else {
self.canvas.viewport()
};
self.canvas.copy(&texture, image_size.clone(), rect)
.map_err(|e| format!("failed to copy data to self: {}", e))?;
}
self.canvas.present();
Ok(())
}
fn close(&mut self) {
self.canvas.window_mut().hide();
}
#[cfg(feature = "save")]
fn save_image(&mut self) -> Option<BackgroundThread<()>> {
let image = self.image.as_ref()?.clone();
Some(BackgroundThread::new(move || {
let _ = crate::prompt_save_image(&format!("{}.png", image.name), &image.data, image.info);
}))
}
}
fn convert_event(event: SdlEvent, focused_windows: &Vec<u32>) -> Option<(u32, Event)> {
match event {
SdlEvent::KeyDown { window_id, keycode, scancode, keymod, repeat, .. } if !focused_windows.contains(&window_id) => {
Some((window_id, Event::KeyboardEvent(convert_keyboard_event(KeyState::Down, keycode, scancode, keymod, repeat))))
},
SdlEvent::KeyUp { window_id, keycode, scancode, keymod, repeat, .. } if !focused_windows.contains(&window_id) => {
Some((window_id, Event::KeyboardEvent(convert_keyboard_event(KeyState::Up, keycode, scancode, keymod, repeat))))
},
SdlEvent::MouseMotion { window_id, which, mousestate, x, y, xrel, yrel, .. } if !focused_windows.contains(&window_id) => {
Some((window_id, Event::MouseMoveEvent(convert_mouse_move_event(which, mousestate, x, y, xrel, yrel))))
},
SdlEvent::MouseButtonDown { window_id, which, mouse_btn, clicks, x, y, .. } if !focused_windows.contains(&window_id) => {
Some((window_id, Event::MouseButtonEvent(convert_mouse_button_event(which, MouseState::Down, mouse_btn, clicks, x, y))))
},
SdlEvent::MouseButtonUp { window_id, which, mouse_btn, clicks, x, y, .. } if !focused_windows.contains(&window_id) => {
Some((window_id, Event::MouseButtonEvent(convert_mouse_button_event(which, MouseState::Up, mouse_btn, clicks, x, y))))
},
_ => None,
}
}
fn convert_keyboard_event(
state: KeyState,
key_code: Option<sdl2::keyboard::Keycode>,
scan_code: Option<sdl2::keyboard::Scancode>,
modifiers: sdl2::keyboard::Mod,
repeat: bool,
) -> KeyboardEvent {
KeyboardEvent {
state,
key: key_code::convert_key_code(key_code),
code: scan_code::convert_scan_code(scan_code),
location: key_location::get_key_location(scan_code),
modifiers: modifiers::convert_modifiers(modifiers),
repeat,
is_composing: false,
}
}
fn convert_mouse_state(
mouse_state: sdl2::mouse::MouseState,
) -> MouseButtonState {
MouseButtonState {
left: mouse_state.left(),
middle: mouse_state.middle(),
right: mouse_state.right(),
}
}
fn convert_mouse_move_event(
mouse_id: u32,
mouse_state: sdl2::mouse::MouseState,
x: i32,
y: i32,
xrel: i32,
yrel: i32,
) -> MouseMoveEvent {
MouseMoveEvent {
mouse_id,
mouse_state: convert_mouse_state(mouse_state),
position_x: x,
position_y: y,
relative_x: xrel,
relative_y: yrel,
}
}
fn convert_mouse_button(
mouse_button: sdl2::mouse::MouseButton,
) -> MouseButton {
match mouse_button {
sdl2::mouse::MouseButton::Left => MouseButton::Left,
sdl2::mouse::MouseButton::Middle => MouseButton::Middle,
sdl2::mouse::MouseButton::Right => MouseButton::Right,
_ => MouseButton::Unknown,
}
}
fn convert_mouse_button_event(
mouse_id: u32,
state: MouseState,
button: sdl2::mouse::MouseButton,
clicks: u8,
x: i32,
y: i32,
) -> MouseButtonEvent {
MouseButtonEvent {
mouse_id,
state,
button: convert_mouse_button(button),
clicks,
position_x: x,
position_y: y,
}
}
fn compute_target_rect_with_aspect_ratio(source: &Rectangle, canvas: &Rectangle) -> Rectangle {
let source_w = f64::from(source.width());
let source_h = f64::from(source.height());
let canvas_w = f64::from(canvas.width());
let canvas_h = f64::from(canvas.height());
let scale_w = canvas_w / source_w;
let scale_h = canvas_h / source_h;
if scale_w < scale_h {
let new_height = (source_h * scale_w).round() as u32;
let top = (canvas.height() - new_height) / 2;
Rectangle::from_xywh(canvas.x(), canvas.y() + top as i32, canvas.width(), new_height)
} else {
let new_width = (source_w * scale_h).round() as u32;
let left = (canvas.width() - new_width) / 2;
Rectangle::from_xywh(canvas.x() + left as i32, canvas.y(), new_width, canvas.height())
}
}