Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat(input): mouse device class and per-class scroll factors
Pointer devices now classify as trackpoint (by name), touchpad (by tap
capability), or mouse — previously mice could only take the global
accel values. Each class block in the `input` section (config.kdl or
input.kdl; `trackpad` accepted as the input.kdl spelling of `touchpad`)
can set accel_speed, accel_profile, and scroll_factor, falling back to
the top-level values.
scroll_factor precedence per device: name-based `device` rules > class
block > generic — apply_input_config now runs before apply_input_rules
so name rules stay the most specific override. The hotplug path already
applied config before rules and needed no change.
Co-Authored-By: Claude Fable 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01PXSsCppeDFmSRRM5NAug5a
src/server/config.rs | 90 ++++++++++++++++++++++++++++++++++++++++++-
src/server/libinput_device.rs | 40 +++++++++++++++----
2 files changed, 120 insertions(+), 10 deletions(-)
diff --git a/src/server/config.rs b/src/server/config.rs
index 1ca37c0..9af8dad 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -246,18 +246,35 @@ pub struct TouchpadConfig {
pub dwt: Option<bool>,
pub dwtp: Option<bool>,
pub gestures: Option<GesturesConfig>,
+ pub accel_speed: Option<f64>,
+ pub accel_profile: Option<String>,
+ pub scroll_factor: Option<f64>,
}
#[derive(Debug, Deserialize, Clone, Default, PartialEq)]
pub struct TrackpointConfig {
pub accel_speed: Option<f64>,
pub accel_profile: Option<String>,
+ pub scroll_factor: Option<f64>,
+}
+
+#[derive(Debug, Deserialize, Clone, Default, PartialEq)]
+pub struct MouseConfig {
+ pub accel_speed: Option<f64>,
+ pub accel_profile: Option<String>,
+ pub scroll_factor: Option<f64>,
}
+/// Pointer device configuration. Per-class blocks (`mouse` / `touchpad` /
+/// `trackpad` alias / `trackpoint`) override the top-level values for
+/// devices of that class; a device is a trackpoint if its name says so, a
+/// touchpad if it supports tap, and a mouse otherwise.
#[derive(Debug, Deserialize, Clone, Default)]
pub struct InputConfig {
pub accel_speed: Option<f64>,
pub accel_profile: Option<String>,
+ pub scroll_factor: Option<f64>,
+ pub mouse: Option<MouseConfig>,
pub touchpad: Option<TouchpadConfig>,
pub trackpoint: Option<TrackpointConfig>,
}
@@ -1349,10 +1366,16 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
if let Some(node) = doc.nodes().iter().find(|n| n.name().value() == "input") {
let accel_speed = get_child_arg_f64_opt(node, "accel_speed");
let accel_profile = get_child_arg_string_opt(node, "accel_profile");
+ let scroll_factor = get_child_arg_f64_opt(node, "scroll_factor");
let mut touchpad = None;
if let Some(children) = node.children() {
- if let Some(tp_node) = children.nodes().iter().find(|n| n.name().value() == "touchpad") {
+ // `trackpad` is the input.kdl spelling, `touchpad` the legacy one.
+ if let Some(tp_node) = children
+ .nodes()
+ .iter()
+ .find(|n| n.name().value() == "touchpad" || n.name().value() == "trackpad")
+ {
let tap_to_click = get_child_arg_bool_opt(tp_node, "tap_to_click");
let natural_scroll = get_child_arg_bool_opt(tp_node, "natural_scroll");
let dwt = get_child_arg_bool_opt(tp_node, "dwt");
@@ -1373,6 +1396,9 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
dwt,
dwtp,
gestures,
+ accel_speed: get_child_arg_f64_opt(tp_node, "accel_speed"),
+ accel_profile: get_child_arg_string_opt(tp_node, "accel_profile"),
+ scroll_factor: get_child_arg_f64_opt(tp_node, "scroll_factor"),
});
}
}
@@ -1385,6 +1411,18 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
trackpoint = Some(TrackpointConfig {
accel_speed,
accel_profile,
+ scroll_factor: get_child_arg_f64_opt(tp_node, "scroll_factor"),
+ });
+ }
+ }
+
+ let mut mouse = None;
+ if let Some(children) = node.children() {
+ if let Some(m_node) = children.nodes().iter().find(|n| n.name().value() == "mouse") {
+ mouse = Some(MouseConfig {
+ accel_speed: get_child_arg_f64_opt(m_node, "accel_speed"),
+ accel_profile: get_child_arg_string_opt(m_node, "accel_profile"),
+ scroll_factor: get_child_arg_f64_opt(m_node, "scroll_factor"),
});
}
}
@@ -1392,6 +1430,8 @@ fn parse_kdl_config(content: &str) -> Result<Config, String> {
input = Some(InputConfig {
accel_speed,
accel_profile,
+ scroll_factor,
+ mouse,
touchpad,
trackpoint,
});
@@ -1725,8 +1765,10 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
state.input_rules = config.device.clone();
state.input_config = config.input.clone().unwrap_or_default();
unsafe {
- state.apply_input_rules();
+ // Config first (its per-class scroll factors are defaults), then the
+ // name-based device rules so they stay the most specific override.
state.apply_input_config();
+ state.apply_input_rules();
}
state.keybinds.clear();
@@ -1984,6 +2026,50 @@ mod tests {
assert_eq!(wm.toggle_overview, Some("swipe_up".to_string()));
}
+ #[test]
+ fn test_kdl_input_device_classes_parsing() {
+ let content = r#"
+ input {
+ accel_profile "flat"
+ accel_speed (f64)1.0
+ scroll_factor (f64)1.0
+ mouse {
+ accel_speed (f64)0.5
+ scroll_factor (f64)2.0
+ }
+ trackpad {
+ tap_to_click (bool)true
+ natural_scroll (bool)true
+ scroll_factor (f64)1.5
+ accel_speed (f64)0.9
+ }
+ trackpoint {
+ accel_speed (f64)0.4
+ accel_profile "adaptive"
+ scroll_factor (f64)3.0
+ }
+ }
+ "#;
+ let config = parse_kdl_config(content).unwrap();
+ let input = config.input.unwrap();
+ assert_eq!(input.accel_profile, Some("flat".to_string()));
+ assert_eq!(input.accel_speed, Some(1.0));
+ assert_eq!(input.scroll_factor, Some(1.0));
+ let mouse = input.mouse.unwrap();
+ assert_eq!(mouse.accel_speed, Some(0.5));
+ assert_eq!(mouse.scroll_factor, Some(2.0));
+ // `trackpad` parses into the touchpad block (input.kdl spelling).
+ let tp = input.touchpad.unwrap();
+ assert_eq!(tp.tap_to_click, Some(true));
+ assert_eq!(tp.natural_scroll, Some(true));
+ assert_eq!(tp.scroll_factor, Some(1.5));
+ assert_eq!(tp.accel_speed, Some(0.9));
+ let tpoint = input.trackpoint.unwrap();
+ assert_eq!(tpoint.accel_speed, Some(0.4));
+ assert_eq!(tpoint.accel_profile, Some("adaptive".to_string()));
+ assert_eq!(tpoint.scroll_factor, Some(3.0));
+ }
+
#[test]
fn test_kdl_surface_border_parsing() {
let content = r##"
diff --git a/src/server/libinput_device.rs b/src/server/libinput_device.rs
index ca55f3a..06d4e00 100644
--- a/src/server/libinput_device.rs
+++ b/src/server/libinput_device.rs
@@ -87,7 +87,9 @@ impl LibinputDevice {
}
}
- // Check if device name contains "trackpoint" (case-insensitive)
+ // Device class: trackpoint by name, touchpad by tap capability,
+ // mouse otherwise. Per-class config blocks override the top-level
+ // values for devices of that class.
let name_ptr = ffi::river_wlr_input_device_get_name((*self.parent_device).wlr_device);
let name = if !name_ptr.is_null() {
std::ffi::CStr::from_ptr(name_ptr).to_string_lossy().to_lowercase()
@@ -95,25 +97,47 @@ impl LibinputDevice {
String::new()
};
let is_trackpoint = name.contains("trackpoint");
+ let is_touchpad = !is_trackpoint && ffi::libinput_device_config_tap_get_finger_count(handle) > 0;
// Acceleration Speed
- let speed = if is_trackpoint {
- config.trackpoint.as_ref().and_then(|t| t.accel_speed).or(config.accel_speed)
+ let class_speed = if is_trackpoint {
+ config.trackpoint.as_ref().and_then(|t| t.accel_speed)
+ } else if is_touchpad {
+ config.touchpad.as_ref().and_then(|t| t.accel_speed)
} else {
- config.accel_speed
+ config.mouse.as_ref().and_then(|m| m.accel_speed)
};
- if let Some(s) = speed {
+ if let Some(s) = class_speed.or(config.accel_speed) {
if ffi::libinput_device_config_accel_get_profiles(handle) != 0 {
ffi::libinput_device_config_accel_set_speed(handle, s);
}
}
+ // Scroll factor (cce's own wheel-delta scaling, not libinput):
+ // class value -> top-level value. Name-based `device` rules run
+ // after this and stay the most specific override.
+ let class_scroll = if is_trackpoint {
+ config.trackpoint.as_ref().and_then(|t| t.scroll_factor)
+ } else if is_touchpad {
+ config.touchpad.as_ref().and_then(|t| t.scroll_factor)
+ } else {
+ config.mouse.as_ref().and_then(|m| m.scroll_factor)
+ };
+ if let Some(f) = class_scroll.or(config.scroll_factor) {
+ if f.is_finite() && f > 0.0 {
+ (*self.parent_device).config.scroll_factor = f;
+ }
+ }
+
// Acceleration Profile
- let profile_str = if is_trackpoint {
- config.trackpoint.as_ref().and_then(|t| t.accel_profile.as_ref()).or(config.accel_profile.as_ref())
+ let class_profile = if is_trackpoint {
+ config.trackpoint.as_ref().and_then(|t| t.accel_profile.as_ref())
+ } else if is_touchpad {
+ config.touchpad.as_ref().and_then(|t| t.accel_profile.as_ref())
} else {
- config.accel_profile.as_ref()
+ config.mouse.as_ref().and_then(|m| m.accel_profile.as_ref())
};
+ let profile_str = class_profile.or(config.accel_profile.as_ref());
if let Some(ref p_str) = profile_str {
if ffi::libinput_device_config_accel_get_profiles(handle) != 0 {
let profile = match p_str.as_str() {