// file: crates/engines/engine-v1-sdl/src/runtime.rs // version: 9 /// Minimal SDL3 runtime used by Desktop and Android POC runners. pub struct SdlRuntime { title: std::string::String, width: u32, height: u32, frame_duration: std::time::Duration, } impl SdlRuntime { /// Creates a minimal SDL3 runtime configuration. #[must_use] pub fn new(title: impl std::convert::Into, width: u32, height: u32, frame_duration: std::time::Duration) -> Self { return Self { title: title.into(), width, height, frame_duration }; } /// Runs a game until the user closes the window or presses Escape. pub fn run(&self, game: &mut G) -> std::result::Result<(), std::string::String> where G: engine_v1_common::EngineGame, { let sdl = match sdl3::init() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; let video = match sdl.video() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; let window = match video.window(&self.title, self.width, self.height).position_centered().resizable().build() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; let mut canvas = window.into_canvas(); let mut events = match sdl.event_pump() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; let mut runner = engine_v1_common::FixedStepRunner::new(self.frame_duration); let mut gesture_start: Option = None; let mut pointer = engine_v1_common::PointerState::inactive(); tracing::info!(title = %self.title, width = self.width, height = self.height, "SDL3 runtime started"); 'running: loop { let mut input = engine_v1_common::InputState::none().with_pointer(pointer); for event in events.poll_iter() { match event { sdl3::event::Event::Quit { .. } => break 'running, sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Escape), .. } => break 'running, sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Left), repeat: false, .. } => { input = input.with_action(engine_v1_common::GameAction::Left, true); }, sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Right), repeat: false, .. } => { input = input.with_action(engine_v1_common::GameAction::Right, true); }, sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Up), repeat: false, .. } => { input = input.with_action(engine_v1_common::GameAction::Up, true); }, sdl3::event::Event::KeyDown { keycode: Some(sdl3::keyboard::Keycode::Down), repeat: false, .. } => { input = input.with_action(engine_v1_common::GameAction::Down, true); }, sdl3::event::Event::MouseButtonDown { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => { pointer = normalize_mouse_pointer(&canvas, true, x, y); gesture_start = Some(pointer); input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); }, sdl3::event::Event::MouseButtonUp { mouse_btn: sdl3::mouse::MouseButton::Left, x, y, .. } => { let released = normalize_mouse_pointer(&canvas, false, x, y); input = apply_swipe_direction(input, gesture_start, released); gesture_start = None; pointer = released; input = input.with_pointer(pointer); }, sdl3::event::Event::MouseMotion { x, y, .. } if pointer.active() => { pointer = normalize_mouse_pointer(&canvas, true, x, y); input = input.with_pointer(pointer); }, sdl3::event::Event::FingerDown { x, y, .. } => { pointer = engine_v1_common::PointerState::normalized(true, x, y); gesture_start = Some(pointer); input = input.with_pointer(pointer).with_action(engine_v1_common::GameAction::Primary, true); }, sdl3::event::Event::FingerMotion { x, y, .. } => { pointer = engine_v1_common::PointerState::normalized(true, x, y); input = input.with_pointer(pointer); }, sdl3::event::Event::FingerUp { x, y, .. } => { let released = engine_v1_common::PointerState::normalized(false, x, y); input = apply_swipe_direction(input, gesture_start, released); gesture_start = None; pointer = released; input = input.with_pointer(pointer); }, sdl3::event::Event::FingerCanceled { x, y, .. } => { gesture_start = None; pointer = engine_v1_common::PointerState::normalized(false, x, y); input = input.with_pointer(pointer); }, _ => {}, } } runner.tick(game, input); match render_scene(&mut canvas, game.scene()) { std::result::Result::Ok(()) => {}, std::result::Result::Err(error) => return std::result::Result::Err(error), } std::thread::sleep(self.frame_duration); } tracing::info!(frames = runner.next_frame_index(), "SDL3 runtime stopped"); return std::result::Result::Ok(()); } } fn normalize_mouse_pointer(canvas: &sdl3::render::WindowCanvas, active: bool, x: f32, y: f32) -> engine_v1_common::PointerState { let (width, height) = canvas.window().size(); if width == 0 || height == 0 { return engine_v1_common::PointerState::normalized(active, 0.0, 0.0); } return engine_v1_common::PointerState::normalized(active, x / width as f32, y / height as f32); } fn render_scene(canvas: &mut sdl3::render::WindowCanvas, scene: engine_v1_common::EngineScene) -> std::result::Result<(), std::string::String> { let background = scene.background(); canvas.set_draw_color(sdl3::pixels::Color::RGBA(background.red(), background.green(), background.blue(), background.alpha())); canvas.clear(); let (width, height) = match canvas.output_size() { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), }; for rectangle in scene.rectangles() { let rectangle = match rectangle { Some(value) => *value, None => continue, }; let color = rectangle.color(); let rect = rectangle.rect(); canvas.set_draw_color(sdl3::pixels::Color::RGBA(color.red(), color.green(), color.blue(), color.alpha())); let physical = sdl3::render::FRect::new(rect.x() * width as f32, rect.y() * height as f32, rect.width() * width as f32, rect.height() * height as f32); match canvas.fill_rect(physical) { std::result::Result::Ok(()) => {}, std::result::Result::Err(error) => return std::result::Result::Err(error.to_string()), } } canvas.present(); return std::result::Result::Ok(()); } const SWIPE_DIRECTION_THRESHOLD: f32 = 0.04; fn apply_swipe_direction( mut input: engine_v1_common::InputState, start: Option, end: engine_v1_common::PointerState, ) -> engine_v1_common::InputState { let start = match start { Some(value) => value, None => return input, }; let horizontal = end.x() - start.x(); let vertical = end.y() - start.y(); let dominant = horizontal.abs().max(vertical.abs()); if dominant < SWIPE_DIRECTION_THRESHOLD { return input; } if horizontal.abs() >= vertical.abs() { if horizontal < 0.0 { input = input.with_action(engine_v1_common::GameAction::Left, true); } else { input = input.with_action(engine_v1_common::GameAction::Right, true); } } else if vertical < 0.0 { input = input.with_action(engine_v1_common::GameAction::Up, true); } else { input = input.with_action(engine_v1_common::GameAction::Down, true); } return input; } #[cfg(test)] #[path = "../unit_tests/swipe.rs"] mod swipe_tests;