Add per-axis scroll speed config for input devices (#2109)

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

* fixes

---------

Co-authored-by: Bernardo Kuri <github@bkuri.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ivan Molodetskikh <yalterz@gmail.com>
This commit is contained in:
Bernardo Kuri
2025-08-18 23:51:32 -06:00
committed by GitHub
parent 43a2648e57
commit 5ea9092a49
3 changed files with 300 additions and 21 deletions
+272 -10
View File
@@ -191,6 +191,25 @@ pub enum TrackLayout {
Window,
}
#[derive(knuffel::Decode, Debug, Default, Clone, Copy, PartialEq)]
pub struct ScrollFactor {
#[knuffel(argument)]
pub base: Option<FloatOrInt<0, 100>>,
#[knuffel(property)]
pub horizontal: Option<FloatOrInt<-100, 100>>,
#[knuffel(property)]
pub vertical: Option<FloatOrInt<-100, 100>>,
}
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<FloatOrInt<0, 100>>,
#[knuffel(child)]
pub scroll_factor: Option<ScrollFactor>,
}
#[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<FloatOrInt<0, 100>>,
#[knuffel(child)]
pub scroll_factor: Option<ScrollFactor>,
}
#[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 {