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 --- niri-config/src/lib.rs | 282 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 272 insertions(+), 10 deletions(-) (limited to 'niri-config') diff --git a/niri-config/src/lib.rs b/niri-config/src/lib.rs index 16ec6dca..dbd6b5ea 100644 --- a/niri-config/src/lib.rs +++ b/niri-config/src/lib.rs @@ -191,6 +191,25 @@ pub enum TrackLayout { Window, } +#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)] +pub struct ScrollFactor { + #[knuffel(argument)] + pub base: Option>, + #[knuffel(property)] + pub horizontal: Option>, + #[knuffel(property)] + pub vertical: Option>, +} + +impl ScrollFactor { + pub fn h_v_factors(&self) -> (f64, f64) { + let base_value = self.base.map(|f| f.0).unwrap_or(1.0); + let h = self.horizontal.map(|f| f.0).unwrap_or(base_value); + let v = self.vertical.map(|f| f.0).unwrap_or(base_value); + (h, v) + } +} + #[derive(knuffel::Decode, Debug, Default, PartialEq)] pub struct Touchpad { #[knuffel(child)] @@ -227,8 +246,8 @@ pub struct Touchpad { pub disabled_on_external_mouse: bool, #[knuffel(child)] pub middle_emulation: bool, - #[knuffel(child, unwrap(argument))] - pub scroll_factor: Option>, + #[knuffel(child)] + pub scroll_factor: Option, } #[derive(knuffel::Decode, Debug, Default, PartialEq)] @@ -251,8 +270,8 @@ pub struct Mouse { pub left_handed: bool, #[knuffel(child)] pub middle_emulation: bool, - #[knuffel(child, unwrap(argument))] - pub scroll_factor: Option>, + #[knuffel(child)] + pub scroll_factor: Option, } #[derive(knuffel::Decode, Debug, Default, PartialEq)] @@ -4054,6 +4073,237 @@ mod tests { .unwrap() } + #[test] + fn parse_scroll_factor_combined() { + // Test combined scroll-factor syntax + let parsed = do_parse( + r#" + input { + mouse { + scroll-factor 2.0 + } + touchpad { + scroll-factor 1.5 + } + } + "#, + ); + + assert_debug_snapshot!(parsed.input.mouse.scroll_factor, @r#" + Some( + ScrollFactor { + base: Some( + FloatOrInt( + 2.0, + ), + ), + horizontal: None, + vertical: None, + }, + ) + "#); + assert_debug_snapshot!(parsed.input.touchpad.scroll_factor, @r#" + Some( + ScrollFactor { + base: Some( + FloatOrInt( + 1.5, + ), + ), + horizontal: None, + vertical: None, + }, + ) + "#); + } + + #[test] + fn parse_scroll_factor_split() { + // Test split horizontal/vertical syntax + let parsed = do_parse( + r#" + input { + mouse { + scroll-factor horizontal=2.0 vertical=-1.0 + } + touchpad { + scroll-factor horizontal=-1.5 vertical=0.5 + } + } + "#, + ); + + assert_debug_snapshot!(parsed.input.mouse.scroll_factor, @r#" + Some( + ScrollFactor { + base: None, + horizontal: Some( + FloatOrInt( + 2.0, + ), + ), + vertical: Some( + FloatOrInt( + -1.0, + ), + ), + }, + ) + "#); + assert_debug_snapshot!(parsed.input.touchpad.scroll_factor, @r#" + Some( + ScrollFactor { + base: None, + horizontal: Some( + FloatOrInt( + -1.5, + ), + ), + vertical: Some( + FloatOrInt( + 0.5, + ), + ), + }, + ) + "#); + } + + #[test] + fn parse_scroll_factor_partial() { + // Test partial specification (only one axis) + let parsed = do_parse( + r#" + input { + mouse { + scroll-factor horizontal=2.0 + } + touchpad { + scroll-factor vertical=-1.5 + } + } + "#, + ); + + assert_debug_snapshot!(parsed.input.mouse.scroll_factor, @r#" + Some( + ScrollFactor { + base: None, + horizontal: Some( + FloatOrInt( + 2.0, + ), + ), + vertical: None, + }, + ) + "#); + assert_debug_snapshot!(parsed.input.touchpad.scroll_factor, @r#" + Some( + ScrollFactor { + base: None, + horizontal: None, + vertical: Some( + FloatOrInt( + -1.5, + ), + ), + }, + ) + "#); + } + + #[test] + fn parse_scroll_factor_mixed() { + // Test mixed base + override syntax + let parsed = do_parse( + r#" + input { + mouse { + scroll-factor 2 vertical=-1 + } + touchpad { + scroll-factor 1.5 horizontal=3 + } + } + "#, + ); + + assert_debug_snapshot!(parsed.input.mouse.scroll_factor, @r#" + Some( + ScrollFactor { + base: Some( + FloatOrInt( + 2.0, + ), + ), + horizontal: None, + vertical: Some( + FloatOrInt( + -1.0, + ), + ), + }, + ) + "#); + assert_debug_snapshot!(parsed.input.touchpad.scroll_factor, @r#" + Some( + ScrollFactor { + base: Some( + FloatOrInt( + 1.5, + ), + ), + horizontal: Some( + FloatOrInt( + 3.0, + ), + ), + vertical: None, + }, + ) + "#); + } + + #[test] + fn scroll_factor_h_v_factors() { + let sf = ScrollFactor { + base: Some(FloatOrInt(2.0)), + horizontal: None, + vertical: None, + }; + assert_debug_snapshot!(sf.h_v_factors(), @r#" + ( + 2.0, + 2.0, + ) + "#); + + let sf = ScrollFactor { + base: None, + horizontal: Some(FloatOrInt(3.0)), + vertical: Some(FloatOrInt(-1.0)), + }; + assert_debug_snapshot!(sf.h_v_factors(), @r#" + ( + 3.0, + -1.0, + ) + "#); + + let sf = ScrollFactor { + base: Some(FloatOrInt(2.0)), + horizontal: Some(FloatOrInt(1.0)), + vertical: None, + }; + assert_debug_snapshot!(sf.h_v_factors(), @r" + ( + 1.0, + 2.0, + ) + "); + } + #[test] fn parse() { let parsed = do_parse( @@ -4370,9 +4620,15 @@ mod tests { disabled_on_external_mouse: true, middle_emulation: false, scroll_factor: Some( - FloatOrInt( - 0.9, - ), + ScrollFactor { + base: Some( + FloatOrInt( + 0.9, + ), + ), + horizontal: None, + vertical: None, + }, ), }, mouse: Mouse { @@ -4394,9 +4650,15 @@ mod tests { left_handed: false, middle_emulation: true, scroll_factor: Some( - FloatOrInt( - 0.2, - ), + ScrollFactor { + base: Some( + FloatOrInt( + 0.2, + ), + ), + horizontal: None, + vertical: None, + }, ), }, trackpoint: Trackpoint { -- cgit