use core::mem; use std::any::Any; use std::cell::{RefCell, UnsafeCell}; use std::ffi::c_void; use std::ops::Deref; use std::ptr::null_mut; use std::rc::Rc; use std::time::Duration; use imgui::internal::RawWrapper; use imgui::{ConfigFlags, DrawCmd, DrawData, Key, MouseCursor, TextureId, Ui}; use sdl2::event::{Event, WindowEvent}; use sdl2::keyboard::Scancode; use sdl2::mouse::{Cursor, SystemCursor}; use sdl2::pixels::PixelFormatEnum; use sdl2::render::{Texture, TextureCreator, TextureQuery, WindowCanvas}; use sdl2::video::GLProfile; use sdl2::video::WindowContext; use sdl2::{keyboard, pixels, EventPump, Sdl, VideoSubsystem}; use crate::common::{Color, Rect}; use crate::framework::backend::{ Backend, BackendEventLoop, BackendRenderer, BackendShader, BackendTexture, SpriteBatchCommand, VertexData, }; use crate::framework::context::Context; use crate::framework::error::{GameError, GameResult}; use crate::framework::graphics::BlendMode; use crate::framework::keyboard::ScanCode; use crate::framework::render_opengl::{GLContext, OpenGLRenderer}; use crate::framework::ui::init_imgui; use crate::Game; use crate::GameError::RenderError; use crate::GAME_SUSPENDED; pub struct SDL2Backend { context: Sdl, size_hint: (u16, u16), } impl SDL2Backend { pub fn new(size_hint: (u16, u16)) -> GameResult> { let context = sdl2::init().map_err(GameError::WindowError)?; let backend = SDL2Backend { context, size_hint }; Ok(Box::new(backend)) } } impl Backend for SDL2Backend { fn create_event_loop(&self) -> GameResult> { SDL2EventLoop::new(&self.context, self.size_hint) } } struct SDL2EventLoop { event_pump: EventPump, refs: Rc>, opengl_available: RefCell, } struct SDL2Context { video: VideoSubsystem, canvas: WindowCanvas, texture_creator: TextureCreator, gl_context: Option, blend_mode: sdl2::render::BlendMode, } impl SDL2EventLoop { pub fn new(sdl: &Sdl, size_hint: (u16, u16)) -> GameResult> { let event_pump = sdl.event_pump().map_err(GameError::WindowError)?; let video = sdl.video().map_err(GameError::WindowError)?; let gl_attr = video.gl_attr(); gl_attr.set_context_profile(GLProfile::Core); gl_attr.set_context_version(3, 0); let mut window = video.window("Cave Story (doukutsu-rs)", size_hint.0 as _, size_hint.1 as _); window.position_centered(); window.resizable(); #[cfg(feature = "render-opengl")] window.opengl(); let window = window.build().map_err(|e| GameError::WindowError(e.to_string()))?; let canvas = window .into_canvas() .accelerated() .present_vsync() .build() .map_err(|e| GameError::RenderError(e.to_string()))?; let texture_creator = canvas.texture_creator(); let opengl_available = if let Ok(v) = std::env::var("CAVESTORY_NO_OPENGL") { v != "1" } else { true }; let event_loop = SDL2EventLoop { event_pump, refs: Rc::new(RefCell::new(SDL2Context { video, canvas, texture_creator, gl_context: None, blend_mode: sdl2::render::BlendMode::Blend, })), opengl_available: RefCell::new(opengl_available), }; Ok(Box::new(event_loop)) } } impl BackendEventLoop for SDL2EventLoop { fn run(&mut self, game: &mut Game, ctx: &mut Context) { let state = unsafe { &mut *game.state.get() }; let (imgui, imgui_sdl2) = unsafe { let renderer: &Box = std::mem::transmute(ctx.renderer.as_ref().unwrap()); (&mut *renderer.imgui.as_ptr(), &mut *renderer.imgui_event.as_ptr()) }; { let (width, height) = self.refs.deref().borrow().canvas.window().size(); ctx.screen_size = (width.max(1) as f32, height.max(1) as f32); imgui.io_mut().display_size = [ctx.screen_size.0, ctx.screen_size.1]; let _ = state.handle_resize(ctx); } loop { #[cfg(target_os = "macos")] unsafe { use objc::*; // no UB: fields are initialized by SDL_GetWindowWMInfo let mut winfo: sdl2_sys::SDL_SysWMinfo = mem::MaybeUninit::uninit().assume_init(); winfo.version.major = sdl2_sys::SDL_MAJOR_VERSION as _; winfo.version.minor = sdl2_sys::SDL_MINOR_VERSION as _; winfo.version.patch = sdl2_sys::SDL_PATCHLEVEL as _; let mut whandle = self.refs.deref().borrow().canvas.window().raw(); if sdl2_sys::SDL_GetWindowWMInfo(whandle, &mut winfo as *mut _) != sdl2_sys::SDL_bool::SDL_FALSE { let window = winfo.info.x11.display as *mut objc::runtime::Object; let _: () = msg_send![window, setTitlebarAppearsTransparent:1]; let _: () = msg_send![window, setTitleVisibility:1]; // NSWindowTitleHidden let mut style_mask: u32 = msg_send![window, styleMask]; style_mask |= 1 << 15; // NSWindowStyleMaskFullSizeContentView let _: () = msg_send![window, setStyleMask: style_mask]; } } for event in self.event_pump.poll_iter() { imgui_sdl2.handle_event(imgui, &event); match event { Event::Quit { .. } => { state.shutdown(); } Event::Window { win_event, .. } => match win_event { WindowEvent::FocusGained | WindowEvent::Shown => { { let mut mutex = GAME_SUSPENDED.lock().unwrap(); *mutex = false; } state.sound_manager.resume(); game.loops = 0; } WindowEvent::FocusLost | WindowEvent::Hidden => { let mut mutex = GAME_SUSPENDED.lock().unwrap(); *mutex = true; state.sound_manager.pause(); } WindowEvent::SizeChanged(width, height) => { ctx.screen_size = (width.max(1) as f32, height.max(1) as f32); if let Some(renderer) = &ctx.renderer { if let Ok(imgui) = renderer.imgui() { imgui.io_mut().display_size = [ctx.screen_size.0, ctx.screen_size.1]; } } state.handle_resize(ctx).unwrap(); } _ => {} }, Event::KeyDown { scancode: Some(scancode), repeat, .. } => { if let Some(drs_scan) = conv_scancode(scancode) { if !repeat { state.process_debug_keys(drs_scan); } ctx.keyboard_context.set_key(drs_scan, true); } } Event::KeyUp { scancode: Some(scancode), .. } => { if let Some(drs_scan) = conv_scancode(scancode) { ctx.keyboard_context.set_key(drs_scan, false); } } _ => {} } } if state.shutdown { log::info!("Shutting down..."); break; } { let mutex = GAME_SUSPENDED.lock().unwrap(); if *mutex { std::thread::sleep(Duration::from_millis(10)); continue; } } game.update(ctx).unwrap(); if let Some(_) = &state.next_scene { game.scene = mem::take(&mut state.next_scene); game.scene.as_mut().unwrap().init(state, ctx).unwrap(); game.loops = 0; state.frame_time = 0.0; } imgui_sdl2.prepare_frame( imgui.io_mut(), self.refs.deref().borrow().canvas.window(), &self.event_pump.mouse_state(), ); game.draw(ctx).unwrap(); } } fn new_renderer(&self) -> GameResult> { #[cfg(feature = "render-opengl")] { let mut refs = self.refs.borrow_mut(); match refs.canvas.window().gl_create_context() { Ok(gl_ctx) => { refs.canvas.window().gl_make_current(&gl_ctx).map_err(|e| GameError::RenderError(e.to_string()))?; refs.gl_context = Some(gl_ctx); } Err(err) => { *self.opengl_available.borrow_mut() = false; log::error!("Failed to initialize OpenGL context, falling back to SDL2 renderer: {}", err); } } } #[cfg(feature = "render-opengl")] if *self.opengl_available.borrow() { let imgui = init_imgui()?; let refs = self.refs.clone(); let user_data = Rc::into_raw(refs) as *mut c_void; unsafe fn get_proc_address(user_data: &mut *mut c_void, name: &str) -> *const c_void { let refs = Rc::from_raw(*user_data as *mut RefCell); let result = { let refs = &mut *refs.as_ptr(); refs.video.gl_get_proc_address(name) as *const _ }; *user_data = Rc::into_raw(refs) as *mut c_void; result } unsafe fn swap_buffers(user_data: &mut *mut c_void) { let refs = Rc::from_raw(*user_data as *mut RefCell); { let refs = &mut *refs.as_ptr(); refs.canvas.window().gl_swap_window(); } *user_data = Rc::into_raw(refs) as *mut c_void; } let gl_context = GLContext { gles2_mode: false, get_proc_address, swap_buffers, user_data }; return Ok(Box::new(OpenGLRenderer::new(gl_context, UnsafeCell::new(imgui)))); } SDL2Renderer::new(self.refs.clone()) } } struct SDL2Renderer { refs: Rc>, imgui: Rc>, imgui_event: Rc>, #[allow(unused)] // the rendering pipeline uses pointers to SDL_Texture, and we manually manage the lifetimes imgui_font_tex: SDL2Texture, } impl SDL2Renderer { #[allow(clippy::new_ret_no_self)] pub fn new(refs: Rc>) -> GameResult> { let mut imgui = init_imgui()?; imgui.set_renderer_name("SDL2Renderer".to_owned()); let imgui_font_tex = { let refs = refs.clone(); let mut fonts = imgui.fonts(); let font_tex = fonts.build_rgba32_texture(); let mut texture = refs .borrow_mut() .texture_creator .create_texture_streaming(PixelFormatEnum::RGBA32, font_tex.width, font_tex.height) .map_err(|e| GameError::RenderError(e.to_string()))?; texture.set_blend_mode(sdl2::render::BlendMode::Blend); texture .with_lock(None, |buffer: &mut [u8], pitch: usize| { for y in 0..(font_tex.height as usize) { for x in 0..(font_tex.width as usize) { let offset = y * pitch + x * 4; let data_offset = (y * font_tex.width as usize + x) * 4; buffer[offset] = font_tex.data[data_offset]; buffer[offset + 1] = font_tex.data[data_offset + 1]; buffer[offset + 2] = font_tex.data[data_offset + 2]; buffer[offset + 3] = font_tex.data[data_offset + 3]; } } }) .map_err(|e| GameError::RenderError(e.to_string()))?; SDL2Texture { refs: refs.clone(), texture: Some(texture), width: font_tex.width as u16, height: font_tex.height as u16, commands: vec![], } }; imgui.fonts().tex_id = TextureId::new(imgui_font_tex.texture.as_ref().unwrap().raw() as usize); let imgui_sdl2 = unsafe { let refs = &mut *refs.as_ptr(); ImguiSdl2::new(&mut imgui, refs.canvas.window()) }; Ok(Box::new(SDL2Renderer { refs, imgui: Rc::new(RefCell::new(imgui)), imgui_event: Rc::new(RefCell::new(imgui_sdl2)), imgui_font_tex, })) } } fn to_sdl(color: Color) -> pixels::Color { let (r, g, b, a) = color.to_rgba(); pixels::Color::RGBA(r, g, b, a) } unsafe fn set_raw_target( renderer: *mut sdl2::sys::SDL_Renderer, raw_texture: *mut sdl2::sys::SDL_Texture, ) -> GameResult { if sdl2::sys::SDL_SetRenderTarget(renderer, raw_texture) == 0 { Ok(()) } else { Err(GameError::RenderError(sdl2::get_error())) } } fn min3(x: f32, y: f32, z: f32) -> f32 { if x < y && x < z { x } else if y < z { y } else { z } } fn max3(x: f32, y: f32, z: f32) -> f32 { if x > y && x > z { x } else if y > z { y } else { z } } impl BackendRenderer for SDL2Renderer { fn renderer_name(&self) -> String { "SDL2_Renderer (fallback)".to_owned() } fn clear(&mut self, color: Color) { let mut refs = self.refs.borrow_mut(); refs.canvas.set_draw_color(to_sdl(color)); refs.canvas.set_blend_mode(sdl2::render::BlendMode::Blend); refs.canvas.clear(); } fn present(&mut self) -> GameResult { let mut refs = self.refs.borrow_mut(); refs.canvas.present(); Ok(()) } fn prepare_draw(&mut self, width: f32, height: f32) -> GameResult { let mut refs = self.refs.borrow_mut(); refs.canvas.set_clip_rect(Some(sdl2::rect::Rect::new(0, 0, width as u32, height as u32))); Ok(()) } fn create_texture_mutable(&mut self, width: u16, height: u16) -> GameResult> { let refs = self.refs.borrow_mut(); let texture = refs .texture_creator .create_texture_target(PixelFormatEnum::RGBA32, width as u32, height as u32) .map_err(|e| GameError::RenderError(e.to_string()))?; Ok(Box::new(SDL2Texture { refs: self.refs.clone(), texture: Some(texture), width, height, commands: vec![] })) } fn create_texture(&mut self, width: u16, height: u16, data: &[u8]) -> GameResult> { let refs = self.refs.borrow_mut(); let mut texture = refs .texture_creator .create_texture_streaming(PixelFormatEnum::RGBA32, width as u32, height as u32) .map_err(|e| GameError::RenderError(e.to_string()))?; texture.set_blend_mode(sdl2::render::BlendMode::Blend); texture .with_lock(None, |buffer: &mut [u8], pitch: usize| { for y in 0..(height as usize) { for x in 0..(width as usize) { let offset = y * pitch + x * 4; let data_offset = (y * width as usize + x) * 4; buffer[offset] = data[data_offset]; buffer[offset + 1] = data[data_offset + 1]; buffer[offset + 2] = data[data_offset + 2]; buffer[offset + 3] = data[data_offset + 3]; } } }) .map_err(|e| GameError::RenderError(e.to_string()))?; Ok(Box::new(SDL2Texture { refs: self.refs.clone(), texture: Some(texture), width, height, commands: vec![] })) } fn set_blend_mode(&mut self, blend: BlendMode) -> GameResult { let mut refs = self.refs.borrow_mut(); refs.blend_mode = match blend { BlendMode::Add => sdl2::render::BlendMode::Add, BlendMode::Alpha => sdl2::render::BlendMode::Blend, BlendMode::Multiply => sdl2::render::BlendMode::Mod, }; Ok(()) } fn set_render_target(&mut self, texture: Option<&Box>) -> GameResult { let renderer = self.refs.deref().borrow().canvas.raw(); match texture { Some(texture) => { let sdl2_texture = texture .as_any() .downcast_ref::() .ok_or(RenderError("This texture was not created by OpenGL backend.".to_string()))?; unsafe { if let Some(target) = &sdl2_texture.texture { set_raw_target(renderer, target.raw())?; } else { set_raw_target(renderer, std::ptr::null_mut())?; } } } None => unsafe { set_raw_target(renderer, std::ptr::null_mut())?; }, } Ok(()) } fn draw_rect(&mut self, rect: Rect, color: Color) -> GameResult<()> { let mut refs = self.refs.borrow_mut(); let (r, g, b, a) = color.to_rgba(); let blend = refs.blend_mode; refs.canvas.set_draw_color(pixels::Color::RGBA(r, g, b, a)); refs.canvas.set_blend_mode(blend); refs.canvas .fill_rect(sdl2::rect::Rect::new( rect.left as i32, rect.top as i32, rect.width() as u32, rect.height() as u32, )) .map_err(|e| GameError::RenderError(e.to_string()))?; Ok(()) } fn draw_outline_rect(&mut self, rect: Rect, line_width: usize, color: Color) -> GameResult<()> { let mut refs = self.refs.borrow_mut(); let (r, g, b, a) = color.to_rgba(); let blend = refs.blend_mode; refs.canvas.set_draw_color(pixels::Color::RGBA(r, g, b, a)); refs.canvas.set_blend_mode(blend); match line_width { 0 => {} // no-op 1 => { refs.canvas .draw_rect(sdl2::rect::Rect::new( rect.left as i32, rect.top as i32, rect.width() as u32, rect.height() as u32, )) .map_err(|e| GameError::RenderError(e.to_string()))?; } _ => { let rects = [ sdl2::rect::Rect::new(rect.left as i32, rect.top as i32, rect.width() as u32, line_width as u32), sdl2::rect::Rect::new( rect.left as i32, rect.bottom as i32 - line_width as i32, rect.width() as u32, line_width as u32, ), sdl2::rect::Rect::new(rect.left as i32, rect.top as i32, line_width as u32, rect.height() as u32), sdl2::rect::Rect::new( rect.right as i32 - line_width as i32, rect.top as i32, line_width as u32, rect.height() as u32, ), ]; refs.canvas.fill_rects(&rects).map_err(|e| GameError::RenderError(e.to_string()))?; } } Ok(()) } fn set_clip_rect(&mut self, rect: Option) -> GameResult { let mut refs = self.refs.borrow_mut(); if let Some(rect) = &rect { refs.canvas.set_clip_rect(Some(sdl2::rect::Rect::new( rect.left as i32, rect.top as i32, rect.width() as u32, rect.height() as u32, ))); } else { refs.canvas.set_clip_rect(None); } Ok(()) } fn imgui(&self) -> GameResult<&mut imgui::Context> { unsafe { Ok(&mut *self.imgui.as_ptr()) } } fn imgui_texture_id(&self, texture: &Box) -> GameResult { let sdl_texture = texture .as_any() .downcast_ref::() .ok_or(GameError::RenderError("This texture was not created by SDL backend.".to_string()))?; Ok(TextureId::new(sdl_texture.texture.as_ref().map(|t| t.raw()).unwrap_or(null_mut()) as usize)) } fn prepare_imgui(&mut self, ui: &Ui) -> GameResult { let refs = self.refs.borrow_mut(); self.imgui_event.borrow_mut().prepare_render(ui, refs.canvas.window()); Ok(()) } fn render_imgui(&mut self, draw_data: &DrawData) -> GameResult { let mut refs = self.refs.borrow_mut(); for draw_list in draw_data.draw_lists() { for cmd in draw_list.commands() { match cmd { DrawCmd::Elements { count, cmd_params } => { refs.canvas.set_clip_rect(Some(sdl2::rect::Rect::new( cmd_params.clip_rect[0] as i32, cmd_params.clip_rect[1] as i32, (cmd_params.clip_rect[2] - cmd_params.clip_rect[0]) as u32, (cmd_params.clip_rect[3] - cmd_params.clip_rect[1]) as u32, ))); let idx_buffer = draw_list.idx_buffer(); let mut vert_x = [0i16; 6]; let mut vert_y = [0i16; 6]; let mut min = [0f32; 2]; let mut max = [0f32; 2]; let mut tex_pos = [0f32; 4]; let mut is_rect = false; for i in (0..count).step_by(3) { if is_rect { is_rect = false; continue; } let v1 = draw_list.vtx_buffer() [cmd_params.vtx_offset + idx_buffer[cmd_params.idx_offset + i] as usize]; let v2 = draw_list.vtx_buffer() [cmd_params.vtx_offset + idx_buffer[cmd_params.idx_offset + i + 1] as usize]; let v3 = draw_list.vtx_buffer() [cmd_params.vtx_offset + idx_buffer[cmd_params.idx_offset + i + 2] as usize]; vert_x[0] = (v1.pos[0] - 0.5) as i16; vert_y[0] = (v1.pos[1] - 0.5) as i16; vert_x[1] = (v2.pos[0] - 0.5) as i16; vert_y[1] = (v2.pos[1] - 0.5) as i16; vert_x[2] = (v3.pos[0] - 0.5) as i16; vert_y[2] = (v3.pos[1] - 0.5) as i16; #[allow(clippy::float_cmp)] if i < count - 3 { let v4 = draw_list.vtx_buffer() [cmd_params.vtx_offset + idx_buffer[cmd_params.idx_offset + i + 3] as usize]; let v5 = draw_list.vtx_buffer() [cmd_params.vtx_offset + idx_buffer[cmd_params.idx_offset + i + 4] as usize]; let v6 = draw_list.vtx_buffer() [cmd_params.vtx_offset + idx_buffer[cmd_params.idx_offset + i + 5] as usize]; min[0] = min3(v1.pos[0], v2.pos[0], v3.pos[0]); min[1] = min3(v1.pos[1], v2.pos[1], v3.pos[1]); max[0] = max3(v1.pos[0], v2.pos[0], v3.pos[0]); max[1] = max3(v1.pos[1], v2.pos[1], v3.pos[1]); is_rect = (v1.pos[0] == min[0] || v1.pos[0] == max[0]) && (v1.pos[1] == min[1] || v1.pos[1] == max[1]) && (v2.pos[0] == min[0] || v2.pos[0] == max[0]) && (v2.pos[1] == min[1] || v2.pos[1] == max[1]) && (v3.pos[0] == min[0] || v3.pos[0] == max[0]) && (v3.pos[1] == min[1] || v3.pos[1] == max[1]) && (v4.pos[0] == min[0] || v4.pos[0] == max[0]) && (v4.pos[1] == min[1] || v4.pos[1] == max[1]) && (v5.pos[0] == min[0] || v5.pos[0] == max[0]) && (v5.pos[1] == min[1] || v5.pos[1] == max[1]) && (v6.pos[0] == min[0] || v6.pos[0] == max[0]) && (v6.pos[1] == min[1] || v6.pos[1] == max[1]); if is_rect { tex_pos[0] = min3(v1.uv[0], v2.uv[0], v3.uv[0]); tex_pos[1] = min3(v1.uv[1], v2.uv[1], v3.uv[1]); tex_pos[2] = max3(v1.uv[0], v2.uv[0], v3.uv[0]); tex_pos[3] = max3(v1.uv[1], v2.uv[1], v3.uv[1]); } } let ptr = cmd_params.texture_id.id() as *mut sdl2::sys::SDL_Texture; if !ptr.is_null() { let mut surf = unsafe { refs.texture_creator.raw_create_texture(ptr) }; let TextureQuery { width, height, .. } = surf.query(); if is_rect { let src = sdl2::rect::Rect::new( (tex_pos[0] * width as f32) as i32, (tex_pos[1] * height as f32) as i32, ((tex_pos[2] - tex_pos[0]) * width as f32) as u32, ((tex_pos[3] - tex_pos[1]) * height as f32) as u32, ); let dest = sdl2::rect::Rect::new( min[0] as i32, min[1] as i32, (max[0] - min[0]) as u32, (max[1] - min[1]) as u32, ); surf.set_color_mod(v1.col[0], v1.col[1], v1.col[2]); surf.set_alpha_mod(v1.col[3]); refs.canvas .copy(&surf, src, dest) .map_err(|e| GameError::RenderError(e.to_string()))?; } else { /*sdl2::sys::gfx::primitives::filledPolygonRGBA( refs.canvas.raw(), vert_x.as_ptr(), vert_y.as_ptr(), 3, v1.col[0], v1.col[1], v1.col[2], v1.col[3], );*/ } } } refs.canvas.set_clip_rect(None); } DrawCmd::ResetRenderState => {} DrawCmd::RawCallback { callback, raw_cmd } => unsafe { callback(draw_list.raw(), raw_cmd) }, } } } Ok(()) } fn draw_triangle_list( &mut self, _vertices: Vec, _texture: Option<&Box>, _shader: BackendShader, ) -> GameResult<()> { Err(GameError::RenderError("Unsupported operation".to_string())) } } struct SDL2Texture { refs: Rc>, texture: Option, width: u16, height: u16, commands: Vec, } impl BackendTexture for SDL2Texture { fn dimensions(&self) -> (u16, u16) { (self.width, self.height) } fn add(&mut self, command: SpriteBatchCommand) { self.commands.push(command); } fn clear(&mut self) { self.commands.clear(); } fn draw(&mut self) -> GameResult { match &mut self.texture { None => Ok(()), Some(texture) => { let mut refs = self.refs.borrow_mut(); for command in &self.commands { match command { SpriteBatchCommand::DrawRect(src, dest) => { texture.set_color_mod(255, 255, 255); texture.set_alpha_mod(255); texture.set_blend_mode(refs.blend_mode); refs.canvas .copy( texture, Some(sdl2::rect::Rect::new( src.left.round() as i32, src.top.round() as i32, src.width().round() as u32, src.height().round() as u32, )), Some(sdl2::rect::Rect::new( dest.left.round() as i32, dest.top.round() as i32, dest.width().round() as u32, dest.height().round() as u32, )), ) .map_err(|e| GameError::RenderError(e.to_string()))?; } SpriteBatchCommand::DrawRectTinted(src, dest, color) => { let (r, g, b, a) = color.to_rgba(); texture.set_color_mod(r, g, b); texture.set_alpha_mod(a); texture.set_blend_mode(refs.blend_mode); refs.canvas .copy( texture, Some(sdl2::rect::Rect::new( src.left.round() as i32, src.top.round() as i32, src.width().round() as u32, src.height().round() as u32, )), Some(sdl2::rect::Rect::new( dest.left.round() as i32, dest.top.round() as i32, dest.width().round() as u32, dest.height().round() as u32, )), ) .map_err(|e| GameError::RenderError(e.to_string()))?; } SpriteBatchCommand::DrawRectFlip(src, dest, flip_x, flip_y) => { texture.set_color_mod(255, 255, 255); texture.set_alpha_mod(255); texture.set_blend_mode(refs.blend_mode); refs.canvas .copy_ex( texture, Some(sdl2::rect::Rect::new( src.left.round() as i32, src.top.round() as i32, src.width().round() as u32, src.height().round() as u32, )), Some(sdl2::rect::Rect::new( dest.left.round() as i32, dest.top.round() as i32, dest.width().round() as u32, dest.height().round() as u32, )), 0.0, None, *flip_x, *flip_y, ) .map_err(|e| GameError::RenderError(e.to_string()))?; } } } Ok(()) } } } fn as_any(&self) -> &dyn Any { self } } impl Drop for SDL2Texture { fn drop(&mut self) { let mut texture_opt = None; mem::swap(&mut self.texture, &mut texture_opt); if let Some(texture) = texture_opt { unsafe { texture.destroy(); } } } } fn conv_scancode(code: keyboard::Scancode) -> Option { match code { Scancode::A => Some(ScanCode::A), Scancode::B => Some(ScanCode::B), Scancode::C => Some(ScanCode::C), Scancode::D => Some(ScanCode::D), Scancode::E => Some(ScanCode::E), Scancode::F => Some(ScanCode::F), Scancode::G => Some(ScanCode::G), Scancode::H => Some(ScanCode::H), Scancode::I => Some(ScanCode::I), Scancode::J => Some(ScanCode::J), Scancode::K => Some(ScanCode::K), Scancode::L => Some(ScanCode::L), Scancode::M => Some(ScanCode::M), Scancode::N => Some(ScanCode::N), Scancode::O => Some(ScanCode::O), Scancode::P => Some(ScanCode::P), Scancode::Q => Some(ScanCode::Q), Scancode::R => Some(ScanCode::R), Scancode::S => Some(ScanCode::S), Scancode::T => Some(ScanCode::T), Scancode::U => Some(ScanCode::U), Scancode::V => Some(ScanCode::V), Scancode::W => Some(ScanCode::W), Scancode::X => Some(ScanCode::X), Scancode::Y => Some(ScanCode::Y), Scancode::Z => Some(ScanCode::Z), Scancode::Num1 => Some(ScanCode::Key1), Scancode::Num2 => Some(ScanCode::Key2), Scancode::Num3 => Some(ScanCode::Key3), Scancode::Num4 => Some(ScanCode::Key4), Scancode::Num5 => Some(ScanCode::Key5), Scancode::Num6 => Some(ScanCode::Key6), Scancode::Num7 => Some(ScanCode::Key7), Scancode::Num8 => Some(ScanCode::Key8), Scancode::Num9 => Some(ScanCode::Key9), Scancode::Num0 => Some(ScanCode::Key0), Scancode::Return => Some(ScanCode::Return), Scancode::Escape => Some(ScanCode::Escape), Scancode::Backspace => Some(ScanCode::Backspace), Scancode::Tab => Some(ScanCode::Tab), Scancode::Space => Some(ScanCode::Space), Scancode::Minus => Some(ScanCode::Minus), Scancode::Equals => Some(ScanCode::Equals), Scancode::LeftBracket => Some(ScanCode::LBracket), Scancode::RightBracket => Some(ScanCode::RBracket), Scancode::Backslash => Some(ScanCode::Backslash), Scancode::NonUsHash => Some(ScanCode::NonUsHash), Scancode::Semicolon => Some(ScanCode::Semicolon), Scancode::Apostrophe => Some(ScanCode::Apostrophe), Scancode::Grave => Some(ScanCode::Grave), Scancode::Comma => Some(ScanCode::Comma), Scancode::Period => Some(ScanCode::Period), Scancode::Slash => Some(ScanCode::Slash), Scancode::CapsLock => Some(ScanCode::Capslock), Scancode::F1 => Some(ScanCode::F1), Scancode::F2 => Some(ScanCode::F2), Scancode::F3 => Some(ScanCode::F3), Scancode::F4 => Some(ScanCode::F4), Scancode::F5 => Some(ScanCode::F5), Scancode::F6 => Some(ScanCode::F6), Scancode::F7 => Some(ScanCode::F7), Scancode::F8 => Some(ScanCode::F8), Scancode::F9 => Some(ScanCode::F9), Scancode::F10 => Some(ScanCode::F10), Scancode::F11 => Some(ScanCode::F11), Scancode::F12 => Some(ScanCode::F12), Scancode::PrintScreen => Some(ScanCode::Sysrq), Scancode::ScrollLock => Some(ScanCode::Scrolllock), Scancode::Pause => Some(ScanCode::Pause), Scancode::Insert => Some(ScanCode::Insert), Scancode::Home => Some(ScanCode::Home), Scancode::PageUp => Some(ScanCode::PageUp), Scancode::Delete => Some(ScanCode::Delete), Scancode::End => Some(ScanCode::End), Scancode::PageDown => Some(ScanCode::PageDown), Scancode::Right => Some(ScanCode::Right), Scancode::Left => Some(ScanCode::Left), Scancode::Down => Some(ScanCode::Down), Scancode::Up => Some(ScanCode::Up), Scancode::NumLockClear => Some(ScanCode::Numlock), Scancode::KpDivide => Some(ScanCode::NumpadDivide), Scancode::KpMultiply => Some(ScanCode::NumpadMultiply), Scancode::KpMinus => Some(ScanCode::NumpadSubtract), Scancode::KpPlus => Some(ScanCode::NumpadAdd), Scancode::KpEnter => Some(ScanCode::NumpadEnter), Scancode::Kp1 => Some(ScanCode::Numpad1), Scancode::Kp2 => Some(ScanCode::Numpad2), Scancode::Kp3 => Some(ScanCode::Numpad3), Scancode::Kp4 => Some(ScanCode::Numpad4), Scancode::Kp5 => Some(ScanCode::Numpad5), Scancode::Kp6 => Some(ScanCode::Numpad6), Scancode::Kp7 => Some(ScanCode::Numpad7), Scancode::Kp8 => Some(ScanCode::Numpad8), Scancode::Kp9 => Some(ScanCode::Numpad9), Scancode::Kp0 => Some(ScanCode::Numpad0), Scancode::NonUsBackslash => Some(ScanCode::NonUsBackslash), Scancode::Application => Some(ScanCode::Apps), Scancode::Power => Some(ScanCode::Power), Scancode::KpEquals => Some(ScanCode::NumpadEquals), Scancode::F13 => Some(ScanCode::F13), Scancode::F14 => Some(ScanCode::F14), Scancode::F15 => Some(ScanCode::F15), Scancode::F16 => Some(ScanCode::F16), Scancode::F17 => Some(ScanCode::F17), Scancode::F18 => Some(ScanCode::F18), Scancode::F19 => Some(ScanCode::F19), Scancode::F20 => Some(ScanCode::F20), Scancode::F21 => Some(ScanCode::F21), Scancode::F22 => Some(ScanCode::F22), Scancode::F23 => Some(ScanCode::F23), Scancode::F24 => Some(ScanCode::F24), Scancode::Stop => Some(ScanCode::Stop), Scancode::Cut => Some(ScanCode::Cut), Scancode::Copy => Some(ScanCode::Copy), Scancode::Paste => Some(ScanCode::Paste), Scancode::Mute => Some(ScanCode::Mute), Scancode::VolumeUp => Some(ScanCode::VolumeUp), Scancode::VolumeDown => Some(ScanCode::VolumeDown), Scancode::KpComma => Some(ScanCode::NumpadComma), Scancode::SysReq => Some(ScanCode::Sysrq), Scancode::Return2 => Some(ScanCode::NumpadEnter), Scancode::LCtrl => Some(ScanCode::LControl), Scancode::LShift => Some(ScanCode::LShift), Scancode::LAlt => Some(ScanCode::LAlt), Scancode::LGui => Some(ScanCode::LWin), Scancode::RCtrl => Some(ScanCode::RControl), Scancode::RShift => Some(ScanCode::RShift), Scancode::RAlt => Some(ScanCode::RAlt), Scancode::RGui => Some(ScanCode::RWin), Scancode::AudioNext => Some(ScanCode::NextTrack), Scancode::AudioPrev => Some(ScanCode::PrevTrack), Scancode::AudioStop => Some(ScanCode::MediaStop), Scancode::AudioPlay => Some(ScanCode::PlayPause), Scancode::AudioMute => Some(ScanCode::Mute), Scancode::MediaSelect => Some(ScanCode::MediaSelect), Scancode::Mail => Some(ScanCode::Mail), Scancode::Calculator => Some(ScanCode::Calculator), Scancode::Sleep => Some(ScanCode::Sleep), _ => None, } } // based on imgui-sdl2 crate pub struct ImguiSdl2 { mouse_press: [bool; 5], ignore_mouse: bool, ignore_keyboard: bool, cursor: Option, sdl_cursor: Option, } struct Sdl2ClipboardBackend(sdl2::clipboard::ClipboardUtil); impl imgui::ClipboardBackend for Sdl2ClipboardBackend { fn get(&mut self) -> Option { if !self.0.has_clipboard_text() { return None; } self.0.clipboard_text().ok() } fn set(&mut self, value: &str) { let _ = self.0.set_clipboard_text(value); } } impl ImguiSdl2 { pub fn new(imgui: &mut imgui::Context, window: &sdl2::video::Window) -> Self { let clipboard_util = window.subsystem().clipboard(); imgui.set_clipboard_backend(Sdl2ClipboardBackend(clipboard_util)); imgui.io_mut().key_map[Key::Tab as usize] = Scancode::Tab as u32; imgui.io_mut().key_map[Key::LeftArrow as usize] = Scancode::Left as u32; imgui.io_mut().key_map[Key::RightArrow as usize] = Scancode::Right as u32; imgui.io_mut().key_map[Key::UpArrow as usize] = Scancode::Up as u32; imgui.io_mut().key_map[Key::DownArrow as usize] = Scancode::Down as u32; imgui.io_mut().key_map[Key::PageUp as usize] = Scancode::PageUp as u32; imgui.io_mut().key_map[Key::PageDown as usize] = Scancode::PageDown as u32; imgui.io_mut().key_map[Key::Home as usize] = Scancode::Home as u32; imgui.io_mut().key_map[Key::End as usize] = Scancode::End as u32; imgui.io_mut().key_map[Key::Delete as usize] = Scancode::Delete as u32; imgui.io_mut().key_map[Key::Backspace as usize] = Scancode::Backspace as u32; imgui.io_mut().key_map[Key::Enter as usize] = Scancode::Return as u32; imgui.io_mut().key_map[Key::Escape as usize] = Scancode::Escape as u32; imgui.io_mut().key_map[Key::Space as usize] = Scancode::Space as u32; imgui.io_mut().key_map[Key::A as usize] = Scancode::A as u32; imgui.io_mut().key_map[Key::C as usize] = Scancode::C as u32; imgui.io_mut().key_map[Key::V as usize] = Scancode::V as u32; imgui.io_mut().key_map[Key::X as usize] = Scancode::X as u32; imgui.io_mut().key_map[Key::Y as usize] = Scancode::Y as u32; imgui.io_mut().key_map[Key::Z as usize] = Scancode::Z as u32; Self { mouse_press: [false; 5], ignore_keyboard: false, ignore_mouse: false, cursor: None, sdl_cursor: None } } pub fn handle_event(&mut self, imgui: &mut imgui::Context, event: &Event) { use sdl2::mouse::MouseButton; fn set_mod(imgui: &mut imgui::Context, keymod: keyboard::Mod) { let ctrl = keymod.intersects(keyboard::Mod::RCTRLMOD | keyboard::Mod::LCTRLMOD); let alt = keymod.intersects(keyboard::Mod::RALTMOD | keyboard::Mod::LALTMOD); let shift = keymod.intersects(keyboard::Mod::RSHIFTMOD | keyboard::Mod::LSHIFTMOD); let super_ = keymod.intersects(keyboard::Mod::RGUIMOD | keyboard::Mod::LGUIMOD); imgui.io_mut().key_ctrl = ctrl; imgui.io_mut().key_alt = alt; imgui.io_mut().key_shift = shift; imgui.io_mut().key_super = super_; } match *event { Event::MouseWheel { y, .. } => { imgui.io_mut().mouse_wheel = y as f32; } Event::MouseButtonDown { mouse_btn, .. } => { if mouse_btn != MouseButton::Unknown { let index = match mouse_btn { MouseButton::Left => 0, MouseButton::Right => 1, MouseButton::Middle => 2, MouseButton::X1 => 3, MouseButton::X2 => 4, MouseButton::Unknown => unreachable!(), }; self.mouse_press[index] = true; } } Event::TextInput { ref text, .. } => { for chr in text.chars() { imgui.io_mut().add_input_character(chr); } } Event::KeyDown { scancode, keymod, .. } => { set_mod(imgui, keymod); if let Some(scancode) = scancode { imgui.io_mut().keys_down[scancode as usize] = true; } } Event::KeyUp { scancode, keymod, .. } => { set_mod(imgui, keymod); if let Some(scancode) = scancode { imgui.io_mut().keys_down[scancode as usize] = false; } } _ => {} } } pub fn prepare_frame( &mut self, io: &mut imgui::Io, window: &sdl2::video::Window, mouse_state: &sdl2::mouse::MouseState, ) { let mouse_util = window.subsystem().sdl().mouse(); let (win_w, win_h) = window.size(); let (draw_w, draw_h) = window.drawable_size(); io.display_size = [win_w as f32, win_h as f32]; io.display_framebuffer_scale = [(draw_w as f32) / (win_w as f32), (draw_h as f32) / (win_h as f32)]; // Merging the mousedown events we received into the current state prevents us from missing // clicks that happen faster than a frame io.mouse_down = [ self.mouse_press[0] || mouse_state.left(), self.mouse_press[1] || mouse_state.right(), self.mouse_press[2] || mouse_state.middle(), self.mouse_press[3] || mouse_state.x1(), self.mouse_press[4] || mouse_state.x2(), ]; self.mouse_press = [false; 5]; let any_mouse_down = io.mouse_down.iter().any(|&b| b); mouse_util.capture(any_mouse_down); io.mouse_pos = [mouse_state.x() as f32, mouse_state.y() as f32]; self.ignore_keyboard = io.want_capture_keyboard; self.ignore_mouse = io.want_capture_mouse; } pub fn prepare_render(&mut self, ui: &imgui::Ui, window: &sdl2::video::Window) { let io = ui.io(); if !io.config_flags.contains(ConfigFlags::NO_MOUSE_CURSOR_CHANGE) { let mouse_util = window.subsystem().sdl().mouse(); match ui.mouse_cursor() { Some(mouse_cursor) if !io.mouse_draw_cursor => { mouse_util.show_cursor(true); let sdl_cursor = match mouse_cursor { MouseCursor::Arrow => SystemCursor::Arrow, MouseCursor::TextInput => SystemCursor::IBeam, MouseCursor::ResizeAll => SystemCursor::SizeAll, MouseCursor::ResizeNS => SystemCursor::SizeNS, MouseCursor::ResizeEW => SystemCursor::SizeWE, MouseCursor::ResizeNESW => SystemCursor::SizeNESW, MouseCursor::ResizeNWSE => SystemCursor::SizeNWSE, MouseCursor::Hand => SystemCursor::Hand, MouseCursor::NotAllowed => SystemCursor::No, }; if self.cursor != Some(mouse_cursor) { let sdl_cursor = Cursor::from_system(sdl_cursor).unwrap(); sdl_cursor.set(); self.cursor = Some(mouse_cursor); self.sdl_cursor = Some(sdl_cursor); } } _ => { self.cursor = None; self.sdl_cursor = None; mouse_util.show_cursor(false); } } } } }