mirror of
https://github.com/niri-wm/niri.git
synced 2026-06-24 02:01:18 +07:00
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:
@@ -37,6 +37,7 @@ input {
|
|||||||
// accel-speed 0.2
|
// accel-speed 0.2
|
||||||
// accel-profile "flat"
|
// accel-profile "flat"
|
||||||
// scroll-factor 1.0
|
// scroll-factor 1.0
|
||||||
|
// scroll-factor vertical=1.0 horizontal=-2.0
|
||||||
// scroll-method "two-finger"
|
// scroll-method "two-finger"
|
||||||
// scroll-button 273
|
// scroll-button 273
|
||||||
// scroll-button-lock
|
// scroll-button-lock
|
||||||
@@ -53,6 +54,7 @@ input {
|
|||||||
// accel-speed 0.2
|
// accel-speed 0.2
|
||||||
// accel-profile "flat"
|
// accel-profile "flat"
|
||||||
// scroll-factor 1.0
|
// scroll-factor 1.0
|
||||||
|
// scroll-factor vertical=1.0 horizontal=-2.0
|
||||||
// scroll-method "no-scroll"
|
// scroll-method "no-scroll"
|
||||||
// scroll-button 273
|
// scroll-button 273
|
||||||
// scroll-button-lock
|
// scroll-button-lock
|
||||||
@@ -252,6 +254,8 @@ Settings specific to `touchpad` and `mouse`:
|
|||||||
|
|
||||||
- `scroll-factor`: <sup>Since: 0.1.10</sup> scales the scrolling speed by this value.
|
- `scroll-factor`: <sup>Since: 0.1.10</sup> scales the scrolling speed by this value.
|
||||||
|
|
||||||
|
<sup>Since: next release</sup> You can also override horizontal and vertical scroll factor separately like so: `scroll-factor horizontal=2.0 vertical=-1.0`
|
||||||
|
|
||||||
Settings specific to `tablet`s:
|
Settings specific to `tablet`s:
|
||||||
|
|
||||||
- `calibration-matrix`: <sup>Since: 25.02</sup> set to six floating point numbers to change the calibration matrix. See the [`LIBINPUT_CALIBRATION_MATRIX` documentation](https://wayland.freedesktop.org/libinput/doc/latest/device-configuration-via-udev.html) for examples.
|
- `calibration-matrix`: <sup>Since: 25.02</sup> set to six floating point numbers to change the calibration matrix. See the [`LIBINPUT_CALIBRATION_MATRIX` documentation](https://wayland.freedesktop.org/libinput/doc/latest/device-configuration-via-udev.html) for examples.
|
||||||
|
|||||||
+266
-4
@@ -191,6 +191,25 @@ pub enum TrackLayout {
|
|||||||
Window,
|
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)]
|
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||||
pub struct Touchpad {
|
pub struct Touchpad {
|
||||||
#[knuffel(child)]
|
#[knuffel(child)]
|
||||||
@@ -227,8 +246,8 @@ pub struct Touchpad {
|
|||||||
pub disabled_on_external_mouse: bool,
|
pub disabled_on_external_mouse: bool,
|
||||||
#[knuffel(child)]
|
#[knuffel(child)]
|
||||||
pub middle_emulation: bool,
|
pub middle_emulation: bool,
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child)]
|
||||||
pub scroll_factor: Option<FloatOrInt<0, 100>>,
|
pub scroll_factor: Option<ScrollFactor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||||
@@ -251,8 +270,8 @@ pub struct Mouse {
|
|||||||
pub left_handed: bool,
|
pub left_handed: bool,
|
||||||
#[knuffel(child)]
|
#[knuffel(child)]
|
||||||
pub middle_emulation: bool,
|
pub middle_emulation: bool,
|
||||||
#[knuffel(child, unwrap(argument))]
|
#[knuffel(child)]
|
||||||
pub scroll_factor: Option<FloatOrInt<0, 100>>,
|
pub scroll_factor: Option<ScrollFactor>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
#[derive(knuffel::Decode, Debug, Default, PartialEq)]
|
||||||
@@ -4054,6 +4073,237 @@ mod tests {
|
|||||||
.unwrap()
|
.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]
|
#[test]
|
||||||
fn parse() {
|
fn parse() {
|
||||||
let parsed = do_parse(
|
let parsed = do_parse(
|
||||||
@@ -4370,10 +4620,16 @@ mod tests {
|
|||||||
disabled_on_external_mouse: true,
|
disabled_on_external_mouse: true,
|
||||||
middle_emulation: false,
|
middle_emulation: false,
|
||||||
scroll_factor: Some(
|
scroll_factor: Some(
|
||||||
|
ScrollFactor {
|
||||||
|
base: Some(
|
||||||
FloatOrInt(
|
FloatOrInt(
|
||||||
0.9,
|
0.9,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
horizontal: None,
|
||||||
|
vertical: None,
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
mouse: Mouse {
|
mouse: Mouse {
|
||||||
off: false,
|
off: false,
|
||||||
@@ -4394,10 +4650,16 @@ mod tests {
|
|||||||
left_handed: false,
|
left_handed: false,
|
||||||
middle_emulation: true,
|
middle_emulation: true,
|
||||||
scroll_factor: Some(
|
scroll_factor: Some(
|
||||||
|
ScrollFactor {
|
||||||
|
base: Some(
|
||||||
FloatOrInt(
|
FloatOrInt(
|
||||||
0.2,
|
0.2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
horizontal: None,
|
||||||
|
vertical: None,
|
||||||
|
},
|
||||||
|
),
|
||||||
},
|
},
|
||||||
trackpoint: Trackpoint {
|
trackpoint: Trackpoint {
|
||||||
off: true,
|
off: true,
|
||||||
|
|||||||
+23
-10
@@ -3086,31 +3086,44 @@ impl State {
|
|||||||
|
|
||||||
self.update_pointer_contents();
|
self.update_pointer_contents();
|
||||||
|
|
||||||
let scroll_factor = match source {
|
let device_scroll_factor = {
|
||||||
AxisSource::Wheel => self.niri.config.borrow().input.mouse.scroll_factor,
|
let config = self.niri.config.borrow();
|
||||||
AxisSource::Finger => self.niri.config.borrow().input.touchpad.scroll_factor,
|
match source {
|
||||||
|
AxisSource::Wheel => config.input.mouse.scroll_factor,
|
||||||
|
AxisSource::Finger => config.input.touchpad.scroll_factor,
|
||||||
_ => None,
|
_ => None,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let scroll_factor = scroll_factor.map(|x| x.0).unwrap_or(1.);
|
|
||||||
|
|
||||||
|
// Get window-specific scroll factor
|
||||||
let window_scroll_factor = pointer
|
let window_scroll_factor = pointer
|
||||||
.current_focus()
|
.current_focus()
|
||||||
.map(|focused| self.niri.find_root_shell_surface(&focused))
|
.map(|focused| self.niri.find_root_shell_surface(&focused))
|
||||||
.and_then(|root| self.niri.layout.find_window_and_output(&root).unzip().0)
|
.and_then(|root| self.niri.layout.find_window_and_output(&root).unzip().0)
|
||||||
.and_then(|window| window.rules().scroll_factor);
|
.and_then(|window| window.rules().scroll_factor)
|
||||||
let scroll_factor = scroll_factor * window_scroll_factor.unwrap_or(1.);
|
.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(|| {
|
let horizontal_amount = horizontal_amount.unwrap_or_else(|| {
|
||||||
// Winit backend, discrete scrolling.
|
// Winit backend, discrete scrolling.
|
||||||
horizontal_amount_v120.unwrap_or(0.0) / 120. * 15.
|
horizontal_amount_v120.unwrap_or(0.0) / 120. * 15.
|
||||||
}) * scroll_factor;
|
}) * horizontal_factor;
|
||||||
|
|
||||||
let vertical_amount = vertical_amount.unwrap_or_else(|| {
|
let vertical_amount = vertical_amount.unwrap_or_else(|| {
|
||||||
// Winit backend, discrete scrolling.
|
// Winit backend, discrete scrolling.
|
||||||
vertical_amount_v120.unwrap_or(0.0) / 120. * 15.
|
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 horizontal_amount_v120 = horizontal_amount_v120.map(|x| x * horizontal_factor);
|
||||||
let vertical_amount_v120 = vertical_amount_v120.map(|x| x * scroll_factor);
|
let vertical_amount_v120 = vertical_amount_v120.map(|x| x * vertical_factor);
|
||||||
|
|
||||||
let mut frame = AxisFrame::new(event.time_msec()).source(source);
|
let mut frame = AxisFrame::new(event.time_msec()).source(source);
|
||||||
if horizontal_amount != 0.0 {
|
if horizontal_amount != 0.0 {
|
||||||
|
|||||||
Reference in New Issue
Block a user