// SPDX-License-Identifier: AGPL-3.0-only //! Debug-only input handling, kept separate from the gameplay input path. //! //! Debug affordances are bound behind a modifier chord so they cannot collide with movement keys: [`DEBUG_MODIFIER`] (F1) is held, and a second key selects the affordance. The currently bound chords are: //! //! - **F1 + V**: filled terrain with vertex points overlaid, showing where the mesher placed geometry without losing the surface. //! - **F1 + B**: filled terrain with the triangle edges overlaid, showing the size and shape of the emitted quads. //! - **F1 + I**: the debug statistics panel. //! //! Holding a [`SOLO_MODIFIER`] (either Shift) as well drops the filled pass, leaving the debug geometry alone against the clear colour: **F1 + Shift + V** for points only, **F1 + Shift + B** for wireframe only. //! //! Each chord toggles: pressing the chord for the active mode returns to [`RenderMode::Filled`]. use renderer::RenderMode; use winit::keyboard::KeyCode; /// The key that must be held for a debug chord to be recognised. const DEBUG_MODIFIER: KeyCode = KeyCode::F1; /// The keys that, held alongside [`DEBUG_MODIFIER`], select the solo form of a debug view. Both shifts are accepted so the chord is reachable with either hand. const SOLO_MODIFIER: [KeyCode; 2] = [KeyCode::ShiftLeft, KeyCode::ShiftRight]; /// The key that, held alongside [`DEBUG_MODIFIER`], selects the vertex-point view. const VERTEX_POINTS_OVERLAY_KEY: KeyCode = KeyCode::KeyV; /// The key that, held alongside [`DEBUG_MODIFIER`], selects the wireframe view. const WIREFRAME_OVERLAY_KEY: KeyCode = KeyCode::KeyB; /// The key that, held alongside [`DEBUG_MODIFIER`], toggles the statistics panel. const STATS_KEY: KeyCode = KeyCode::KeyI; /// A debug operation requested by the input layer, applied by the caller. /// /// The layer deliberately returns an intent rather than acting directly, so it owns no renderer or window handles and stays a pure function of key events. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum DebugAction { /// Applies the given rasterisation mode to the renderer. SetRenderMode(RenderMode), /// Enables or disables emission of the statistics panel. SetStatsOverlay(bool), } /// Owns debug-only input state and translates key events into [`DebugAction`]s. #[derive(Default)] pub(crate) struct DebugControls { /// Whether [`DEBUG_MODIFIER`] is currently held. Chords are recognised only while this is set. modifier_held: bool, /// Whether a [`SOLO_MODIFIER`] is currently held, selecting the solo form of the chord. solo_held: bool, /// The rasterisation mode most recently requested, used to make each chord a toggle back to [`RenderMode::Filled`]. render_mode: RenderMode, /// Whether the statistics panel is being emitted. stats_enabled: bool, } impl DebugControls { /// Translates one key event into a debug action, updating internal state. /// /// Returns [`None`] when the event is not part of a debug chord, which is the common case; the caller then handles the key normally. Actions fire on the press edge only, so one physical tap toggles once rather than once per press and once per release. pub(crate) fn handle_key(&mut self, code: KeyCode, pressed: bool) -> Option { if code == DEBUG_MODIFIER { self.modifier_held = pressed; return None; } // A solo modifier is tracked unconditionally rather than only while the debug modifier is held, so its state is correct whichever of the two is pressed first. if SOLO_MODIFIER.contains(&code) { self.solo_held = pressed; return None; } if !pressed || !self.modifier_held { return None; } // The statistics chord is resolved before the raster table so the two axes never contend for a key. The solo modifier selects between overlaid and standalone geometry and has no meaning for a panel that draws none, so it is ignored here. if code == STATS_KEY { self.stats_enabled = !self.stats_enabled; return Some(DebugAction::SetStatsOverlay(self.stats_enabled)); } let requested = render_mode_for_key(code, self.solo_held)?; // Re-pressing the chord for the active mode returns to the normal path, so a single chord both enables and disables its mode. self.render_mode = if self.render_mode == requested { RenderMode::Filled } else { requested }; Some(DebugAction::SetRenderMode(self.render_mode)) } } /// Maps a chord key, and whether a [`SOLO_MODIFIER`] is held, to the render mode it selects. Returns [`None`] if the key is unbound. /// /// This is the single table a new rasterisation debug mode is added to: one key, one overlaid form, one solo form. const fn render_mode_for_key(code: KeyCode, solo: bool) -> Option { match (code, solo) { (VERTEX_POINTS_OVERLAY_KEY, false) => Some(RenderMode::FilledPoints), (VERTEX_POINTS_OVERLAY_KEY, true) => Some(RenderMode::Points), (WIREFRAME_OVERLAY_KEY, false) => Some(RenderMode::FilledWireframe), (WIREFRAME_OVERLAY_KEY, true) => Some(RenderMode::Wireframe), _ => None, } } #[cfg(test)] #[path = "tests/debug.rs"] mod tests;