diff --git a/.dockerignore b/.dockerignore index 92245fe..604a1c7 100644 --- a/.dockerignore +++ b/.dockerignore @@ -5,3 +5,7 @@ !include !cmake/ !docker/startup.sh +!share/ + +# Rust bindings +!bindings/rust/ diff --git a/CMakeLists.txt b/CMakeLists.txt index f0182c5..67d3303 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -163,6 +163,13 @@ if (CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) install(FILES "${CMAKE_CURRENT_BINARY_DIR}/include/inputtino/${export_file_name}" COMPONENT libinputtino-dev DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/inputtino") + configure_file( + ${CMAKE_CURRENT_SOURCE_DIR}/share/pkgconfig/libinputtino.pc.in + ${CMAKE_CURRENT_BINARY_DIR}/libinputtino.pc + @ONLY + ) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/libinputtino.pc + DESTINATION ${CMAKE_INSTALL_DATAROOTDIR}/pkgconfig) endif () endif () diff --git a/bindings/rust/.dockerignore b/bindings/rust/.dockerignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/bindings/rust/.dockerignore @@ -0,0 +1 @@ +target/ diff --git a/bindings/rust/Cargo.toml b/bindings/rust/Cargo.toml index e1aa98f..8e85b06 100644 --- a/bindings/rust/Cargo.toml +++ b/bindings/rust/Cargo.toml @@ -1,10 +1,13 @@ [package] name = "inputtino" -version = "0.1.0" +version = "2024.8.1" edition = "2021" license = "MIT" rust-version = "1.72" links = "libinputtino" +homepage = "https://github.com/games-on-whales/inputtino" +authors = ["ABeltramo"] +description = "Rust bindings for inputtino" [lib] name = "inputtino_rs" @@ -13,3 +16,11 @@ path = "src/lib.rs" [build-dependencies] bindgen = "0.69.4" cmake = "0.1" +pkg-config = "0.3.30" + +[dev-dependencies] +input = "0.9.0" +rustix = { version = "0.38.18", features = ["fs"] } +approx = "0.5.1" +sdl2 = "0.37.0" +serial_test = "3.1.1" diff --git a/bindings/rust/build.rs b/bindings/rust/build.rs index ce2c5c9..b8f5883 100644 --- a/bindings/rust/build.rs +++ b/bindings/rust/build.rs @@ -1,4 +1,5 @@ extern crate bindgen; +extern crate pkg_config; use std::env; use std::path::PathBuf; @@ -6,26 +7,48 @@ use std::path::PathBuf; use cmake::Config; fn main() { - let build_static = false; + // Options + let build_c_bindings = env::var("INPUTTINO_BUILD_C_BINDINGS").unwrap_or("FALSE".to_string()) == "TRUE"; + let build_static = env::var("INPUTTINO_BUILD_STATIC").unwrap_or("FALSE".to_string()) == "TRUE"; - // This is the directory where the `c` library is located. - let libdir_path = PathBuf::from("../../") - // Canonicalize the path as `rustc-link-search` requires an absolute - // path. - .canonicalize() - .expect("cannot canonicalize path"); + // The bindgen::Builder is the main entry point + // to bindgen, and lets you build up options for + // the resulting bindings. + let mut bindings = bindgen::Builder::default() + .use_core() + .default_enum_style(bindgen::EnumVariation::Rust { + non_exhaustive: false, + }) + // Set the INPUTTINO_STATIC_DEFINE macro + .clang_arg(if build_static { "-D INPUTTINO_STATIC_DEFINE=1" } else { "" }) + // The input header we would like to generate bindings for. + .header("wrapper.hpp"); - // Compile the library using CMake - let dst = Config::new(libdir_path) - .target("libinputtino") - .define("BUILD_SHARED_LIBS", if build_static { "OFF" } else { "ON" }) - .define("LIBINPUTTINO_INSTALL", "ON") - .define("BUILD_TESTING", "OFF") - .define("BUILD_SERVER", "OFF") - .define("BUILD_C_BINDINGS", "ON") - .profile("Release") - .define("CMAKE_CONFIGURATION_TYPES", "Release") - .build(); + if build_c_bindings { + let libdir_path = PathBuf::from("../../") + // Canonicalize the path as `rustc-link-search` requires an absolute + // path. + .canonicalize() + .expect("cannot canonicalize path"); + + // Compile the library using CMake + let dst = Config::new(libdir_path) + .target("libinputtino") + .define("BUILD_SHARED_LIBS", if build_static { "OFF" } else { "ON" }) + .define("LIBINPUTTINO_INSTALL", "ON") + .define("BUILD_TESTING", "OFF") + .define("BUILD_SERVER", "OFF") + .define("BUILD_C_BINDINGS", "ON") + .profile("Release") + .define("CMAKE_CONFIGURATION_TYPES", "Release") + .build(); + + println!("cargo:rustc-link-search=native={}/lib", dst.display()); + bindings = bindings.clang_arg(format!("-I{}/include/", dst.display())) + } else { + let lib = pkg_config::probe_library("libinputtino").unwrap(); + bindings = bindings.clang_arg(format!("-I{}", lib.include_paths[0].display())); + } // Dependencies if !build_static { @@ -33,32 +56,13 @@ fn main() { println!("cargo:rustc-link-lib=stdc++"); } - //libinputtino - println!("cargo:rustc-link-search=native={}/lib", dst.display()); println!("cargo:rustc-link-lib={}libinputtino", if build_static { "static=" } else { "" }); - // The bindgen::Builder is the main entry point - // to bindgen, and lets you build up options for - // the resulting bindings. - let bindings = bindgen::Builder::default() - .use_core() - .default_enum_style(bindgen::EnumVariation::Rust { - non_exhaustive: false, - }) - // Add the include directory - .clang_arg(format!("-I{}/include/", dst.display())) - // Set the INPUTTINO_STATIC_DEFINE macro - .clang_arg(if build_static {"-D INPUTTINO_STATIC_DEFINE=1"} else {""}) - // The input header we would like to generate bindings for. - .header("wrapper.hpp") - // Finish the builder and generate the bindings. - .generate() - // Unwrap the Result and panic on failure. - .expect("Unable to generate bindings"); + let out = bindings.generate().expect("Unable to generate bindings"); // Write the bindings to the $OUT_DIR/bindings.rs file. let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()).join("bindings.rs"); - bindings + out .write_to_file(out_path) .expect("Couldn't write bindings!"); } diff --git a/bindings/rust/src/c_bindings.rs b/bindings/rust/src/c_bindings.rs new file mode 100644 index 0000000..37c7abd --- /dev/null +++ b/bindings/rust/src/c_bindings.rs @@ -0,0 +1,5 @@ +#![allow(non_upper_case_globals)] +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(dead_code)] +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/bindings/rust/src/common.rs b/bindings/rust/src/common.rs new file mode 100644 index 0000000..798ec27 --- /dev/null +++ b/bindings/rust/src/common.rs @@ -0,0 +1,76 @@ +use std::ffi::{CString}; +use crate::c_bindings; + +#[allow(dead_code)] +pub struct InputtinoDeviceDefinition { + pub def: c_bindings::InputtinoDeviceDefinition, + // Keep those around since we are passing them as pointers + name: CString, + phys: CString, + uniq: CString, +} + +impl InputtinoDeviceDefinition { + pub fn new(name: &str, vendor_id: u16, product_id: u16, version: u16, phys: &str, uniq: &str) -> Self { + let name = CString::new(name).unwrap(); + let phys = CString::new(phys).unwrap(); + let uniq = CString::new(uniq).unwrap(); + let def = c_bindings::InputtinoDeviceDefinition { + name: name.as_ptr(), + vendor_id: vendor_id, + product_id: product_id, + version: version, + device_phys: phys.as_ptr(), // TODO: optional, if not present random MAC address + device_uniq: uniq.as_ptr(), + }; + InputtinoDeviceDefinition { def, name, phys, uniq } + } +} + +pub unsafe extern "C" fn error_handler_fn(error_message: *const ::core::ffi::c_char, + user_data: *mut ::core::ffi::c_void) { + let error_str = std::ffi::CStr::from_ptr(error_message); + let user_data = user_data as *mut CString; + *user_data = CString::from(error_str); +} + + +#[macro_export] +macro_rules! get_nodes { + ( $fn_call:expr,$var:expr ) => { + { + let mut nodes_count: core::ffi::c_int = 0; + let nodes = $fn_call($var, &mut nodes_count); + if nodes.is_null() { + return Err("Failed to get nodes".to_string()); + } + + let mut result = Vec::new(); + for i in 0..nodes_count { + let node = std::ffi::CString::from_raw(*nodes.offset(i as isize)); + result.push(node.to_str().unwrap().to_string()); + } + Ok(result) + } + }; +} + +#[macro_export] +macro_rules! make_device { + ($fn_call:expr, $device:expr) => { + { + let error_str = std::ptr::null_mut(); + let error_handler = crate::c_bindings::InputtinoErrorHandler { + eh: Some(error_handler_fn), + user_data: error_str, + }; + let device = $fn_call(&$device.def, &error_handler); + if device.is_null() { // TODO: test this + let error_msg = (error_str as *mut std::ffi::CString).as_ref().unwrap().to_str().unwrap(); + Err("Failed to create Mouse: ".to_string() + error_msg) + } else { + Ok(device) + } + } + }; +} diff --git a/bindings/rust/src/joypad_switch.rs b/bindings/rust/src/joypad_switch.rs new file mode 100644 index 0000000..af04003 --- /dev/null +++ b/bindings/rust/src/joypad_switch.rs @@ -0,0 +1,72 @@ +use std::ffi::{c_int, c_void}; +use crate::c_bindings::{inputtino_joypad_switch_create, inputtino_joypad_switch_destroy, inputtino_joypad_switch_get_nodes, inputtino_joypad_switch_set_on_rumble, inputtino_joypad_switch_set_pressed_buttons, inputtino_joypad_switch_set_stick, inputtino_joypad_switch_set_triggers}; +use crate::common::{InputtinoDeviceDefinition, error_handler_fn}; +use crate::{get_nodes, make_device}; + +// re-export INPUTTINO_JOYPAD_BTN and INPUTTINO_JOYPAD_STICK_POSITION +pub use crate::c_bindings::{INPUTTINO_JOYPAD_BTN, INPUTTINO_JOYPAD_STICK_POSITION}; + +pub struct InputtinoSwitchJoypad { + joypad: *mut crate::c_bindings::InputtinoSwitchJoypad, + on_rumble_fn: Box ()>, +} + +impl InputtinoSwitchJoypad { + pub fn new(device: &InputtinoDeviceDefinition) -> Result { + unsafe { + let dev = make_device!(inputtino_joypad_switch_create, device); + match dev { + Ok(joypad) => { + Ok(InputtinoSwitchJoypad { joypad, on_rumble_fn: Box::new(|_, _| {}) }) + } + Err(e) => Err(e), + } + } + } + + pub fn get_nodes(&self) -> Result, String> { + unsafe { + get_nodes!(inputtino_joypad_switch_get_nodes, self.joypad) + } + } + + pub fn set_pressed(&self, buttons: i32) { + unsafe { + inputtino_joypad_switch_set_pressed_buttons(self.joypad, buttons); + } + } + + pub fn set_triggers(&self, left_trigger: i16, right_trigger: i16) { + unsafe { + inputtino_joypad_switch_set_triggers(self.joypad, left_trigger, right_trigger); + } + } + + pub fn set_stick(&self, stick_type: INPUTTINO_JOYPAD_STICK_POSITION, x: i16, y: i16) { + unsafe { + inputtino_joypad_switch_set_stick(self.joypad, stick_type, x, y); + } + } + + pub fn set_on_rumble(&mut self, on_rumble_fn: impl FnMut(i32, i32) -> () + 'static) { + self.on_rumble_fn = Box::new(on_rumble_fn); + unsafe { + let state_ptr = self as *const _ as *mut c_void; + inputtino_joypad_switch_set_on_rumble(self.joypad, Some(on_rumble_c_fn), state_ptr); + } + } +} + +impl Drop for InputtinoSwitchJoypad { + fn drop(&mut self) { + unsafe { + inputtino_joypad_switch_destroy(self.joypad); + } + } +} + +#[allow(dead_code)] +pub unsafe extern "C" fn on_rumble_c_fn(left_motor: c_int, right_motor: c_int, user_data: *mut ::core::ffi::c_void) { + let joypad: &mut InputtinoSwitchJoypad = &mut *(user_data as *mut InputtinoSwitchJoypad); + ((*joypad).on_rumble_fn)(left_motor, right_motor); +} diff --git a/bindings/rust/src/joypad_xbox.rs b/bindings/rust/src/joypad_xbox.rs new file mode 100644 index 0000000..f5b5dd1 --- /dev/null +++ b/bindings/rust/src/joypad_xbox.rs @@ -0,0 +1,72 @@ +use std::ffi::{c_int, c_void}; +use crate::{get_nodes, make_device}; +use crate::common::{InputtinoDeviceDefinition, error_handler_fn}; +use crate::c_bindings::{inputtino_joypad_xone_create, inputtino_joypad_xone_destroy, inputtino_joypad_xone_get_nodes, inputtino_joypad_xone_set_on_rumble, inputtino_joypad_xone_set_pressed_buttons, inputtino_joypad_xone_set_stick, inputtino_joypad_xone_set_triggers}; + +// re-export INPUTTINO_JOYPAD_BTN and INPUTTINO_JOYPAD_STICK_POSITION +pub use crate::c_bindings::{INPUTTINO_JOYPAD_BTN, INPUTTINO_JOYPAD_STICK_POSITION}; + +pub struct InputtinoXOneJoypad { + joypad: *mut crate::c_bindings::InputtinoXOneJoypad, + on_rumble_fn: Box ()>, +} + +impl InputtinoXOneJoypad { + pub fn new(device: &InputtinoDeviceDefinition) -> Result { + unsafe { + let dev = make_device!(inputtino_joypad_xone_create, device); + match dev { + Ok(joypad) => { + Ok(InputtinoXOneJoypad { joypad, on_rumble_fn: Box::new(|_, _| {}) }) + } + Err(e) => Err(e), + } + } + } + + pub fn get_nodes(&self) -> Result, String> { + unsafe { + get_nodes!(inputtino_joypad_xone_get_nodes, self.joypad) + } + } + + pub fn set_pressed(&self, buttons: i32) { + unsafe { + inputtino_joypad_xone_set_pressed_buttons(self.joypad, buttons); + } + } + + pub fn set_triggers(&self, left_trigger: i16, right_trigger: i16) { + unsafe { + inputtino_joypad_xone_set_triggers(self.joypad, left_trigger, right_trigger); + } + } + + pub fn set_stick(&self, stick_type: INPUTTINO_JOYPAD_STICK_POSITION, x: i16, y: i16) { + unsafe { + inputtino_joypad_xone_set_stick(self.joypad, stick_type, x, y); + } + } + + pub fn set_on_rumble(&mut self, on_rumble_fn: impl FnMut(i32, i32) -> () + 'static) { + self.on_rumble_fn = Box::new(on_rumble_fn); + unsafe { + let state_ptr = self as *const _ as *mut c_void; + inputtino_joypad_xone_set_on_rumble(self.joypad, Some(on_rumble_c_fn), state_ptr); + } + } +} + +impl Drop for InputtinoXOneJoypad { + fn drop(&mut self) { + unsafe { + inputtino_joypad_xone_destroy(self.joypad); + } + } +} + +#[allow(dead_code)] +pub unsafe extern "C" fn on_rumble_c_fn(left_motor: c_int, right_motor: c_int, user_data: *mut ::core::ffi::c_void) { + let joypad: &mut InputtinoXOneJoypad = &mut *(user_data as *mut InputtinoXOneJoypad); + ((*joypad).on_rumble_fn)(left_motor, right_motor); +} diff --git a/bindings/rust/src/keyboard.rs b/bindings/rust/src/keyboard.rs new file mode 100644 index 0000000..d24902a --- /dev/null +++ b/bindings/rust/src/keyboard.rs @@ -0,0 +1,45 @@ +use crate::common::{error_handler_fn, InputtinoDeviceDefinition}; +use crate::{get_nodes, make_device}; +use crate::c_bindings::{inputtino_keyboard_create, inputtino_keyboard_get_nodes, inputtino_keyboard_press, inputtino_keyboard_release, inputtino_keyboard_destroy}; + +pub struct InputtinoKeyboard { + kb: *mut crate::c_bindings::InputtinoKeyboard, +} + +impl InputtinoKeyboard { + pub fn new(device: &InputtinoDeviceDefinition) -> Result { + unsafe { + let dev = make_device!(inputtino_keyboard_create, device); + match dev { + Ok(kb) => Ok(InputtinoKeyboard { kb }), + Err(e) => Err(e), + } + } + } + + pub fn get_nodes(&self) -> Result, String> { + unsafe { + get_nodes!(inputtino_keyboard_get_nodes, self.kb) + } + } + + pub fn press_key(&self, key: i16) { + unsafe { + inputtino_keyboard_press(self.kb, key); + } + } + + pub fn release_key(&self, key: i16) { + unsafe { + inputtino_keyboard_release(self.kb, key); + } + } +} + +impl Drop for InputtinoKeyboard { + fn drop(&mut self) { + unsafe { + inputtino_keyboard_destroy(self.kb); + } + } +} diff --git a/bindings/rust/src/lib.rs b/bindings/rust/src/lib.rs index 03ef284..2be6c25 100644 --- a/bindings/rust/src/lib.rs +++ b/bindings/rust/src/lib.rs @@ -1,50 +1,8 @@ -#![allow(non_upper_case_globals)] -#![allow(non_camel_case_types)] -#![allow(non_snake_case)] - -include!(concat!(env!("OUT_DIR"), "/bindings.rs")); - -#[cfg(test)] -mod tests{ - - use std::ffi::{CStr, CString}; - use super::*; - - #[test] - fn test_inputtino_mouse(){ - let device_name = CString::new("Rusty Mouse").unwrap(); - let device_phys = CString::new("Rusty Mouse Phys").unwrap(); - let device_uniq = CString::new("Rusty Mouse Uniq").unwrap(); - let def = InputtinoDeviceDefinition { - name: device_name.as_ptr(), - vendor_id: 0, - product_id: 0, - version: 0, - device_phys: device_phys.as_ptr(), - device_uniq: device_uniq.as_ptr(), - }; - let _error_handler_fn = | error_message: *const ::core::ffi::c_char, user_data: *mut ::core::ffi::c_void | { - unsafe{ println!("Error: {:?}", CStr::from_ptr(error_message).to_str().unwrap()); } - }; - let error_handler = InputtinoErrorHandler { - eh: None, // TODO: InputtinoErrorHandlerFn::new(error_handler_fn) ??? - user_data: std::ptr::null_mut(), - }; - - unsafe{ - let mouse = inputtino_mouse_create(&def, &error_handler); - assert!(!mouse.is_null()); - - let mut nodes_count: core::ffi::c_int = 0; - let nodes = inputtino_mouse_get_nodes(mouse, & mut nodes_count); - assert!(nodes_count == 2); - assert!(!nodes.is_null()); - // Check that the nodes start with /dev/input/event - assert!(CString::from_raw(*nodes.offset(0)).to_str().unwrap().starts_with("/dev/input/event")); - assert!(CString::from_raw(*nodes.offset(1)).to_str().unwrap().starts_with("/dev/input/event")); - - inputtino_mouse_destroy(mouse); - } - } - -} +pub mod common; +pub mod mouse; +pub mod keyboard; +pub mod joypad_xbox; +pub mod joypad_switch; + +// Private low level automatic c bindings +mod c_bindings; diff --git a/bindings/rust/src/mouse.rs b/bindings/rust/src/mouse.rs new file mode 100644 index 0000000..9ddc9b4 --- /dev/null +++ b/bindings/rust/src/mouse.rs @@ -0,0 +1,113 @@ +use crate::{get_nodes, make_device}; +use crate::common::{InputtinoDeviceDefinition, error_handler_fn}; +use crate::c_bindings::{inputtino_mouse_create, inputtino_mouse_destroy, inputtino_mouse_get_nodes, inputtino_mouse_move, inputtino_mouse_move_absolute, inputtino_mouse_press_button, inputtino_mouse_release_button, inputtino_mouse_scroll_horizontal, inputtino_mouse_scroll_vertical}; + +pub use crate::c_bindings::{INPUTTINO_MOUSE_BUTTON}; + +pub struct InputtinoMouse { + mouse: *mut crate::c_bindings::InputtinoMouse, +} + +impl InputtinoMouse { + pub fn new(device: &InputtinoDeviceDefinition) -> Result { + unsafe { + let dev = make_device!(inputtino_mouse_create, device); + match dev { + Ok(mouse) => Ok(InputtinoMouse { mouse }), + Err(e) => Err(e), + } + } + } + + pub fn get_nodes(&self) -> Result, String> { + unsafe { + get_nodes!(inputtino_mouse_get_nodes, self.mouse) + } + } + + pub fn move_rel(&self, x: i32, y: i32) { + unsafe { + inputtino_mouse_move(self.mouse, x, y); + } + } + + pub fn move_abs(&self, x: i32, y: i32, screen_width: i32, screen_height: i32) { + unsafe { + inputtino_mouse_move_absolute(self.mouse, x, y, screen_width, screen_height); + } + } + + pub fn press_button(&self, button: INPUTTINO_MOUSE_BUTTON) { + unsafe { + inputtino_mouse_press_button(self.mouse, button); + } + } + + pub fn release_button(&self, button: INPUTTINO_MOUSE_BUTTON) { + unsafe { + inputtino_mouse_release_button(self.mouse, button); + } + } + + pub fn scroll_vertical(&self, amount: i32) { + unsafe { + inputtino_mouse_scroll_vertical(self.mouse, amount); + } + } + + pub fn scroll_horizontal(&self, amount: i32) { + unsafe { + inputtino_mouse_scroll_horizontal(self.mouse, amount); + } + } +} + +impl Drop for InputtinoMouse { + fn drop(&mut self) { + unsafe { + inputtino_mouse_destroy(self.mouse); + } + } +} + +#[cfg(test)] +mod tests { + use std::ffi::{CString}; + use super::*; + + #[test] + fn test_inputtino_c_mouse() { + let device_name = CString::new("Rusty Mouse").unwrap(); + let device_phys = CString::new("Rusty Mouse Phys").unwrap(); + let device_uniq = CString::new("Rusty Mouse Uniq").unwrap(); + let def = crate::c_bindings::InputtinoDeviceDefinition { + name: device_name.as_ptr(), + vendor_id: 0, + product_id: 0, + version: 0, + device_phys: device_phys.as_ptr(), + device_uniq: device_uniq.as_ptr(), + }; + // TODO: test this somehow + let error_str = std::ptr::null_mut(); + let error_handler = crate::c_bindings::InputtinoErrorHandler { + eh: Some(error_handler_fn), + user_data: error_str, + }; + + unsafe { + let mouse = inputtino_mouse_create(&def, &error_handler); + assert!(!mouse.is_null()); + + let mut nodes_count: core::ffi::c_int = 0; + let nodes = inputtino_mouse_get_nodes(mouse, &mut nodes_count); + assert_eq!(nodes_count, 2); + assert!(!nodes.is_null()); + // Check that the nodes start with /dev/input/event + assert!(CString::from_raw(*nodes.offset(0)).to_str().unwrap().starts_with("/dev/input/event")); + assert!(CString::from_raw(*nodes.offset(1)).to_str().unwrap().starts_with("/dev/input/event")); + + inputtino_mouse_destroy(mouse); + } + } +} diff --git a/bindings/rust/tests/common/mod.rs b/bindings/rust/tests/common/mod.rs new file mode 100644 index 0000000..2d135ee --- /dev/null +++ b/bindings/rust/tests/common/mod.rs @@ -0,0 +1,33 @@ +use std::os::fd::OwnedFd; +use std::path::Path; +use input::{Event, Libinput, LibinputInterface}; +use rustix::fs::{Mode, OFlags, open}; + +pub struct NixInterface; + +impl LibinputInterface for NixInterface { + fn open_restricted(&mut self, path: &Path, flags: i32) -> Result { + open(path, OFlags::from_bits_truncate(flags as u32), Mode::empty()) + .map_err(|err| err.raw_os_error()) + } + fn close_restricted(&mut self, fd: OwnedFd) { + let _ = fd; + } +} + +pub trait SyncEvent { + fn wait_next_event(&mut self) -> Option; +} + +impl SyncEvent for Libinput { + fn wait_next_event(&mut self) -> Option { + for _ in 0..10 { // loop maximum 10 times to avoid infinite loop + self.dispatch().unwrap(); + if let Some(event) = self.next() { + return Some(event); + } + std::thread::sleep(std::time::Duration::from_millis(50)); + } + return None; + } +} diff --git a/bindings/rust/tests/joypads.rs b/bindings/rust/tests/joypads.rs new file mode 100644 index 0000000..2516e8e --- /dev/null +++ b/bindings/rust/tests/joypads.rs @@ -0,0 +1,242 @@ +use serial_test::serial; +use inputtino_rs::common::InputtinoDeviceDefinition; +use inputtino_rs::joypad_switch::InputtinoSwitchJoypad; +use inputtino_rs::joypad_xbox::{InputtinoXOneJoypad, INPUTTINO_JOYPAD_BTN, INPUTTINO_JOYPAD_STICK_POSITION}; + +#[test] +#[serial] +fn test_xbox_joypad() { + let device = InputtinoDeviceDefinition::new("Rusty XOne controller", 0x045e, 0x02dd, 0x0100, "00:11:22:33:44", "00:11:22:33:44"); + let mut joypad = InputtinoXOneJoypad::new(&device).unwrap(); + + let nodes = joypad.get_nodes().unwrap(); + { + assert_eq!(nodes.len(), 2); + assert!(nodes[0].starts_with("/dev/input/event")); + assert!(nodes[1].starts_with("/dev/input/js")); + } + + let sdl = sdl2::init().unwrap(); + let joystick_subsystem = sdl.game_controller().unwrap(); + let mut sdl_js = joystick_subsystem.open(0).unwrap(); + let mut event_pump = sdl.event_pump().unwrap(); + + for event in event_pump.poll_iter() { + match event { + sdl2::event::Event::JoyDeviceAdded { which, .. } => { + assert_eq!(which, 0); + } + sdl2::event::Event::ControllerDeviceAdded { which, .. } => { + assert_eq!(which, 0); + } + _ => panic!("Unexpected event : {:?}", event), + } + } + + assert_eq!(sdl_js.name(), "Xbox One Controller"); + assert!(sdl_js.has_rumble()); + + { + joypad.set_pressed(INPUTTINO_JOYPAD_BTN::A as i32); + for event in event_pump.wait_timeout_iter(50) { + match event { + sdl2::event::Event::ControllerButtonDown { button, .. } => { + assert_eq!(button, sdl2::controller::Button::A); + } + sdl2::event::Event::JoyButtonDown { button_idx, .. } => { + assert_eq!(button_idx, sdl2::controller::Button::A as u8); + break; + } + _ => panic!("Unexpected event : {:?}", event), + } + } + } + + { + joypad.set_triggers(0, 0); + for event in event_pump.wait_timeout_iter(50) { + match event { + sdl2::event::Event::ControllerAxisMotion { axis, value, .. } => { + assert_eq!(axis, sdl2::controller::Axis::TriggerLeft); + assert_eq!(value, 0); + } + sdl2::event::Event::JoyAxisMotion { axis_idx, value, .. } => { + assert_eq!(axis_idx, sdl2::controller::Axis::TriggerLeft as u8); + assert_eq!(value, 0); + break; + } + _ => panic!("Unexpected event : {:?}", event), + } + } + } + + { + joypad.set_stick(INPUTTINO_JOYPAD_STICK_POSITION::LS, 0, 0); + for event in event_pump.wait_timeout_iter(50) { + match event { + sdl2::event::Event::ControllerAxisMotion { axis, value, .. } => { + assert_eq!(axis, sdl2::controller::Axis::LeftX); + assert_eq!(value, 0); + } + sdl2::event::Event::JoyAxisMotion { axis_idx, value, .. } => { + assert_eq!(axis_idx, sdl2::controller::Axis::LeftX as u8); + assert_eq!(value, 0); + break; + } + _ => panic!("Unexpected event : {:?}", event), + } + } + } + + { + joypad.set_stick(INPUTTINO_JOYPAD_STICK_POSITION::RS, 0, 0); + for event in event_pump.wait_timeout_iter(50) { + match event { + sdl2::event::Event::ControllerAxisMotion { axis, value, .. } => { + assert_eq!(axis, sdl2::controller::Axis::RightX); + assert_eq!(value, 0); + } + sdl2::event::Event::JoyAxisMotion { axis_idx, value, .. } => { + assert_eq!(axis_idx, sdl2::controller::Axis::RightX as u8); + assert_eq!(value, 0); + break; + } + _ => panic!("Unexpected event : {:?}", event), + } + } + } + + { + joypad.set_on_rumble(move |left, right| { + assert_eq!(left, 100); + assert_eq!(right, 200); + }); + let res = sdl_js.set_rumble(100, 200, 150); + assert!(res.is_ok()); + std::thread::sleep(std::time::Duration::from_millis(25)); + joypad.set_on_rumble(move |left, right| { + assert_eq!(left, 0); + assert_eq!(right, 0); + }); + std::thread::sleep(std::time::Duration::from_millis(125)); + } +} + +#[test] +#[serial] +fn test_switch_joypad() { + let device = InputtinoDeviceDefinition::new("Rusty Switch controller", 0x045e, 0x02dd, 0x0100, "00:11:22:33:44", "00:11:22:33:44"); + let mut joypad = InputtinoSwitchJoypad::new(&device).unwrap(); + + let nodes = joypad.get_nodes().unwrap(); + { + assert_eq!(nodes.len(), 2); + assert!(nodes[0].starts_with("/dev/input/event")); + assert!(nodes[1].starts_with("/dev/input/js")); + } + + let sdl = sdl2::init().unwrap(); + let joystick_subsystem = sdl.game_controller().unwrap(); + let mut sdl_js = joystick_subsystem.open(0).unwrap(); + let mut event_pump = sdl.event_pump().unwrap(); + + for event in event_pump.poll_iter() { + match event { + sdl2::event::Event::JoyDeviceAdded { which, .. } => { + assert_eq!(which, 0); + } + sdl2::event::Event::ControllerDeviceAdded { which, .. } => { + assert_eq!(which, 0); + } + _ => panic!("Unexpected event : {:?}", event), + } + } + + assert_eq!(sdl_js.name(), "Xbox One Controller"); + assert!(sdl_js.has_rumble()); + + { + joypad.set_pressed(INPUTTINO_JOYPAD_BTN::A as i32); + for event in event_pump.wait_timeout_iter(50) { + match event { + sdl2::event::Event::ControllerButtonDown { button, .. } => { + assert_eq!(button, sdl2::controller::Button::B); + } + sdl2::event::Event::JoyButtonDown { button_idx, .. } => { + assert_eq!(button_idx, sdl2::controller::Button::B as u8); + break; + } + _ => panic!("Unexpected event : {:?}", event), + } + } + } + + { + joypad.set_triggers(0, 0); + for event in event_pump.wait_timeout_iter(50) { + match event { + sdl2::event::Event::ControllerAxisMotion { axis, value, .. } => { + assert_eq!(axis, sdl2::controller::Axis::TriggerLeft); + assert_eq!(value, 0); + } + sdl2::event::Event::JoyAxisMotion { axis_idx, value, .. } => { + assert_eq!(axis_idx, sdl2::controller::Axis::TriggerLeft as u8); + assert_eq!(value, 0); + break; + } + _ => panic!("Unexpected event : {:?}", event), + } + } + } + + { + joypad.set_stick(INPUTTINO_JOYPAD_STICK_POSITION::LS, 0, 0); + for event in event_pump.wait_timeout_iter(50) { + match event { + sdl2::event::Event::ControllerAxisMotion { axis, value, .. } => { + assert_eq!(axis, sdl2::controller::Axis::LeftX); + assert_eq!(value, 0); + } + sdl2::event::Event::JoyAxisMotion { axis_idx, value, .. } => { + assert_eq!(axis_idx, sdl2::controller::Axis::LeftX as u8); + assert_eq!(value, 0); + break; + } + _ => panic!("Unexpected event : {:?}", event), + } + } + } + + { + joypad.set_stick(INPUTTINO_JOYPAD_STICK_POSITION::RS, 0, 0); + for event in event_pump.wait_timeout_iter(50) { + match event { + sdl2::event::Event::ControllerAxisMotion { axis, value, .. } => { + assert_eq!(axis, sdl2::controller::Axis::RightX); + assert_eq!(value, 0); + } + sdl2::event::Event::JoyAxisMotion { axis_idx, value, .. } => { + assert_eq!(axis_idx, sdl2::controller::Axis::RightX as u8); + assert_eq!(value, 0); + break; + } + _ => panic!("Unexpected event : {:?}", event), + } + } + } + + { + joypad.set_on_rumble(move |left, right| { + assert_eq!(left, 100); + assert_eq!(right, 200); + }); + let res = sdl_js.set_rumble(100, 200, 150); + assert!(res.is_ok()); + std::thread::sleep(std::time::Duration::from_millis(25)); + joypad.set_on_rumble(move |left, right| { + assert_eq!(left, 0); + assert_eq!(right, 0); + }); + std::thread::sleep(std::time::Duration::from_millis(125)); + } +} diff --git a/bindings/rust/tests/keyboard.rs b/bindings/rust/tests/keyboard.rs new file mode 100644 index 0000000..2113218 --- /dev/null +++ b/bindings/rust/tests/keyboard.rs @@ -0,0 +1,61 @@ +use input::{Event, Libinput}; +use input::event::{DeviceEvent}; +use input::event::keyboard::KeyboardEventTrait; +use inputtino_rs::{common::InputtinoDeviceDefinition, + keyboard::InputtinoKeyboard}; +mod common; +use crate::common::{NixInterface, SyncEvent}; + + +#[test] +fn test_inputtino_keyboard() { + let device = InputtinoDeviceDefinition::new("Rusty Keyboard", 0xAB, 0xCD, 0xEF, "Rusty Keyboard Phys", "Rusty Keyboard Uniq"); + let keyboard = InputtinoKeyboard::new(&device).unwrap(); + let nodes = keyboard.get_nodes().unwrap(); + { + assert_eq!(nodes.len(), 1); + + // Check that the nodes start with /dev/input/event + assert!(nodes[0].starts_with("/dev/input/event")); + } + + let mut input = Libinput::new_from_path(NixInterface); + let kb_dev = input.path_add_device(nodes[0].as_str()).expect("to get the device"); + + { + assert_eq!(kb_dev.name(), "Rusty Keyboard"); + assert_eq!(kb_dev.id_vendor(), 0xAB); + assert_eq!(kb_dev.id_product(), 0xCD); + for event in &mut input { + assert!(matches!(event, Event::Device(DeviceEvent::Added(_)))); + } + } + + { // Test keyboard key press + keyboard.press_key(0x41); // KEY_A + + let ev = input.wait_next_event().unwrap(); + assert!(matches!(ev, Event::Keyboard(_))); + match ev { + Event::Keyboard(ev) => { + assert_eq!(ev.key(), 30); // KEY_A + assert_eq!(ev.key_state(), input::event::keyboard::KeyState::Pressed); + } + _ => unreachable!(), + } + } + + { // Test keyboard key release + keyboard.release_key(0x41); + + let ev = input.wait_next_event().unwrap(); + assert!(matches!(ev, Event::Keyboard(_))); + match ev { + Event::Keyboard(ev) => { + assert_eq!(ev.key(), 30); // KEY_A + assert_eq!(ev.key_state(), input::event::keyboard::KeyState::Released); + } + _ => unreachable!(), + } + } +} diff --git a/bindings/rust/tests/mouse.rs b/bindings/rust/tests/mouse.rs new file mode 100644 index 0000000..dfff4e2 --- /dev/null +++ b/bindings/rust/tests/mouse.rs @@ -0,0 +1,125 @@ +#[macro_use] +extern crate approx; + +use inputtino_rs::{ + mouse::{InputtinoMouse, INPUTTINO_MOUSE_BUTTON}, + common::InputtinoDeviceDefinition, +}; +use input::{Event, Libinput}; +use input::event::{DeviceEvent, PointerEvent}; +use input::event::pointer::{Axis, ButtonState}; + +mod common; +use crate::common::{NixInterface, SyncEvent}; + +#[test] +fn test_inputtino_mouse() { + let device = InputtinoDeviceDefinition::new("Rusty Mouse", 0xAB, 0xCD, 0xEF, "Rusty Mouse Phys", "Rusty Mouse Uniq"); + let mouse = InputtinoMouse::new(&device).unwrap(); + let nodes = mouse.get_nodes().unwrap(); + { + assert_eq!(nodes.len(), 2); + + // Check that the nodes start with /dev/input/event + assert!(nodes[0].starts_with("/dev/input/event")); + assert!(nodes[1].starts_with("/dev/input/event")); + } + + let mut input = Libinput::new_from_path(NixInterface); + let dev_rel = input.path_add_device(nodes[0].as_str()).expect("to get the device"); + let dev_abs = input.path_add_device(nodes[1].as_str()).expect("to get the device"); + + { + assert_eq!(dev_rel.name(), "Rusty Mouse"); + assert_eq!(dev_abs.name(), "Rusty Mouse (absolute)"); + assert_eq!(dev_rel.id_vendor(), 0xAB); + assert_eq!(dev_abs.id_vendor(), 0xAB); + assert_eq!(dev_rel.id_product(), 0xCD); + assert_eq!(dev_abs.id_product(), 0xCD); + for event in &mut input { + assert!(matches!(event, Event::Device(DeviceEvent::Added(_)))); + } + } + + { // Test mouse relative motion + mouse.move_rel(10, 20); + + let ev = input.wait_next_event().unwrap(); + assert!(matches!(ev, Event::Pointer(PointerEvent::Motion(_)))); + match ev { + Event::Pointer(PointerEvent::Motion(ev)) => { + assert_eq!(ev.dx_unaccelerated(), 10.0); + assert_eq!(ev.dy_unaccelerated(), 20.0); + } + _ => unreachable!(), + } + } + + { // Test mouse absolute motion + mouse.move_abs(100, 200, 1920, 1080); + + let ev = input.wait_next_event().unwrap(); + assert!(matches!(ev, Event::Pointer(PointerEvent::MotionAbsolute(_)))); + match ev { + Event::Pointer(PointerEvent::MotionAbsolute(ev)) => { + assert_relative_eq!(ev.absolute_x_transformed(1920), 100.0, max_relative=0.1); + assert_relative_eq!(ev.absolute_y_transformed(1080), 200.0, max_relative=0.1); + } + _ => unreachable!(), + } + } + + { // Test mouse button press + mouse.press_button(INPUTTINO_MOUSE_BUTTON::LEFT); + + let ev = input.wait_next_event().unwrap(); + assert!(matches!(ev, Event::Pointer(PointerEvent::Button(_)))); + match ev { + Event::Pointer(PointerEvent::Button(ev)) => { + assert_eq!(ev.button(), 272); + assert_eq!(ev.button_state(), ButtonState::Pressed); + } + _ => unreachable!(), + } + } + + { + mouse.release_button(INPUTTINO_MOUSE_BUTTON::LEFT); + + let ev = input.wait_next_event().unwrap(); + assert!(matches!(ev, Event::Pointer(PointerEvent::Button(_)))); + match ev { + Event::Pointer(PointerEvent::Button(ev)) => { + assert_eq!(ev.button(), 272); + assert_eq!(ev.button_state(), ButtonState::Released); + } + _ => unreachable!(), + } + } + + { + mouse.scroll_vertical(100); + + let ev = input.wait_next_event().unwrap(); + assert!(matches!(ev, Event::Pointer(PointerEvent::ScrollWheel(_)))); + match ev { + Event::Pointer(PointerEvent::ScrollWheel(ev)) => { + assert_eq!(ev.scroll_value_v120(Axis::Vertical), -100.0); + } + _ => unreachable!(), + } + } + + { + mouse.scroll_horizontal(100); + + let ev = input.wait_next_event().unwrap(); + assert!(matches!(ev, Event::Pointer(PointerEvent::ScrollWheel(_)))); + match ev { + Event::Pointer(PointerEvent::ScrollWheel(ev)) => { + assert_eq!(ev.scroll_value_v120(Axis::Horizontal), 100.0); + } + _ => unreachable!(), + } + } +} diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..87f4d1c --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,70 @@ +ARG BASE_IMAGE=ghcr.io/games-on-whales/base:edge + +#################################### +FROM $BASE_IMAGE AS build-libinputtino + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + ccache \ + ninja-build \ + cmake \ + clang \ + pkg-config \ + git \ + libevdev-dev \ + && rm -rf /var/lib/apt/lists/* + + +COPY . /inputtino/ +WORKDIR /inputtino + +ENV CCACHE_DIR=/cache/ccache +ENV CMAKE_BUILD_DIR=/cache/cmake-build +RUN --mount=type=cache,target=/cache/ccache \ + cmake -B$CMAKE_BUILD_DIR \ + -DCMAKE_INSTALL_PREFIX:PATH=/usr \ + -DCMAKE_BUILD_TYPE=Release \ + -DBUILD_TESTING=OFF \ + -DBUILD_C_BINDINGS=ON \ + -DLIBINPUTTINO_INSTALL=ON \ + -DBUILD_SHARED_LIBS=ON \ + -G Ninja && \ + ninja -C $CMAKE_BUILD_DIR install + +#################################### +FROM $BASE_IMAGE AS base-libinputtino + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends libevdev2 \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build-libinputtino /usr/include/inputtino /usr/include/inputtino +COPY --from=build-libinputtino /usr/share/pkgconfig/libinputtino.pc /usr/share/pkgconfig/libinputtino.pc +COPY --from=build-libinputtino /usr/lib/x86_64-linux-gnu/liblibinputtino* /usr/lib/x86_64-linux-gnu/ + +#################################### +FROM base-libinputtino AS build-rust-bindings + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update -y && \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + pkg-config \ + build-essential \ + clang \ + curl \ + && curl https://sh.rustup.rs -sSf | bash -s -- -y \ + && rm -rf /var/lib/apt/lists/* + +ENV PATH="${HOME}/.cargo/bin:${PATH}" + +COPY ./bindings/rust /inputtino-rust +WORKDIR /inputtino-rust + + +RUN cargo build --release + diff --git a/share/pkgconfig/libinputtino.pc.in b/share/pkgconfig/libinputtino.pc.in new file mode 100644 index 0000000..eb05b2e --- /dev/null +++ b/share/pkgconfig/libinputtino.pc.in @@ -0,0 +1,17 @@ +prefix=@CMAKE_INSTALL_PREFIX@ +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@/@PROJECT_NAME@ + +prefix=@CMAKE_INSTALL_PREFIX@ +exec_prefix=@CMAKE_INSTALL_PREFIX@ +libdir=@INSTALL_LIB_DIR@ +includedir=${prefix}/@CMAKE_INSTALL_INCLUDEDIR@/@PROJECT_NAME@ + +Name: libinputtino +Description: @PROJECT_DESCRIPTION@ +URL: @PROJECT_HOMEPAGE_URL@ +Version: @PROJECT_VERSION@ + +Requires: +Libs: -L${libdir} -llibevdev +Cflags: -I${includedir} + diff --git a/src/uinput/mouse.cpp b/src/uinput/mouse.cpp index 30b15f5..7c4b549 100644 --- a/src/uinput/mouse.cpp +++ b/src/uinput/mouse.cpp @@ -63,14 +63,14 @@ static Result create_mouse(const DeviceDefinition &device) return libevdev_uinput_ptr{uidev, ::libevdev_uinput_destroy}; } -static Result create_mouse_abs() { +static Result create_mouse_abs(const DeviceDefinition &device) { libevdev *dev = libevdev_new(); libevdev_uinput *uidev; - libevdev_set_name(dev, "Wolf mouse (abs) virtual device"); - libevdev_set_id_vendor(dev, 0xAB00); - libevdev_set_id_product(dev, 0xAB02); - libevdev_set_id_version(dev, 0xAB00); + libevdev_set_name(dev, (device.name + " (absolute)").c_str()); + libevdev_set_id_vendor(dev, device.vendor_id); + libevdev_set_id_product(dev, device.product_id); + libevdev_set_id_version(dev, device.version); libevdev_set_id_bustype(dev, BUS_USB); libevdev_enable_property(dev, INPUT_PROP_DIRECT); @@ -114,7 +114,7 @@ Result Mouse::create(const DeviceDefinition &device) { return Error(mouse_rel_or_error.getErrorMessage()); } - auto mouse_abs_or_error = create_mouse_abs(); + auto mouse_abs_or_error = create_mouse_abs(device); if (mouse_abs_or_error) { mouse._state->mouse_abs = std::move(*mouse_abs_or_error); } else { diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 68a926d..2c370d7 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,5 @@ +include(FetchContent) + # Testing library FetchContent_Declare( Catch2