git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commita8e266b2853cdc270e91cd54397694644dd7f05a
parent21e33d61b3
authorLucas Galante <[email protected]>
date2026-06-15 12:10
feat: implement scroll scaling and sensitivity config

 src/clearctl.rs              |  1 +
 src/server/config.rs         | 11 ++++++++
 src/server/cursor.rs         | 18 ++++++++++--
 src/server/input_manager.rs  | 13 +++++++++
 src/server/window_manager.rs | 65 ++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 106 insertions(+), 2 deletions(-)

diff --git a/src/clearctl.rs b/src/clearctl.rs
index a8b811a..752989a 100644
--- a/src/clearctl.rs
+++ b/src/clearctl.rs
@@ -44,6 +44,7 @@ fn usage(name: &str, to_stderr: bool) {
     print("  restart");
     print("  reload");
     print("  repeat <rate> <delay>");
+    print("  input <device_name|*> scroll-factor <value>");
     print("  config-done");
     print("  spawn <command>");
     print("  notify <title> [body]");
diff --git a/src/server/config.rs b/src/server/config.rs
index 565590e..fde12a7 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -171,6 +171,12 @@ fn default_scale() -> f64 {
     1.0
 }
 
+#[derive(Debug, Deserialize, Clone)]
+pub struct InputDeviceConfigRule {
+    pub name: String,
+    pub scroll_factor: Option<f64>,
+}
+
 #[derive(Debug, Deserialize)]
 pub struct Config {
     #[serde(default)]
@@ -189,6 +195,8 @@ pub struct Config {
     pub startup: Vec<StartupConfig>,
     #[serde(default)]
     pub output: Option<OutputConfig>,
+    #[serde(default)]
+    pub device: Vec<InputDeviceConfigRule>,
 }
 
 #[derive(Debug, Deserialize)]
@@ -598,6 +606,9 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
         std::env::set_var(key, &expanded);
     }
 
+    state.input_rules = config.device.clone();
+    unsafe { state.apply_input_rules(); }
+
     state.keybinds.clear();
     for kb in &config.keybind {
         let mods = parse_modifiers(&kb.mods);
diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 7d47b2b..0458992 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -658,12 +658,26 @@ unsafe extern "C" fn handle_axis(listener: *mut ffi::wl_listener, data: *mut std
     let event = data as *mut ffi::wlr_pointer_axis_event;
     
     let seat = &mut *cursor.seat;
+    
+    let mut delta = (*event).delta;
+    let mut delta_discrete = (*event).delta_discrete;
+
+    if !(*event).pointer.is_null() {
+        let wlr_device = &mut (*(*event).pointer).base as *mut ffi::wlr_input_device;
+        let device_ptr = ffi::river_wlr_input_device_get_data(wlr_device) as *mut crate::input_device::InputDevice;
+        if !device_ptr.is_null() {
+            let factor = (*device_ptr).config.scroll_factor;
+            delta *= factor;
+            delta_discrete = (delta_discrete as f64 * factor) as i32;
+        }
+    }
+
     ffi::wlr_seat_pointer_notify_axis(
         seat.wlr_seat,
         (*event).time_msec,
         (*event).orientation,
-        (*event).delta,
-        (*event).delta_discrete,
+        delta,
+        delta_discrete,
         (*event).source,
         (*event).relative_direction,
     );
diff --git a/src/server/input_manager.rs b/src/server/input_manager.rs
index fff4453..54f8d54 100644
--- a/src/server/input_manager.rs
+++ b/src/server/input_manager.rs
@@ -309,6 +309,19 @@ unsafe extern "C" fn handle_new_input(listener: *mut ffi::wl_listener, data: *mu
 
     let device = crate::input_device::InputDevice::new(im.default_seat, wlr_device, false);
     
+    let name_ptr = (*wlr_device).name;
+    if !name_ptr.is_null() {
+        let name = std::ffi::CStr::from_ptr(name_ptr).to_string_lossy();
+        let wm = &mut (*im.server).wm;
+        for rule in &wm.input_rules {
+            if rule.name == "*" || name.contains(&rule.name) {
+                if let Some(factor) = rule.scroll_factor {
+                    (*device).config.scroll_factor = factor;
+                }
+            }
+        }
+    }
+    
     let dev_type = ffi::river_wlr_input_device_get_type(wlr_device);
     if dev_type == ffi::wlr_input_device_type_WLR_INPUT_DEVICE_KEYBOARD {
         crate::keyboard::Keyboard::create(device);
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index e040ddf..009ef07 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -67,6 +67,7 @@ pub struct WindowManager {
     pub startup: Vec<crate::config::StartupConfig>,
     pub status_sender: Option<crate::status_server::StatusSender>,
     pub output_scale: f32,
+    pub input_rules: Vec<crate::config::InputDeviceConfigRule>,
 }
 
 impl WindowManager {
@@ -77,6 +78,7 @@ impl WindowManager {
         self.scheduled.output_config = std::ptr::null_mut();
         self.sent.output_config = std::ptr::null_mut();
         self.output_scale = 1.0;
+        self.input_rules = Vec::new();
         Ok(())
     }
 
@@ -118,6 +120,7 @@ impl WindowManager {
         self.ipc_timer = std::ptr::null_mut();
         self.startup = Vec::new();
         self.status_sender = None;
+        self.input_rules = Vec::new();
 
         ffi::wl_list_init(&mut self.sent.outputs);
         ffi::wl_list_init(&mut self.sent.seats);
@@ -1548,9 +1551,71 @@ impl WindowManager {
                 self.dirty_windowing();
                 "ok\n".to_string()
             }
+            "input" => {
+                if parts.len() < 4 { return "error: usage: input <device_name|*> scroll-factor <value>\n".to_string(); }
+                let device_name = parts[1];
+                let key = parts[2];
+                let val = parts[3];
+                if key == "scroll-factor" {
+                    if let Ok(factor) = val.parse::<f64>() {
+                        if factor < 0.0 {
+                            return "error: scroll factor cannot be negative\n".to_string();
+                        }
+                        let mut found = false;
+                        let devices_head = &mut (*self.server).input_manager.devices as *mut ffi::wl_list as *mut WlList;
+                        let mut curr = (*devices_head).next;
+                        while curr != devices_head {
+                            let next = (*curr).next;
+                            let device = crate::container_of!(curr, crate::input_device::InputDevice, link);
+                            let name_ptr = (*(*device).wlr_device).name;
+                            if !name_ptr.is_null() {
+                                let name = std::ffi::CStr::from_ptr(name_ptr).to_string_lossy();
+                                if device_name == "*" || name.contains(device_name) {
+                                    (*device).config.scroll_factor = factor;
+                                    found = true;
+                                }
+                            }
+                            curr = next;
+                        }
+                        if found {
+                            "ok\n".to_string()
+                        } else {
+                            "error: no matching device found\n".to_string()
+                        }
+                    } else {
+                        "error: invalid scroll-factor value\n".to_string()
+                    }
+                } else {
+                    format!("error: unknown input command: {}\n", key)
+                }
+            }
             _ => format!("error: unknown command: {}\n", action),
         }
     }
+
+    pub unsafe fn apply_input_rules(&mut self) {
+        if self.server.is_null() {
+            return;
+        }
+        let devices_head = &mut (*self.server).input_manager.devices as *mut ffi::wl_list as *mut WlList;
+        let mut curr = (*devices_head).next;
+        while curr != devices_head {
+            let next = (*curr).next;
+            let device = crate::container_of!(curr, crate::input_device::InputDevice, link);
+            let name_ptr = (*(*device).wlr_device).name;
+            if !name_ptr.is_null() {
+                let name = std::ffi::CStr::from_ptr(name_ptr).to_string_lossy();
+                for rule in &self.input_rules {
+                    if rule.name == "*" || name.contains(&rule.name) {
+                        if let Some(factor) = rule.scroll_factor {
+                            (*device).config.scroll_factor = factor;
+                        }
+                    }
+                }
+            }
+            curr = next;
+        }
+    }
 }
 
 unsafe extern "C" fn handle_ipc_timer(data: *mut std::ffi::c_void) -> std::os::raw::c_int {