feat(core): unified input state (keyboard/mouse/scroll)
Add InputState (DRAFT Etapes 15, sous-volt 15.B): pressed/held/released semantics via HashSet rotation, mouse position/delta/scroll accumulators. Wire it into App (pub field), forward winit WindowEvents in the event handler, and begin_frame/end_frame around AppHandler::update. Handleable key/mouse/scroll logic extracted into private helpers so all five unit tests avoid constructing winit KeyEvent (private fields).
This commit is contained in:
+13
-1
@@ -23,7 +23,7 @@
|
||||
//! once right after GPU initialization so users can register shaders/meshes/materials/entities.
|
||||
|
||||
use crate::AppHandler;
|
||||
use crate::core::{Context, Renderer};
|
||||
use crate::core::{Context, InputState, Renderer};
|
||||
use crate::scene::Scene;
|
||||
use crate::utils::WsgError;
|
||||
use crate::utils::conf::{APP_DEFAULT_HEIGHT, APP_DEFAULT_TITLE, APP_DEFAULT_WIDTH};
|
||||
@@ -46,6 +46,10 @@ use winit::window::{Window, WindowAttributes};
|
||||
pub struct App {
|
||||
/// Resource depot and entity graph — users register Meshes/Materials here during `AppHandler::setup`.
|
||||
pub scene: Scene,
|
||||
/// Unified input state (keyboard/mouse/scroll, DRAFT Étape 15). Fed by the winit window events
|
||||
/// and rotated each frame by `begin_frame`/`end_frame` around `AppHandler::update`. Read it in
|
||||
/// `update` via `app.input` (e.g. `app.input.key_held(KeyCode::KeyW)`).
|
||||
pub input: InputState,
|
||||
/// Window title, read by the runner when the window is created in `resumed`.
|
||||
pub(crate) title: String,
|
||||
/// Window width, read by the runner when the window is created in `resumed`.
|
||||
@@ -203,6 +207,7 @@ impl AppBuilder {
|
||||
let event_loop = EventLoop::new().map_err(|_| WsgError::WindowSystem)?;
|
||||
Ok(App {
|
||||
scene: Scene::new(),
|
||||
input: InputState::new(),
|
||||
title: self.title,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
@@ -265,6 +270,7 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
||||
|
||||
let mut app = App {
|
||||
scene,
|
||||
input: InputState::new(),
|
||||
title: self.title.clone(),
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
@@ -284,7 +290,11 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
||||
let Some(app) = self.app.as_mut() else {
|
||||
return;
|
||||
};
|
||||
// Étape 15 (input) : débute la frame d'input (rotation pressed/released + reset deltas),
|
||||
// exécute la logique utilisateur, puis clôt (nettoie les états transitoires).
|
||||
app.input.begin_frame();
|
||||
self.handler.update(app);
|
||||
app.input.end_frame();
|
||||
app.window().request_redraw();
|
||||
}
|
||||
|
||||
@@ -299,6 +309,8 @@ impl<H: AppHandler> ApplicationHandler for AppRunner<H> {
|
||||
let Some(app) = self.app.as_mut() else {
|
||||
return;
|
||||
};
|
||||
// Étape 15 (input) : alimente l'état unifié depuis les événements winit (clavier/souris/molette).
|
||||
app.input.handle_window_event(&event);
|
||||
match event {
|
||||
WindowEvent::Resized(size) => {
|
||||
// Garde (D3) : minimiser la fenêtre envoie Resized(0x0) ; ne jamais reconfigurer à 0.
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
//! # Input Module — Unified Input State (Étape 15, ROADMAP 2.3)
|
||||
//!
|
||||
//! State d'entrée **unifié** (clavier / souris / molette) à la sémantique cross-frame
|
||||
//! **pressed / held / released**, alimenté par les événements **winit** (`WindowEvent`), côté **CPU
|
||||
//! (Rust)** — WGSL (langage de shader, GPU) n'a pas d'I/O. Ce module est incarné dans `App::input`
|
||||
//! et piloté par la boucle : `begin_frame()` avant `AppHandler::update`, `end_frame()` après.
|
||||
//!
|
||||
//! ## Conventions
|
||||
//! - **Clavier** : identifié par `KeyCode` (physique, indépendant de la disposition AZERTY/QWERTY :
|
||||
//! la touche Z sur AZERTY est `KeyCode::KeyW`). `pressed`/`released` valent une seule frame,
|
||||
//! `held` reste vrai tant que la touche est enfoncée.
|
||||
//! - **Souris** : position absolue (pixels), delta par frame (dérivé des `CursorMoved`, donc des
|
||||
//! déplacements relatifs valables pour une caméra orbitale en glisser), boutons
|
||||
//! `pressed`/`held`/`released`, molette (`scroll`), `y > 0` = molette vers le haut.
|
||||
//! - **Gamepad** : réservé pour une future v1 minimale (DRAFT D7) ; l'API est prête à accueillir
|
||||
//! un `GamepadState` sans casser l'existant (l'exemple final n'a besoin que du clavier + souris).
|
||||
//!
|
||||
//! ## Exemples de requête (dans `AppHandler::update`)
|
||||
//! ```
|
||||
//! # use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
//! # fn demo(input: &wsg_lib::core::input::InputState) {
|
||||
//! if input.key_held(KeyCode::KeyW) { /* avancer */ }
|
||||
//! if input.key_pressed(KeyCode::Space) { /* sauter */ }
|
||||
//! let (dx, dy) = input.mouse_delta();
|
||||
//! if input.mouse_button_held(winit::event::MouseButton::Left) { /* orbiter */ }
|
||||
//! let (_, zoom) = input.scroll_delta();
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use std::collections::HashSet;
|
||||
use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
|
||||
use winit::keyboard::{KeyCode, PhysicalKey};
|
||||
|
||||
/// État de saisie unifié, agrégat des groupes clavier, souris et molette. Il est **remis à jour à
|
||||
/// chaque frame** par `App` via `begin_frame`/`end_frame`, et lu par l'utilisateur dans
|
||||
/// `AppHandler::update` via `app.input`.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct InputState {
|
||||
// ---- Clavier ----
|
||||
/// Touches physiquement enfoncées au moment présent (persiste entre les frames).
|
||||
held: HashSet<KeyCode>,
|
||||
/// Touches enfoncées pendant la frame courante (valides une seule frame).
|
||||
pressed: HashSet<KeyCode>,
|
||||
/// Touches relâchées pendant la frame courante (valides une seule frame).
|
||||
released: HashSet<KeyCode>,
|
||||
/// Accumulateur de `pressed` entre deux `begin_frame` (consommé à la rotation).
|
||||
frame_pressed: HashSet<KeyCode>,
|
||||
/// Accumulateur de `released` entre deux `begin_frame`.
|
||||
frame_released: HashSet<KeyCode>,
|
||||
|
||||
// ---- Souris ----
|
||||
/// Position absolue du curseur en pixels (dernière reçue).
|
||||
mouse_position: (f32, f32),
|
||||
/// Position absolue précédente, pour dériver le delta de `CursorMoved`.
|
||||
last_mouse_position: Option<(f32, f32)>,
|
||||
/// Déplacement relatif cumulé pendant la frame courante.
|
||||
mouse_delta: (f32, f32),
|
||||
/// Boutons enfoncés au moment présent.
|
||||
held_buttons: HashSet<MouseButton>,
|
||||
/// Boutons pressés pendant la frame courante.
|
||||
pressed_buttons: HashSet<MouseButton>,
|
||||
/// Boutons relâchés pendant la frame courante.
|
||||
released_buttons: HashSet<MouseButton>,
|
||||
/// Accumulateurs des boutons entre deux `begin_frame`.
|
||||
frame_pressed_buttons: HashSet<MouseButton>,
|
||||
frame_released_buttons: HashSet<MouseButton>,
|
||||
|
||||
// ---- Molette ----
|
||||
/// Défilement cumulé pendant la frame courante (x, y).
|
||||
scroll: (f32, f32),
|
||||
// ---- Gamepad (réservé) ----
|
||||
// (DRAFT D7 : v1 minimale optionnelle, reportée — l'API s'étendra sans rupture.)
|
||||
}
|
||||
|
||||
impl InputState {
|
||||
/// Crée un `InputState` vierge (toutes états vides). Équivalent à `Default`.
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Consomme un événement de fenêtre winit et met à jour l'état interne (accumulateurs). Les
|
||||
/// événements non pertinents sont ignorés. La rotation vers les ensembles requêtables
|
||||
/// (`pressed`/`released`) se fait au prochain `begin_frame`.
|
||||
pub fn handle_window_event(&mut self, event: &WindowEvent) {
|
||||
match event {
|
||||
WindowEvent::KeyboardInput { event: ke, .. } => {
|
||||
let PhysicalKey::Code(code) = ke.physical_key else {
|
||||
return; // touches non-figurées (ex. clavier système) ignorées
|
||||
};
|
||||
self.key_input(code, ke.state);
|
||||
}
|
||||
WindowEvent::MouseInput { state, button, .. } => self.mouse_button(*button, *state),
|
||||
WindowEvent::CursorMoved { position, .. } => {
|
||||
self.cursor_move(position.x as f32, position.y as f32);
|
||||
}
|
||||
WindowEvent::MouseWheel { delta, .. } => match delta {
|
||||
MouseScrollDelta::LineDelta(x, y) => self.wheel(*x, *y),
|
||||
MouseScrollDelta::PixelDelta(p) => self.wheel(p.x as f32, p.y as f32),
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enregistre un événement clavier brut (touche physique + état), appelé par
|
||||
/// [`InputState::handle_window_event`]. Séparé pour être testable sans construire un `KeyEvent`.
|
||||
fn key_input(&mut self, code: KeyCode, state: ElementState) {
|
||||
match state {
|
||||
ElementState::Pressed => {
|
||||
self.held.insert(code);
|
||||
self.frame_pressed.insert(code);
|
||||
}
|
||||
ElementState::Released => {
|
||||
self.held.remove(&code);
|
||||
self.frame_released.insert(code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Enregistre un événement bouton de souris brut, appelé par [`InputState::handle_window_event`].
|
||||
fn mouse_button(&mut self, button: MouseButton, state: ElementState) {
|
||||
match state {
|
||||
ElementState::Pressed => {
|
||||
self.held_buttons.insert(button);
|
||||
self.frame_pressed_buttons.insert(button);
|
||||
}
|
||||
ElementState::Released => {
|
||||
self.held_buttons.remove(&button);
|
||||
self.frame_released_buttons.insert(button);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Met à jour la position du curseur et cumule le déplacement relatif. Appelé par
|
||||
/// [`InputState::handle_window_event`].
|
||||
fn cursor_move(&mut self, x: f32, y: f32) {
|
||||
if let Some((px, py)) = self.last_mouse_position {
|
||||
self.mouse_delta.0 += x - px;
|
||||
self.mouse_delta.1 += y - py;
|
||||
}
|
||||
self.last_mouse_position = Some((x, y));
|
||||
self.mouse_position = (x, y);
|
||||
}
|
||||
|
||||
/// Cumule le défilement de la molette. Appelé par [`InputState::handle_window_event`].
|
||||
fn wheel(&mut self, dx: f32, dy: f32) {
|
||||
self.scroll.0 += dx;
|
||||
self.scroll.1 += dy;
|
||||
}
|
||||
|
||||
/// Démarre une nouvelle frame pour l'input : **fait tourner** les accumulateurs d'événements
|
||||
/// (accumulés entre deux `begin_frame`) vers les ensembles requêtables `pressed`/`released`, et
|
||||
/// remet à zéro le delta de souris et la molette. À appeler **avant** `AppHandler::update`.
|
||||
pub fn begin_frame(&mut self) {
|
||||
self.pressed = std::mem::take(&mut self.frame_pressed);
|
||||
self.released = std::mem::take(&mut self.frame_released);
|
||||
self.pressed_buttons = std::mem::take(&mut self.frame_pressed_buttons);
|
||||
self.released_buttons = std::mem::take(&mut self.frame_released_buttons);
|
||||
self.mouse_delta = (0.0, 0.0);
|
||||
self.scroll = (0.0, 0.0);
|
||||
}
|
||||
|
||||
/// Clôt une frame : vide les ensembles transitoires `pressed`/`released` (déjà consommés par
|
||||
/// `update`). Les états `held` et la position sont conservés. À appeler **après**
|
||||
/// `AppHandler::update` (ou `render`).
|
||||
pub fn end_frame(&mut self) {
|
||||
self.pressed.clear();
|
||||
self.released.clear();
|
||||
self.pressed_buttons.clear();
|
||||
self.released_buttons.clear();
|
||||
}
|
||||
|
||||
// ---- Requêtes clavier ----
|
||||
|
||||
/// Vrai si `code` a été **enfoncée** pendant la frame courante (une seule frame).
|
||||
pub fn key_pressed(&self, code: KeyCode) -> bool {
|
||||
self.pressed.contains(&code)
|
||||
}
|
||||
/// Vrai si `code` est **maintenue** enfoncée (persiste entre les frames).
|
||||
pub fn key_held(&self, code: KeyCode) -> bool {
|
||||
self.held.contains(&code)
|
||||
}
|
||||
/// Vrai si `code` a été **relâchée** pendant la frame courante (une seule frame).
|
||||
pub fn key_released(&self, code: KeyCode) -> bool {
|
||||
self.released.contains(&code)
|
||||
}
|
||||
|
||||
// ---- Requêtes souris ----
|
||||
|
||||
/// Position absolue du curseur en pixels (dernière position reçue).
|
||||
pub fn mouse_position(&self) -> (f32, f32) {
|
||||
self.mouse_position
|
||||
}
|
||||
/// Déplacement relatif de la souris cumulé pendant la frame courante.
|
||||
pub fn mouse_delta(&self) -> (f32, f32) {
|
||||
self.mouse_delta
|
||||
}
|
||||
/// Défilement de la molette cumulé pendant la frame courante (`(dx, dy)`, `dy > 0` = vers le haut).
|
||||
pub fn scroll_delta(&self) -> (f32, f32) {
|
||||
self.scroll
|
||||
}
|
||||
/// Vrai si `button` a été **pressé** pendant la frame courante (une seule frame).
|
||||
pub fn mouse_button_pressed(&self, button: MouseButton) -> bool {
|
||||
self.pressed_buttons.contains(&button)
|
||||
}
|
||||
/// Vrai si `button` est **maintenu** enfoncé (persiste entre les frames).
|
||||
pub fn mouse_button_held(&self, button: MouseButton) -> bool {
|
||||
self.held_buttons.contains(&button)
|
||||
}
|
||||
/// Vrai si `button` a été **relâché** pendant la frame courante (une seule frame).
|
||||
pub fn mouse_button_released(&self, button: MouseButton) -> bool {
|
||||
self.released_buttons.contains(&button)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use winit::event::MouseButton;
|
||||
|
||||
#[test]
|
||||
fn keyboard_pressed_held_released_lifecycle() {
|
||||
let mut input = InputState::new();
|
||||
input.key_input(KeyCode::KeyW, ElementState::Pressed);
|
||||
input.begin_frame();
|
||||
assert!(input.key_pressed(KeyCode::KeyW));
|
||||
assert!(input.key_held(KeyCode::KeyW));
|
||||
assert!(!input.key_released(KeyCode::KeyW));
|
||||
input.end_frame();
|
||||
|
||||
// Frame suivante sans nouvel événement : plus "pressed", toujours "held".
|
||||
input.begin_frame();
|
||||
assert!(!input.key_pressed(KeyCode::KeyW));
|
||||
assert!(input.key_held(KeyCode::KeyW));
|
||||
input.end_frame();
|
||||
|
||||
// Relâchement.
|
||||
input.key_input(KeyCode::KeyW, ElementState::Released);
|
||||
input.begin_frame();
|
||||
assert!(input.key_released(KeyCode::KeyW));
|
||||
assert!(!input.key_held(KeyCode::KeyW));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_buttons_lifecycle() {
|
||||
let mut input = InputState::new();
|
||||
input.mouse_button(MouseButton::Left, ElementState::Pressed);
|
||||
input.begin_frame();
|
||||
assert!(input.mouse_button_pressed(MouseButton::Left));
|
||||
assert!(input.mouse_button_held(MouseButton::Left));
|
||||
input.end_frame();
|
||||
input.begin_frame();
|
||||
assert!(!input.mouse_button_pressed(MouseButton::Left));
|
||||
assert!(input.mouse_button_held(MouseButton::Left));
|
||||
input.mouse_button(MouseButton::Left, ElementState::Released);
|
||||
input.end_frame();
|
||||
input.begin_frame();
|
||||
assert!(input.mouse_button_released(MouseButton::Left));
|
||||
assert!(!input.mouse_button_held(MouseButton::Left));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_delta_and_position_accumulate() {
|
||||
let mut input = InputState::new();
|
||||
input.begin_frame();
|
||||
input.cursor_move(10.0, 20.0);
|
||||
input.cursor_move(30.0, 40.0);
|
||||
assert_eq!(input.mouse_delta(), (20.0, 20.0));
|
||||
assert_eq!(input.mouse_position(), (30.0, 40.0));
|
||||
input.end_frame();
|
||||
|
||||
// Nouvelle frame : le delta est remis à zéro au début, la position reste.
|
||||
input.begin_frame();
|
||||
assert_eq!(input.mouse_delta(), (0.0, 0.0));
|
||||
assert_eq!(input.mouse_position(), (30.0, 40.0));
|
||||
input.cursor_move(31.0, 42.0);
|
||||
assert_eq!(input.mouse_delta(), (1.0, 2.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scroll_accumulates_per_frame() {
|
||||
let mut input = InputState::new();
|
||||
input.begin_frame();
|
||||
input.wheel(1.0, 2.0);
|
||||
input.wheel(0.5, -1.0);
|
||||
assert_eq!(input.scroll_delta(), (1.5, 1.0));
|
||||
input.end_frame();
|
||||
input.begin_frame();
|
||||
assert_eq!(input.scroll_delta(), (0.0, 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_is_empty() {
|
||||
let input = InputState::default();
|
||||
assert!(!input.key_held(KeyCode::KeyW));
|
||||
assert!(!input.mouse_button_held(MouseButton::Left));
|
||||
assert_eq!(input.mouse_delta(), (0.0, 0.0));
|
||||
assert_eq!(input.scroll_delta(), (0.0, 0.0));
|
||||
}
|
||||
}
|
||||
@@ -11,9 +11,11 @@
|
||||
|
||||
pub mod context;
|
||||
pub mod frame;
|
||||
pub mod input;
|
||||
pub mod renderer;
|
||||
|
||||
// Re-exports
|
||||
pub use context::Context;
|
||||
pub use frame::Frame;
|
||||
pub use input::InputState;
|
||||
pub use renderer::Renderer;
|
||||
|
||||
Reference in New Issue
Block a user