From 5ea9092a4954f8c5cd795f0ebec910666b367d5f Mon Sep 17 00:00:00 2001 From: Bernardo Kuri Date: Mon, 18 Aug 2025 23:51:32 -0600 Subject: Add per-axis scroll speed config for input devices (#2109) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add per-axis scroll speed config for input devices. Accepts negative values to inverse scroll direction. Properly complements/overrides global `scroll-direction` setting. Includes docs and tests. * Update per-axis scroll factor implementation after testing - Refined configuration structure in niri-config - Updated input handling to use per-axis scroll factors - Added comprehensive test snapshots - Updated documentation with per-axis examples 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Simplify per-axis scroll factor implementation per review feedback - Make documentation concise and clear - Remove unnecessary comments and test helper functions - Use inline snapshots for tests - Rename get_factors() to h_v_factors() for clarity - Remove unnecessary .clone() calls (ScrollFactor is Copy) - Reduce test count to essential cases only - Fix comment about window factor override behavior 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Remove unnecessary ScrollFactor::new() helper function The maintainer prefers minimal code, so removing this helper and constructing ScrollFactor directly in tests. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Fix scroll factor behavior - all settings now multiply with window factor Per maintainer feedback, both combined and per-axis scroll settings should multiply with the window-specific scroll factor, not override it. This ensures consistent behavior regardless of configuration method. Also removed the now-unused has_per_axis_override() method. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Final cleanup: remove redundant comments and unused snapshot files - Removed unused snapshot files (now using inline snapshots) - Removed redundant inline comments in tests - Simplified test descriptions to be more concise 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Convert scroll factor parsing tests to use assert_debug_snapshot Updates parse_scroll_factor_combined, parse_scroll_factor_split, and parse_scroll_factor_partial tests to use assert_debug_snapshot instead of manual assert_eq comparisons, as requested in PR review. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Convert to inline snapshots as requested - Convert all scroll factor parsing tests to use inline snapshots instead of external files - Remove external snapshot files to keep test directory clean - All tests still pass with inline snapshot assertions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Fix missed assert_eq in parse_scroll_factor_mixed test Converts the remaining assert_eq calls to assert_debug_snapshot with inline snapshots in the mixed syntax test function. Also fixes raw string delimiters from ### to #. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * Convert scroll_factor_h_v_factors test to use assert_debug_snapshot Makes all scroll factor tests consistent by using snapshots instead of assert_eq for better maintainability. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude * fixes --------- Co-authored-by: Bernardo Kuri Co-authored-by: Claude Co-authored-by: Ivan Molodetskikh --- src/input/mod.rs | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/input/mod.rs b/src/input/mod.rs index c2667dfc..cc07aed3 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -3086,31 +3086,44 @@ impl State { self.update_pointer_contents(); - let scroll_factor = match source { - AxisSource::Wheel => self.niri.config.borrow().input.mouse.scroll_factor, - AxisSource::Finger => self.niri.config.borrow().input.touchpad.scroll_factor, - _ => None, + let device_scroll_factor = { + let config = self.niri.config.borrow(); + match source { + AxisSource::Wheel => config.input.mouse.scroll_factor, + AxisSource::Finger => config.input.touchpad.scroll_factor, + _ => None, + } }; - let scroll_factor = scroll_factor.map(|x| x.0).unwrap_or(1.); + // Get window-specific scroll factor let window_scroll_factor = pointer .current_focus() .map(|focused| self.niri.find_root_shell_surface(&focused)) .and_then(|root| self.niri.layout.find_window_and_output(&root).unzip().0) - .and_then(|window| window.rules().scroll_factor); - let scroll_factor = scroll_factor * window_scroll_factor.unwrap_or(1.); + .and_then(|window| window.rules().scroll_factor) + .unwrap_or(1.); + + // Determine final scroll factors based on configuration + let (horizontal_factor, vertical_factor) = device_scroll_factor + .map(|x| x.h_v_factors()) + .unwrap_or((1.0, 1.0)); + let (horizontal_factor, vertical_factor) = ( + horizontal_factor * window_scroll_factor, + vertical_factor * window_scroll_factor, + ); let horizontal_amount = horizontal_amount.unwrap_or_else(|| { // Winit backend, discrete scrolling. horizontal_amount_v120.unwrap_or(0.0) / 120. * 15. - }) * scroll_factor; + }) * horizontal_factor; + let vertical_amount = vertical_amount.unwrap_or_else(|| { // Winit backend, discrete scrolling. vertical_amount_v120.unwrap_or(0.0) / 120. * 15. - }) * scroll_factor; + }) * vertical_factor; - let horizontal_amount_v120 = horizontal_amount_v120.map(|x| x * scroll_factor); - let vertical_amount_v120 = vertical_amount_v120.map(|x| x * scroll_factor); + let horizontal_amount_v120 = horizontal_amount_v120.map(|x| x * horizontal_factor); + let vertical_amount_v120 = vertical_amount_v120.map(|x| x * vertical_factor); let mut frame = AxisFrame::new(event.time_msec()).source(source); if horizontal_amount != 0.0 { -- cgit