From 2f59af15dfa96f5c2ffc523b93b53e6f3c489db3 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Thu, 16 Jul 2026 14:02:18 -0700 Subject: [PATCH] Add Binding::Scroll as an axis input source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mouse wheel's per-frame delta is a Vec2, so it maps directly onto axis actions alongside gamepad sticks. hardware_to_actions now reads AccumulatedMouseScroll in the Axis branch (max-magnitude combine); the digital branch ignores it, and Scalar is unchanged. Since an axis is refreshed every frame and the scroll delta is zero when idle, on_action fires exactly while the wheel turns and the piped value is the delta — no persistence or edge-guarding needed. Enables e.g. `.bind(Action::Zoom, [Binding::Scroll])`. Co-Authored-By: Claude Opus 4.8 --- engine/src/actions.rs | 51 +++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/engine/src/actions.rs b/engine/src/actions.rs index ac4d777..769a622 100644 --- a/engine/src/actions.rs +++ b/engine/src/actions.rs @@ -137,6 +137,7 @@ use core::marker::PhantomData; use bevy::{ ecs::{schedule::ScheduleLabel, system::ScheduleSystem}, + input::mouse::AccumulatedMouseScroll, platform::collections::{HashMap, HashSet}, prelude::*, ui::Checked, @@ -258,8 +259,8 @@ pub trait GameAction: Copy + Eq + Hash + Send + Sync + 'static { /// /// [`Key`](Self::Key), [`Mouse`](Self::Mouse) and [`Gamepad`](Self::Gamepad) are /// digital (for [`Instant`](ActionKind::Instant)/[`Toggle`](ActionKind::Toggle) -/// actions); [`Stick`](Self::Stick) is analog (for [`Axis`](ActionKind::Axis) -/// actions). +/// actions); [`Stick`](Self::Stick) and [`Scroll`](Self::Scroll) are analog (for +/// [`Axis`](ActionKind::Axis) actions). #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect)] pub enum Binding { /// A keyboard key. @@ -271,6 +272,12 @@ pub enum Binding { /// A gamepad analog source producing a [`Vec2`] (matched across all connected /// gamepads; deadzone already applied by Bevy). Stick(Stick), + /// The mouse wheel, producing this frame's scroll delta as a [`Vec2`] + /// (`x` = horizontal, `y` = vertical). Like a stick, it drives + /// [`Axis`](ActionKind::Axis) actions — but it is a *delta* (non-zero only + /// while scrolling), so the axis reads zero on idle frames and + /// [`on_action`] fires exactly while the wheel turns. + Scroll, } /// An analog 2D source on a gamepad. @@ -557,28 +564,39 @@ fn hardware_to_actions( keys: Res>, mouse: Res>, pads: Query<&Gamepad>, + scroll: Res, map: Res>, mut state: ResMut>, ) { for (&action, bindings) in &map.bindings { match action.kind() { - // Analog: pick the most-deflected reading across sticks/gamepads and - // store it every frame (zero when nothing is connected or centered). + // Analog: pick the strongest reading across analog sources and store it + // every frame (zero when nothing is deflected / not scrolling). ActionKind::Axis => { let mut value = Vec2::ZERO; for binding in bindings { - let Binding::Stick(stick) = binding else { - continue; - }; - for pad in &pads { - let reading = match stick { - Stick::Left => pad.left_stick(), - Stick::Right => pad.right_stick(), - Stick::DPad => pad.dpad(), - }; - if reading.length_squared() > value.length_squared() { - value = reading; + let reading = match binding { + Binding::Stick(stick) => { + // Best deflection across all connected gamepads. + let mut best = Vec2::ZERO; + for pad in &pads { + let r = match stick { + Stick::Left => pad.left_stick(), + Stick::Right => pad.right_stick(), + Stick::DPad => pad.dpad(), + }; + if r.length_squared() > best.length_squared() { + best = r; + } + } + best } + // This frame's wheel delta (zero when not scrolling). + Binding::Scroll => scroll.delta, + _ => continue, + }; + if reading.length_squared() > value.length_squared() { + value = reading; } } state.set_axis(action, value); @@ -591,7 +609,8 @@ fn hardware_to_actions( Binding::Key(k) => keys.just_pressed(*k), Binding::Mouse(m) => mouse.just_pressed(*m), Binding::Gamepad(g) => pads.iter().any(|pad| pad.just_pressed(*g)), - Binding::Stick(_) => false, // not meaningful for digital actions + // Analog sources are not meaningful for digital actions. + Binding::Stick(_) | Binding::Scroll => false, }; if just_pressed { match kind {