Repository: Arvamer/gilrs Branch: master Commit: 78581b7d5842 Files: 64 Total size: 427.7 KB Directory structure: gitextract_3om1ehps/ ├── .gitignore ├── .gitlab-ci.yml ├── .gitmodules ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── appveyor.yml ├── gilrs/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── build.rs │ ├── examples/ │ │ ├── ev.rs │ │ ├── ff.rs │ │ ├── ff_pos.rs │ │ ├── gamepad_info.rs │ │ ├── gui.rs │ │ └── wasm/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── index.html │ │ ├── wasm_gui.ps1 │ │ └── wasm_gui.sh │ └── src/ │ ├── constants.rs │ ├── ev/ │ │ ├── filter.rs │ │ ├── mod.rs │ │ └── state.rs │ ├── ff/ │ │ ├── base_effect.rs │ │ ├── effect_source.rs │ │ ├── mod.rs │ │ ├── server.rs │ │ └── time.rs │ ├── gamepad.rs │ ├── lib.rs │ ├── mapping/ │ │ ├── mod.rs │ │ └── parser.rs │ └── utils.rs ├── gilrs-core/ │ ├── CHANGELOG.md │ ├── Cargo.toml │ ├── README.md │ ├── examples/ │ │ └── ev_core.rs │ └── src/ │ ├── lib.rs │ ├── platform/ │ │ ├── default/ │ │ │ ├── ff.rs │ │ │ ├── gamepad.rs │ │ │ └── mod.rs │ │ ├── linux/ │ │ │ ├── ff.rs │ │ │ ├── gamepad.rs │ │ │ ├── ioctl.rs │ │ │ ├── mod.rs │ │ │ └── udev.rs │ │ ├── macos/ │ │ │ ├── ff.rs │ │ │ ├── gamepad.rs │ │ │ ├── io_kit.rs │ │ │ └── mod.rs │ │ ├── mod.rs │ │ ├── wasm/ │ │ │ ├── ff.rs │ │ │ ├── gamepad.rs │ │ │ └── mod.rs │ │ ├── windows_wgi/ │ │ │ ├── ff.rs │ │ │ ├── gamepad.rs │ │ │ └── mod.rs │ │ └── windows_xinput/ │ │ ├── ff.rs │ │ ├── gamepad.rs │ │ └── mod.rs │ └── utils.rs └── rustfmt.toml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ target Cargo.lock !.gitignore ================================================ FILE: .gitlab-ci.yml ================================================ image: "rust:1.80" variables: CARGO_HOME: $CI_PROJECT_DIR/cargo GIT_SUBMODULE_STRATEGY: normal prepare:lockfile: stage: .pre image: "rust:1" script: - CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback cargo generate-lockfile artifacts: paths: - Cargo.lock test:x86_64-unknown-linux-gnu: stage: test before_script: - apt-get update -yqq && apt-get install -yqq libudev-dev # eframe dependencies for testing gui example - apt-get install -yqq libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libspeechd-dev libxkbcommon-dev libssl-dev - rustc -Vv && cargo -Vv script: - cargo test --verbose --all --features serde-serialize test:i686-unknown-linux-gnu: stage: test variables: PKG_CONFIG_ALLOW_CROSS: "1" before_script: - dpkg --add-architecture i386 - apt-get update -yqq && apt-get install -yqq gcc-multilib libudev-dev libudev1:i386 libudev-dev:i386 - rustc -Vv && cargo -Vv - rustup target add i686-unknown-linux-gnu script: - cargo test --verbose --target=i686-unknown-linux-gnu --lib --features serde-serialize build:wasm32-unknown-unknown: image: "rust:1.80" stage: test before_script: - rustc -Vv && cargo -Vv - rustup target add wasm32-unknown-unknown script: - cargo test --no-run --target wasm32-unknown-unknown --all --features serde-serialize check:x86_64-apple-darwin: stage: test before_script: - rustc -Vv && cargo -Vv - rustup target add x86_64-apple-darwin script: - cargo check --target=x86_64-apple-darwin --verbose --all --features serde-serialize ================================================ FILE: .gitmodules ================================================ [submodule "SDL_GameControllerDB"] path = gilrs/SDL_GameControllerDB url = https://github.com/gabomdq/SDL_GameControllerDB.git ================================================ FILE: Cargo.toml ================================================ [workspace] resolver = "2" members = [ "gilrs" ] ================================================ FILE: LICENSE-APACHE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: LICENSE-MIT ================================================ Copyright (c) 2016-2018 Mateusz Sieczko and other GilRs Developers Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ GilRs - Game Input Library for Rust =================================== [![pipeline status](https://gitlab.com/gilrs-project/gilrs/badges/master/pipeline.svg)](https://gitlab.com/gilrs-project/gilrs/commits/master) [![Crates.io](https://img.shields.io/crates/v/gilrs.svg)](https://crates.io/crates/gilrs) [![Documentation](https://docs.rs/gilrs/badge.svg)](https://docs.rs/gilrs/) [![Minimum rustc version](https://img.shields.io/badge/rustc-1.80.0+-yellow.svg)](https://gitlab.com/gilrs-project/gilrs) GilRs abstract platform specific APIs to provide unified interfaces for working with gamepads. Main features: - Unified gamepad layout—buttons and axes are represented by familiar names - Support for SDL2 mappings including `SDL_GAMECONTROLLERCONFIG` environment variable which Steam uses - Hotplugging—GilRs will try to assign new IDs for new gamepads and reuse the same ID for gamepads which reconnected - Force feedback (rumble) - Power information (is gamepad wired, current battery status) The project's main repository [is on GitLab](https://gitlab.com/gilrs-project/gilrs) although there is also a [GitHub mirror](https://github.com/Arvamer/gilrs). Please use GitLab's issue tracker and merge requests. This repository contains submodule; after you clone it, don't forget to run `git submodule init; git submodule update` (or clone with `--recursive` flag) or you will get compile errors. Example ------- ```toml [dependencies] gilrs = "0.11.0" ``` ```rust use gilrs::{Gilrs, Button, Event}; let mut gilrs = Gilrs::new().unwrap(); // Iterate over all connected gamepads for (_id, gamepad) in gilrs.gamepads() { println!("{} is {:?}", gamepad.name(), gamepad.power_info()); } let mut active_gamepad = None; loop { // Examine new events while let Some(Event { id, event, time, .. }) = gilrs.next_event() { println!("{:?} New event from {}: {:?}", time, id, event); active_gamepad = Some(id); } // You can also use cached gamepad state if let Some(gamepad) = active_gamepad.map(|id| gilrs.gamepad(id)) { if gamepad.is_pressed(Button::South) { println!("Button South is pressed (XBox - A, PS - X)"); } } } ``` Supported features ------------------ | | Input | Hotplugging | Force feedback | |------------------|:-----:|:-----------:|:--------------:| | Linux/BSD (evdev)| ✓ | ✓ | ✓ | | Windows | ✓ | ✓ | ✓ | | OS X | ✓ | ✓ | ✕ | | Wasm | ✓ | ✓ | n/a | | Android | ✕ | ✕ | ✕ | Platform specific notes ====================== Linux/BSD (evdev) ----- With evdev, GilRs read (and write, in case of force feedback) directly from appropriate `/dev/input/event*` file. This mean that user have to have read and write access to this file. On most distros it shouldn't be a problem, but if it is, you will have to create udev rule. On FreeBSD generic HID gamepads use hgame(4) and special use Linux driver via `webcamd`. To build GilRs, you will need pkg-config and libudev .pc file. On some distributions this file is packaged in separate archive (e.g., `libudev-dev` in Debian, `libudev-devd` in FreeBSD). Windows ----- Windows defaults to using Windows Gaming Input instead of XInput. If you need to use XInput you can disable the `wgi` feature (it's enabled by default) and enable the `xinput` feature. Windows Gaming Input requires an in focus window to be associated with the process to receive events. You can still switch back to using xInput by turning off default features and enabling the xinput feature. Note: Some (Older?) devices may still report inputs without a window but this is not the case for all devices so if you are writing a terminal based game, use the xinput feature instead. Wasm ----- Wasm implementation uses stdweb, or wasm-bindgen with the wasm-bindgen feature. For stdweb, you will need [cargo-web](https://github.com/koute/cargo-web) to build gilrs for wasm32-unknown-unknown. For wasm-bindgen, you will need the wasm-bindgen cli or a tool like [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/). Unlike other platforms, events are only generated when you call `Gilrs::next_event()`. See [`./gilrs/examples/wasm/README.md`](./gilrs/examples/wasm/README.md) for running the examples using Wasm. License ======= This project is licensed under the terms of both the Apache License (Version 2.0) and the MIT license. See LICENSE-APACHE and LICENSE-MIT for details. ================================================ FILE: appveyor.yml ================================================ # Based on the "trust" template v0.1.1 # https://github.com/japaric/trust/tree/v0.1.1 image: - Visual Studio 2022 environment: global: RUST_VERSION: 1.80.0 CRATE_NAME: gilrs matrix: - TARGET: x86_64-pc-windows-msvc install: - ps: >- If ($Env:TARGET -eq 'x86_64-pc-windows-gnu') { $Env:PATH += ';C:\msys64\mingw64\bin' } ElseIf ($Env:TARGET -eq 'i686-pc-windows-gnu') { $Env:PATH += ';C:\msys64\mingw32\bin' } - curl -sSf -o rustup-init.exe https://win.rustup.rs/ - rustup-init.exe -y --default-host %TARGET% --default-toolchain %RUST_VERSION% - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin - rustup toolchain install stable - rustc -Vv - cargo -V - git submodule update --init --recursive - set CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS=fallback - cargo +stable generate-lockfile - set CARGO_RESOLVER_INCOMPATIBLE_RUST_VERSIONS= test_script: - cargo test --verbose --target %TARGET% cache: - C:\Users\appveyor\.cargo\registry - target # Building is done in the test phase, so we disable Appveyor's build phase. build: false ================================================ FILE: gilrs/CHANGELOG.md ================================================ Change Log ========== See also [gilrs-core changelog](../gilrs-core/CHANGELOG.md). v0.11.0 - 2024-09-15 ---------- ### Breaking changes - Mark Error enums, `EventType` and `Event` as `non_exhaustive` ### Added - Added `EventType::ForceFeedbackEffectCompleted` ### Changed - Minimal supported Rust version is now 1.73 - Updated dependencies - Updated bundled mappings ### Fixed - Fixed potential overflow in `btn_value` v0.10.6 - 2024-03-16 ---------- ### Fixed - `axis_dpad_to_button` filter will now properly generate release event when axis value change from -1 to 1 (or 1 to -1) while skipping 0. v0.10.5 - 2024-03-06 ---------- ### Added - Added `vendor_id()` and `product_id()` to `Gamepad`. v0.10.4 - 2023-12-03 ---------- ### Fixed - Fixed `Gilrs::set_mapping*` returning `MappingError::NotConnected` for connected gamepads. - Fix not setting other axis to 0 when in deadzone range. v0.10.3 - 2023-11-11 ---------- ### Changed - All thread spawned by gilrs are now named. (!102) - MSRV is now 1.65. v0.10.2 - 2023-04-23 ---------- ### Added - `Gilrs::next_event_blocking()` ### Fixed - Parse more SDL specific buttons in mappings - Recognize "xinput" UUID in mappings - Parse axis ranges in button mappings v0.10.1 - 2022-11-13 ---------- ### Added - Supporting files and documentation for running the GUI example using Wasm in a browser. See [examples/wasm/README.md](./examples/wasm/README.md) ### Changed - Bundled SDL mappings are now filtered by platform, reducing binary size. ### Fixed - GUI example crash when the current platform does not support force feedback. v0.10.0 - 2022-11-06 -------------------- ### Changed - Windows now defaults to using Windows Gaming Input instead of xinput. If you need to use xInput you can disable the `wgi` feature (It's enabled by default) and enable the `xinput` feature. ``` toml gilrs = {version = "0.10.0", default-features = false, features = ["wgi"]} ``` - Apps on Windows will now require a focused window to receive inputs by default. This is a limitation of Windows Gaming Input. It requires an in focus Window be associated with the process to receive events. You can still switch back to using xInput by turning off default features and enabling the `xinput` feature. Note: Some (Older?) devices may still report inputs without a window but this is not the case for all devices so if you are writing a terminal based game, use the `xinput` feature instead. - Minimal supported rust version is now 1.64. v0.9.0 - 2022-05-22 ------------------- ### Changed - wasm: web-sys/wasm-bindgen is now used by default, dependency on stdweb and `wasm-bindgen` feature are removed. - Minimal supported rust version is now 1.56. - Changed `impl Into\ for GamepadId` to `impl From\ for usize` ### Fixed - wasm: `next_event()` no longer panic if `getGamepads()` is not available. v0.8.2 - 2021-12-30 ------------------- ### Changed - Minimal supported rust version is now 1.47 - `axis_dpad_to_btn` now also emits `ButtonChanged` events ### Fixed - Fixed overflow when calculating axis value and min/max range was i32::MIN/MAX (@wathiede) v0.8.1 - 2021-03-30 ------------------- ### Changed - Updated bundled mappings v0.8.0 - 2020-10-09 ------------------- ### Added - `Jitter`, `Repeat`, `GilrsBuilder`, and `Mapping` now implement `Default`. - Errors now implement `source()`. - `Code` now implements `Deserialize` and `Serialize` (@theunkn0wn1). - Dpad is now supported on macOS (@cleancut). ### Changed - Minimal supported version is now 1.40 - Non exhaustive enums now use `#[non_exhaustive]` instead of hidden variant. - Renamed cargo feature `serde` to `serde-serialize`. - Improved conversion of axis value to float. Values like 127 (when axis range is 0-255) will now be correctly converted to 0.0. ### Removed - Errors now longer implement deprecated methods (`source()` and `description()`). v0.7.4 - 2020-02-06 ------------------- ### Added - Added method to stop playing force feedback effect. (@photex) ### Fixed - Fixed bug that caused forced feedback effects to never stop. (@photex) v0.7.3 - 2019-11-30 ------------------- ### Added - Added support for serialization and deserialization for `Button`, `Axis` and `AxisOrButton` with optional `serde` feature (@aleksijuvani). ### Fixed - Fixed defaults mappings containing elements that gamepad doesn't have. This also fixes state not working for `LeftTrigger` button on Windows. v0.7.2 - 2019-08-06 ------------------- ### Fixed - Fixed loading mappings for wrong platform v0.7.1 - 2019-03-04 ------------------- ### Fixed - Compilation on macOS. - xinput: Calling `set_ff_state()` on devices that were never connected. - `GamepadId` was not reexported from private module. v0.7.0 - 2019-02-21 ------------------- ### Added - `GamepadId` - `Gilrs::gamepad(id)`. This function is replacement for `Index` operator and can return disconnected gamepads. - Initial support for macOS (@jtakakura). There are still some functionality missing, check related issues in #58. - Wasm support, using stdweb (@ryanisaacg). ### Changed - Change `Gamepad::uuid -> Uuid` to `Gamepad::uuid -> [u8; 16]` - `gilrs` now uses `gilrs-core` crate as backend. Because of it, there are some breaking changes to API. - Functions that returned `&Gamepad` now return `Gamepad<'_>` proxy object. - Renamed `Gilrs::get(id)` to `Gilrs::connected_gamepad(id)`. - Moved `Gamepad::set_mapping{,_strict}()` to `Gilrs`. These functions now also take gamepad id as additional argument. - Minimal supported version is now 1.31.1. The crate can still be build with older rustc, but it may change during next patch release. - Instead using `usize` for gamepad ID, `GamepadId` is now used. - Updated bundled SDL_GameControllerDB. ### Removed - All functions that returned `&mut Gamepad`. - `Gilrs` no longer implements `Index` and `IndexMut` operators. Use `Gilrs::gamepad(id)` instead. - `Gamepad::status()` and `Status` enum. `Gamepad::is_connected()` is now sufficient to determine status of gamepad. ### Fixed - xinput: Incorrect gamepad ID when more than one gamepad is connected (@DTibbs). - Deadzone filter no longer emits additional events. This resulted in emitting more events until values normalized on some, often unrelated (like 0 for axis around 0.5), value. - Mappings from environment variable had lower priority than bundled mappings. v0.6.1 - 2018-07-18 ------------------- ### Added - `ev::Code::into_u32()` (@rukai). - `ev::{Button, Axis, AxisOrBtn}` now implements `Hash` (@sheath). ### Changed - The URL of repository has changed to https://gitlab.com/gilrs-project/gilrs - Updated bundled SDL_GameControllerDB. ### Fixed - Various fixes to logging at incorrect log level. Thanks to @fuggles for locating and reporting these issues. - Possible panic in `Repeat` filter. - `Axis::DPadY` was inverted on Linux. v0.6.0 - 2018-02-11 ------------------- ### Added - Support for parsing SLD 2.0.6 mappings. - `ButtonChanged` event. It contains value in range [0.0, 1.0]. - `GilrsBuilder::set_axis_to_btn()`. It allow to customize on which values `ButtonePressed` and `ButtonReleased` are emitted. - `GilrsBuilder::set_update_state` which control whether gamepad state should be updated automatically. - `ButtonState::value()`. - `Mapping::insert_{btn,axis}()`. - `Gampead::os_name()` and `Gamepad::map_name()`. (@rukai) - `GilrsBuilder::add_env_mappings()` and `GilrsBuilder::add_included_mappings()`, allow to configure whether to load mappings from `SDL_GAMECONTROLLERCONFIG` env and bundled mappings. (@rukai) - `Gilrs::insert_event()`. - `Axis::second_axis()` – returns the other axis of gamepad element. For example, this function will return `LeftStickX` for `LeftStickY`. ### Removed - `Mapping` no longer implements `Index` and `IndexMut` operators. Use `Mapping::insert_{btn,axis}()` methods to add new mappings. - `Axis::{LeftTrigger, LeftTrigger2, RightTrigger, RightTrigger2}`. All events with these are now button events. `ButtonChanged` event contains value. - `Gilrs::gamepad()` and `Gilrs::gamepad_mut()` – use `Index` operator instead. ### Changed - Gilrs now require Rust 1.20.0 or newer. - Updated bundled mappings. - Renamed `Filter::filter` to `Filter::filter_ev` because RFC 2124 added `filter` method to `Option` (our `Filter` is implemented for `Option`). - `Gamepad::deadzone()` now returns `Option` instead of `f32`. - All axis events are now in range [-1.0, 1.0]. - `NativeEvCode` is replaced by `ev::Code`, a strongly typed struct that also distinguish between axes and buttons. - You can now create mappings from any axis to any button. - `State` now tracks floating-point value of buttons. - `State::value()` can now be used to also examine value of buttons. - By default, gamepad state is updated automatically. If you customize event filters, you can disable this behaviour using `GilrsBuilder::set_update_state`. - `Gilrs::new()` and `GilrsBuilder::build()` now returns `Result`. Dummy context can still be used, but only if result of failure is unsupported platform. - Renamed `Gilrs::connected_gamepad()` and `Gilrs::connected_gamepad_mut()` to `get()` and `get_mut()`. - `Filter` and `FilterFn` now borrows `Gilrs` mutably. - Windows: Gamepads are now named "Xbox Controller" instead of "XInput Controller". (@rukai) ### Fixed - Incorrect ranges for some axes. - Deadzone filter should no longer produce values outside of allowed range. - When calculating deadzone, the value of second axis is no longer ignored. This fixes situation, when sometimes axis would stay on value small to 0.0, when it should be 0.0 instead. - Deadzone threshold was half of what it should be. - Linux: Fixed axis value normalization if neither minimal value is 0 nor midpoint is 0. (@scottpleb) - Linux: Ensure that axis values are clamped after normalization. (@scottpleb) - Linux: Compilation error on architectures with `c_char = u8`. v0.5.0 - 2017-09-24 ------------------- ### Added - `Mapping::remove_button()` and `Mapping::remove_axis()`. - `GilrsBuilder` for customizing how `Gilrs` is created. - Event filters. See `ev::filter` module for more info. - `Gilrs::next_event()` - use it with `while let` loop in your event loop. This allow to avoid borrow checker problems that `EventIterator` caused. - New event – `Dropped`. Used by filters to indicate that you should ignore this event. - New event – `ButtonRepeated`. Can be emitted by `Repeat` filter. - `Axis::{DPadX, DPadY}` - `Gamepad::{button_name, axis_name, button_code, axis_code}` functions for accessing mapping data. - `Gamepad::axis_data, button_data` – part of new extended gamepad state. - `Gamepad::id()` – returns gamepad ID. - `Gilrs::update, inc, counter, reset_counter` – part of new extended gamepad state. ### Removed - `Gilrs::with_mappings()` – use `GilrsBuilder`. - `Gilrs::poll_events()` and `EventIterator` – use `Gilrs::next_event()` instead. ### Changed - Minimal rust version is now 1.19 - New gamepad state. Now can store state for any button or axis (previously was only useful for named buttons and axes). Additionally it now also know when last event happened. Basic usage with `is_pressed()` and `value()` methods is same, but check out documentation for new features. - Gamepad state now must be explicitly updated with `Gilrs::update(Event)`. This change was necessary because filters can change events. - `Event` is now a struct and contains common information like id of gamepad and timestamp (new). Old enum was renamed to `EventType` and can be accessed from `Event.event` public field. - New force feedback module, including support for Windows. There are to many changes to list them all here, so pleas check documentation and examples. - Renamed `ff::Error::EffectNotSupported` to `ff::Error::NotSupported`. - `Button::Unknown` and `Axis::Unknown` have now value of 0. - `Gamepad::set_mapping()` (and `_strict` variant) now returns error when creating mapping with `Button::Unknown` or `Axis::Unknown`. Additionally `_strict` version does not allow `Button::{C, Z}` and Axis::{LeftZ, RightZ}. - xinput: New values for `NativEvCode` ### Fixed - Panic on `unreachable!()` when creating mapping with `Button::{C, Z, Unknown}` or `Axis::{LeftZ, RightZ}`. v0.4.4 — 2017-06-16 ------------------- ### Changed - Gilrs no longer uses `ioctl` crate on Linux. Because `ioctl` was deprecated and all versions yanked, it was causing problems for new builds that didn't have `ioctl` crate listed in Cargo.lock. v0.4.3 — 2017-03-12 ------------------- ### Added - You can now iterate over mutable references to connected gamepads using `Gilrs::gamepads_mut()`. ### Fixed - Fixed `unreachable!()` panic on 32bit Linux - Improved converting axes values to `f32` when using XInput v0.4.2 - 2017-01-15 ------------------- ### Changed - Updated SDL_GameControllerDB to latest revision. - Changes in axes values that are less than 1% are now ignored. ### Fixed - Fixed multiple axes mapped to same axis name when mappings are incomplete. - Values returned with `AxisChanged` event now have correctly applied deadzones. - Linux: Correctly handle event queue overrun. v0.4.1 - 2016-12-12 ------------------- ### Fixed - Type inference error introduced by generic index in `<[T]>::get` v0.4.0 - 2016-12-11 ------------------- ### Added - `Gamepad::mappings_source(&self)` which can be used to filter gamepads which not provide unified controller layout - `MappingsSource` enum - You can now set custom mapping for gamepad with `Gamepad::set_mapping(…)` - `Gilrs::with_mappings(&str)` to create Gilrs with additional gamepad mappings ### Changed - Button and axis events now also have native event codes - On Linux, if button or axis is not known, is now reported as `Unknown` (previously all unknown events have been ignored) - More devices are now treated as gamepads on Linux (use `mappings_source()` to filter unwanted gamepads) - Renamed `{Gamepad,GamepadState}::is_btn_pressed(Button)` to `is_pressed(Button)` - Renamed `{Gamepad,GamepadState}::axis_val(Axis)` to `value(Axis)` ### Fixed - Integer overflow if button with keyboard code was pressed on Linux - `Gilrs` should no longer panic if there are some unexpected problems with Udev - Fixed normalization of axes values on Linux v0.3.1 - 2016-09-23 ------------------- ### Fixed - Fixed compilation error on non-x86_64 Linux v0.3.0 - 2016-09-22 ------------------- ### Added - `Gamepad::power_info(&self)` - `ff::Direction::from_radians(f32)` and `ff::Direction::from_vector([f32; 2])` - `Gilrs::gamepads(&self)` which returns iterator over all connected gamepads - `GamepadState` now implements `is_btn_pressed(Button)` and `axis_val(Axis)` - `Gilrs` now implements `Index`and `IndexMut` ### Changed - Rename `Button::Unknow` to `Button::Unknown` - `Gamepad::name(&self)` now returns `&str` instead of `&String` - Improved dead zone detection - `Effect::play(&self, u16)` now returns `Result<(), Error>` - Linux: Reduced memory usage ### Removed - `ff::Direction` no longer implements `From` ### Fixed - Buttons west and east are no longer swapped when using SDL2 mappings - Linux: infinite loop after gamepad disconnects - Linux: SDL2 mappings for gamepads that can also report mouse and keyboard events now should works v0.2.0 - 2016-08-18 ------ ### Changed - Rename `Gilrs::pool_events()` to `Gilrs::poll_events()` ### Fixed - Linux: Disconnected events are now emitted properly - Linux: All force feedback effects are now dropped when gamepad disconnects ================================================ FILE: gilrs/Cargo.toml ================================================ [package] name = "gilrs" version = "0.11.0" authors = ["Arvamer "] license = "Apache-2.0/MIT" exclude = ["controller.svg"] description = "Game Input Library for Rust" documentation = "https://docs.rs/gilrs/" repository = "https://gitlab.com/gilrs-project/gilrs" readme = "../README.md" keywords = ["gamepad", "joystick", "input"] categories = ["game-engines"] edition = "2021" rust-version = "1.80.0" [badges] gitlab = { repository = "gilrs-project/gilrs" } [dependencies] vec_map = "0.8" uuid = "1.0.0" log = "0.4.1" fnv = "1.0" serde = { version = "1.0", features = ["derive"], optional = true } gilrs-core = { path = "../gilrs-core", version = "0.6.0", default-features = false } [dev-dependencies] eframe = "0.30.0" env_logger = "0.11.5" console_log = "1.0.0" egui_plot = "0.30.0" web-sys = "0.3.76" wasm-bindgen-futures = "0.4.49" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] # fixed to avoid msrv bump to 1.81 -- can remove once we're past that home = "=0.5.9" [target.'cfg(target_arch = "wasm32")'.dev-dependencies] console_error_panic_hook = "0.1.7" [package.metadata.docs.rs] features = ["serde-serialize"] [features] default = ["wgi"] serde-serialize = ["serde", "gilrs-core/serde-serialize"] xinput = ["gilrs-core/xinput"] wgi = ["gilrs-core/wgi"] ================================================ FILE: gilrs/build.rs ================================================ //! This build script takes the gamecontrollerdb.txt from the SDL repo and removes any //! mappings that aren't for the current platform and removes comments etc. //! //! This reduces the binary size fairly significantly compared to including mappings for every //! platform. //! Especially Wasm since it doesn't use SDL mappings and binary size is important. use std::env; use std::fs::File; use std::io::{BufRead, BufReader, Write}; use std::path::{Path, PathBuf}; #[cfg(windows)] const PATH_SEPARATOR: &str = "backslash"; #[cfg(not(windows))] const PATH_SEPARATOR: &str = "slash"; fn main() { println!("cargo:rustc-check-cfg=cfg(path_separator, values(\"slash\",\"backslash\"))"); println!(r#"cargo:rustc-cfg=path_separator="{}""#, PATH_SEPARATOR); let out_dir = env::var("OUT_DIR").unwrap(); let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap(); let sdl_platform = "platform:".to_string() + match env::var("CARGO_CFG_TARGET_FAMILY").unwrap().as_str() { "unix" => match env::var("CARGO_CFG_TARGET_OS").unwrap().as_str() { "android" => "Android", "macos" => "Mac OS X", _ => "Linux", }, "windows" => "Windows", "wasm" => "Web", _ => "Unknown", }; let sdl_game_controller_db_path: PathBuf = PathBuf::from_iter(vec!["SDL_GameControllerDB", "gamecontrollerdb.txt"]); // Tell cargo to re-run this script only when SDL's gamecontrollerdb.txt changes. println!( "cargo:rerun-if-changed={}", sdl_game_controller_db_path.to_string_lossy() ); let mut new_file = File::create(Path::new(&out_dir).join("gamecontrollerdb.txt")) .expect("failed to create gamecontrollerdb.txt for target"); let path = Path::new(&cargo_manifest_dir).join(sdl_game_controller_db_path); let original_file = File::open(&path).unwrap_or_else(|_| { panic!( "Could not open gamecontrollerdb.txt {:?}. Did you forget to pull the \ `SDL_GameControllerDB` submodule?", &path ) }); let original_reader = BufReader::new(original_file); original_reader .lines() .map(|x| match x { Ok(x) => x, Err(e) => panic!("Failed to read line from gamecontrollerdb.txt: {e}"), }) .filter(|line| { line.trim_end() .trim_end_matches(',') .ends_with(&sdl_platform) }) .for_each(|line| { let mut line = line; line.push('\n'); new_file .write_all(line.as_bytes()) .expect("Failed to write line to gamecontrollerdb.txt in OUT_DIR"); }); } ================================================ FILE: gilrs/examples/ev.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use gilrs::ev::filter::{Filter, Repeat}; use gilrs::GilrsBuilder; use std::process; fn main() { env_logger::init(); let mut gilrs = match GilrsBuilder::new().set_update_state(false).build() { Ok(g) => g, Err(gilrs::Error::NotImplemented(g)) => { eprintln!("Current platform is not supported"); g } Err(e) => { eprintln!("Failed to create gilrs context: {}", e); process::exit(-1); } }; let repeat_filter = Repeat::new(); loop { while let Some(ev) = gilrs .next_event_blocking(None) .filter_ev(&repeat_filter, &mut gilrs) { gilrs.update(&ev); println!("{:?}", ev); } if gilrs.counter() % 25 == 0 { for (id, gamepad) in gilrs.gamepads() { println!( "Power info of gamepad {}({}): {:?}", id, gamepad.name(), gamepad.power_info() ); } } gilrs.inc(); } } ================================================ FILE: gilrs/examples/ff.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use gilrs::ff::{BaseEffect, BaseEffectType, EffectBuilder, Replay, Ticks}; use gilrs::Gilrs; use std::thread; use std::time::Duration; fn main() { env_logger::init(); let mut gilrs = Gilrs::new().unwrap(); let support_ff = gilrs .gamepads() .filter_map(|(id, gp)| if gp.is_ff_supported() { Some(id) } else { None }) .collect::>(); let duration = Ticks::from_ms(150); let effect = EffectBuilder::new() .add_effect(BaseEffect { kind: BaseEffectType::Strong { magnitude: 60_000 }, scheduling: Replay { play_for: duration, with_delay: duration * 3, ..Default::default() }, envelope: Default::default(), }) .add_effect(BaseEffect { kind: BaseEffectType::Weak { magnitude: 60_000 }, scheduling: Replay { after: duration * 2, play_for: duration, with_delay: duration * 3, }, ..Default::default() }) .gamepads(&support_ff) .finish(&mut gilrs) .unwrap(); effect.play().unwrap(); thread::sleep(Duration::from_secs(11)); } ================================================ FILE: gilrs/examples/ff_pos.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use gilrs::ff::{BaseEffect, BaseEffectType, DistanceModel, EffectBuilder}; use gilrs::{Axis, Button, EventType, Gilrs}; use std::io::{self, Write}; use std::thread; use std::time::Duration; #[derive(Copy, Clone, PartialEq, Debug)] enum Modify { DistModel, RefDistance, RolloffFactor, MaxDistance, } impl Modify { fn next(&mut self) { use crate::Modify::*; *self = match *self { DistModel => RefDistance, RefDistance => RolloffFactor, RolloffFactor => MaxDistance, MaxDistance => DistModel, }; print!("\x1b[2K\r{:?}", self); io::stdout().flush().unwrap(); } fn prev(&mut self) { use crate::Modify::*; *self = match *self { DistModel => MaxDistance, RefDistance => DistModel, RolloffFactor => RefDistance, MaxDistance => RolloffFactor, }; print!("\x1b[2K\r{:?}", self); io::stdout().flush().unwrap(); } } fn main() { env_logger::init(); let mut gilrs = Gilrs::new().unwrap(); println!("Connected gamepads:"); let mut support_ff = Vec::new(); for (idx, gp) in gilrs.gamepads() { let ff = gp.is_ff_supported(); println!( "{}) {} ({})", idx, gp.name(), if ff { "Force feedback supported" } else { "Force feedback not supported" } ); if ff { support_ff.push(idx); } } println!("----------------------------------------"); println!( "Use sticks to move listener. Triggers change properties of distance model. South/west \ button changes active property. Press east button on action pad to quit." ); let pos1 = [-100.0, 0.0, 0.0]; let mut effect_builder = EffectBuilder::new() .add_effect(BaseEffect { kind: BaseEffectType::Strong { magnitude: 45_000 }, ..Default::default() }) .add_effect(BaseEffect { kind: BaseEffectType::Weak { magnitude: 45_000 }, ..Default::default() }) .distance_model(DistanceModel::None) .gamepads(&support_ff) .clone(); let left_effect = effect_builder.position(pos1).finish(&mut gilrs).unwrap(); left_effect.play().unwrap(); println!("Playing one effects…"); println!("Position of effect sources: {:?}", pos1); let mut listeners = support_ff .iter() .map(|&idx| (idx, [0.0, 0.0, 0.0])) .collect::>(); let mut ref_distance = 10.0; let mut rolloff_factor = 0.5; let mut max_distance = 100.0; let mut modify = Modify::DistModel; let mut model = 0usize; 'main: loop { while let Some(event) = gilrs.next_event() { match event.event { EventType::ButtonReleased(Button::East, ..) => break 'main, EventType::ButtonReleased(Button::South, ..) => modify.next(), EventType::ButtonReleased(Button::West, ..) => modify.prev(), EventType::ButtonReleased(Button::LeftTrigger, ..) if modify == Modify::DistModel => { model = model.wrapping_sub(1); } EventType::ButtonReleased(Button::RightTrigger, ..) if modify == Modify::DistModel => { model = model.wrapping_add(1); } _ => (), } } for &mut (idx, ref mut pos) in &mut listeners { let velocity = 0.5; let gp = gilrs.gamepad(idx); let (sx, sy) = (gp.value(Axis::LeftStickX), gp.value(Axis::LeftStickY)); if sx.abs() > 0.5 || sy.abs() > 0.5 { if sx.abs() > 0.5 { pos[0] += velocity * sx.signum(); } if sy.abs() > 0.5 { pos[1] += velocity * sy.signum(); } gilrs.gamepad(idx).set_listener_position(*pos).unwrap(); let dist = ((pos[0] - pos1[0]).powi(2) + (pos[1] - pos1[1]).powi(2)).sqrt(); print!( "\x1b[2K\rPosition of listener {:2} has changed: [{:6.1}, {:6.1}].Distance: \ {:.1}", idx, pos[0], pos[1], dist ); io::stdout().flush().unwrap(); } let x = if gp.is_pressed(Button::LeftTrigger) { -1.0 } else if gp.is_pressed(Button::RightTrigger) { 1.0 } else { continue; }; match modify { Modify::RolloffFactor => rolloff_factor += x * velocity * 0.1, Modify::RefDistance => ref_distance += x * velocity * 0.1, Modify::MaxDistance => max_distance += x * velocity * 1.0, Modify::DistModel => (), // DistanceModel handled in event loop } let model = match model % 4 { 0 => DistanceModel::None, 1 => DistanceModel::LinearClamped { ref_distance, rolloff_factor, max_distance, }, 2 => DistanceModel::InverseClamped { ref_distance, rolloff_factor, max_distance, }, 3 => DistanceModel::ExponentialClamped { ref_distance, rolloff_factor, max_distance, }, _ => unreachable!(), }; match left_effect.set_distance_model(model) { Ok(()) => print!("\x1b[2K\r{:?}", model), Err(e) => print!("\x1b[2K\r{}", e), } io::stdout().flush().unwrap(); } thread::sleep(Duration::from_millis(16)); } } ================================================ FILE: gilrs/examples/gamepad_info.rs ================================================ use gilrs::{Axis, Button, Gilrs}; use uuid::Uuid; fn main() { env_logger::init(); let gilrs = Gilrs::new().unwrap(); for (id, gamepad) in gilrs.gamepads() { println!( r#"Gamepad {id} ({name}): Map name: {map_name:?} Os name: {os_name} UUID: {uuid} Is connected: {is_connected} Power info: {power_info:?} Mapping source: {mapping_source:?} Is ff supported: {ff} Deadzone Left X: {dlx:?} Deadzone Left Y: {dly:?} Deadzone Right X: {drx:?} Deadzone Right Y: {dry:?} Deadzone Left Trigger: {dlt:?} Deadzone Right Trigger: {drt:?} Deadzone Left Trigger 2: {dlt2:?} Deadzone Right Trigger 2: {drt2:?} "#, id = id, name = gamepad.name(), map_name = gamepad.map_name(), os_name = gamepad.os_name(), uuid = Uuid::from_bytes(gamepad.uuid()).as_hyphenated(), is_connected = gamepad.is_connected(), power_info = gamepad.power_info(), mapping_source = gamepad.mapping_source(), ff = gamepad.is_ff_supported(), dlx = gamepad .axis_code(Axis::LeftStickX) .and_then(|code| gamepad.deadzone(code)), dly = gamepad .axis_code(Axis::LeftStickY) .and_then(|code| gamepad.deadzone(code)), drx = gamepad .axis_code(Axis::RightStickX) .and_then(|code| gamepad.deadzone(code)), dry = gamepad .axis_code(Axis::RightStickY) .and_then(|code| gamepad.deadzone(code)), dlt = gamepad .button_code(Button::LeftTrigger) .and_then(|code| gamepad.deadzone(code)), drt = gamepad .button_code(Button::RightTrigger) .and_then(|code| gamepad.deadzone(code)), dlt2 = gamepad .button_code(Button::LeftTrigger2) .and_then(|code| gamepad.deadzone(code)), drt2 = gamepad .button_code(Button::RightTrigger2) .and_then(|code| gamepad.deadzone(code)), ); } } ================================================ FILE: gilrs/examples/gui.rs ================================================ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release use eframe::egui; use eframe::egui::Vec2; use egui::RichText; use egui_plot::{MarkerShape, Plot, PlotPoints, Points}; use gilrs::ev::AxisOrBtn; use gilrs::ff::{BaseEffect, BaseEffectType, Effect, EffectBuilder, Repeat, Ticks}; use gilrs::{Axis, GamepadId, Gilrs, GilrsBuilder}; use gilrs_core::PowerInfo; use std::time::UNIX_EPOCH; use uuid::Uuid; struct MyEguiApp { gilrs: Gilrs, current_gamepad: Option, log_messages: [Option; 300], // These will be none if Force feedback isn't supported for this platform e.g. Wasm ff_strong: Option, ff_weak: Option, } impl Default for MyEguiApp { fn default() -> Self { #[cfg(target_arch = "wasm32")] console_log::init().unwrap(); const INIT: Option = None; let mut gilrs = GilrsBuilder::new().set_update_state(false).build().unwrap(); let ff_strong = EffectBuilder::new() .add_effect(BaseEffect { kind: BaseEffectType::Strong { magnitude: 60_000 }, scheduling: Default::default(), envelope: Default::default(), }) .repeat(Repeat::For(Ticks::from_ms(100))) .finish(&mut gilrs) .ok(); let ff_weak = EffectBuilder::new() .add_effect(BaseEffect { kind: BaseEffectType::Weak { magnitude: 60_000 }, scheduling: Default::default(), envelope: Default::default(), }) .repeat(Repeat::For(Ticks::from_ms(100))) .finish(&mut gilrs) .ok(); Self { gilrs, current_gamepad: None, log_messages: [INIT; 300], ff_strong, ff_weak, } } } impl MyEguiApp { fn log(&mut self, message: String) { self.log_messages[0..].rotate_right(1); self.log_messages[0] = Some(message); } } impl MyEguiApp { fn new(_cc: &eframe::CreationContext<'_>) -> Self { Self::default() } } impl eframe::App for MyEguiApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { while let Some(event) = self.gilrs.next_event() { self.log(format!( "{} : {} : {:?}", event .time .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis(), event.id, event.event )); self.gilrs.update(&event); if self.current_gamepad.is_none() { self.current_gamepad = Some(event.id); } } egui::SidePanel::left("side_panel").show(ctx, |ui| { ui.heading("Controllers"); ui.separator(); for (id, gamepad) in self.gilrs.gamepads() { if ui .selectable_label( self.current_gamepad == Some(id), format!("{id}: {}", gamepad.name()), ) .clicked() { self.current_gamepad = Some(id); }; } ui.allocate_space(ui.available_size()); }); egui::TopBottomPanel::bottom("log") .resizable(true) .default_height(200.0) .show(ctx, |ui| { ui.heading("Event Log"); egui::ScrollArea::vertical() .max_height(ui.available_height()) .show(ui, |ui| { for message in self.log_messages.iter().flatten() { ui.label(message); } ui.allocate_space(ui.available_size()); }); }); egui::CentralPanel::default().show(ctx, |ui| { egui::ScrollArea::both().show(ui, |ui| { if let Some(gamepad_id) = self.current_gamepad { let gamepad = self.gilrs.gamepad(gamepad_id); let gamepad_state = gamepad.state(); ui.horizontal(|ui| { ui.vertical(|ui| { ui.heading("Info"); egui::Grid::new("info_grid") .striped(true) .num_columns(2) .show(ui, |ui| { ui.label("Name"); ui.label(gamepad.name()); ui.end_row(); if let Some(vendor) = gamepad.vendor_id() { ui.label("Vendor ID"); ui.label(format!("{vendor:04x}")); ui.end_row(); } if let Some(product) = gamepad.product_id() { ui.label("Product ID"); ui.label(format!("{product:04x}")); ui.end_row(); } ui.label("Gilrs ID"); ui.label(gamepad.id().to_string()); ui.end_row(); if let Some(map_name) = gamepad.map_name() { ui.label("Map Name"); ui.label(map_name); ui.end_row(); } ui.label("Map Source"); ui.label(format!("{:?}", gamepad.mapping_source())); ui.end_row(); ui.label("Uuid"); let uuid = Uuid::from_bytes(gamepad.uuid()).to_string(); ui.horizontal(|ui| { ui.label(&uuid); if ui.button("Copy").clicked() { ui.output_mut(|platform_output| { platform_output.copied_text = uuid; }); } }); ui.end_row(); ui.label("Power"); ui.label(match gamepad.power_info() { PowerInfo::Unknown => "Unknown".to_string(), PowerInfo::Wired => "Wired".to_string(), PowerInfo::Discharging(p) => format!("Discharging {p}"), PowerInfo::Charging(p) => format!("Charging {p}"), PowerInfo::Charged => "Charged".to_string(), }); ui.end_row(); }); }); if gamepad.is_ff_supported() { ui.vertical(|ui| { ui.label("Force Feedback"); if let Some(ff_strong) = &self.ff_strong { if ui.button("Play Strong").clicked() { ff_strong.add_gamepad(&gamepad).unwrap(); ff_strong.play().unwrap(); } } if let Some(ff_weak) = &self.ff_weak { if ui.button("Play Weak").clicked() { ff_weak.add_gamepad(&gamepad).unwrap(); ff_weak.play().unwrap(); } } }); } }); ui.horizontal(|ui| { ui.vertical(|ui| { ui.set_width(300.0); ui.heading("Buttons"); for (code, button_data) in gamepad_state.buttons() { let name = match gamepad.axis_or_btn_name(code) { Some(AxisOrBtn::Btn(b)) => format!("{b:?}"), _ => "Unknown".to_string(), }; ui.add( egui::widgets::ProgressBar::new(button_data.value()).text( RichText::new(format!( "{name:<14} {:<5} {:.4} {}", button_data.is_pressed(), button_data.value(), code )) .monospace(), ), ); } }); ui.vertical(|ui| { ui.set_width(300.0); ui.heading("Axes"); ui.horizontal(|ui| { for (name, x, y) in [ ("Left Stick", Axis::LeftStickX, Axis::LeftStickY), ("Right Stick", Axis::RightStickX, Axis::RightStickY), ] { ui.vertical(|ui| { ui.label(name); let y_axis = gamepad .axis_data(y) .map(|a| a.value()) .unwrap_or_default() as f64; let x_axis = gamepad .axis_data(x) .map(|a| a.value()) .unwrap_or_default() as f64; Plot::new(format!("{name}_plot")) .width(150.0) .height(150.0) .min_size(Vec2::splat(3.25)) .include_x(1.25) .include_y(1.25) .include_x(-1.25) .include_y(-1.25) .allow_drag(false) .allow_zoom(false) .allow_boxed_zoom(false) .allow_scroll(false) .show(ui, |plot_ui| { plot_ui.points( Points::new(PlotPoints::new(vec![[ x_axis, y_axis, ]])) .shape(MarkerShape::Circle) .radius(4.0), ); }); }); } }); for (code, axis_data) in gamepad_state.axes() { let name = match gamepad.axis_or_btn_name(code) { None => code.to_string(), Some(AxisOrBtn::Btn(b)) => format!("{b:?}"), Some(AxisOrBtn::Axis(a)) => format!("{a:?}"), }; ui.add( egui::widgets::ProgressBar::new( (axis_data.value() * 0.5) + 0.5, ) .text( RichText::new(format!( "{:+.4} {name:<15} {}", axis_data.value(), code )) .monospace(), ), ); } }); }); } else { ui.label("Press a button on a controller or select it from the left."); } ui.allocate_space(ui.available_size()); }); }); ctx.request_repaint(); } } #[cfg(not(target_arch = "wasm32"))] fn main() { env_logger::init(); let native_options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default().with_inner_size(Vec2::new(1024.0, 768.0)), ..Default::default() }; let _ = eframe::run_native( "Gilrs Input Tester", native_options, Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))), ); } #[cfg(target_arch = "wasm32")] fn main() { use eframe::wasm_bindgen::JsCast; console_error_panic_hook::set_once(); let web_options = eframe::WebOptions::default(); wasm_bindgen_futures::spawn_local(async { let document = web_sys::window() .expect("No window") .document() .expect("No document"); let canvas = document .get_element_by_id("the_canvas_id") .expect("Failed to find the_canvas_id") .dyn_into::() .expect("the_canvas_id was not a HtmlCanvasElement"); let _ = eframe::WebRunner::new() .start( canvas, web_options, Box::new(|cc| Ok(Box::new(MyEguiApp::new(cc)))), ) .await; }); } ================================================ FILE: gilrs/examples/wasm/.gitignore ================================================ target/ ================================================ FILE: gilrs/examples/wasm/README.md ================================================ # Wasm Example These are instructions for running the GUI example in your web browser using Wasm. Currently only the GUI example is set up to run with Wasm. ### Ubuntu requirements ```bash sudo apt install build-essential sudo apt-get install libssl-dev pkg-config ``` ### Setup ```pwsh rustup target add wasm32-unknown-unknown cargo install wasm-bindgen-cli cargo install basic-http-server ``` ### Build and Run Run these from the workspace root. ```pwsh cargo build --release --example gui --target wasm32-unknown-unknown wasm-bindgen --out-name wasm_example --out-dir gilrs/examples/wasm/target --target web target/wasm32-unknown-unknown/release/examples/gui.wasm basic-http-server gilrs/examples/wasm ``` Now open your web browser and navigate to http://127.0.0.1:4000 ================================================ FILE: gilrs/examples/wasm/index.html ================================================ Gilrs Example ================================================ FILE: gilrs/examples/wasm/wasm_gui.ps1 ================================================ ### This script can be used instead of the "Build and Run" step in `./gilrs/examples/wasm/README.md`. ### Useful for gilrs devs that want a single script to to point their IDE to for run configurations. ### Supports Powershell 5 and up on Windows or Linux ### Make sure to run the install steps from the readme first. # Start at this script's path and go up three levels to the workspace root. # Ensures a consistent path regardless of the working directory when you run the script. $Path = $PSScriptRoot | Split-Path | Split-Path | Split-Path $ProjectDir = Resolve-Path $Path Set-Location $ProjectDir cargo build --release --example gui --target wasm32-unknown-unknown wasm-bindgen --out-name wasm_example --out-dir gilrs/examples/wasm/target --target web target/wasm32-unknown-unknown/release/examples/gui.wasm basic-http-server gilrs/examples/wasm ================================================ FILE: gilrs/examples/wasm/wasm_gui.sh ================================================ #!/usr/bin/env bash ### This script can be used instead of the "Build and Run" step in `./gilrs/examples/wasm/README.md`. ### Useful for gilrs devs that want a single script to to point their IDE to for run configurations. ### Make sure to run the install steps from the readme first. set -e # Start at this script's path and go up three levels to the workspace root. # Ensures a consistent path regardless of the working directory when you run the script. SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) PROJECT_DIR=$(dirname "$(dirname "$(dirname "$SCRIPT_DIR")")") cd "$PROJECT_DIR" || exit cargo build --release --example gui --target wasm32-unknown-unknown wasm-bindgen --out-name wasm_example --out-dir gilrs/examples/wasm/target --target web target/wasm32-unknown-unknown/release/examples/gui.wasm basic-http-server gilrs/examples/wasm ================================================ FILE: gilrs/src/constants.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. pub const BTN_UNKNOWN: u16 = 0; pub const BTN_SOUTH: u16 = 1; pub const BTN_EAST: u16 = 2; pub const BTN_C: u16 = 3; pub const BTN_NORTH: u16 = 4; pub const BTN_WEST: u16 = 5; pub const BTN_Z: u16 = 6; pub const BTN_LT: u16 = 7; pub const BTN_RT: u16 = 8; pub const BTN_LT2: u16 = 9; pub const BTN_RT2: u16 = 10; pub const BTN_SELECT: u16 = 11; pub const BTN_START: u16 = 12; pub const BTN_MODE: u16 = 13; pub const BTN_LTHUMB: u16 = 14; pub const BTN_RTHUMB: u16 = 15; pub const BTN_DPAD_UP: u16 = 16; pub const BTN_DPAD_DOWN: u16 = 17; pub const BTN_DPAD_LEFT: u16 = 18; pub const BTN_DPAD_RIGHT: u16 = 19; pub const AXIS_UNKNOWN: u16 = 0; pub const AXIS_LSTICKX: u16 = 1; pub const AXIS_LSTICKY: u16 = 2; pub const AXIS_LEFTZ: u16 = 3; pub const AXIS_RSTICKX: u16 = 4; pub const AXIS_RSTICKY: u16 = 5; pub const AXIS_RIGHTZ: u16 = 6; pub const AXIS_DPADX: u16 = 7; pub const AXIS_DPADY: u16 = 8; ================================================ FILE: gilrs/src/ev/filter.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. //! Alter events in various ways. //! //! This modules contains "event filters" that can change, drop or create new events. To use them, //! import `Filter` trait and call `filter()` function on `Option`. Because `filter` also //! returns `Option` you can combine multiple filters by using `filter()` function on //! returned event. //! //! Filters in this modules have public fields that can be used to configure their behaviour. You //! can also create them with default values using `new()` method. If filter is not configurable, //! it is implemented as function (for example `deadzone()`). //! //! # Example //! //! ``` //! use gilrs::{GilrsBuilder, Filter}; //! use gilrs::ev::filter::{Jitter, Repeat, deadzone}; //! //! let mut gilrs = GilrsBuilder::new().with_default_filters(false).build().unwrap(); //! let jitter = Jitter { threshold: 0.02 }; //! let repeat = Repeat::new(); //! //! // Event loop //! loop { //! while let Some(event) = gilrs //! .next_event() //! .filter_ev(&jitter, &mut gilrs) //! .filter_ev(&deadzone, &mut gilrs) //! .filter_ev(&repeat, &mut gilrs) //! { //! gilrs.update(&event); //! println!("{:?}", event); //! } //! # break; //! } //! ``` //! # Implementing custom filters //! //! If you want to implement your own filters, you will have to implement `FilterFn` trait. //! **Do not return `None` if you got `Some(event)`**. If you want to discard an event, uses //! `EventType::Dropped`. Returning `None` means that there are no more events to process and //! will end `while let` loop. //! //! ## Example //! //! Example implementations of filter that will drop all events with `Unknown` axis or button. //! //! ``` //! use gilrs::ev::filter::FilterFn; //! use gilrs::{Gilrs, Event, EventType, Button, Axis, Filter}; //! //! struct UnknownSlayer; //! //! impl FilterFn for UnknownSlayer { //! fn filter(&self, ev: Option, _gilrs: &mut Gilrs) -> Option { //! match ev { //! Some(Event { event: EventType::ButtonPressed(Button::Unknown, ..), id, .. }) //! | Some(Event { event: EventType::ButtonReleased(Button::Unknown, ..), id, .. }) //! | Some(Event { event: EventType::AxisChanged(Axis::Unknown, ..), id, .. }) //! => Some(Event::new(id, EventType::Dropped)), //! _ => ev, //! } //! } //! } //! ``` //! //! `FilterFn` is also implemented for all `Fn(Option, &Gilrs) -> Option`, so above //! example could be simplified to passing closure to `filter()` function. use crate::ev::{Axis, AxisOrBtn, Button, Code, Event, EventType}; use crate::gamepad::{Gamepad, Gilrs}; use crate::utils; use std::time::Duration; /// Discard axis events that changed less than `threshold`. #[derive(Copy, Clone, PartialEq, Debug)] pub struct Jitter { pub threshold: f32, } impl Jitter { /// Creates new `Jitter` filter with threshold set to 0.01. pub fn new() -> Self { Jitter { threshold: 0.01 } } } impl Default for Jitter { fn default() -> Self { Self::new() } } impl FilterFn for Jitter { fn filter(&self, ev: Option, gilrs: &mut Gilrs) -> Option { match ev { Some(Event { event: EventType::AxisChanged(_, val, axis), id, .. }) => match gilrs.gamepad(id).state().axis_data(axis) { Some(data) if val != 0.0 && (val - data.value()).abs() < self.threshold => { Some(Event::new(id, EventType::Dropped)) } _ => ev, }, _ => ev, } } } fn apply_deadzone(x: f32, y: f32, threshold: f32) -> (f32, f32) { let magnitude = utils::clamp((x * x + y * y).sqrt(), 0.0, 1.0); if magnitude <= threshold { (0.0, 0.0) } else { let norm = ((magnitude - threshold) / (1.0 - threshold)) / magnitude; (x * norm, y * norm) } } fn deadzone_nonzero_axis_idx(axis: Axis) -> Option { Some(match axis { Axis::DPadX => 0, Axis::DPadY => 1, Axis::LeftStickX => 2, Axis::LeftStickY => 3, Axis::RightStickX => 4, Axis::RightStickY => 5, _ => { return None; } }) } /// Drops events in dead zone and remaps value to keep it in standard range. pub fn deadzone(ev: Option, gilrs: &mut Gilrs) -> Option { match ev { Some(Event { event: EventType::AxisChanged(axis, val, nec), id, time, }) => { let threshold = match gilrs.gamepad(id).deadzone(nec) { Some(t) => t, None => return ev, }; if let Some((other_axis, other_code)) = axis .second_axis() .and_then(|axis| gilrs.gamepad(id).axis_code(axis).map(|code| (axis, code))) { let other_val = gilrs.gamepad(id).state().value(other_code); let val = apply_deadzone(val, other_val, threshold); // Since this is the second axis, deadzone_nonzero_axis_idx() will always returns something. let other_axis_idx = deadzone_nonzero_axis_idx(other_axis).unwrap(); if val.0 == 0. && val.1 == 0. && gilrs.gamepads_data[id.0].have_sent_nonzero_for_axis[other_axis_idx] && gilrs.gamepad(id).state().value(other_code) != 0. { // Clear other axis that is now within the dead zone threshold. gilrs.insert_event(Event { id, time, event: EventType::AxisChanged(other_axis, 0., other_code), }); gilrs.gamepads_data[id.0].have_sent_nonzero_for_axis[other_axis_idx] = false; } Some(if gilrs.gamepad(id).state().value(nec) == val.0 { Event::new(id, EventType::Dropped) } else { if let Some(axis_idx) = deadzone_nonzero_axis_idx(axis) { gilrs.gamepads_data[id.0].have_sent_nonzero_for_axis[axis_idx] = val.0 != 0.; } Event { id, time, event: EventType::AxisChanged(axis, val.0, nec), } }) } else { let val = apply_deadzone(val, 0.0, threshold).0; Some(if gilrs.gamepad(id).state().value(nec) == val { Event::new(id, EventType::Dropped) } else { if let Some(axis_idx) = deadzone_nonzero_axis_idx(axis) { gilrs.gamepads_data[id.0].have_sent_nonzero_for_axis[axis_idx] = val != 0.; } Event { id, time, event: EventType::AxisChanged(axis, val, nec), } }) } } Some(Event { event: EventType::ButtonChanged(btn, val, nec), id, time, }) => { let gp = &gilrs.gamepad(id); let threshold = match gp.deadzone(nec) { Some(t) => t, None => return ev, }; let val = apply_deadzone(val, 0.0, threshold).0; Some(if gp.state().value(nec) == val { Event::new(id, EventType::Dropped) } else { Event { id, time, event: EventType::ButtonChanged(btn, val, nec), } }) } _ => ev, } } /// Maps axis dpad events to button dpad events. /// /// This filter will do nothing if gamepad has dpad buttons (to prevent double events for same /// element) and if standard `NativeEvCode` for dpads is used by some other buttons. It will always /// try to map if SDL mappings contains mappings for all four hats. pub fn axis_dpad_to_button(ev: Option, gilrs: &mut Gilrs) -> Option { use gilrs_core::native_ev_codes as necs; fn can_map(gp: &Gamepad<'_>) -> bool { let hats_mapped = gp.mapping().hats_mapped(); if hats_mapped == 0b0000_1111 { true } else if hats_mapped == 0 { gp.axis_or_btn_name(Code(necs::BTN_DPAD_RIGHT)).is_none() && gp.axis_or_btn_name(Code(necs::BTN_DPAD_LEFT)).is_none() && gp.axis_or_btn_name(Code(necs::BTN_DPAD_DOWN)).is_none() && gp.axis_or_btn_name(Code(necs::BTN_DPAD_UP)).is_none() && gp.button_code(Button::DPadRight).is_none() } else { // Not all hats are mapped so let's ignore it for now. false } } let ev = ev?; let gamepad = gilrs.gamepad(ev.id); if !can_map(&gamepad) { return Some(ev); } let mut out_event = ev.drop(); match ev.event { EventType::AxisChanged(Axis::DPadX, val, _) => { let mut release_left = false; let mut release_right = false; if val == 1.0 { // The axis value might change from left (-1.0) to right (1.0) immediately without // us getting an additional event for the release at the center position (0.0). release_left = gamepad.state().is_pressed(Code(necs::BTN_DPAD_LEFT)); gilrs.insert_event(Event { event: EventType::ButtonChanged( Button::DPadRight, 1.0, Code(necs::BTN_DPAD_RIGHT), ), ..ev }); out_event = Event { event: EventType::ButtonPressed(Button::DPadRight, Code(necs::BTN_DPAD_RIGHT)), ..ev }; } else if val == -1.0 { // The axis value might change from right (1.0) to left (-1.0) immediately without // us getting an additional event for the release at the center position (0.0). release_right = gamepad.state().is_pressed(Code(necs::BTN_DPAD_RIGHT)); gilrs.insert_event(Event { event: EventType::ButtonChanged( Button::DPadLeft, 1.0, Code(necs::BTN_DPAD_LEFT), ), ..ev }); out_event = Event { event: EventType::ButtonPressed(Button::DPadLeft, Code(necs::BTN_DPAD_LEFT)), ..ev }; } else { release_left = gamepad.state().is_pressed(Code(necs::BTN_DPAD_LEFT)); release_right = gamepad.state().is_pressed(Code(necs::BTN_DPAD_RIGHT)); } if release_right { if !out_event.is_dropped() { gilrs.insert_event(out_event); } gilrs.insert_event(Event { event: EventType::ButtonChanged( Button::DPadRight, 0.0, Code(necs::BTN_DPAD_RIGHT), ), ..ev }); out_event = Event { event: EventType::ButtonReleased(Button::DPadRight, Code(necs::BTN_DPAD_RIGHT)), ..ev }; } if release_left { if !out_event.is_dropped() { gilrs.insert_event(out_event); } gilrs.insert_event(Event { event: EventType::ButtonChanged( Button::DPadLeft, 0.0, Code(necs::BTN_DPAD_LEFT), ), ..ev }); out_event = Event { event: EventType::ButtonReleased(Button::DPadLeft, Code(necs::BTN_DPAD_LEFT)), ..ev }; } Some(out_event) } EventType::AxisChanged(Axis::DPadY, val, _) => { let mut release_up = false; let mut release_down = false; if val == 1.0 { // The axis value might change from down (-1.0) to up (1.0) immediately without us // getting an additional event for the release at the center position (0.0). release_down = gamepad.state().is_pressed(Code(necs::BTN_DPAD_DOWN)); gilrs.insert_event(Event { event: EventType::ButtonChanged(Button::DPadUp, 1.0, Code(necs::BTN_DPAD_UP)), ..ev }); out_event = Event { event: EventType::ButtonPressed(Button::DPadUp, Code(necs::BTN_DPAD_UP)), ..ev }; } else if val == -1.0 { // The axis value might change from up (1.0) to down (-1.0) immediately without us // getting an additional event for the release at the center position (0.0). release_up = gamepad.state().is_pressed(Code(necs::BTN_DPAD_UP)); gilrs.insert_event(Event { event: EventType::ButtonChanged( Button::DPadDown, 1.0, Code(necs::BTN_DPAD_DOWN), ), ..ev }); out_event = Event { event: EventType::ButtonPressed(Button::DPadDown, Code(necs::BTN_DPAD_DOWN)), ..ev }; } else { release_up = gamepad.state().is_pressed(Code(necs::BTN_DPAD_UP)); release_down = gamepad.state().is_pressed(Code(necs::BTN_DPAD_DOWN)); } if release_up { if !out_event.is_dropped() { gilrs.insert_event(out_event); } gilrs.insert_event(Event { event: EventType::ButtonChanged(Button::DPadUp, 0.0, Code(necs::BTN_DPAD_UP)), ..ev }); out_event = Event { event: EventType::ButtonReleased(Button::DPadUp, Code(necs::BTN_DPAD_UP)), ..ev }; } if release_down { if !out_event.is_dropped() { gilrs.insert_event(out_event); } gilrs.insert_event(Event { event: EventType::ButtonChanged( Button::DPadDown, 0.0, Code(necs::BTN_DPAD_DOWN), ), ..ev }); out_event = Event { event: EventType::ButtonReleased(Button::DPadDown, Code(necs::BTN_DPAD_DOWN)), ..ev }; } Some(out_event) } _ => Some(ev), } } /// Repeats pressed keys. #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct Repeat { pub after: Duration, pub every: Duration, } impl Repeat { /// Creates new `Repeat` filter with `after` set to 500ms and `every` set to 30ms. pub fn new() -> Self { Repeat { after: Duration::from_millis(500), every: Duration::from_millis(30), } } } impl Default for Repeat { fn default() -> Self { Self::new() } } impl FilterFn for Repeat { fn filter(&self, ev: Option, gilrs: &mut Gilrs) -> Option { match ev { Some(ev) => Some(ev), None => { let now = utils::time_now(); for (id, gamepad) in gilrs.gamepads() { for (nec, btn_data) in gamepad.state().buttons() { match ( btn_data.is_pressed(), btn_data.is_repeating(), now.duration_since(btn_data.timestamp()), ) { (true, false, Ok(dur)) if dur >= self.after => { let btn_name = match gamepad.axis_or_btn_name(nec) { Some(AxisOrBtn::Btn(b)) => b, _ => Button::Unknown, }; return Some(Event { id, event: EventType::ButtonRepeated(btn_name, nec), time: btn_data.timestamp() + self.after, }); } (true, true, Ok(dur)) if dur >= self.every => { let btn_name = match gamepad.axis_or_btn_name(nec) { Some(AxisOrBtn::Btn(b)) => b, _ => Button::Unknown, }; return Some(Event { id, event: EventType::ButtonRepeated(btn_name, nec), time: btn_data.timestamp() + self.every, }); } _ => (), } } } None } } } } /// Allow filtering events. /// /// See module level documentation for more info. pub trait Filter { fn filter_ev(&self, filter: &F, gilrs: &mut Gilrs) -> Option; } /// Actual filter implementation. /// /// See module level documentation for more info. pub trait FilterFn { fn filter(&self, ev: Option, gilrs: &mut Gilrs) -> Option; } impl FilterFn for F where F: Fn(Option, &mut Gilrs) -> Option, { fn filter(&self, ev: Option, gilrs: &mut Gilrs) -> Option { self(ev, gilrs) } } impl Filter for Option { fn filter_ev(&self, filter: &F, gilrs: &mut Gilrs) -> Option { let e = filter.filter(*self, gilrs); debug_assert!( !(self.is_some() && e.is_none()), "Filter changed Some(event) into None. See ev::filter documentation for more info." ); e } } impl Filter for Event { fn filter_ev(&self, filter: &F, gilrs: &mut Gilrs) -> Option { let e = filter.filter(Some(*self), gilrs); debug_assert!( e.is_some(), "Filter changed Some(event) into None. See ev::filter documentation for more info." ); e } } ================================================ FILE: gilrs/src/ev/mod.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. //! Gamepad state and other event related functionality. pub mod filter; pub mod state; use std::{ fmt::{Display, Formatter, Result as FmtResult}, time::SystemTime, }; use crate::{constants::*, gamepad::GamepadId, utils}; #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; /// Platform specific event code. /// /// This type represents single gamepads's element like specific axis or button. /// It can't be directly created, but you can get it from events or using /// `Gamepad`'s methods [`button_code`](crate::Gamepad::button_code) and /// [`axis_code`](crate::Gamepad::axis_code). If `serde-serialize` feature is /// enabled, `Code` can be serialized and deserialized, but keep in mind that /// layout **is** platform-specific. So it's not possible to serialize `Code` on /// Linux and deserialize it on Windows. This also apply to `Display` implementation. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] pub struct Code(pub(crate) gilrs_core::EvCode); impl Display for Code { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { self.0.fmt(f) } } impl Code { pub fn into_u32(&self) -> u32 { self.0.into_u32() } } /// Holds information about gamepad event. #[derive(Copy, Clone, PartialEq, Debug)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[non_exhaustive] pub struct Event { /// Id of gamepad. pub id: GamepadId, /// Event's data. pub event: EventType, /// Time when event was emitted. pub time: SystemTime, } impl Event { /// Creates new event with current time. pub fn new(id: GamepadId, event: EventType) -> Self { Event { id, event, time: utils::time_now(), } } /// Returns `Event` with `EventType::Dropped`. pub fn drop(mut self) -> Event { self.event = EventType::Dropped; self } /// Returns true if event is `Dropped` and should be ignored. pub fn is_dropped(&self) -> bool { self.event == EventType::Dropped } } #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[non_exhaustive] /// Gamepad event. pub enum EventType { /// Some button on gamepad has been pressed. ButtonPressed(Button, Code), /// This event can be generated by [`ev::Repeat`](filter/struct.Repeat.html) event filter. ButtonRepeated(Button, Code), /// Previously pressed button has been released. ButtonReleased(Button, Code), /// Value of button has changed. Value can be in range [0.0, 1.0]. ButtonChanged(Button, f32, Code), /// Value of axis has changed. Value can be in range [-1.0, 1.0]. AxisChanged(Axis, f32, Code), /// Gamepad has been connected. If gamepad's UUID doesn't match one of disconnected gamepads, /// newly connected gamepad will get new ID. Connected, /// Gamepad has been disconnected. Disconnected gamepad will not generate any new events. Disconnected, /// There was an `Event`, but it was dropped by one of filters. You should ignore it. Dropped, /// A force feedback effect has ran for its duration and stopped. ForceFeedbackEffectCompleted, } #[repr(u16)] #[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] /// Gamepad's elements which state can be represented by value from 0.0 to 1.0. /// /// ![Controller layout](https://gilrs-project.gitlab.io/gilrs/img/controller.svg) pub enum Button { // Action Pad South = BTN_SOUTH, East = BTN_EAST, North = BTN_NORTH, West = BTN_WEST, C = BTN_C, Z = BTN_Z, // Triggers LeftTrigger = BTN_LT, LeftTrigger2 = BTN_LT2, RightTrigger = BTN_RT, RightTrigger2 = BTN_RT2, // Menu Pad Select = BTN_SELECT, Start = BTN_START, Mode = BTN_MODE, // Sticks LeftThumb = BTN_LTHUMB, RightThumb = BTN_RTHUMB, // D-Pad DPadUp = BTN_DPAD_UP, DPadDown = BTN_DPAD_DOWN, DPadLeft = BTN_DPAD_LEFT, DPadRight = BTN_DPAD_RIGHT, #[default] Unknown = BTN_UNKNOWN, } impl Button { pub fn is_action(self) -> bool { use crate::Button::*; matches!(self, South | East | North | West | C | Z) } pub fn is_trigger(self) -> bool { use crate::Button::*; matches!( self, LeftTrigger | LeftTrigger2 | RightTrigger | RightTrigger2 ) } pub fn is_menu(self) -> bool { use crate::Button::*; matches!(self, Select | Start | Mode) } pub fn is_stick(self) -> bool { use crate::Button::*; matches!(self, LeftThumb | RightThumb) } pub fn is_dpad(self) -> bool { use crate::Button::*; matches!(self, DPadUp | DPadDown | DPadLeft | DPadRight) } pub fn to_nec(self) -> Option { use gilrs_core::native_ev_codes as necs; match self { Button::South => Some(necs::BTN_SOUTH), Button::East => Some(necs::BTN_EAST), Button::North => Some(necs::BTN_NORTH), Button::West => Some(necs::BTN_WEST), Button::C => Some(necs::BTN_C), Button::Z => Some(necs::BTN_Z), Button::LeftTrigger => Some(necs::BTN_LT), Button::LeftTrigger2 => Some(necs::BTN_LT2), Button::RightTrigger => Some(necs::BTN_RT), Button::RightTrigger2 => Some(necs::BTN_RT2), Button::Select => Some(necs::BTN_SELECT), Button::Start => Some(necs::BTN_START), Button::Mode => Some(necs::BTN_MODE), Button::LeftThumb => Some(necs::BTN_LTHUMB), Button::RightThumb => Some(necs::BTN_RTHUMB), Button::DPadUp => Some(necs::BTN_DPAD_UP), Button::DPadDown => Some(necs::BTN_DPAD_DOWN), Button::DPadLeft => Some(necs::BTN_DPAD_LEFT), Button::DPadRight => Some(necs::BTN_DPAD_RIGHT), _ => None, } .map(Code) } } #[repr(u16)] #[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] /// Gamepad's elements which state can be represented by value from -1.0 to 1.0. /// /// ![Controller layout](https://gilrs-project.gitlab.io/gilrs/img/controller.svg) pub enum Axis { LeftStickX = AXIS_LSTICKX, LeftStickY = AXIS_LSTICKY, LeftZ = AXIS_LEFTZ, RightStickX = AXIS_RSTICKX, RightStickY = AXIS_RSTICKY, RightZ = AXIS_RIGHTZ, DPadX = AXIS_DPADX, DPadY = AXIS_DPADY, Unknown = AXIS_UNKNOWN, } impl Axis { /// Returns true if axis is `LeftStickX`, `LeftStickY`, `RightStickX` or `RightStickY`. pub fn is_stick(self) -> bool { use crate::Axis::*; matches!(self, LeftStickX | LeftStickY | RightStickX | RightStickY) } /// Returns the other axis from same element of gamepad, if any. /// /// | input | output | /// |-------------|-------------------| /// |`LeftStickX` |`Some(LeftStickY)` | /// |`LeftStickY` |`Some(LeftStickX)` | /// |`RightStickX`|`Some(RightStickY)`| /// |`RightStickY`|`Some(RightStickX)`| /// |`DpadX` |`Some(DpadY)` | /// |`DpadY` |`Some(DpadX)` | /// | … |`None` | pub fn second_axis(self) -> Option { use crate::Axis::*; match self { LeftStickX => Some(LeftStickY), LeftStickY => Some(LeftStickX), RightStickX => Some(RightStickY), RightStickY => Some(RightStickX), DPadX => Some(DPadY), DPadY => Some(DPadX), _ => None, } } } /// Represents `Axis` or `Button`. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] pub enum AxisOrBtn { Axis(Axis), Btn(Button), } impl AxisOrBtn { pub(crate) fn is_button(&self) -> bool { matches!(self, AxisOrBtn::Btn(_)) } } ================================================ FILE: gilrs/src/ev/state.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use crate::ev::Code; use fnv::FnvHashMap; use std::collections::hash_map; use std::iter::Iterator; use std::time::SystemTime; /// Cached gamepad state. #[derive(Clone, Debug)] pub struct GamepadState { // Indexed by EvCode (nec) buttons: FnvHashMap, // Indexed by EvCode (nec) axes: FnvHashMap, } impl GamepadState { pub(crate) fn new() -> Self { GamepadState { buttons: FnvHashMap::default(), axes: FnvHashMap::default(), } } /// Returns `true` if given button is pressed. Returns `false` if there is no information about /// `btn` or it is not pressed. pub fn is_pressed(&self, btn: Code) -> bool { self.buttons .get(&btn) .map(|s| s.is_pressed()) .unwrap_or(false) } /// Returns value of `el` or 0.0 when there is no information about it. `el` can be either axis /// or button. pub fn value(&self, el: Code) -> f32 { self.axes .get(&el) .map(|s| s.value()) .or_else(|| self.buttons.get(&el).map(|s| s.value())) .unwrap_or(0.0) } /// Iterate over buttons data. pub fn buttons(&self) -> ButtonDataIter<'_> { ButtonDataIter(self.buttons.iter()) } /// Iterate over axes data. pub fn axes(&self) -> AxisDataIter<'_> { AxisDataIter(self.axes.iter()) } /// Returns button state and when it changed. pub fn button_data(&self, btn: Code) -> Option<&ButtonData> { self.buttons.get(&btn) } /// Returns axis state and when it changed. pub fn axis_data(&self, axis: Code) -> Option<&AxisData> { self.axes.get(&axis) } pub(crate) fn set_btn_pressed( &mut self, btn: Code, pressed: bool, counter: u64, timestamp: SystemTime, ) { let data = self.buttons.entry(btn).or_insert_with(|| { ButtonData::new( if pressed { 1.0 } else { 0.0 }, pressed, false, counter, timestamp, ) }); data.is_pressed = pressed; data.is_repeating = false; data.counter = counter; data.last_event_ts = timestamp; } pub(crate) fn set_btn_repeating(&mut self, btn: Code, counter: u64, timestamp: SystemTime) { let data = self .buttons .entry(btn) .or_insert_with(|| ButtonData::new(1.0, true, true, counter, timestamp)); data.is_repeating = true; data.counter = counter; data.last_event_ts = timestamp; } pub(crate) fn set_btn_value( &mut self, btn: Code, value: f32, counter: u64, timestamp: SystemTime, ) { let data = self .buttons .entry(btn) .or_insert_with(|| ButtonData::new(value, false, false, counter, timestamp)); data.value = value; data.counter = counter; data.last_event_ts = timestamp; } pub(crate) fn update_axis(&mut self, axis: Code, data: AxisData) { self.axes.insert(axis, data); } } /// Iterator over `ButtonData`. pub struct ButtonDataIter<'a>(hash_map::Iter<'a, Code, ButtonData>); /// Iterator over `AxisData`. pub struct AxisDataIter<'a>(hash_map::Iter<'a, Code, AxisData>); impl<'a> Iterator for ButtonDataIter<'a> { type Item = (Code, &'a ButtonData); fn next(&mut self) -> Option { self.0.next().map(|(k, v)| (*k, v)) } } impl<'a> Iterator for AxisDataIter<'a> { type Item = (Code, &'a AxisData); fn next(&mut self) -> Option { self.0.next().map(|(k, v)| (*k, v)) } } /// Information about button stored in `State`. #[derive(Clone, Copy, Debug)] pub struct ButtonData { last_event_ts: SystemTime, counter: u64, value: f32, is_pressed: bool, is_repeating: bool, } impl ButtonData { pub(crate) fn new( value: f32, pressed: bool, repeating: bool, counter: u64, time: SystemTime, ) -> Self { ButtonData { last_event_ts: time, counter, value, is_pressed: pressed, is_repeating: repeating, } } /// Returns `true` if button is pressed. pub fn is_pressed(&self) -> bool { self.is_pressed } /// Returns value of button. pub fn value(&self) -> f32 { self.value } /// Returns `true` if button is repeating. pub fn is_repeating(&self) -> bool { self.is_repeating } /// Returns value of counter when button state last changed. pub fn counter(&self) -> u64 { self.counter } /// Returns when button state last changed. pub fn timestamp(&self) -> SystemTime { self.last_event_ts } } /// Information about axis stored in `State`. #[derive(Clone, Copy, Debug)] pub struct AxisData { last_event_ts: SystemTime, last_event_c: u64, value: f32, } impl AxisData { pub(crate) fn new(value: f32, counter: u64, time: SystemTime) -> Self { AxisData { last_event_ts: time, last_event_c: counter, value, } } /// Returns value of axis. pub fn value(&self) -> f32 { self.value } /// Returns value of counter when axis value last changed. pub fn counter(&self) -> u64 { self.last_event_c } /// Returns when axis value last changed. pub fn timestamp(&self) -> SystemTime { self.last_event_ts } } ================================================ FILE: gilrs/src/ff/base_effect.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::ops::Mul; use super::time::Ticks; /// Kind of [`BaseEffect`](struct.BaseEffect.html). /// /// Currently base effect support only xinput model of force feedback, which means that gamepad /// have weak and strong motor. #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[non_exhaustive] pub enum BaseEffectType { Weak { magnitude: u16 }, Strong { magnitude: u16 }, } impl BaseEffectType { fn magnitude(&self) -> u16 { match *self { BaseEffectType::Weak { magnitude } => magnitude, BaseEffectType::Strong { magnitude } => magnitude, } } } impl Mul for BaseEffectType { type Output = BaseEffectType; fn mul(self, rhs: f32) -> Self::Output { let mg = (self.magnitude() as f32 * rhs) as u16; match self { BaseEffectType::Weak { .. } => BaseEffectType::Weak { magnitude: mg }, BaseEffectType::Strong { .. } => BaseEffectType::Strong { magnitude: mg }, } } } impl Default for BaseEffectType { fn default() -> Self { BaseEffectType::Weak { magnitude: 0 } } } /// Basic building block used to create more complex force feedback effects. /// /// For each base effect you can specify it's type, for how long should it be played and it's /// strength during playback. #[derive(Copy, Clone, PartialEq, Debug, Default)] pub struct BaseEffect { /// Type of base effect. pub kind: BaseEffectType, /// Defines playback duration and delays between each repetition. pub scheduling: Replay, // TODO: maybe allow other f(t)? /// Basic attenuation function. pub envelope: Envelope, } impl BaseEffect { /// Returns `Weak` or `Strong` after applying envelope. pub(super) fn magnitude_at(&self, ticks: Ticks) -> BaseEffectType { if let Some(wrapped) = self.scheduling.wrap(ticks) { let att = self.scheduling.at(wrapped) * self.envelope.at(wrapped, self.scheduling.play_for); self.kind * att } else { self.kind * 0.0 } } } // TODO: Image with "envelope" #[derive(Copy, Clone, PartialEq, Debug, Default)] /// Envelope shaped attenuation(time) function. pub struct Envelope { pub attack_length: Ticks, pub attack_level: f32, pub fade_length: Ticks, pub fade_level: f32, } impl Envelope { pub(super) fn at(&self, ticks: Ticks, dur: Ticks) -> f32 { debug_assert!(self.fade_length < dur); debug_assert!(self.attack_length + self.fade_length < dur); if ticks < self.attack_length { self.attack_level + ticks.0 as f32 * (1.0 - self.attack_level) / self.attack_length.0 as f32 } else if ticks + self.fade_length > dur { 1.0 + (ticks + self.fade_length - dur).0 as f32 * (self.fade_level - 1.0) / self.fade_length.0 as f32 } else { 1.0 } } } /// Defines scheduling of the basic force feedback effect. /// /// ```text /// ____________ ____________ ____________ /// | | | | | /// _______| |____________| |____________| /// after play_for with_delay play_for with_delay play_for /// ``` #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub struct Replay { /// Start playback `after` ticks after `Effect::play()` is called. pub after: Ticks, /// Playback duration. pub play_for: Ticks, /// If playback should be repeated delay it for `with_delay` ticks. pub with_delay: Ticks, } impl Replay { pub(super) fn at(&self, ticks: Ticks) -> f32 { if ticks >= self.play_for { 0.0 } else { 1.0 } } /// Returns duration of effect calculated as `play_for + with_delay`. pub fn dur(&self) -> Ticks { self.play_for + self.with_delay } /// Returns `None` if effect hasn't started; or wrapped value fn wrap(&self, ticks: Ticks) -> Option { ticks.checked_sub(self.after).map(|t| t % self.dur()) } } impl Default for Replay { fn default() -> Self { Replay { after: Ticks(0), play_for: Ticks(1), with_delay: Ticks(0), } } } ================================================ FILE: gilrs/src/ff/effect_source.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::error::Error; use std::ops::{AddAssign, Mul}; use std::{fmt, mem}; use crate::{Event, EventType, GamepadId}; use super::base_effect::{BaseEffect, BaseEffectType}; use super::time::{Repeat, Ticks}; use vec_map::VecMap; /// Specifies how distance between effect source and listener attenuates effect. /// /// They are based on /// [OpenAL Specification](http://openal.org/documentation/openal-1.1-specification.pdf) (chapter /// 3.4), but the best way to see how they differ is to run `ff_pos` example. /// /// Make sure that all parameters are ≥ 0. Additionally `Linear` and `LinearClamped` models don't /// like if `ref_distance == max_distance` while others would prefer `ref_distance > 0`. #[derive(Clone, Copy, Debug, Default, PartialEq)] pub enum DistanceModel { /// Effect is not attenuated by distance. #[default] None, /// Linear distance model. Linear { ref_distance: f32, rolloff_factor: f32, max_distance: f32, }, /// Linear distance clamped model. LinearClamped { ref_distance: f32, rolloff_factor: f32, max_distance: f32, }, /// Inverse distance model. Inverse { ref_distance: f32, rolloff_factor: f32, }, /// Inverse distance clamped model. InverseClamped { ref_distance: f32, rolloff_factor: f32, max_distance: f32, }, /// Exponential distance model. Exponential { ref_distance: f32, rolloff_factor: f32, }, /// Exponential distance clamped model. ExponentialClamped { ref_distance: f32, rolloff_factor: f32, max_distance: f32, }, } impl DistanceModel { fn attenuation(self, mut distance: f32) -> f32 { // For now we will follow OpenAL[1] specification for distance models. See chapter 3.4 for // more details. // // [1]: http://openal.org/documentation/openal-1.1-specification.pdf match self { DistanceModel::Linear { ref_distance, max_distance, rolloff_factor, } => { distance = distance.min(max_distance); 1.0 - rolloff_factor * (distance - ref_distance) / (max_distance - ref_distance) } DistanceModel::LinearClamped { ref_distance, max_distance, rolloff_factor, } => { distance = distance.max(ref_distance); distance = distance.min(max_distance); 1.0 - rolloff_factor * (distance - ref_distance) / (max_distance - ref_distance) } DistanceModel::Inverse { ref_distance, rolloff_factor, } => ref_distance / (ref_distance + rolloff_factor * (distance - ref_distance)), DistanceModel::InverseClamped { ref_distance, max_distance, rolloff_factor, } => { distance = distance.max(ref_distance); distance = distance.min(max_distance); ref_distance / (ref_distance + rolloff_factor * (distance - ref_distance)) } DistanceModel::Exponential { ref_distance, rolloff_factor, } => (distance / ref_distance).powf(-rolloff_factor), DistanceModel::ExponentialClamped { ref_distance, max_distance, rolloff_factor, } => { distance = distance.max(ref_distance); distance = distance.min(max_distance); (distance / ref_distance).powf(-rolloff_factor) } DistanceModel::None => 1.0, } } pub(crate) fn validate(self) -> Result<(), DistanceModelError> { let (ref_distance, rolloff_factor, max_distance) = match self { DistanceModel::Inverse { ref_distance, rolloff_factor, } => { if ref_distance <= 0.0 { return Err(DistanceModelError::InvalidModelParameter); } (ref_distance, rolloff_factor, 0.0) } DistanceModel::InverseClamped { ref_distance, max_distance, rolloff_factor, } => { if ref_distance <= 0.0 { return Err(DistanceModelError::InvalidModelParameter); } (ref_distance, rolloff_factor, max_distance) } DistanceModel::Linear { ref_distance, max_distance, rolloff_factor, } => { if ref_distance == max_distance { return Err(DistanceModelError::InvalidModelParameter); } (ref_distance, rolloff_factor, max_distance) } DistanceModel::LinearClamped { ref_distance, max_distance, rolloff_factor, } => { if ref_distance == max_distance { return Err(DistanceModelError::InvalidModelParameter); } (ref_distance, rolloff_factor, max_distance) } DistanceModel::Exponential { ref_distance, rolloff_factor, } => { if ref_distance <= 0.0 { return Err(DistanceModelError::InvalidModelParameter); } (ref_distance, rolloff_factor, 0.0) } DistanceModel::ExponentialClamped { ref_distance, max_distance, rolloff_factor, } => { if ref_distance <= 0.0 { return Err(DistanceModelError::InvalidModelParameter); } (ref_distance, rolloff_factor, max_distance) } DistanceModel::None => (0.0, 0.0, 0.0), }; if ref_distance < 0.0 { Err(DistanceModelError::InvalidReferenceDistance) } else if rolloff_factor < 0.0 { Err(DistanceModelError::InvalidRolloffFactor) } else if max_distance < 0.0 { Err(DistanceModelError::InvalidMaxDistance) } else { Ok(()) } } } /// Error that can be returned when passing [`DistanceModel`](struct.DistanceModel.html) with /// invalid value. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum DistanceModelError { /// Reference distance is < 0. InvalidReferenceDistance, /// Rolloff factor is < 0. InvalidRolloffFactor, /// Max distance is < 0. InvalidMaxDistance, /// Possible divide by zero InvalidModelParameter, } impl Error for DistanceModelError {} impl fmt::Display for DistanceModelError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self { DistanceModelError::InvalidReferenceDistance => "reference distance is < 0", DistanceModelError::InvalidRolloffFactor => "rolloff factor is < 0", DistanceModelError::InvalidMaxDistance => "max distance is < 0", DistanceModelError::InvalidModelParameter => "possible divide by zero", }; f.write_str(s) } } #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub(super) enum EffectState { Playing { since: Ticks }, Stopped, } #[derive(Clone, PartialEq, Debug)] pub(crate) struct EffectSource { base_effects: Vec, // TODO: Use bitset pub(super) devices: VecMap<()>, pub(super) repeat: Repeat, pub(super) distance_model: DistanceModel, pub(super) position: [f32; 3], pub(super) gain: f32, pub(super) state: EffectState, pub(super) completion_events: Vec, } impl EffectSource { pub(super) fn new( base_effects: Vec, devices: VecMap<()>, repeat: Repeat, dist_model: DistanceModel, position: [f32; 3], gain: f32, ) -> Self { EffectSource { base_effects, devices, repeat, distance_model: dist_model, position, gain, state: EffectState::Stopped, completion_events: vec![], } } pub(super) fn combine_base_effects(&mut self, ticks: Ticks, actor_pos: [f32; 3]) -> Magnitude { let ticks = match self.state { EffectState::Playing { since } => { debug_assert!(ticks >= since); ticks - since } EffectState::Stopped => return Magnitude::zero(), }; match self.repeat { Repeat::For(max_dur) if ticks > max_dur => { self.state = EffectState::Stopped; self.devices.keys().for_each(|id| { let event = Event::new(GamepadId(id), EventType::ForceFeedbackEffectCompleted); self.completion_events.push(event); }); } _ => (), } let attenuation = self .distance_model .attenuation(self.position.distance(actor_pos)) * self.gain; if attenuation < 0.05 { return Magnitude::zero(); } let mut final_magnitude = Magnitude::zero(); for effect in &self.base_effects { match effect.magnitude_at(ticks) { BaseEffectType::Strong { magnitude } => { final_magnitude.strong = final_magnitude.strong.saturating_add(magnitude) } BaseEffectType::Weak { magnitude } => { final_magnitude.weak = final_magnitude.weak.saturating_add(magnitude) } }; } final_magnitude * attenuation } pub(super) fn flush_completion_events(&mut self) -> Vec { mem::take(&mut self.completion_events) } } /// (strong, weak) pair. #[derive(Copy, Clone, Debug)] pub(super) struct Magnitude { pub strong: u16, pub weak: u16, } impl Magnitude { pub fn zero() -> Self { Magnitude { strong: 0, weak: 0 } } } impl Mul for Magnitude { type Output = Magnitude; fn mul(self, rhs: f32) -> Self::Output { debug_assert!(rhs >= 0.0); let strong = self.strong as f32 * rhs; let strong = if strong > u16::MAX as f32 { u16::MAX } else { strong as u16 }; let weak = self.weak as f32 * rhs; let weak = if weak > u16::MAX as f32 { u16::MAX } else { weak as u16 }; Magnitude { strong, weak } } } impl AddAssign for Magnitude { fn add_assign(&mut self, rhs: Magnitude) { self.strong = self.strong.saturating_add(rhs.strong); self.weak = self.weak.saturating_add(rhs.weak); } } trait SliceVecExt { type Base; fn distance(self, from: Self) -> Self::Base; } impl SliceVecExt for [f32; 3] { type Base = f32; fn distance(self, from: Self) -> f32 { ((from[0] - self[0]).powi(2) + (from[1] - self[1]).powi(2) + (from[2] - self[2]).powi(2)) .sqrt() } } ================================================ FILE: gilrs/src/ff/mod.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. // This code is not used on wasm #![cfg_attr(target_arch = "wasm32", allow(dead_code))] //! Force feedback module. //! //! To use force feedback, you have to create one or more [`Effect`s](struct.Effect.html). Each //! `Effect` contains one or more [`BasicEffect`s](struct.BasicEffect.html) and parameters that //! describe effect's source, like it's position, gain or used //! [`DistanceModel`](enum.DistanceModel.html). Final strength of effect is based on saturating sum //! (to `u16::MAX`) of all base effects and time from the start of playback, attenuation from //! distance between effect source and listener (represented by gamepad) and effect's gain. //! //! See also [`Gilrs::set_listener_position()`](../struct.Gilrs.html#method.set_listener_position) //! and [`Gamepad::is_ff_supported()`](../struct.Gamepad.html#method.is_ff_supported). //! //! # Example //! //! ```rust //! use gilrs::Gilrs; //! use gilrs::ff::{EffectBuilder, Replay, BaseEffect, BaseEffectType, Ticks}; //! //! let mut gilrs = Gilrs::new().unwrap(); //! let support_ff = gilrs //! .gamepads() //! .filter_map(|(id, gp)| if gp.is_ff_supported() { Some(id) } else { None }) //! .collect::>(); //! //! let duration = Ticks::from_ms(150); //! let effect = EffectBuilder::new() //! .add_effect(BaseEffect { //! kind: BaseEffectType::Strong { magnitude: 60_000 }, //! scheduling: Replay { play_for: duration, with_delay: duration * 3, ..Default::default() }, //! envelope: Default::default(), //! }) //! .add_effect(BaseEffect { //! kind: BaseEffectType::Weak { magnitude: 60_000 }, //! scheduling: Replay { after: duration * 2, play_for: duration, with_delay: duration * 3 }, //! ..Default::default() //! }) //! .gamepads(&support_ff) //! .finish(&mut gilrs).unwrap(); //! //! effect.play().unwrap(); //! ``` //! //! See [`examples/ff_pos.rs`](https://gitlab.com/gilrs-project/gilrs/blob/v0.11.0/examples/ff_pos.rs) for //! more advanced example. mod base_effect; mod effect_source; pub(crate) mod server; mod time; pub use self::base_effect::{BaseEffect, BaseEffectType, Envelope, Replay}; pub use self::effect_source::{DistanceModel, DistanceModelError}; #[allow(unused_imports)] pub(crate) use self::time::TICK_DURATION; pub use self::time::{Repeat, Ticks}; use std::error::Error as StdError; use std::hash::{Hash, Hasher}; use std::sync::mpsc::{SendError, Sender}; use std::{f32, fmt}; use self::effect_source::EffectSource; use crate::ff::server::Message; use crate::gamepad::{Gamepad, GamepadId, Gilrs}; use crate::utils; use vec_map::VecMap; /// Handle to force feedback effect. /// /// `Effect` represents force feedback effect that can be played on one or more gamepads. It uses a /// form of reference counting, so it can be cheaply cloned. To create new `Effect` use /// [`EffectBuilder`](struct.EffectBuilder.html). /// /// All methods on can return `Error::SendFailed` although it shouldn't normally happen. pub struct Effect { id: usize, tx: Sender, } impl PartialEq for Effect { fn eq(&self, other: &Effect) -> bool { self.id == other.id } } impl Eq for Effect {} impl Hash for Effect { fn hash(&self, state: &mut H) { self.id.hash(state); } } impl Clone for Effect { fn clone(&self) -> Self { let _ = self.tx.send(Message::HandleCloned { id: self.id }); Effect { id: self.id, tx: self.tx.clone(), } } } impl Drop for Effect { fn drop(&mut self) { let _ = self.tx.send(Message::HandleDropped { id: self.id }); } } impl Effect { /// Plays effect on all associated gamepads. pub fn play(&self) -> Result<(), Error> { self.tx.send(Message::Play { id: self.id })?; Ok(()) } pub fn stop(&self) -> Result<(), Error> { self.tx.send(Message::Stop { id: self.id })?; Ok(()) } /// Changes gamepads that are associated with effect. Effect will be only played on gamepads /// from last call to this function. /// /// # Errors /// /// Returns `Error::Disconnected(id)` or `Error::FfNotSupported(id)` on first gamepad in `ids` /// that is disconnected or doesn't support force feedback. pub fn set_gamepads(&self, ids: &[GamepadId], gilrs: &Gilrs) -> Result<(), Error> { let mut gamepads = VecMap::new(); for dev in ids.iter().cloned() { if !gilrs .connected_gamepad(dev) .ok_or(Error::Disconnected(dev))? .is_ff_supported() { return Err(Error::FfNotSupported(dev)); } else { gamepads.insert(dev.0, ()); } } self.tx.send(Message::SetGamepads { id: self.id, gamepads, })?; Ok(()) } /// Adds gamepad to the list of gamepads associated with effect. /// /// # Errors /// /// Returns `Error::Disconnected(id)` or `Error::FfNotSupported(id)` if gamepad is not connected /// or does not support force feedback. pub fn add_gamepad(&self, gamepad: &Gamepad<'_>) -> Result<(), Error> { if !gamepad.is_connected() { Err(Error::Disconnected(gamepad.id())) } else if !gamepad.is_ff_supported() { Err(Error::FfNotSupported(gamepad.id())) } else { self.tx.send(Message::AddGamepad { id: self.id, gamepad_id: gamepad.id(), })?; Ok(()) } } /// Changes what should happen to effect when it ends. pub fn set_repeat(&self, repeat: Repeat) -> Result<(), Error> { self.tx.send(Message::SetRepeat { id: self.id, repeat, })?; Ok(()) } /// Changes distance model associated with effect. /// /// # Errors /// /// Returns `Error::InvalidDistanceModel` if `model` is not valid. See /// [`DistanceModel`](enum.DistanceModelError.html) for details. pub fn set_distance_model(&self, model: DistanceModel) -> Result<(), Error> { model.validate()?; self.tx .send(Message::SetDistanceModel { id: self.id, model })?; Ok(()) } /// Changes position of the source of effect. pub fn set_position>(&self, position: Vec3f) -> Result<(), Error> { let position = position.into(); self.tx.send(Message::SetPosition { id: self.id, position, })?; Ok(()) } /// Changes gain of the effect. `gain` will be clamped to \[0.0, f32::MAX\]. pub fn set_gain(&self, gain: f32) -> Result<(), Error> { let gain = utils::clamp(gain, 0.0, f32::MAX); self.tx.send(Message::SetGain { id: self.id, gain })?; Ok(()) } } /// Creates new [`Effect`](struct.Effect.html). #[derive(Clone, PartialEq, Debug)] pub struct EffectBuilder { base_effects: Vec, devices: VecMap<()>, repeat: Repeat, dist_model: DistanceModel, position: [f32; 3], gain: f32, } impl EffectBuilder { /// Creates new builder with following defaults: no gamepads, no base effects, repeat set to /// infinitely, no distance model, position in (0.0, 0.0, 0.0) and gain 1.0. Use `finish()` to /// create new effect. pub fn new() -> Self { EffectBuilder { base_effects: Vec::new(), devices: VecMap::new(), repeat: Repeat::Infinitely, dist_model: DistanceModel::None, position: [0.0, 0.0, 0.0], gain: 1.0, } } /// Adds new [`BaseEffect`](struct.BaseEffect.html). pub fn add_effect(&mut self, effect: BaseEffect) -> &mut Self { self.base_effects.push(effect); self } /// Changes gamepads that are associated with effect. Effect will be only played on gamepads /// from last call to this function. pub fn gamepads(&mut self, ids: &[GamepadId]) -> &mut Self { for dev in ids { self.devices.insert(dev.0, ()); } self } /// Adds gamepad to the list of gamepads associated with effect. pub fn add_gamepad(&mut self, gamepad: &Gamepad<'_>) -> &mut Self { self.devices.insert(gamepad.id().0, ()); self } /// Changes what should happen to effect when it ends. pub fn repeat(&mut self, repeat: Repeat) -> &mut Self { self.repeat = repeat; self } /// Changes distance model associated with effect. pub fn distance_model(&mut self, model: DistanceModel) -> &mut Self { self.dist_model = model; self } /// Changes position of the source of effect. pub fn position>(&mut self, position: Vec3f) -> &mut Self { self.position = position.into(); self } /// Changes gain of the effect. `gain` will be clamped to \[0.0, f32::MAX\]. pub fn gain(&mut self, gain: f32) -> &mut Self { self.gain = utils::clamp(gain, 0.0, f32::MAX); self } /// Validates all parameters and creates new effect. /// /// # Errors /// /// Returns `Error::Disconnected(id)` or `Error::FfNotSupported(id)` on first gamepad in `ids` /// that is disconnected or doesn't support force feedback. /// /// Returns `Error::InvalidDistanceModel` if `model` is not valid. See /// [`DistanceModel`](enum.DistanceModelError.html) for details. pub fn finish(&mut self, gilrs: &mut Gilrs) -> Result { for (dev, _) in &self.devices { let dev = GamepadId(dev); if !gilrs .connected_gamepad(dev) .ok_or(Error::Disconnected(dev))? .is_ff_supported() { return Err(Error::FfNotSupported(dev)); } } self.dist_model.validate()?; let effect = EffectSource::new( self.base_effects.clone(), self.devices.clone(), self.repeat, self.dist_model, self.position, self.gain, ); let id = gilrs.next_ff_id(); let tx = gilrs.ff_sender(); tx.send(Message::Create { id, effect: Box::new(effect), })?; Ok(Effect { id, tx: tx.clone() }) } } impl Default for EffectBuilder { fn default() -> Self { Self::new() } } /// Basic error type in force feedback module. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum Error { /// Force feedback is not supported by device with this ID FfNotSupported(GamepadId), /// Device is not connected Disconnected(GamepadId), /// Distance model is invalid. InvalidDistanceModel(DistanceModelError), /// The other end of channel was dropped. SendFailed, /// Unexpected error has occurred Other, } impl StdError for Error { fn source(&self) -> Option<&(dyn StdError + 'static)> { match self { Error::InvalidDistanceModel(m) => Some(m), _ => None, } } } impl fmt::Display for Error { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { let sbuf; let s = match self { Error::FfNotSupported(id) => { sbuf = format!( "force feedback is not supported by device with id {}.", id.0 ); sbuf.as_ref() } Error::Disconnected(id) => { sbuf = format!("device with id {} is not connected.", id.0); sbuf.as_ref() } Error::InvalidDistanceModel(_) => "distance model is invalid", Error::SendFailed => "receiving end of a channel is disconnected.", Error::Other => "unespected error has occurred.", }; fmt.write_str(s) } } impl From> for Error { fn from(_: SendError) -> Self { Error::SendFailed } } impl From for Error { fn from(f: DistanceModelError) -> Self { Error::InvalidDistanceModel(f) } } #[cfg(test)] mod tests { use super::*; #[test] fn envelope() { let env = Envelope { attack_length: Ticks(10), attack_level: 0.2, fade_length: Ticks(10), fade_level: 0.2, }; let dur = Ticks(40); assert_eq!(env.at(Ticks(0), dur), 0.2); assert_eq!(env.at(Ticks(5), dur), 0.6); assert_eq!(env.at(Ticks(10), dur), 1.0); assert_eq!(env.at(Ticks(20), dur), 1.0); assert_eq!(env.at(Ticks(30), dur), 1.0); assert_eq!(env.at(Ticks(35), dur), 0.6); assert_eq!(env.at(Ticks(40), dur), 0.19999999); } #[test] fn envelope_default() { let env = Envelope::default(); let dur = Ticks(40); assert_eq!(env.at(Ticks(0), dur), 1.0); assert_eq!(env.at(Ticks(20), dur), 1.0); assert_eq!(env.at(Ticks(40), dur), 1.0); } #[test] fn replay() { let replay = Replay { after: Ticks(10), play_for: Ticks(50), with_delay: Ticks(20), }; assert_eq!(replay.at(Ticks(0)), 1.0); assert_eq!(replay.at(Ticks(9)), 1.0); assert_eq!(replay.at(Ticks(10)), 1.0); assert_eq!(replay.at(Ticks(30)), 1.0); assert_eq!(replay.at(Ticks(59)), 0.0); assert_eq!(replay.at(Ticks(60)), 0.0); assert_eq!(replay.at(Ticks(70)), 0.0); } } ================================================ FILE: gilrs/src/ff/server.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use super::effect_source::{DistanceModel, EffectSource, EffectState, Magnitude}; use super::time::{Repeat, Ticks, TICK_DURATION}; use std::ops::{Deref, DerefMut}; use std::sync::mpsc::{self, Receiver, Sender}; use std::thread; use std::time::{Duration, Instant}; use crate::gamepad::GamepadId; use crate::Event; use gilrs_core::FfDevice; use vec_map::VecMap; #[derive(Debug)] pub(crate) enum Message { Create { id: usize, effect: Box, }, HandleCloned { id: usize, }, HandleDropped { id: usize, }, Play { id: usize, }, Stop { id: usize, }, Open { id: usize, device: FfDevice, }, Close { id: usize, }, SetListenerPosition { id: usize, position: [f32; 3], }, SetGamepads { id: usize, gamepads: VecMap<()>, }, AddGamepad { id: usize, gamepad_id: GamepadId, }, SetRepeat { id: usize, repeat: Repeat, }, SetDistanceModel { id: usize, model: DistanceModel, }, SetPosition { id: usize, position: [f32; 3], }, SetGain { id: usize, gain: f32, }, } pub(crate) enum FfMessage { EffectCompleted { event: Event }, } impl Message { // Whether to use trace level logging or debug fn use_trace_level(&self) -> bool { use self::Message::*; matches!( self, &SetListenerPosition { .. } | &HandleCloned { .. } | &HandleDropped { .. } ) } } #[derive(Debug)] struct Device { inner: FfDevice, position: [f32; 3], } struct Effect { source: EffectSource, /// Number of created effect's handles. count: usize, } impl Effect { fn inc(&mut self) -> usize { self.count += 1; self.count } fn dec(&mut self) -> usize { self.count -= 1; self.count } } impl From for Effect { fn from(source: EffectSource) -> Self { Effect { source, count: 1 } } } impl Deref for Effect { type Target = EffectSource; fn deref(&self) -> &Self::Target { &self.source } } impl DerefMut for Effect { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.source } } impl From for Device { fn from(inner: FfDevice) -> Self { Device { inner, position: [0.0, 0.0, 0.0], } } } pub(crate) fn run(tx: Sender, rx: Receiver) { let mut effects = VecMap::::new(); let mut devices = VecMap::::new(); let sleep_dur = Duration::from_millis(TICK_DURATION.into()); let mut tick = Ticks(0); let mut completion_events = Vec::::new(); loop { let t1 = Instant::now(); while let Ok(ev) = rx.try_recv() { if ev.use_trace_level() { trace!("New ff event: {:?}", ev); } else { debug!("New ff event: {:?}", ev); } match ev { Message::Create { id, effect } => { effects.insert(id, (*effect).into()); } Message::Play { id } => { if let Some(effect) = effects.get_mut(id) { effect.source.state = EffectState::Playing { since: tick } } else { error!("{:?} with wrong ID", ev); } } Message::Stop { id } => { if let Some(effect) = effects.get_mut(id) { effect.source.state = EffectState::Stopped } else { error!("{:?} with wrong ID", ev); } } Message::Open { id, device } => { devices.insert(id, device.into()); } Message::Close { id } => { devices.remove(id); } Message::SetListenerPosition { id, position } => { if let Some(device) = devices.get_mut(id) { device.position = position; } else { error!("{:?} with wrong ID", ev); } } Message::HandleCloned { id } => { if let Some(effect) = effects.get_mut(id) { effect.inc(); } else { error!("{:?} with wrong ID", ev); } } Message::HandleDropped { id } => { let mut drop = false; if let Some(effect) = effects.get_mut(id) { if effect.dec() == 0 { drop = true; } } else { error!("{:?} with wrong ID", ev); } if drop { effects.remove(id); } } Message::SetGamepads { id, gamepads } => { if let Some(eff) = effects.get_mut(id) { eff.source.devices = gamepads; } else { error!("Invalid effect id {} when changing gamepads.", id); } } Message::AddGamepad { id, gamepad_id } => { if let Some(eff) = effects.get_mut(id) { eff.source.devices.insert(gamepad_id.0, ()); } else { error!("Invalid effect id {} when changing gamepads.", id); } } Message::SetRepeat { id, repeat } => { if let Some(eff) = effects.get_mut(id) { eff.source.repeat = repeat; } else { error!("Invalid effect id {} when changing repeat mode.", id); } } Message::SetDistanceModel { id, model } => { if let Some(eff) = effects.get_mut(id) { eff.source.distance_model = model; } else { error!("Invalid effect id {} when changing distance model.", id); } } Message::SetPosition { id, position } => { if let Some(eff) = effects.get_mut(id) { eff.source.position = position; } else { error!("Invalid effect id {}.", id); } } Message::SetGain { id, gain } => { if let Some(eff) = effects.get_mut(id) { eff.source.gain = gain; } else { error!("Invalid effect id {} when changing effect gain.", id); } } } } combine_and_play(&mut effects, &mut devices, tick, &mut completion_events); completion_events.iter().for_each(|ev| { let _ = tx.send(FfMessage::EffectCompleted { event: *ev }); }); completion_events.clear(); let dur = Instant::now().duration_since(t1); if dur > sleep_dur { // TODO: Should we add dur - sleep_dur to next iteration's dur? warn!( "One iteration of a force feedback loop took more than {}ms!", TICK_DURATION ); } else { thread::sleep(sleep_dur - dur); } tick.inc(); } } pub(crate) fn init() -> (Sender, Receiver) { let (tx, _rx) = mpsc::channel(); let (_tx2, rx2) = mpsc::channel(); // Wasm doesn't support threads and force feedback #[cfg(not(target_arch = "wasm32"))] std::thread::Builder::new() .name("gilrs".to_owned()) .spawn(move || run(_tx2, _rx)) .expect("failed to spawn thread"); (tx, rx2) } fn combine_and_play( effects: &mut VecMap, devices: &mut VecMap, tick: Ticks, completion_events: &mut Vec, ) { for (dev_id, dev) in devices { let mut magnitude = Magnitude::zero(); for (_, ref mut effect) in effects.iter_mut() { if effect.devices.contains_key(dev_id) { magnitude += effect.combine_base_effects(tick, dev.position); completion_events.extend(effect.flush_completion_events()); } } trace!( "({:?}) Setting ff state of {:?} to {:?}", tick, dev, magnitude ); dev.inner.set_ff_state( magnitude.strong, magnitude.weak, Duration::from_millis(u64::from(TICK_DURATION) * 2), ); } } ================================================ FILE: gilrs/src/ff/time.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::ops::{Add, AddAssign, Mul, MulAssign, Rem, Sub, SubAssign}; use std::time::Duration; use crate::utils; pub(crate) const TICK_DURATION: u32 = 50; /// Represents duration. /// /// This type is only useful as input parameter for other functions in force feedback module. To /// create it, use `from_ms()` method. Keep in mind that `Ticks` **is not precise** representation /// of time. /// /// # Example /// /// ```rust /// use gilrs::ff::Ticks; /// use std::time::Duration; /// /// let t1 = Ticks::from_ms(110); /// let t2 = Ticks::from(Duration::from_millis(130)); /// /// /// `Ticks` is not precise. /// assert_eq!(t1, t2); /// ``` #[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Default)] pub struct Ticks(pub(super) u32); impl Ticks { pub fn from_ms(dur: u32) -> Self { Ticks(utils::ceil_div(dur, TICK_DURATION)) } pub(super) fn inc(&mut self) { self.0 += 1 } pub(super) fn checked_sub(self, rhs: Ticks) -> Option { self.0.checked_sub(rhs.0).map(Ticks) } } impl From for Ticks { fn from(dur: Duration) -> Self { Ticks::from_ms(dur.as_secs() as u32 * 1000 + dur.subsec_millis()) } } impl Add for Ticks { type Output = Ticks; fn add(self, rhs: Ticks) -> Self::Output { Ticks(self.0 + rhs.0) } } impl AddAssign for Ticks { fn add_assign(&mut self, rhs: Ticks) { self.0 += rhs.0 } } impl Sub for Ticks { type Output = Ticks; fn sub(self, rhs: Ticks) -> Self::Output { Ticks(self.0 - rhs.0) } } impl SubAssign for Ticks { fn sub_assign(&mut self, rhs: Ticks) { self.0 -= rhs.0 } } impl Mul for Ticks { type Output = Ticks; fn mul(self, rhs: u32) -> Self::Output { Ticks(self.0 * rhs) } } impl MulAssign for Ticks { fn mul_assign(&mut self, rhs: u32) { self.0 *= rhs; } } impl Rem for Ticks { type Output = Ticks; fn rem(self, rhs: Ticks) -> Self::Output { Ticks(self.0 % rhs.0) } } /// Describes how long effect should be played. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub enum Repeat { /// Play effect until stop() is called. #[default] Infinitely, /// Play effect for specified time. For(Ticks), } ================================================ FILE: gilrs/src/gamepad.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use crate::{ ev::{ state::{AxisData, ButtonData, GamepadState}, Axis, AxisOrBtn, Button, Code, Event, EventType, }, ff::{ server::{self, FfMessage, Message}, Error as FfError, }, mapping::{Mapping, MappingData, MappingDb}, utils, MappingError, }; use gilrs_core::{ self, AxisInfo, Error as PlatformError, Event as RawEvent, EventType as RawEventType, }; use uuid::Uuid; use std::cmp::Ordering; use std::{ collections::VecDeque, error, fmt::{self, Display}, sync::mpsc::{Receiver, Sender}, time::Duration, }; pub use gilrs_core::PowerInfo; #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; const DEFAULT_DEADZONE: f32 = 0.1; /// Main object responsible of managing gamepads. /// /// In order to get gamepad handle, use `gamepad()`, or `connected_gamepad()`. The main difference /// between these two is that `gamepad()` will also return handle to gamepad that is currently /// disconnected. However, both functions will return `None` if gamepad with given id has never /// existed. /// /// # Event loop /// /// All interesting actions like button was pressed or new controller was connected are represented /// by struct [`Event`](struct.Event.html). Use `next_event()` function to retrieve event from /// queue. /// /// ``` /// use gilrs::{Gilrs, Event, EventType, Button}; /// /// let mut gilrs = Gilrs::new().unwrap(); /// /// // Event loop /// loop { /// while let Some(event) = gilrs.next_event() { /// match event { /// Event { id, event: EventType::ButtonPressed(Button::South, _), .. } => { /// println!("Player {}: jump!", id) /// } /// Event { id, event: EventType::Disconnected, .. } => { /// println!("We lost player {}", id) /// } /// _ => (), /// }; /// } /// # break; /// } /// ``` /// /// # Cached gamepad state /// /// `Gilrs` also menage cached gamepad state. Updating state is done automatically, unless it's /// disabled by `GilrsBuilder::set_update_state(false)`. However, if you are using custom filters, /// you still have to update state manually – to do this call `update()` method. /// /// To access state you can use `Gamepad::state()` function. Gamepad also implement some state /// related functions directly, see [`Gamepad`](struct.Gamepad.html) for more. /// /// ## Counter /// /// `Gilrs` has additional functionality, referred here as *counter*. The idea behind it is simple, /// each time you end iteration of update loop, you call `Gilrs::inc()` which will increase /// internal counter by one. When state of one if elements changes, value of counter is saved. When /// checking state of one of elements you can tell exactly when this event happened. Timestamps are /// not good solution here because they can tell you when *system* observed event, not when you /// processed it. On the other hand, they are good when you want to implement key repeat or software /// debouncing. /// /// ``` /// use gilrs::{Gilrs, Button}; /// /// let mut gilrs = Gilrs::new().unwrap(); /// let mut player_one = None; /// /// loop { /// while let Some(ev) = gilrs.next_event() { /// if player_one.is_none() { /// player_one = Some(ev.id); /// } /// /// // Do other things with event /// } /// /// if let Some(id) = player_one { /// let gamepad = gilrs.gamepad(id); /// /// if gamepad.is_pressed(Button::DPadLeft) { /// // go left /// } /// /// match gamepad.button_data(Button::South) { /// Some(d) if d.is_pressed() && d.counter() == gilrs.counter() => { /// // jump only if button was observed to be pressed in this iteration /// } /// _ => () /// } /// } /// /// // Increase counter /// gilrs.inc(); /// # break; /// } /// #[derive(Debug)] pub struct Gilrs { inner: gilrs_core::Gilrs, next_id: usize, tx: Sender, rx: Receiver, counter: u64, mappings: MappingDb, default_filters: bool, events: VecDeque, axis_to_btn_pressed: f32, axis_to_btn_released: f32, pub(crate) update_state: bool, pub(crate) gamepads_data: Vec, } impl Gilrs { /// Creates new `Gilrs` with default settings. See [`GilrsBuilder`](struct.GilrsBuilder.html) /// for more details. pub fn new() -> Result { GilrsBuilder::new().build() } /// Returns next pending event. If there is no pending event, `None` is /// returned. This function will not block current thread and should be safe /// to call in async context. Doesn't block the thread it is run in pub fn next_event(&mut self) -> Option { self.next_event_inner(false, None) } /// Same as [Gilrs::next_event], but blocks the thread it is run in. Useful /// for apps that aren't run inside a loop and just react to the user's input, /// like GUI apps. /// /// ## Platform support /// /// This function is not supported on web and will always panic. pub fn next_event_blocking(&mut self, timeout: Option) -> Option { self.next_event_inner(true, timeout) } fn next_event_inner( &mut self, is_blocking: bool, blocking_timeout: Option, ) -> Option { use crate::ev::filter::{axis_dpad_to_button, deadzone, Filter, Jitter}; let ev = if self.default_filters { let jitter_filter = Jitter::new(); loop { let ev = self .next_event_priv(is_blocking, blocking_timeout) .filter_ev(&axis_dpad_to_button, self) .filter_ev(&jitter_filter, self) .filter_ev(&deadzone, self); // Skip all dropped events, there is no reason to return them match ev { Some(ev) if ev.is_dropped() => (), _ => break ev, } } } else { self.next_event_priv(is_blocking, blocking_timeout) }; if self.update_state { if let Some(ref ev) = ev { self.update(ev); } } ev } /// Returns next pending event. fn next_event_priv( &mut self, is_blocking: bool, blocking_timeout: Option, ) -> Option { if let Ok(msg) = self.rx.try_recv() { return match msg { FfMessage::EffectCompleted { event } => Some(event), } } if let Some(ev) = self.events.pop_front() { Some(ev) } else { let event = if is_blocking { self.inner.next_event_blocking(blocking_timeout) } else { self.inner.next_event() }; match event { Some(RawEvent { id, event: event_type, time, .. }) => { trace!("Original event: {:?}", event); let id = GamepadId(id); let event = match event_type { RawEventType::ButtonPressed(nec) => { let nec = Code(nec); match self.gamepad(id).axis_or_btn_name(nec) { Some(AxisOrBtn::Btn(b)) => { self.events.push_back(Event { id, time, event: EventType::ButtonChanged(b, 1.0, nec), }); EventType::ButtonPressed(b, nec) } Some(AxisOrBtn::Axis(a)) => EventType::AxisChanged(a, 1.0, nec), None => { self.events.push_back(Event { id, time, event: EventType::ButtonChanged(Button::Unknown, 1.0, nec), }); EventType::ButtonPressed(Button::Unknown, nec) } } } RawEventType::ButtonReleased(nec) => { let nec = Code(nec); match self.gamepad(id).axis_or_btn_name(nec) { Some(AxisOrBtn::Btn(b)) => { self.events.push_back(Event { id, time, event: EventType::ButtonChanged(b, 0.0, nec), }); EventType::ButtonReleased(b, nec) } Some(AxisOrBtn::Axis(a)) => EventType::AxisChanged(a, 0.0, nec), None => { self.events.push_back(Event { id, time, event: EventType::ButtonChanged(Button::Unknown, 0.0, nec), }); EventType::ButtonReleased(Button::Unknown, nec) } } } RawEventType::AxisValueChanged(val, nec) => { // Let's trust at least our backend code let axis_info = *self.gamepad(id).inner.axis_info(nec).unwrap(); let nec = Code(nec); match self.gamepad(id).axis_or_btn_name(nec) { Some(AxisOrBtn::Btn(b)) => { let val = btn_value(&axis_info, val); if val >= self.axis_to_btn_pressed && !self.gamepad(id).state().is_pressed(nec) { self.events.push_back(Event { id, time, event: EventType::ButtonChanged(b, val, nec), }); EventType::ButtonPressed(b, nec) } else if val <= self.axis_to_btn_released && self.gamepad(id).state().is_pressed(nec) { self.events.push_back(Event { id, time, event: EventType::ButtonChanged(b, val, nec), }); EventType::ButtonReleased(b, nec) } else { EventType::ButtonChanged(b, val, nec) } } Some(AxisOrBtn::Axis(a)) => { EventType::AxisChanged(a, axis_value(&axis_info, val, a), nec) } None => EventType::AxisChanged( Axis::Unknown, axis_value(&axis_info, val, Axis::Unknown), nec, ), } } RawEventType::Connected => { match id.0.cmp(&self.gamepads_data.len()) { Ordering::Equal => { self.gamepads_data.push(GamepadData::new( id, self.tx.clone(), self.inner.gamepad(id.0).unwrap(), &self.mappings, )); } Ordering::Less => { self.gamepads_data[id.0] = GamepadData::new( id, self.tx.clone(), self.inner.gamepad(id.0).unwrap(), &self.mappings, ); } Ordering::Greater => { error!( "Platform implementation error: got Connected event with \ id {}, when expected id {}", id.0, self.gamepads_data.len() ); } } EventType::Connected } RawEventType::Disconnected => { let _ = self.tx.send(Message::Close { id: id.0 }); EventType::Disconnected } _ => { unimplemented!() } }; Some(Event { id, event, time }) } None => None, } } } /// Updates internal state according to `event`. /// /// Please note, that it's not necessary to call this function unless you modify events by using /// additional filters and disabled automatic updates when creating `Gilrs`. pub fn update(&mut self, event: &Event) { use crate::EventType::*; let counter = self.counter; let data = match self.gamepads_data.get_mut(event.id.0) { Some(d) => d, None => return, }; match event.event { ButtonPressed(_, nec) => { data.state.set_btn_pressed(nec, true, counter, event.time); } ButtonReleased(_, nec) => { data.state.set_btn_pressed(nec, false, counter, event.time); } ButtonRepeated(_, nec) => { data.state.set_btn_repeating(nec, counter, event.time); } ButtonChanged(_, value, nec) => { data.state.set_btn_value(nec, value, counter, event.time); } AxisChanged(_, value, nec) => { data.state .update_axis(nec, AxisData::new(value, counter, event.time)); } Disconnected | Connected | Dropped | ForceFeedbackEffectCompleted => (), } } /// Increases internal counter by one. Counter data is stored with state and can be used to /// determine when last event happened. You probably want to use this function in your update /// loop after processing events. pub fn inc(&mut self) { // Counter is 62bit. See `ButtonData`. if self.counter == 0x3FFF_FFFF_FFFF_FFFF { self.counter = 0; } else { self.counter += 1; } } /// Returns counter. Counter data is stored with state and can be used to determine when last /// event happened. pub fn counter(&self) -> u64 { self.counter } /// Sets counter to 0. pub fn reset_counter(&mut self) { self.counter = 0; } fn finish_gamepads_creation(&mut self) { let tx = self.tx.clone(); for id in 0..self.inner.last_gamepad_hint() { let gamepad = self.inner.gamepad(id).unwrap(); self.gamepads_data.push(GamepadData::new( GamepadId(id), tx.clone(), gamepad, &self.mappings, )) } } /// Returns handle to gamepad with given ID. Unlike `connected_gamepad()`, this function will /// also return handle to gamepad that is currently disconnected. /// /// ``` /// # let mut gilrs = gilrs::Gilrs::new().unwrap(); /// use gilrs::{Button, EventType}; /// /// loop { /// while let Some(ev) = gilrs.next_event() { /// // unwrap() should never panic because we use id from event /// let is_up_pressed = gilrs.gamepad(ev.id).is_pressed(Button::DPadUp); /// /// match ev.event { /// EventType::ButtonPressed(Button::South, _) if is_up_pressed => { /// // do something… /// } /// _ => (), /// } /// } /// # break; /// } /// ``` pub fn gamepad(&self, id: GamepadId) -> Gamepad { Gamepad { inner: self.inner.gamepad(id.0).unwrap(), data: &self.gamepads_data[id.0], } } /// Returns a reference to connected gamepad or `None`. pub fn connected_gamepad(&self, id: GamepadId) -> Option> { // Make sure that it will not panic even with invalid GamepadId, so ConnectedGamepadIterator // will always work. if let Some(data) = self.gamepads_data.get(id.0) { let inner = self.inner.gamepad(id.0)?; if inner.is_connected() { Some(Gamepad { inner, data }) } else { None } } else { None } } /// Returns iterator over all connected gamepads and their ids. /// /// ``` /// # let gilrs = gilrs::Gilrs::new().unwrap(); /// for (id, gamepad) in gilrs.gamepads() { /// assert!(gamepad.is_connected()); /// println!("Gamepad with id {} and name {} is connected", /// id, gamepad.name()); /// } /// ``` pub fn gamepads(&self) -> ConnectedGamepadsIterator<'_> { ConnectedGamepadsIterator(self, 0) } /// Adds `ev` at the end of internal event queue. It can later be retrieved with `next_event()`. pub fn insert_event(&mut self, ev: Event) { self.events.push_back(ev); } pub(crate) fn ff_sender(&self) -> &Sender { &self.tx } /// Sets gamepad's mapping and returns SDL2 representation of them. Returned mappings may not be /// compatible with SDL2 - if it is important, use /// [`set_mapping_strict()`](#method.set_mapping_strict). /// /// The `name` argument can be a string slice with custom gamepad name or `None`. If `None`, /// gamepad name reported by driver will be used. /// /// # Errors /// /// This function return error if `name` contains comma, `mapping` have axis and button entry /// for same element (for example `Axis::LetfTrigger` and `Button::LeftTrigger`) or gamepad does /// not have any element with `EvCode` used in mapping. `Button::Unknown` and /// `Axis::Unknown` are not allowd as keys to `mapping` – in this case, /// `MappingError::UnknownElement` is returned. /// /// Error is also returned if this function is not implemented or gamepad is not connected. /// /// # Example /// /// ``` /// use gilrs::{Mapping, Button}; /// /// # let mut gilrs = gilrs::Gilrs::new().unwrap(); /// let mut data = Mapping::new(); /// // … /// /// // or `match gilrs.set_mapping(0, &data, None) {` /// match gilrs.set_mapping(0, &data, "Custom name") { /// Ok(sdl) => println!("SDL2 mapping: {}", sdl), /// Err(e) => println!("Failed to set mapping: {}", e), /// }; /// ``` /// /// See also `examples/mapping.rs`. pub fn set_mapping<'b, O: Into>>( &mut self, gamepad_id: usize, mapping: &MappingData, name: O, ) -> Result { if let Some(gamepad) = self.inner.gamepad(gamepad_id) { if !gamepad.is_connected() { return Err(MappingError::NotConnected); } let name = match name.into() { Some(s) => s, None => gamepad.name(), }; let (mapping, s) = Mapping::from_data( mapping, gamepad.buttons(), gamepad.axes(), name, Uuid::from_bytes(gamepad.uuid()), )?; // We checked if gamepad is connected, so it should never panic let data = &mut self.gamepads_data[gamepad_id]; data.mapping = mapping; Ok(s) } else { Err(MappingError::NotConnected) } } /// Similar to [`set_mapping()`](#method.set_mapping) but returned string should be compatible /// with SDL2. /// /// # Errors /// /// Returns `MappingError::NotSdl2Compatible` if `mapping` have an entry for `Button::{C, Z}` /// or `Axis::{LeftZ, RightZ}`. pub fn set_mapping_strict<'b, O: Into>>( &mut self, gamepad_id: usize, mapping: &MappingData, name: O, ) -> Result { if mapping.button(Button::C).is_some() || mapping.button(Button::Z).is_some() || mapping.axis(Axis::LeftZ).is_some() || mapping.axis(Axis::RightZ).is_some() { Err(MappingError::NotSdl2Compatible) } else { self.set_mapping(gamepad_id, mapping, name) } } pub(crate) fn next_ff_id(&mut self) -> usize { // TODO: reuse free ids let id = self.next_id; self.next_id = match self.next_id.checked_add(1) { Some(x) => x, None => panic!("Failed to assign ID to new effect"), }; id } } /// Allow to create `Gilrs ` with customized behaviour. pub struct GilrsBuilder { mappings: MappingDb, default_filters: bool, axis_to_btn_pressed: f32, axis_to_btn_released: f32, update_state: bool, env_mappings: bool, included_mappings: bool, } impl GilrsBuilder { /// Create builder with default settings. Use `build()` to create `Gilrs`. pub fn new() -> Self { GilrsBuilder { mappings: MappingDb::new(), default_filters: true, axis_to_btn_pressed: 0.75, axis_to_btn_released: 0.65, update_state: true, env_mappings: true, included_mappings: true, } } /// If `true`, use [`axis_dpad_to_button`](ev/filter/fn.axis_dpad_to_button.html), /// [`Jitter`](ev/filter/struct.Jitter.html) and [`deadzone`](ev/filter/fn.deadzone.html) /// filters with default parameters. Defaults to `true`. pub fn with_default_filters(mut self, default_filters: bool) -> Self { self.default_filters = default_filters; self } /// Adds SDL mappings. pub fn add_mappings(mut self, mappings: &str) -> Self { self.mappings.insert(mappings); self } /// If true, will add SDL mappings from `SDL_GAMECONTROLLERCONFIG` environment variable. /// Defaults to true. pub fn add_env_mappings(mut self, env_mappings: bool) -> Self { self.env_mappings = env_mappings; self } /// If true, will add SDL mappings included from /// https://github.com/gabomdq/SDL_GameControllerDB. Defaults to true. pub fn add_included_mappings(mut self, included_mappings: bool) -> Self { self.included_mappings = included_mappings; self } /// Sets values on which `ButtonPressed` and `ButtonReleased` events will be emitted. `build()` /// will return error if `pressed ≤ released` or if one of values is outside [0.0, 1.0]. /// /// Defaults to 0.75 for `pressed` and 0.65 for `released`. pub fn set_axis_to_btn(mut self, pressed: f32, released: f32) -> Self { self.axis_to_btn_pressed = pressed; self.axis_to_btn_released = released; self } /// Disable or enable automatic state updates. You should use this if you use custom filters; /// in this case you have to update state manually anyway. pub fn set_update_state(mut self, enabled: bool) -> Self { self.update_state = enabled; self } /// Creates `Gilrs`. pub fn build(mut self) -> Result { if self.included_mappings { self.mappings.add_included_mappings(); } if self.env_mappings { self.mappings.add_env_mappings(); } debug!("Loaded {} mappings.", self.mappings.len()); if self.axis_to_btn_pressed <= self.axis_to_btn_released || self.axis_to_btn_pressed < 0.0 || self.axis_to_btn_pressed > 1.0 || self.axis_to_btn_released < 0.0 || self.axis_to_btn_released > 1.0 { return Err(Error::InvalidAxisToBtn); } let mut is_dummy = false; let inner = match gilrs_core::Gilrs::new() { Ok(g) => g, Err(PlatformError::NotImplemented(g)) => { is_dummy = true; g } Err(PlatformError::Other(e)) => return Err(Error::Other(e)), Err(_) => unimplemented!(), }; let (tx, rx) = server::init(); let mut gilrs = Gilrs { inner, next_id: 0, tx, rx, counter: 0, mappings: self.mappings, default_filters: self.default_filters, events: VecDeque::new(), axis_to_btn_pressed: self.axis_to_btn_pressed, axis_to_btn_released: self.axis_to_btn_released, update_state: self.update_state, gamepads_data: Vec::new(), }; gilrs.finish_gamepads_creation(); if is_dummy { Err(Error::NotImplemented(gilrs)) } else { Ok(gilrs) } } } impl Default for GilrsBuilder { fn default() -> Self { Self::new() } } /// Iterator over all connected gamepads. pub struct ConnectedGamepadsIterator<'a>(&'a Gilrs, usize); impl<'a> Iterator for ConnectedGamepadsIterator<'a> { type Item = (GamepadId, Gamepad<'a>); fn next(&mut self) -> Option<(GamepadId, Gamepad<'a>)> { loop { if self.1 == self.0.inner.last_gamepad_hint() { return None; } if let Some(gp) = self.0.connected_gamepad(GamepadId(self.1)) { let idx = self.1; self.1 += 1; return Some((GamepadId(idx), gp)); } self.1 += 1; } } } /// Represents handle to game controller. /// /// Using this struct you can access cached gamepad state, information about gamepad such as name /// or UUID and manage force feedback effects. #[derive(Debug, Copy, Clone)] pub struct Gamepad<'a> { data: &'a GamepadData, inner: &'a gilrs_core::Gamepad, } impl Gamepad<'_> { /// Returns the mapping name if it exists otherwise returns the os provided name. pub fn name(&self) -> &str { if let Some(map_name) = self.map_name() { map_name } else { self.os_name() } } /// if `mapping_source()` is `SdlMappings` returns the name of the mapping used by the gamepad. /// Otherwise returns `None`. pub fn map_name(&self) -> Option<&str> { self.data.map_name() } /// Returns the name of the gamepad supplied by the OS. pub fn os_name(&self) -> &str { self.inner.name() } /// Returns gamepad's UUID. /// /// It is recommended to process with the [UUID crate](https://crates.io/crates/uuid). /// Use `Uuid::from_bytes` method to create a `Uuid` from the returned bytes. pub fn uuid(&self) -> [u8; 16] { self.inner.uuid() } /// Returns the vendor ID, as assigned by the USB-IF, when available. pub fn vendor_id(&self) -> Option { self.inner.vendor_id() } /// Returns the product ID, as assigned by the vendor, when available. pub fn product_id(&self) -> Option { self.inner.product_id() } /// Returns cached gamepad state. pub fn state(&self) -> &GamepadState { &self.data.state } /// Returns true if gamepad is connected. pub fn is_connected(&self) -> bool { self.inner.is_connected() } /// Examines cached gamepad state to check if given button is pressed. Panics if `btn` is /// `Unknown`. /// /// If you know `Code` of the element that you want to examine, it's recommended to use methods /// directly on `State`, because this version have to check which `Code` is mapped to element of /// gamepad. pub fn is_pressed(&self, btn: Button) -> bool { self.data.is_pressed(btn) } /// Examines cached gamepad state to check axis's value. Panics if `axis` is `Unknown`. /// /// If you know `Code` of the element that you want to examine, it's recommended to use methods /// directly on `State`, because this version have to check which `Code` is mapped to element of /// gamepad. pub fn value(&self, axis: Axis) -> f32 { self.data.value(axis) } /// Returns button state and when it changed. /// /// If you know `Code` of the element that you want to examine, it's recommended to use methods /// directly on `State`, because this version have to check which `Code` is mapped to element of /// gamepad. pub fn button_data(&self, btn: Button) -> Option<&ButtonData> { self.data.button_data(btn) } /// Returns axis state and when it changed. /// /// If you know `Code` of the element that you want to examine, it's recommended to use methods /// directly on `State`, because this version have to check which `Code` is mapped to element of /// gamepad. pub fn axis_data(&self, axis: Axis) -> Option<&AxisData> { self.data.axis_data(axis) } /// Returns device's power supply state. See [`PowerInfo`](enum.PowerInfo.html) for details. pub fn power_info(&self) -> PowerInfo { self.inner.power_info() } /// Returns source of gamepad mapping. Can be used to filter gamepads which do not provide /// unified controller layout. /// /// ``` /// use gilrs::MappingSource; /// # let mut gilrs = gilrs::Gilrs::new().unwrap(); /// /// for (_, gamepad) in gilrs.gamepads().filter( /// |gp| gp.1.mapping_source() != MappingSource::None) /// { /// println!("{} is ready to use!", gamepad.name()); /// } /// ``` pub fn mapping_source(&self) -> MappingSource { if self.data.mapping.is_default() { // TODO: check if it's Driver or None MappingSource::Driver } else { MappingSource::SdlMappings } } /// Returns true if force feedback is supported by device. pub fn is_ff_supported(&self) -> bool { self.inner.is_ff_supported() } /// Change gamepad position used by force feedback effects. pub fn set_listener_position>( &self, position: Vec3, ) -> Result<(), FfError> { if !self.is_connected() { Err(FfError::Disconnected(self.id())) } else if !self.is_ff_supported() { Err(FfError::FfNotSupported(self.id())) } else { self.data.tx.send(Message::SetListenerPosition { id: self.data.id.0, position: position.into(), })?; Ok(()) } } /// Returns `AxisOrBtn` mapped to `Code`. pub fn axis_or_btn_name(&self, ec: Code) -> Option { self.data.axis_or_btn_name(ec) } /// Returns `Code` associated with `btn`. pub fn button_code(&self, btn: Button) -> Option { self.data.button_code(btn) } /// Returns `Code` associated with `axis`. pub fn axis_code(&self, axis: Axis) -> Option { self.data.axis_code(axis) } /// Returns area in which axis events should be ignored. pub fn deadzone(&self, axis: Code) -> Option { self.inner.axis_info(axis.0).map(|i| { let range = i.max as f32 - i.min as f32; if range == 0.0 { 0.0 } else { i.deadzone .map(|d| d as f32 / range * 2.0) .unwrap_or(DEFAULT_DEADZONE) } }) } /// Returns ID of gamepad. pub fn id(&self) -> GamepadId { self.data.id } pub(crate) fn mapping(&self) -> &Mapping { &self.data.mapping } } #[derive(Debug)] pub(crate) struct GamepadData { state: GamepadState, mapping: Mapping, tx: Sender, id: GamepadId, // Flags used by the deadzone filter. pub(crate) have_sent_nonzero_for_axis: [bool; 6], } impl GamepadData { fn new( id: GamepadId, tx: Sender, gamepad: &gilrs_core::Gamepad, db: &MappingDb, ) -> Self { let uuid = Uuid::from_bytes(gamepad.uuid()); let mapping = db .get(uuid) .map( |s| match Mapping::parse_sdl_mapping(s, gamepad.buttons(), gamepad.axes()) { Ok(result) => result, Err(e) => { warn!( "Unable to parse SDL mapping for UUID {uuid}\n\t{e:?}\n\tDefault mapping \ will be used.", ); Mapping::default(gamepad) } }, ) .unwrap_or_else(|| { warn!("No mapping found for UUID {uuid}\n\tDefault mapping will be used."); Mapping::default(gamepad) }); if gamepad.is_ff_supported() && gamepad.is_connected() { if let Some(device) = gamepad.ff_device() { let _ = tx.send(Message::Open { id: id.0, device }); } } GamepadData { state: GamepadState::new(), mapping, tx, id, have_sent_nonzero_for_axis: Default::default(), } } /// if `mapping_source()` is `SdlMappings` returns the name of the mapping used by the gamepad. /// Otherwise returns `None`. /// /// Warning: Mappings are set after event `Connected` is processed, therefore this function will /// always return `None` before first calls to `Gilrs::next_event()`. pub fn map_name(&self) -> Option<&str> { if self.mapping.is_default() { None } else { Some(self.mapping.name()) } } /// Examines cached gamepad state to check if the given button is pressed. Panics if `btn` is /// `Unknown`. /// /// If you know `Code` of the element that you want to examine, it's recommended to use methods /// directly on `State`, because this version has to check which `Code` is mapped to element of /// gamepad. pub fn is_pressed(&self, btn: Button) -> bool { assert_ne!(btn, Button::Unknown); self.button_code(btn) .or_else(|| btn.to_nec()) .map(|nec| self.state.is_pressed(nec)) .unwrap_or(false) } /// Examines cached gamepad state to check axis's value. Panics if `axis` is `Unknown`. /// /// If you know `Code` of the element that you want to examine, it's recommended to use methods /// directly on `State`, because this version has to check which `Code` is mapped to element of /// gamepad. pub fn value(&self, axis: Axis) -> f32 { assert_ne!(axis, Axis::Unknown); self.axis_code(axis) .map(|nec| self.state.value(nec)) .unwrap_or(0.0) } /// Returns button state and when it changed. /// /// If you know `Code` of the element that you want to examine, it's recommended to use methods /// directly on `State`, because this version has to check which `Code` is mapped to element of /// gamepad. pub fn button_data(&self, btn: Button) -> Option<&ButtonData> { self.button_code(btn) .and_then(|nec| self.state.button_data(nec)) } /// Returns axis state and when it changed. /// /// If you know `Code` of the element that you want to examine, it's recommended to use methods /// directly on `State`, because this version has to check which `Code` is mapped to element of /// gamepad. pub fn axis_data(&self, axis: Axis) -> Option<&AxisData> { self.axis_code(axis) .and_then(|nec| self.state.axis_data(nec)) } /// Returns `AxisOrBtn` mapped to `Code`. pub fn axis_or_btn_name(&self, ec: Code) -> Option { self.mapping.map(&ec.0) } /// Returns `Code` associated with `btn`. pub fn button_code(&self, btn: Button) -> Option { self.mapping.map_rev(&AxisOrBtn::Btn(btn)).map(Code) } /// Returns `Code` associated with `axis`. pub fn axis_code(&self, axis: Axis) -> Option { self.mapping.map_rev(&AxisOrBtn::Axis(axis)).map(Code) } } /// Source of gamepad mappings. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum MappingSource { /// Gamepad uses SDL mappings. SdlMappings, /// Gamepad does not use any mappings but driver should provide unified controller layout. Driver, /// Gamepad does not use any mappings and most gamepad events will probably be `Button::Unknown` /// or `Axis::Unknown` None, } /// Gamepad ID. /// /// It's not possible to create an instance of this type directly, but you can obtain one from a Gamepad /// handle or any event. ID is valid for the entire lifetime of the `Gilrs` context. #[derive(Copy, Clone, Eq, PartialEq, Debug, Hash)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] pub struct GamepadId(pub(crate) usize); impl From for usize { fn from(x: GamepadId) -> usize { x.0 } } impl Display for GamepadId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } fn axis_value(info: &AxisInfo, val: i32, axis: Axis) -> f32 { let mut range = info.max as f32 - info.min as f32; let mut val = val as f32 - info.min as f32; if let Some(i_range) = info.max.checked_sub(info.min) { // Only consider adjusting range & val if calculating the range doesn't cause overflow. If // the range is so large overflow occurs, adjusting values by 1.0 would be insignificant. if i_range % 2 == 1 { // Add one to range and val, so the value is centered (like 127/255) will be mapped 0.0 range += 1.0; val += 1.0; } } val = val / range * 2.0 - 1.0; if gilrs_core::IS_Y_AXIS_REVERSED && (axis == Axis::LeftStickY || axis == Axis::RightStickY || axis == Axis::DPadY) && val != 0.0 { val = -val; } utils::clamp(val, -1.0, 1.0) } fn btn_value(info: &AxisInfo, val: i32) -> f32 { let range = info.max as f32 - info.min as f32; let mut val = val as f32 - info.min as f32; val /= range; utils::clamp(val, 0.0, 1.0) } /// Error type which can be returned when creating `Gilrs`. #[non_exhaustive] #[derive(Debug)] pub enum Error { /// Gilrs does not support the current platform, but you can use dummy context from this error if /// gamepad input is not essential. NotImplemented(Gilrs), /// Either `pressed ≤ released` or one of values is outside [0.0, 1.0] range. InvalidAxisToBtn, /// Platform specific error. Other(Box), } impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Error::NotImplemented(_) => f.write_str("Gilrs does not support current platform."), Error::InvalidAxisToBtn => f.write_str( "Either `pressed ≤ released` or one of values is outside [0.0, 1.0] range.", ), Error::Other(ref e) => e.fmt(f), } } } impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { Error::Other(e) => Some(e.as_ref()), _ => None, } } } const _: () = { const fn assert_send() {} #[cfg(not(target_arch = "wasm32"))] assert_send::(); }; #[cfg(test)] mod tests { use super::{axis_value, btn_value, Axis, AxisInfo}; #[test] fn axis_value_documented_case() { let info = AxisInfo { min: 0, max: 255, deadzone: None, }; let axis = Axis::LeftStickY; assert_eq!(0., axis_value(&info, 127, axis)); } #[test] fn axis_value_overflow() { let info = AxisInfo { min: i32::MIN, max: i32::MAX, deadzone: None, }; let axis = Axis::LeftStickY; assert_eq!(0., axis_value(&info, -1, axis)); assert_eq!(0., axis_value(&info, 0, axis)); assert_eq!(0., axis_value(&info, 1, axis)); assert_eq!(1.0, axis_value(&info, i32::MIN, axis)); assert_eq!(-1.0, axis_value(&info, i32::MAX, axis)); } #[test] fn btn_value_overflow() { let info = AxisInfo { min: i32::MIN, max: i32::MAX, deadzone: None, }; assert_eq!(0.5, btn_value(&info, -1)); assert_eq!(0.5, btn_value(&info, 0)); assert_eq!(0.5, btn_value(&info, 1)); assert_eq!(0.0, btn_value(&info, i32::MIN)); assert_eq!(1.0, btn_value(&info, i32::MAX)); } } ================================================ FILE: gilrs/src/lib.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. //! GilRs - Game Input Library for Rust //! =================================== //! //! GilRs abstract platform specific APIs to provide unified interfaces for working with gamepads. //! //! Main features: //! //! - Unified gamepad layout—buttons and axes are represented by familiar names //! - Support for SDL2 mappings including `SDL_GAMECONTROLLERCONFIG` environment //! variable which Steam uses //! - Hotplugging—GilRs will try to assign new IDs for new gamepads and reuse the same //! ID for gamepads which reconnected //! - Force feedback (rumble) //! - Power information (is gamepad wired, current battery status) //! //! Example //! ------- //! //! ```rust //! use gilrs::{Gilrs, Button, Event}; //! //! let mut gilrs = Gilrs::new().unwrap(); //! //! // Iterate over all connected gamepads //! for (_id, gamepad) in gilrs.gamepads() { //! println!("{} is {:?}", gamepad.name(), gamepad.power_info()); //! } //! //! let mut active_gamepad = None; //! //! loop { //! // Examine new events //! while let Some(Event { id, event, time, .. }) = gilrs.next_event() { //! println!("{:?} New event from {}: {:?}", time, id, event); //! active_gamepad = Some(id); //! } //! //! // You can also use cached gamepad state //! if let Some(gamepad) = active_gamepad.map(|id| gilrs.gamepad(id)) { //! if gamepad.is_pressed(Button::South) { //! println!("Button South is pressed (XBox - A, PS - X)"); //! } //! } //! # break; //! } //! ``` //! //! Supported features //! ------------------ //! //! | | Input | Hotplugging | Force feedback | //! |------------------|:-----:|:-----------:|:--------------:| //! | Linux/BSD (evdev)| ✓ | ✓ | ✓ | //! | Windows | ✓ | ✓ | ✓ | //! | OS X | ✓ | ✓ | ✕ | //! | Wasm | ✓ | ✓ | n/a | //! | Android | ✕ | ✕ | ✕ | //! //! Controller layout //! ----------------- //! //! ![Controller layout](https://gilrs-project.gitlab.io/gilrs/img/controller.svg) //! [original image by nicefrog](http://opengameart.org/content/generic-gamepad-template) //! //! Mappings //! -------- //! //! GilRs use SDL-compatible controller mappings to fix on Linux legacy drivers that doesn't follow //! [Linux Gamepad API](https://www.kernel.org/doc/Documentation/input/gamepad.txt) and to provide //! unified button layout for platforms that doesn't make any guarantees about it. The main source //! is [SDL_GameControllerDB](https://github.com/gabomdq/SDL_GameControllerDB), but library also //! support loading mappings from environment variable `SDL_GAMECONTROLLERCONFIG` (which Steam //! use). //! //! Cargo features //! -------------- //! //! - `serde-serialize` - enable deriving of serde's `Serialize` and `Deserialize` for //! various types. //! - `wgi` - use Windows Gaming Input on Windows (enabled by default). //! - `xinput` - use XInput on Windows. //! //! Platform specific notes //! ====================== //! //! Linux/BSD (evdev) //! ----- //! //! With evdev, GilRs read (and write, in case of force feedback) directly from appropriate //! `/dev/input/event*` file. This mean that user have to have read and write access to this file. //! On most distros it shouldn't be a problem, but if it is, you will have to create udev rule. //! On FreeBSD generic HID gamepads use hgame(4) and special use Linux driver via `webcamd`. //! //! To build GilRs, you will need pkg-config and libudev .pc file. On some distributions this file //! is packaged in separate archive (e.g., `libudev-dev` in Debian, `libudev-devd` in FreeBSD). //! //! Windows //! ----- //! //! Windows defaults to using Windows Gaming Input instead of XInput. If you need to use XInput you //! can disable the `wgi` feature (it's enabled by default) and enable the `xinput` feature. //! //! Windows Gaming Input requires an in focus window to be associated with the process to receive //! events. You can still switch back to using xInput by turning off default features and enabling //! the xinput feature. //! //! Note: Some (Older?) devices may still report inputs without a window but this is not the case //! for all devices so if you are writing a terminal based game, use the xinput feature instead. //! //! Wasm //! ----- //! //! Wasm implementation uses stdweb, or wasm-bindgen with the wasm-bindgen feature. //! For stdweb, you will need [cargo-web](https://github.com/koute/cargo-web) to build gilrs for //! wasm32-unknown-unknown. For wasm-bindgen, you will need the wasm-bindgen cli or a tool like //! [wasm-pack](https://rustwasm.github.io/wasm-pack/installer/). //! Unlike other platforms, events are only generated when you call `Gilrs::next_event()`. #[macro_use] extern crate log; mod constants; mod gamepad; mod mapping; mod utils; pub mod ev; pub mod ff; pub use crate::ev::filter::Filter; pub use crate::ev::{Axis, Button, Event, EventType}; pub use crate::gamepad::{ ConnectedGamepadsIterator, Error, Gamepad, GamepadId, Gilrs, GilrsBuilder, MappingSource, PowerInfo, }; pub use crate::mapping::{MappingData as Mapping, MappingError}; ================================================ FILE: gilrs/src/mapping/mod.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. #![cfg_attr(target_os = "windows", allow(dead_code))] mod parser; use crate::ev::{self, Axis, AxisOrBtn, Button}; use crate::utils::PATH_SEPARATOR; use gilrs_core::native_ev_codes as nec; use gilrs_core::EvCode; use std::collections::HashMap; use std::env; use std::error::Error; use std::fmt::{Display, Formatter, Result as FmtResult, Write as _}; use fnv::FnvHashMap; use uuid::Uuid; use vec_map::VecMap; use self::parser::{Error as ParserError, ErrorKind as ParserErrorKind, Parser, Token}; /// Platform name used by SDL mappings #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))] const SDL_PLATFORM_NAME: &str = "Linux"; #[cfg(target_os = "macos")] const SDL_PLATFORM_NAME: &str = "Mac OS X"; #[cfg(target_os = "windows")] const SDL_PLATFORM_NAME: &str = "Windows"; #[cfg(all( not(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd")), not(target_os = "macos"), not(target_os = "windows") ))] const SDL_PLATFORM_NAME: &str = "Unknown"; #[derive(Debug)] #[cfg_attr(test, derive(PartialEq))] /// Store mappings from one `EvCode` (`u16`) to another. /// /// This struct is internal, `MappingData` is exported in public interface as `Mapping`. pub struct Mapping { mappings: FnvHashMap, name: String, default: bool, hats_mapped: u8, } impl Mapping { pub fn new() -> Self { Mapping { mappings: FnvHashMap::default(), name: String::new(), default: false, hats_mapped: 0, } } pub fn default(gamepad: &gilrs_core::Gamepad) -> Self { use self::Axis as Ax; use self::AxisOrBtn::*; macro_rules! fnv_map { ( $( $key:expr => $elem:expr ),* ) => { { let mut map = FnvHashMap::default(); $( map.insert($key, $elem); )* map } }; } let mut mappings = fnv_map![ nec::BTN_SOUTH => Btn(Button::South), nec::BTN_EAST => Btn(Button::East), nec::BTN_C => Btn(Button::C), nec::BTN_NORTH => Btn(Button::North), nec::BTN_WEST => Btn(Button::West), nec::BTN_Z => Btn(Button::Z), nec::BTN_LT => Btn(Button::LeftTrigger), nec::BTN_RT => Btn(Button::RightTrigger), nec::BTN_LT2 => Btn(Button::LeftTrigger2), nec::BTN_RT2 => Btn(Button::RightTrigger2), nec::BTN_SELECT => Btn(Button::Select), nec::BTN_START => Btn(Button::Start), nec::BTN_MODE => Btn(Button::Mode), nec::BTN_LTHUMB => Btn(Button::LeftThumb), nec::BTN_RTHUMB => Btn(Button::RightThumb), nec::BTN_DPAD_UP => Btn(Button::DPadUp), nec::BTN_DPAD_DOWN => Btn(Button::DPadDown), nec::BTN_DPAD_LEFT => Btn(Button::DPadLeft), nec::BTN_DPAD_RIGHT => Btn(Button::DPadRight), nec::AXIS_LT => Btn(Button::LeftTrigger), nec::AXIS_RT => Btn(Button::RightTrigger), nec::AXIS_LT2 => Btn(Button::LeftTrigger2), nec::AXIS_RT2 => Btn(Button::RightTrigger2), nec::AXIS_LSTICKX => Axis(Ax::LeftStickX), nec::AXIS_LSTICKY => Axis(Ax::LeftStickY), nec::AXIS_LEFTZ => Axis(Ax::LeftZ), nec::AXIS_RSTICKX => Axis(Ax::RightStickX), nec::AXIS_RSTICKY => Axis(Ax::RightStickY), nec::AXIS_RIGHTZ => Axis(Ax::RightZ), nec::AXIS_DPADX => Axis(Ax::DPadX), nec::AXIS_DPADY => Axis(Ax::DPadY) ]; // Remove all mappings that don't have corresponding element in gamepad. Partial fix to #83 let axes = [ nec::AXIS_DPADX, nec::AXIS_DPADY, nec::AXIS_LEFTZ, nec::AXIS_LSTICKX, nec::AXIS_LSTICKY, nec::AXIS_RSTICKX, nec::AXIS_RSTICKY, nec::AXIS_LT, nec::AXIS_LT2, nec::AXIS_RT, nec::AXIS_RT2, nec::AXIS_RIGHTZ, ]; let btns = [ nec::BTN_SOUTH, nec::BTN_NORTH, nec::BTN_WEST, nec::BTN_WEST, nec::BTN_C, nec::BTN_Z, nec::BTN_LT, nec::BTN_LT2, nec::BTN_RT, nec::BTN_RT2, nec::BTN_SELECT, nec::BTN_START, nec::BTN_MODE, nec::BTN_LTHUMB, nec::BTN_RTHUMB, nec::BTN_DPAD_DOWN, nec::BTN_DPAD_LEFT, nec::BTN_DPAD_RIGHT, nec::BTN_DPAD_UP, ]; for axis in &axes { if !gamepad.axes().contains(axis) { mappings.remove(axis); } } for btn in &btns { if !gamepad.buttons().contains(btn) { mappings.remove(btn); } } Mapping { mappings, name: String::new(), default: true, hats_mapped: 0, } } pub fn name(&self) -> &str { &self.name } pub fn from_data( data: &MappingData, buttons: &[EvCode], axes: &[EvCode], name: &str, uuid: Uuid, ) -> Result<(Self, String), MappingError> { use crate::constants::*; if !Self::is_name_valid(name) { return Err(MappingError::InvalidName); } let mut mappings = FnvHashMap::default(); let mut sdl_mappings = format!("{},{},", uuid.as_simple(), name); { let mut add_button = |ident, ev_code, mapped_btn| { Self::add_button( ident, ev_code, mapped_btn, buttons, &mut sdl_mappings, &mut mappings, ) }; for (button, &ev_code) in &data.buttons { match button as u16 { BTN_SOUTH => add_button("a", ev_code, Button::South)?, BTN_EAST => add_button("b", ev_code, Button::East)?, BTN_WEST => add_button("x", ev_code, Button::West)?, BTN_NORTH => add_button("y", ev_code, Button::North)?, BTN_LT => add_button("leftshoulder", ev_code, Button::LeftTrigger)?, BTN_RT => add_button("rightshoulder", ev_code, Button::RightTrigger)?, BTN_LT2 => add_button("lefttrigger", ev_code, Button::LeftTrigger2)?, BTN_RT2 => add_button("righttrigger", ev_code, Button::RightTrigger2)?, BTN_SELECT => add_button("back", ev_code, Button::Select)?, BTN_START => add_button("start", ev_code, Button::Start)?, BTN_MODE => add_button("guide", ev_code, Button::Mode)?, BTN_LTHUMB => add_button("leftstick", ev_code, Button::LeftThumb)?, BTN_RTHUMB => add_button("rightstick", ev_code, Button::RightThumb)?, BTN_DPAD_UP => add_button("dpup", ev_code, Button::DPadUp)?, BTN_DPAD_DOWN => add_button("dpdown", ev_code, Button::DPadDown)?, BTN_DPAD_LEFT => add_button("dpleft", ev_code, Button::DPadLeft)?, BTN_DPAD_RIGHT => add_button("dpright", ev_code, Button::DPadRight)?, BTN_C => add_button("c", ev_code, Button::C)?, BTN_Z => add_button("z", ev_code, Button::Z)?, BTN_UNKNOWN => return Err(MappingError::UnknownElement), _ => unreachable!(), } } } { let mut add_axis = |ident, ev_code, mapped_axis| { Self::add_axis( ident, ev_code, mapped_axis, axes, &mut sdl_mappings, &mut mappings, ) }; for (axis, &ev_code) in &data.axes { match axis as u16 { AXIS_LSTICKX => add_axis("leftx", ev_code, Axis::LeftStickX)?, AXIS_LSTICKY => add_axis("lefty", ev_code, Axis::LeftStickY)?, AXIS_RSTICKX => add_axis("rightx", ev_code, Axis::RightStickX)?, AXIS_RSTICKY => add_axis("righty", ev_code, Axis::RightStickY)?, AXIS_LEFTZ => add_axis("leftz", ev_code, Axis::LeftZ)?, AXIS_RIGHTZ => add_axis("rightz", ev_code, Axis::RightZ)?, AXIS_UNKNOWN => return Err(MappingError::UnknownElement), _ => unreachable!(), } } } let mapping = Mapping { mappings, name: name.to_owned(), default: false, hats_mapped: 0, }; Ok((mapping, sdl_mappings)) } pub fn parse_sdl_mapping( line: &str, buttons: &[EvCode], axes: &[EvCode], ) -> Result { let mut mapping = Mapping::new(); let mut parser = Parser::new(line); let mut uuid: Option = None; while let Some(token) = parser.next_token() { if let Err(ref e) = token { if e.kind() == &ParserErrorKind::EmptyValue { continue; } } let token = token?; match token { Token::Platform(platform) => { if platform != SDL_PLATFORM_NAME { warn!("Mappings for different platform – {}", platform); } } Token::Uuid(v) => uuid = Some(v), Token::Name(name) => mapping.name = name.to_owned(), Token::AxisMapping { from, to, .. } => { let axis = axes.get(from as usize).cloned(); if let Some(axis) = axis { mapping.mappings.insert(axis, to); } else { warn!( "SDL-mapping {} {}: Unknown axis a{}", uuid.unwrap(), mapping.name, from ) } } Token::ButtonMapping { from, to, .. } => { let btn = buttons.get(from as usize).cloned(); if let Some(btn) = btn { mapping.mappings.insert(btn, to); } else { warn!( "SDL-mapping {} {}: Unknown button b{}", uuid.unwrap(), mapping.name, from ) } } Token::HatMapping { hat, direction, to, .. } => { if hat != 0 { warn!( "Hat mappings are only supported for dpads (requested to map hat \ {}.{} to {:?}", hat, direction, to ); } else { // We don't have anything like "hat" in gilrs, so let's jus assume that // user want to map dpad axes. // // We have to add mappings for axes AND buttons, because axis_dpad_to_button // filter may transform event to button event. let (from_axis, from_btn) = match direction { 1 => (nec::AXIS_DPADY, nec::BTN_DPAD_UP), 4 => (nec::AXIS_DPADY, nec::BTN_DPAD_DOWN), 2 => (nec::AXIS_DPADX, nec::BTN_DPAD_RIGHT), 8 => (nec::AXIS_DPADX, nec::BTN_DPAD_LEFT), 0 => continue, // FIXME: I have no idea what 0 means here _ => return Err(ParseSdlMappingError::UnknownHatDirection), }; if to.is_button() { match to { AxisOrBtn::Btn(Button::DPadLeft | Button::DPadRight) => { mapping .mappings .insert(from_axis, AxisOrBtn::Axis(Axis::DPadX)); } AxisOrBtn::Btn(Button::DPadUp | Button::DPadDown) => { mapping .mappings .insert(from_axis, AxisOrBtn::Axis(Axis::DPadY)); } _ => (), } mapping.mappings.insert(from_btn, to); } else { mapping.mappings.insert(from_axis, to); } mapping.hats_mapped |= direction as u8; } } } } Ok(mapping) } fn add_button( ident: &str, ev_code: EvCode, mapped_btn: Button, buttons: &[EvCode], sdl_mappings: &mut String, mappings: &mut FnvHashMap, ) -> Result<(), MappingError> { let n_btn = buttons .iter() .position(|&x| x == ev_code) .ok_or(MappingError::InvalidCode(ev::Code(ev_code)))?; let _ = write!(sdl_mappings, "{}:b{},", ident, n_btn); mappings.insert(ev_code, AxisOrBtn::Btn(mapped_btn)); Ok(()) } fn add_axis( ident: &str, ev_code: EvCode, mapped_axis: Axis, axes: &[EvCode], sdl_mappings: &mut String, mappings: &mut FnvHashMap, ) -> Result<(), MappingError> { let n_axis = axes .iter() .position(|&x| x == ev_code) .ok_or(MappingError::InvalidCode(ev::Code(ev_code)))?; let _ = write!(sdl_mappings, "{}:a{},", ident, n_axis); mappings.insert(ev_code, AxisOrBtn::Axis(mapped_axis)); Ok(()) } fn is_name_valid(name: &str) -> bool { !name.chars().any(|x| x == ',') } pub fn map(&self, code: &EvCode) -> Option { self.mappings.get(code).cloned() } pub fn map_rev(&self, el: &AxisOrBtn) -> Option { self.mappings.iter().find(|x| x.1 == el).map(|x| *x.0) } pub fn is_default(&self) -> bool { self.default } /// Return bit field with mapped hats. Only for mappings created from SDL format this function /// can return non-zero value. pub fn hats_mapped(&self) -> u8 { self.hats_mapped } } #[derive(Clone, PartialEq, Eq, Debug)] pub enum ParseSdlMappingError { UnknownHatDirection, ParseError(ParserError), } impl From for ParseSdlMappingError { fn from(f: ParserError) -> Self { ParseSdlMappingError::ParseError(f) } } impl Error for ParseSdlMappingError { fn source(&self) -> Option<&(dyn Error + 'static)> { if let ParseSdlMappingError::ParseError(ref err) = self { Some(err) } else { None } } } impl Display for ParseSdlMappingError { fn fmt(&self, fmt: &mut Formatter<'_>) -> FmtResult { match self { ParseSdlMappingError::UnknownHatDirection => { fmt.write_str("hat direction wasn't 1, 2, 4 or 8") } ParseSdlMappingError::ParseError(_) => fmt.write_str("parsing error"), } } } #[derive(Debug)] pub struct MappingDb { mappings: HashMap, } impl MappingDb { pub fn new() -> Self { MappingDb { mappings: HashMap::new(), } } pub fn add_included_mappings(&mut self) { self.insert(include_str!(concat!( env!("OUT_DIR"), PATH_SEPARATOR!(), "gamecontrollerdb.txt" ))); } pub fn add_env_mappings(&mut self) { if let Ok(mapping) = env::var("SDL_GAMECONTROLLERCONFIG") { self.insert(&mapping); } } pub fn insert(&mut self, s: &str) { for mapping in s.lines() { let pat = "platform:"; if let Some(offset) = mapping.find(pat).map(|o| o + pat.len()) { let s = &mapping[offset..]; let end = s.find(',').unwrap_or(s.len()); if &s[..end] != SDL_PLATFORM_NAME { continue; } } mapping .split(',') .next() .and_then(|s| Uuid::parse_str(s).ok()) .and_then(|uuid| self.mappings.insert(uuid, mapping.to_owned())); } } pub fn get(&self, uuid: Uuid) -> Option<&str> { self.mappings.get(&uuid).map(String::as_ref) } pub fn len(&self) -> usize { self.mappings.len() } } /// Stores data used to map gamepad buttons and axes. /// /// After you add all mappings, use /// [`Gamepad::set_mapping(…)`](struct.Gamepad.html#method.set_mapping) to change mapping of /// existing gamepad. /// /// See `examples/mapping.rs` for more detailed example. #[derive(Debug, Clone, Default)] // Re-exported as Mapping pub struct MappingData { buttons: VecMap, axes: VecMap, } impl MappingData { /// Creates new `Mapping`. pub fn new() -> Self { MappingData { buttons: VecMap::with_capacity(18), axes: VecMap::with_capacity(11), } } /// Returns `EvCode` associated with button index. pub fn button(&self, idx: Button) -> Option { self.buttons.get(idx as usize).cloned().map(ev::Code) } /// Returns `EvCode` associated with axis index. pub fn axis(&self, idx: Axis) -> Option { self.axes.get(idx as usize).cloned().map(ev::Code) } /// Inserts new button mapping. pub fn insert_btn(&mut self, from: ev::Code, to: Button) -> Option { self.buttons.insert(to as usize, from.0).map(ev::Code) } /// Inserts new axis mapping. pub fn insert_axis(&mut self, from: ev::Code, to: Axis) -> Option { self.axes.insert(to as usize, from.0).map(ev::Code) } /// Removes button and returns associated `NativEvCode`. pub fn remove_button(&mut self, idx: Button) -> Option { self.buttons.remove(idx as usize).map(ev::Code) } /// Removes axis and returns associated `NativEvCode`. pub fn remove_axis(&mut self, idx: Axis) -> Option { self.axes.remove(idx as usize).map(ev::Code) } } /// The error type for functions related to gamepad mapping. #[derive(Copy, Clone, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum MappingError { /// Gamepad does not have element referenced by `EvCode`. InvalidCode(ev::Code), /// Name contains comma (','). InvalidName, /// This function is not implemented for current platform. NotImplemented, /// Gamepad is not connected. NotConnected, /// Same gamepad element is referenced by axis and button. DuplicatedEntry, /// `Mapping` with `Button::Unknown` or `Axis::Unknown`. UnknownElement, /// `Mapping` have button or axis that are not present in SDL2. NotSdl2Compatible, } impl Error for MappingError {} impl Display for MappingError { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { let sbuf; let s = match self { MappingError::InvalidCode(code) => { sbuf = format!("gamepad does not have element with {}", code); sbuf.as_ref() } MappingError::InvalidName => "name can not contain comma", MappingError::NotImplemented => { "current platform does not implement setting custom mappings" } MappingError::NotConnected => "gamepad is not connected", MappingError::DuplicatedEntry => { "same gamepad element is referenced by axis and button" } MappingError::UnknownElement => "Button::Unknown and Axis::Unknown are not allowed", MappingError::NotSdl2Compatible => "one of buttons or axes is not compatible with SDL2", }; f.write_str(s) } } #[cfg(test)] mod tests { use super::*; use crate::ev::{Axis, Button}; use gilrs_core::native_ev_codes as nec; use gilrs_core::EvCode; use uuid::Uuid; // Do not include platform, mapping from (with UUID modified) // https://github.com/gabomdq/SDL_GameControllerDB/blob/master/gamecontrollerdb.txt const TEST_STR: &str = "03000000260900008888000000010001,GameCube {WiseGroup USB \ box},a:b0,b:b2,y:b3,x:b1,start:b7,rightshoulder:b6,dpup:h0.1,dpleft:\ h0.8,dpdown:h0.4,dpright:h0.2,leftx:a0,lefty:a1,rightx:a2,righty:a3,\ lefttrigger:a4,righttrigger:a5,"; const BUTTONS: [EvCode; 15] = [ nec::BTN_SOUTH, nec::BTN_EAST, nec::BTN_C, nec::BTN_NORTH, nec::BTN_WEST, nec::BTN_Z, nec::BTN_LT, nec::BTN_RT, nec::BTN_LT2, nec::BTN_RT2, nec::BTN_SELECT, nec::BTN_START, nec::BTN_MODE, nec::BTN_LTHUMB, nec::BTN_RTHUMB, ]; const AXES: [EvCode; 12] = [ nec::AXIS_LSTICKX, nec::AXIS_LSTICKY, nec::AXIS_LEFTZ, nec::AXIS_RSTICKX, nec::AXIS_RSTICKY, nec::AXIS_RIGHTZ, nec::AXIS_DPADX, nec::AXIS_DPADY, nec::AXIS_RT, nec::AXIS_LT, nec::AXIS_RT2, nec::AXIS_LT2, ]; #[test] fn mapping() { Mapping::parse_sdl_mapping(TEST_STR, &BUTTONS, &AXES).unwrap(); } #[test] fn from_data() { let uuid = Uuid::nil(); let name = "Best Gamepad"; let buttons = BUTTONS.iter().cloned().map(ev::Code).collect::>(); let axes = AXES.iter().cloned().map(ev::Code).collect::>(); let mut data = MappingData::new(); data.insert_axis(axes[0], Axis::LeftStickX); data.insert_axis(axes[1], Axis::LeftStickY); data.insert_axis(axes[2], Axis::LeftZ); data.insert_axis(axes[3], Axis::RightStickX); data.insert_axis(axes[4], Axis::RightStickY); data.insert_axis(axes[5], Axis::RightZ); data.insert_btn(buttons[0], Button::South); data.insert_btn(buttons[1], Button::East); data.insert_btn(buttons[3], Button::North); data.insert_btn(buttons[4], Button::West); data.insert_btn(buttons[5], Button::Select); data.insert_btn(buttons[6], Button::Start); data.insert_btn(buttons[7], Button::DPadDown); data.insert_btn(buttons[8], Button::DPadLeft); data.insert_btn(buttons[9], Button::RightThumb); let (mappings, sdl_mappings) = Mapping::from_data(&data, &BUTTONS, &AXES, name, uuid).unwrap(); let sdl_mappings = Mapping::parse_sdl_mapping(&sdl_mappings, &BUTTONS, &AXES).unwrap(); assert_eq!(mappings, sdl_mappings); let incorrect_mappings = Mapping::from_data(&data, &BUTTONS, &AXES, "Inval,id name", uuid); assert_eq!(Err(MappingError::InvalidName), incorrect_mappings); data.insert_btn(ev::Code(nec::BTN_DPAD_RIGHT), Button::DPadRight); let incorrect_mappings = Mapping::from_data(&data, &BUTTONS, &AXES, name, uuid); assert_eq!( Err(MappingError::InvalidCode(ev::Code(nec::BTN_DPAD_RIGHT))), incorrect_mappings ); data.insert_btn(ev::Code(BUTTONS[3]), Button::Unknown); let incorrect_mappings = Mapping::from_data(&data, &BUTTONS, &AXES, name, uuid); assert_eq!(Err(MappingError::UnknownElement), incorrect_mappings); } #[test] fn with_mappings() { let mappings = format!( "\nShould be ignored\nThis also should,be ignored\n\n{}", TEST_STR ); let mut db = MappingDb::new(); db.add_included_mappings(); db.insert(&mappings); assert_eq!( Some(TEST_STR), db.get(Uuid::parse_str("03000000260900008888000000010001").unwrap()) ); } } ================================================ FILE: gilrs/src/mapping/parser.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::error::Error as StdError; use std::fmt::{self, Display}; use uuid::Uuid; use crate::ev::{Axis, AxisOrBtn, Button}; // Must be sorted! static AXES_SDL: [&str; 31] = [ "a", "b", "back", "c", "dpdown", "dpleft", "dpright", "dpup", "guide", "leftshoulder", "leftstick", "lefttrigger", "leftx", "lefty", "leftz", "misc1", "paddle1", "paddle2", "paddle3", "paddle4", "rightshoulder", "rightstick", "righttrigger", "rightx", "righty", "rightz", "start", "touchpad", "x", "y", "z", ]; static AXES: [AxisOrBtn; 31] = [ AxisOrBtn::Btn(Button::South), AxisOrBtn::Btn(Button::East), AxisOrBtn::Btn(Button::Select), AxisOrBtn::Btn(Button::C), AxisOrBtn::Btn(Button::DPadDown), AxisOrBtn::Btn(Button::DPadLeft), AxisOrBtn::Btn(Button::DPadRight), AxisOrBtn::Btn(Button::DPadUp), AxisOrBtn::Btn(Button::Mode), AxisOrBtn::Btn(Button::LeftTrigger), AxisOrBtn::Btn(Button::LeftThumb), AxisOrBtn::Btn(Button::LeftTrigger2), AxisOrBtn::Axis(Axis::LeftStickX), AxisOrBtn::Axis(Axis::LeftStickY), AxisOrBtn::Axis(Axis::LeftZ), AxisOrBtn::Btn(Button::Unknown), AxisOrBtn::Btn(Button::Unknown), AxisOrBtn::Btn(Button::Unknown), AxisOrBtn::Btn(Button::Unknown), AxisOrBtn::Btn(Button::Unknown), AxisOrBtn::Btn(Button::RightTrigger), AxisOrBtn::Btn(Button::RightThumb), AxisOrBtn::Btn(Button::RightTrigger2), AxisOrBtn::Axis(Axis::RightStickX), AxisOrBtn::Axis(Axis::RightStickY), AxisOrBtn::Axis(Axis::RightZ), AxisOrBtn::Btn(Button::Start), AxisOrBtn::Btn(Button::Unknown), AxisOrBtn::Btn(Button::West), AxisOrBtn::Btn(Button::North), AxisOrBtn::Btn(Button::Z), ]; pub struct Parser<'a> { data: &'a str, pos: usize, state: State, } impl<'a> Parser<'a> { pub fn new(mapping: &'a str) -> Self { Parser { data: mapping, pos: 0, state: State::Uuid, } } pub fn next_token(&mut self) -> Option, Error>> { if self.pos >= self.data.len() { None } else { Some(match self.state { State::Uuid => self.parse_uuid(), State::Name => self.parse_name(), State::KeyVal => self.parse_key_val(), State::Invalid => Err(Error::new(ErrorKind::InvalidParserState, self.pos)), }) } } fn parse_uuid(&mut self) -> Result, Error> { let next_comma = self.next_comma_or_end(); let uuid_field = &self.data[self.pos..next_comma]; let uuid = if uuid_field == "xinput" { Ok(Token::Uuid(Uuid::nil())) } else { Uuid::parse_str(uuid_field) .map(Token::Uuid) .map_err(|_| Error::new(ErrorKind::InvalidGuid, self.pos)) }; if uuid.is_err() { self.state = State::Invalid; } else if next_comma == self.data.len() { self.state = State::Invalid; return Err(Error::new(ErrorKind::UnexpectedEnd, self.pos)); } else { self.state = State::Name; self.pos = next_comma + 1; } uuid } fn parse_name(&mut self) -> Result, Error> { let next_comma = self.next_comma_or_end(); let name = &self.data[self.pos..next_comma]; self.state = State::KeyVal; self.pos = next_comma + 1; Ok(Token::Name(name)) } fn parse_key_val(&mut self) -> Result, Error> { let next_comma = self.next_comma_or_end(); let pair = &self.data[self.pos..next_comma]; let pos = self.pos; self.pos = next_comma + 1; let mut split = pair.split(':'); let key = split .next() .ok_or_else(|| Error::new(ErrorKind::InvalidKeyValPair, pos))?; let value = split .next() .ok_or_else(|| Error::new(ErrorKind::InvalidKeyValPair, pos))?; if split.next().is_some() { return Err(Error::new(ErrorKind::InvalidKeyValPair, pos)); } if value.is_empty() { return Err(Error::new(ErrorKind::EmptyValue, pos)); } if key == "platform" { return Ok(Token::Platform(value)); } let mut input = AxisRange::Full; let mut output = AxisRange::Full; let mut inverted = false; let mut is_axis = false; let key = match key.get(0..1) { Some("+") => { output = AxisRange::UpperHalf; &key[1..] } Some("-") => { output = AxisRange::LowerHalf; &key[1..] } _ => key, }; let from = match value.get(0..1) { Some("+") if value.get(1..2) == Some("a") => { is_axis = true; input = AxisRange::UpperHalf; if value.get((value.len() - 1)..) == Some("~") { inverted = true; &value[2..(value.len() - 1)] } else { &value[2..] } } Some("-") if value.get(1..2) == Some("a") => { is_axis = true; input = AxisRange::LowerHalf; if value.get((value.len() - 1)..) == Some("~") { inverted = true; &value[2..(value.len() - 1)] } else { &value[2..] } } Some("a") => { is_axis = true; if value.get((value.len() - 1)..) == Some("~") { inverted = true; &value[1..(value.len() - 1)] } else { &value[1..] } } Some("b") => &value[1..], Some("h") => { let dot_idx = value .find('.') .ok_or_else(|| Error::new(ErrorKind::InvalidValue, pos))?; let hat = value[1..dot_idx] .parse() .map_err(|_| Error::new(ErrorKind::InvalidValue, pos + 1))?; let direction = value .get((dot_idx + 1)..) .and_then(|s| s.parse().ok()) .ok_or_else(|| Error::new(ErrorKind::InvalidValue, pos + dot_idx + 1))?; let idx = AXES_SDL .binary_search(&key) .map_err(|_| Error::new(ErrorKind::UnknownButton, pos))?; return Ok(Token::HatMapping { hat, direction, to: AXES[idx], output, }); } _ => return Err(Error::new(ErrorKind::InvalidValue, pos)), } .parse::() .map_err(|_| Error::new(ErrorKind::InvalidValue, pos))?; if is_axis { let idx = AXES_SDL .binary_search(&key) .map_err(|_| Error::new(ErrorKind::UnknownAxis, pos))?; Ok(Token::AxisMapping { from, to: AXES[idx], input, output, inverted, }) } else { let idx = AXES_SDL .binary_search(&key) .map_err(|_| Error::new(ErrorKind::UnknownButton, pos))?; Ok(Token::ButtonMapping { from, to: AXES[idx], output, }) } } fn next_comma_or_end(&self) -> usize { self.data[self.pos..] .find(',') .map(|x| x + self.pos) .unwrap_or_else(|| self.data.len()) } } #[derive(Debug)] pub enum Token<'a> { Uuid(Uuid), Platform(&'a str), Name(&'a str), #[allow(dead_code)] AxisMapping { from: u16, to: AxisOrBtn, input: AxisRange, output: AxisRange, inverted: bool, }, ButtonMapping { from: u16, to: AxisOrBtn, #[allow(dead_code)] output: AxisRange, }, // This is just SDL representation, we will convert this to axis mapping later HatMapping { hat: u16, // ? direction: u16, to: AxisOrBtn, #[allow(dead_code)] output: AxisRange, }, } #[repr(u8)] #[derive(Debug)] pub enum AxisRange { LowerHalf, UpperHalf, Full, } #[derive(Copy, Clone, Eq, PartialEq)] enum State { Uuid, Name, KeyVal, Invalid, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Error { pub(crate) position: usize, kind: ErrorKind, } impl Error { pub fn new(kind: ErrorKind, position: usize) -> Self { Error { position, kind } } pub fn kind(&self) -> &ErrorKind { &self.kind } } #[derive(Debug, Clone, PartialEq, Eq)] #[non_exhaustive] pub enum ErrorKind { InvalidGuid, InvalidKeyValPair, InvalidValue, EmptyValue, UnknownAxis, UnknownButton, InvalidParserState, UnexpectedEnd, } impl StdError for Error {} impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let s = match self.kind { ErrorKind::InvalidGuid => "GUID is invalid", ErrorKind::InvalidKeyValPair => "expected key value pair", ErrorKind::InvalidValue => "value is not valid", ErrorKind::EmptyValue => "value is empty", ErrorKind::UnknownAxis => "invalid axis name", ErrorKind::UnknownButton => "invalid button name", ErrorKind::InvalidParserState => "attempt to parse after unrecoverable error", ErrorKind::UnexpectedEnd => "mapping does not have all required fields", }; f.write_fmt(format_args!("{} at {}", s, self.position)) } } #[cfg(test)] mod tests { use crate::mapping::parser::{ErrorKind, Parser}; use crate::utils::PATH_SEPARATOR; #[test] fn test_all_sdl_mappings_for_parse_errors() { let included_mappings = include_str!(concat!( env!("OUT_DIR"), PATH_SEPARATOR!(), "gamecontrollerdb.txt" )) .lines(); let mut errors = 0; let mut index = 0; for line in included_mappings { let mut parser = Parser::new(line); while let Some(token) = parser.next_token() { if let Err(ref e) = token { if e.kind() != &ErrorKind::EmptyValue { errors += 1; println!("{e:?}"); println!( "{}: {} (...) {}\n", index, line.chars().take(50).collect::(), line.chars().skip(e.position).take(15).collect::() ); if e.kind() == &ErrorKind::InvalidParserState { break; } } } index += 1; } } assert_eq!(errors, 0); } } ================================================ FILE: gilrs/src/utils.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. pub use gilrs_core::utils::*; /// Like `(a: f32 / b).ceil()` but for integers. pub fn ceil_div(a: u32, b: u32) -> u32 { if a == 0 { 0 } else { 1 + ((a - 1) / b) } } pub fn clamp(x: f32, min: f32, max: f32) -> f32 { x.clamp(min, max) } #[cfg(path_separator = "backslash")] macro_rules! PATH_SEPARATOR { () => { r"\" }; } #[cfg(path_separator = "slash")] macro_rules! PATH_SEPARATOR { () => { r"/" }; } pub(crate) use PATH_SEPARATOR; #[cfg(test)] mod tests { use super::*; #[test] fn t_clamp() { assert_eq!(clamp(-1.0, 0.0, 1.0), 0.0); assert_eq!(clamp(0.5, 0.0, 1.0), 0.5); assert_eq!(clamp(2.0, 0.0, 1.0), 1.0); } } ================================================ FILE: gilrs-core/CHANGELOG.md ================================================ Change Log ========== v0.6.6 - 2025-09-28 ---------- ### Fixed - Fixed mappings on macOS. v0.6.5 - 2025-09-21 ---------- ### Changed - Updated `windows` and `nix` crates v0.6.4 - 2025-04-06 ---------- ### Changed - Updated `windows` crate v0.6.3 - 2025-03-10 ---------- ### Fixed - Fixed panic on macOS when gamepad’s vec had unexpected length after failing to open a device ### Changed - Updated `windows` crate v0.6.2 - 2025-02-09 ---------- ### Fixed - Fixed possible panic on Windows when `NonRoamableId()` could return error. ### Changed - Minimal supported Rust version is now 1.80. v0.6.1 - 2025-01-13 ---------- ### Changed - Minimal supported Rust version is now 1.74 - Updated `windows` crate v0.6.0 - 2024-09-15 ---------- ### Breaking changes - Mark Error enums, `EventType` and `Event` as `non_exhaustive` ### Changed - Minimal supported Rust version is now 1.73 - Updated dependencies v0.5.15 - 2024-08-25 ---------- ### Fixed - wasm: Fixed panic when browser assigned unexpected gamepad ID - windows: Fixed panic when receiving connected/disconnected events after instance of `Gilrs` was dropped. - windows: Don’t panic on Reading::update() returning error v0.5.13 - 2024-07-08 ---------- ### Changed - Updated `windows` crate v0.5.12 - 2024-06-15 ---------- ### Fixed - Fixed building on FreeBSD and DragonFly by not using linux implementation ### Changed - Updated dependencies v0.5.11 - 2024-03-06 ---------- ### Added - Added `vendor_id()` and `product_id()` to `Gamepad`. ### Changed - Updated `windows` crate to 0.54. v0.5.10 - 2023-12-17 ---------- ### Changed - Updated `windows` crate to 0.52. v0.5.9 - 2023-11-13 ---------- ### Fixed - Disabled unnecessary default features for `inotify`. v0.5.8 - 2023-11-11 ---------- ### Added - Flatpak is now supported by using inotify instead of udev. (!104) ### Changed - All thread spawned by gilrs are now named. (!102) - MSRV is now 1.65. ### Fixed - Linux: Fixed delay in Gilrs::new by limiting udev scan to the input subsystem. (!101) ### Fixed v0.5.7 - 2023-08-22 ---------- ### Fixed - windows: Join wgi thread on `Gilrs`'s drop - wasm: Fix trigger2 only sending binary values ## Changed - Update `windows` to 0.51 v0.5.6 - 2023-06-19 ---------- ### Fixed - Linux: fixed panic when calling `get_power_info` on disconnected gamepad. v0.5.5 - 2023-04-23 ---------- ### Added - `Gilrs::next_event_blocking()` v0.5.4 - 2023-04-03 ---------- ### Changed - Updated `io-kit-sys`, `windows` and `nix` v0.5.3 - 2023-03-29 ---------- ### Changed - Updated `windows` to 0.44 ### Fixed - web: Fixed handling of disconnected gamepads v0.5.2 - 2022-12-16 ---------- ### Changed - `Gilrs` is now `Send` on Linux. ### Fixed - Crash when app is launched through steam on Windows (see https://github.com/microsoft/windows-rs/issues/2252 for details). v0.5.1 - 2022-11-13 ------------------- ### Fixed - macOS: Fixed that hat axes were sometimes added before other axes breaking SDL mappings. - web: Fixed swapped north and west buttons for gamepads with "standard" mapping v0.5.0 - 2022-11-06 -------------------- ### Changed - Windows now defaults to using Windows Gaming Input instead of xinput. If you need to use xInput you can disable the `wgi` feature (It's enabled by default) and enable the `xinput` feature. ``` toml gilrs-core = {version = "0.5.0", default-features = false, features = ["wgi"]} ``` - Apps on Windows will now require a focused window to receive inputs by default. This is a limitation of Windows Gaming Input. It requires an in focus Window be associated with the process to receive events. You can still switch back to using xInput by turning off default features and enabling the `xinput` feature. - Minimal supported rust version is now 1.64. ### Fixed - `Gamepad::axes()` on macos now also returns "hat" axes. This should fix dpad on single Switch Joy-Con. v0.4.1 - 2022-05-29 ------------------- ### Changed - Updated io-kit-sys to 0.2 and core-foundation to 0.9 (@jtakakura). - Reduced numer of enabled features for nix crate (@rtzoeller). v0.4.0 - 2022-05-22 ------------------- ### Changed - wasm: web-sys/wasm-bindgen is now used by default, dependency on stdweb and `wasm-bindgen` feature are removed. - Minimal supported rust version is now 1.56. - Updated `uuid` and `nix` to current version. ### Fixed - wasm: `next_event()` no longer panic if `getGamepads()` is not available. v0.3.2 - 2021-12-30 ------------------- ### Changed - Updated dependencies v0.3.1 - 2021-03-30 ------------------- ### Added - Add support for wasm-bindgen (@coolreader18) v0.3.0 - 2020-10-09 ------------------- ### Added - macos: dpad is supported as a set of dpad axes (gilrs filters dpad axes to dpad buttons) (@cleancut). ### Changed - Minimal supported version is now 1.40 v0.2.6 - 2020-05-11 ------------------- Fixed compilation on musl. v0.2.5 - 2019-11-30 ------------------- Updated dependencies. v0.2.4 - 2019-09-05 ------------------- ### Fixed - Fixed compilation on platforms with dummy impl v0.2.3 - 2019-08-06 ------------------- ### Fixed - xinput: Removed unneeded logging - macos: `IS_Y_AXIS_REVERSED` is now correctly set to `true` - macos: Fixed UUID calculation v0.2.2 - 2019-04-06 ------------------- ### Changed - Windows: XInput is now dynamically loaded using rusty-xinput ### Fixed - xinput: incorrect `is_connected()` after hotplugging - wasm: Incorrect gamepad IDs in `Disconnected` event (@ryanisaacg) v0.2.1 - 2019-02-25 ------------------- ### Fixed - Compilation error on macOS v0.2.0 - 2019-02-21 ------------------- ### Added - Initial support for macOS (@jtakakura). There are still some functionality missing, check related issues in #58. - Wasm support, using stdweb (@ryanisaacg). ### Changed - `AxisInfo::deadzone` is now a `Option`. - Minimal supported version is now 1.31.1. The crate can still be build with older rustc, but it may change during next patch release. ### Removed - `AxisInfo::deadzone()` function. ### Fixed - xinput: Incorrect gamepad ID when more than one gamepad is connected ( @DTibbs). ================================================ FILE: gilrs-core/Cargo.toml ================================================ [package] name = "gilrs-core" version = "0.6.6" authors = ["Mateusz Sieczko "] license = "Apache-2.0/MIT" description = "Minimal event-based abstraction for working with gamepads" documentation = "https://docs.rs/gilrs-core/" repository = "https://gitlab.com/gilrs-project/gilrs" readme = "README.md" keywords = ["gamepad", "joystick", "input"] categories = ["game-engines"] edition = "2021" rust-version = "1.80.0" [dependencies] uuid = "1.0.0" log = "0.4.1" serde = { version = "1.0", features = ["derive"], optional = true } [dev-dependencies] env_logger = "0.11.5" [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd"))'.dependencies] libudev-sys = "0.1.4" libc = "0.2" nix = { version = "0.30.1", default-features = false, features = ["ioctl", "event"] } vec_map = "0.8" inotify = { version = "0.11.0", default-features = false } [target.'cfg(target_os = "macos")'.dependencies] objc2-core-foundation = { version = "0.3.2", default-features = false, features = [ "std", "CFArray", "CFDictionary", "CFNumber", "CFString", "CFRunLoop", ] } objc2-io-kit = { version = "0.3.2", default-features = false, features = [ "std", "libc", "hid", ] } vec_map = "0.8" [target.'cfg(target_os = "windows")'.dependencies] winapi = { version = "0.3.4", features = ["xinput"], optional = true } rusty-xinput = { version = "1.2.0", optional = true } windows = { version = ">=0.44, <=0.62", optional = true, features = [ "Gaming_Input", "Foundation_Collections", "Devices_Power", "System_Power", "Gaming_Input_ForceFeedback", ] } [target.'cfg(target_arch = "wasm32")'.dependencies] js-sys = { version = "0.3" } web-sys = { version = "0.3", features = [ "Gamepad", "GamepadButton", "GamepadMappingType", "Window", "Navigator", "DomException", ] } wasm-bindgen = "0.2" [package.metadata.docs.rs] features = ["serde"] [features] default = ["wgi"] serde-serialize = ["serde"] xinput = ["rusty-xinput", "winapi"] wgi = ["windows"] ================================================ FILE: gilrs-core/README.md ================================================ GilRs Core ========== [![pipeline status](https://gitlab.com/gilrs-project/gilrs/badges/master/pipeline.svg)](https://gitlab.com/gilrs-project/gilrs-core/commits/master) [![Minimum rustc version](https://img.shields.io/badge/rustc-1.64.0+-yellow.svg)](https://gitlab.com/gilrs-project/gilrs) This library is minimal event-based abstraction for working with gamepads. If you are looking for something more high level, take a look at `gilrs` crate. Platform specific notes ====================== Linux ----- On Linux, GilRs read (and write, in case of force feedback) directly from appropriate `/dev/input/event*` file. This means that user has to have read and write access to this file. On most distros it shouldn’t be a problem, but if it is, you will have to create udev rule. To build GilRs, you will need pkg-config and libudev .pc file. On some distributions this file is packaged in separate archive (for example `libudev-dev` in Debian). License ======= This project is licensed under the terms of both the Apache License (Version 2.0) and the MIT license. See LICENSE-APACHE and LICENSE-MIT for details. ================================================ FILE: gilrs-core/examples/ev_core.rs ================================================ use gilrs_core::Gilrs; fn main() { env_logger::init(); let mut gilrs = Gilrs::new().unwrap(); loop { while let Some(ev) = gilrs.next_event_blocking(None) { println!("{:0x?}", ev); } } } ================================================ FILE: gilrs-core/src/lib.rs ================================================ #[macro_use] extern crate log; use std::fmt; use std::fmt::Display; use std::fmt::Formatter; use std::error; use std::time::Duration; use std::time::SystemTime; mod platform; pub mod utils; /// True, if Y axis of sticks commonly points downwards. pub const IS_Y_AXIS_REVERSED: bool = platform::IS_Y_AXIS_REVERSED; /// Allow control of gamepad's force feedback. #[derive(Debug)] pub struct FfDevice { inner: platform::FfDevice, } impl FfDevice { /// Sets magnitude for strong and weak ff motors. pub fn set_ff_state(&mut self, strong: u16, weak: u16, min_duration: Duration) { self.inner.set_ff_state(strong, weak, min_duration) } } /// Holds information about gamepad event. #[derive(Copy, Clone, PartialEq, Eq, Debug)] #[non_exhaustive] pub struct Event { /// Id of gamepad. pub id: usize, /// Event's data. pub event: EventType, /// Time when event was emitted. pub time: SystemTime, } impl Event { /// Creates new event with current time. pub fn new(id: usize, event: EventType) -> Self { let time = utils::time_now(); Event { id, event, time } } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] /// Gamepad event. #[non_exhaustive] pub enum EventType { ButtonPressed(EvCode), ButtonReleased(EvCode), AxisValueChanged(i32, EvCode), Connected, Disconnected, } /// Holds information about expected axis range and deadzone. #[derive(Copy, Clone, Debug)] pub struct AxisInfo { pub min: i32, pub max: i32, pub deadzone: Option, } /// State of device's power supply. /// /// Battery level is reported as integer between 0 and 100. /// /// ## Example /// /// ``` /// use gilrs_core::PowerInfo; /// # let gilrs = gilrs_core::Gilrs::new().unwrap(); /// /// match gilrs.gamepad(0).map(|g| g.power_info()) { /// Some(PowerInfo::Discharging(lvl)) if lvl <= 10 => println!("Low battery level, you should \ /// plug your gamepad"), /// _ => (), /// }; /// ``` #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum PowerInfo { /// Failed to determine power status. Unknown, /// Device doesn't have battery. Wired, /// Device is running on the battery. Discharging(u8), /// Battery is charging. Charging(u8), /// Battery is charged. Charged, } /// Struct used to manage gamepads and retrieve events. #[derive(Debug)] pub struct Gilrs { inner: platform::Gilrs, } impl Gilrs { pub fn new() -> Result { let inner = platform::Gilrs::new().map_err(|e| match e { PlatformError::NotImplemented(inner) => Error::NotImplemented(Gilrs { inner }), PlatformError::Other(e) => Error::Other(e), })?; Ok(Gilrs { inner }) } /// Returns oldest event or `None` if all events were processed. pub fn next_event(&mut self) -> Option { self.inner.next_event() } /// Returns oldest event, waiting for new event if necessary. pub fn next_event_blocking(&mut self, timeout: Option) -> Option { self.inner.next_event_blocking(timeout) } /// Borrows `Gamepad` or return `None` if index is invalid. Returned gamepad may be disconnected. pub fn gamepad(&self, id: usize) -> Option<&Gamepad> { unsafe { let gp: Option<&platform::Gamepad> = self.inner.gamepad(id); gp.map(|gp| &*(gp as *const _ as *const Gamepad)) } } /// Returns id greater than id of last connected gamepad. The returned value is only hint /// and may be much larger than number of observed gamepads. For example, it may return maximum /// number of connected gamepads on platforms when this limit is small. /// /// `gamepad(id)` should return `Some` if using id that is smaller than value returned from this /// function. pub fn last_gamepad_hint(&self) -> usize { self.inner.last_gamepad_hint() } } /// Provides information about gamepad. #[derive(Debug)] #[repr(transparent)] pub struct Gamepad { inner: platform::Gamepad, } impl Gamepad { /// Returns name of gamepad. pub fn name(&self) -> &str { self.inner.name() } /// Returns true if gamepad is connected. pub fn is_connected(&self) -> bool { self.inner.is_connected() } /// Returns UUID that represents gamepad model. /// /// Returned UUID should be the same as SLD2 uses. If platform does not provide any method to /// distinguish between gamepad models, nil UUID is returned. /// /// It is recommended to process with the [UUID crate](https://crates.io/crates/uuid). /// Use `Uuid::from_bytes` method to create a `Uuid` from the returned bytes. pub fn uuid(&self) -> [u8; 16] { *self.inner.uuid().as_bytes() } /// Returns the vendor ID, as assigned by the USB-IF, when available. pub fn vendor_id(&self) -> Option { self.inner.vendor_id() } /// Returns the product ID, as assigned by the vendor, when available. pub fn product_id(&self) -> Option { self.inner.product_id() } /// Returns device's power supply state. pub fn power_info(&self) -> PowerInfo { self.inner.power_info() } /// Returns true if force feedback is supported by device, pub fn is_ff_supported(&self) -> bool { self.inner.is_ff_supported() } /// Creates `FfDevice` corresponding to this gamepad. pub fn ff_device(&self) -> Option { self.inner.ff_device().map(|inner| FfDevice { inner }) } /// Returns slice with EvCodes that may appear in button related events. pub fn buttons(&self) -> &[EvCode] { unsafe { let bt: &[platform::EvCode] = self.inner.buttons(); &*(bt as *const _ as *const [EvCode]) } } /// Returns slice with EvCodes that may appear in axis related events. pub fn axes(&self) -> &[EvCode] { unsafe { let ax: &[platform::EvCode] = self.inner.axes(); &*(ax as *const _ as *const [EvCode]) } } /// Returns information about a specific axis. `None` may be returned if a device doesn't have an axis /// with provided `EvCode`. pub fn axis_info(&self, nec: EvCode) -> Option<&AxisInfo> { self.inner.axis_info(nec.0) } } #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; /// Platform specific representation of axis or button. #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[repr(transparent)] pub struct EvCode(platform::EvCode); impl EvCode { pub fn into_u32(self) -> u32 { self.0.into_u32() } } impl Display for EvCode { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// Error type which can be returned when creating `Gilrs`. /// /// Private version of `Error` that use `platform::Gilrs`. #[derive(Debug)] enum PlatformError { /// Gilrs does not support the current platform, but you can use dummy context from this error if /// gamepad input is not essential. #[allow(dead_code)] NotImplemented(platform::Gilrs), /// Platform specific error. #[allow(dead_code)] Other(Box), } impl Display for PlatformError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { PlatformError::NotImplemented(_) => { f.write_str("Gilrs does not support current platform.") } PlatformError::Other(ref e) => e.fmt(f), } } } impl error::Error for PlatformError { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { PlatformError::Other(e) => Some(e.as_ref()), _ => None, } } } /// Error type which can be returned when creating `Gilrs`. #[non_exhaustive] #[derive(Debug)] pub enum Error { /// Gilrs does not support current platform, but you can use dummy context from this error if /// gamepad input is not essential. NotImplemented(Gilrs), /// Platform specific error. Other(Box), } impl Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match *self { Error::NotImplemented(_) => f.write_str("Gilrs does not support current platform."), Error::Other(ref e) => e.fmt(f), } } } impl error::Error for Error { fn source(&self) -> Option<&(dyn error::Error + 'static)> { match self { Error::Other(e) => Some(e.as_ref()), _ => None, } } } /// Provides the most common mappings of physical location of gamepad elements to their EvCodes. /// Some (or most) gamepads may use different mappings. pub mod native_ev_codes { use super::EvCode; use crate::platform::native_ev_codes as nec; pub const AXIS_LSTICKX: EvCode = EvCode(nec::AXIS_LSTICKX); pub const AXIS_LSTICKY: EvCode = EvCode(nec::AXIS_LSTICKY); pub const AXIS_LEFTZ: EvCode = EvCode(nec::AXIS_LEFTZ); pub const AXIS_RSTICKX: EvCode = EvCode(nec::AXIS_RSTICKX); pub const AXIS_RSTICKY: EvCode = EvCode(nec::AXIS_RSTICKY); pub const AXIS_RIGHTZ: EvCode = EvCode(nec::AXIS_RIGHTZ); pub const AXIS_DPADX: EvCode = EvCode(nec::AXIS_DPADX); pub const AXIS_DPADY: EvCode = EvCode(nec::AXIS_DPADY); pub const AXIS_RT: EvCode = EvCode(nec::AXIS_RT); pub const AXIS_LT: EvCode = EvCode(nec::AXIS_LT); pub const AXIS_RT2: EvCode = EvCode(nec::AXIS_RT2); pub const AXIS_LT2: EvCode = EvCode(nec::AXIS_LT2); pub const BTN_SOUTH: EvCode = EvCode(nec::BTN_SOUTH); pub const BTN_EAST: EvCode = EvCode(nec::BTN_EAST); pub const BTN_C: EvCode = EvCode(nec::BTN_C); pub const BTN_NORTH: EvCode = EvCode(nec::BTN_NORTH); pub const BTN_WEST: EvCode = EvCode(nec::BTN_WEST); pub const BTN_Z: EvCode = EvCode(nec::BTN_Z); pub const BTN_LT: EvCode = EvCode(nec::BTN_LT); pub const BTN_RT: EvCode = EvCode(nec::BTN_RT); pub const BTN_LT2: EvCode = EvCode(nec::BTN_LT2); pub const BTN_RT2: EvCode = EvCode(nec::BTN_RT2); pub const BTN_SELECT: EvCode = EvCode(nec::BTN_SELECT); pub const BTN_START: EvCode = EvCode(nec::BTN_START); pub const BTN_MODE: EvCode = EvCode(nec::BTN_MODE); pub const BTN_LTHUMB: EvCode = EvCode(nec::BTN_LTHUMB); pub const BTN_RTHUMB: EvCode = EvCode(nec::BTN_RTHUMB); pub const BTN_DPAD_UP: EvCode = EvCode(nec::BTN_DPAD_UP); pub const BTN_DPAD_DOWN: EvCode = EvCode(nec::BTN_DPAD_DOWN); pub const BTN_DPAD_LEFT: EvCode = EvCode(nec::BTN_DPAD_LEFT); pub const BTN_DPAD_RIGHT: EvCode = EvCode(nec::BTN_DPAD_RIGHT); } ================================================ FILE: gilrs-core/src/platform/default/ff.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::time::Duration; #[derive(Debug)] /// Represents gamepad. Reexported as FfDevice pub struct Device; impl Device { /// Sets magnitude for strong and weak ff motors. pub fn set_ff_state(&mut self, strong: u16, weak: u16, min_duration: Duration) {} } ================================================ FILE: gilrs-core/src/platform/default/gamepad.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. #![allow(unused_variables)] use super::FfDevice; use crate::{AxisInfo, Event, PlatformError, PowerInfo}; use uuid::Uuid; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::time::Duration; #[derive(Debug)] pub struct Gilrs {} impl Gilrs { pub(crate) fn new() -> Result { Err(PlatformError::NotImplemented(Gilrs {})) } pub(crate) fn next_event(&mut self) -> Option { None } pub(crate) fn next_event_blocking(&mut self, timeout: Option) -> Option { None } pub fn gamepad(&self, id: usize) -> Option<&Gamepad> { None } /// Returns index greater than index of last connected gamepad. pub fn last_gamepad_hint(&self) -> usize { 0 } } #[derive(Debug)] pub struct Gamepad { _priv: u8, // required for `#[repr(transparent)]` } impl Gamepad { pub fn name(&self) -> &str { "" } pub fn uuid(&self) -> Uuid { Uuid::nil() } pub fn vendor_id(&self) -> Option { None } pub fn product_id(&self) -> Option { None } pub fn power_info(&self) -> PowerInfo { PowerInfo::Unknown } pub fn is_ff_supported(&self) -> bool { false } /// Creates Ffdevice corresponding to this gamepad. pub fn ff_device(&self) -> Option { Some(FfDevice) } pub fn buttons(&self) -> &[EvCode] { &[] } pub fn axes(&self) -> &[EvCode] { &[] } pub(crate) fn axis_info(&self, nec: EvCode) -> Option<&AxisInfo> { None } pub fn is_connected(&self) -> bool { false } } #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct EvCode(u16); impl EvCode { pub fn into_u32(self) -> u32 { self.0 as u32 } } impl Display for EvCode { fn fmt(&self, f: &mut Formatter) -> FmtResult { self.0.fmt(f) } } pub mod native_ev_codes { use super::EvCode; pub const AXIS_LSTICKX: EvCode = EvCode(0); pub const AXIS_LSTICKY: EvCode = EvCode(1); pub const AXIS_LEFTZ: EvCode = EvCode(2); pub const AXIS_RSTICKX: EvCode = EvCode(3); pub const AXIS_RSTICKY: EvCode = EvCode(4); pub const AXIS_RIGHTZ: EvCode = EvCode(5); pub const AXIS_DPADX: EvCode = EvCode(6); pub const AXIS_DPADY: EvCode = EvCode(7); pub const AXIS_RT: EvCode = EvCode(8); pub const AXIS_LT: EvCode = EvCode(9); pub const AXIS_RT2: EvCode = EvCode(10); pub const AXIS_LT2: EvCode = EvCode(11); pub const BTN_SOUTH: EvCode = EvCode(12); pub const BTN_EAST: EvCode = EvCode(13); pub const BTN_C: EvCode = EvCode(14); pub const BTN_NORTH: EvCode = EvCode(15); pub const BTN_WEST: EvCode = EvCode(16); pub const BTN_Z: EvCode = EvCode(17); pub const BTN_LT: EvCode = EvCode(18); pub const BTN_RT: EvCode = EvCode(19); pub const BTN_LT2: EvCode = EvCode(20); pub const BTN_RT2: EvCode = EvCode(21); pub const BTN_SELECT: EvCode = EvCode(22); pub const BTN_START: EvCode = EvCode(23); pub const BTN_MODE: EvCode = EvCode(24); pub const BTN_LTHUMB: EvCode = EvCode(25); pub const BTN_RTHUMB: EvCode = EvCode(26); pub const BTN_DPAD_UP: EvCode = EvCode(27); pub const BTN_DPAD_DOWN: EvCode = EvCode(28); pub const BTN_DPAD_LEFT: EvCode = EvCode(29); pub const BTN_DPAD_RIGHT: EvCode = EvCode(30); } ================================================ FILE: gilrs-core/src/platform/default/mod.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. mod ff; mod gamepad; pub use self::ff::Device as FfDevice; pub use self::gamepad::{native_ev_codes, EvCode, Gamepad, Gilrs}; // True, if Y axis of sticks points downwards. pub const IS_Y_AXIS_REVERSED: bool = false; ================================================ FILE: gilrs-core/src/platform/linux/ff.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::fs::File; use std::io::{Error as IoError, ErrorKind, Result as IoResult, Write}; use std::os::unix::io::AsRawFd; use std::{mem, slice}; use super::ioctl::{self, ff_effect, ff_replay, ff_rumble_effect, input_event}; use nix::errno::Errno; use std::time::Duration; #[derive(Debug)] pub struct Device { effect: i16, file: File, } impl Device { pub(crate) fn new(path: &str) -> IoResult { let file = File::create(path)?; let mut effect = ff_effect { type_: FF_RUMBLE, id: -1, direction: 0, trigger: Default::default(), replay: Default::default(), u: Default::default(), }; #[allow(clippy::unnecessary_mut_passed)] let res = unsafe { ioctl::eviocsff(file.as_raw_fd(), &mut effect) }; if res.is_err() { Err(IoError::new(ErrorKind::Other, "Failed to create effect")) } else { Ok(Device { effect: effect.id, file, }) } } pub fn set_ff_state(&mut self, strong: u16, weak: u16, min_duration: Duration) { let duration = min_duration.as_secs() * 1000 + u64::from(min_duration.subsec_millis()); let duration = if duration > u64::from(u16::MAX) { u16::MAX } else { duration as u16 }; let mut effect = ff_effect { type_: FF_RUMBLE, id: self.effect, direction: 0, trigger: Default::default(), replay: ff_replay { delay: 0, length: duration, }, u: Default::default(), }; unsafe { let rumble = &mut effect.u as *mut _ as *mut ff_rumble_effect; (*rumble).strong_magnitude = strong; (*rumble).weak_magnitude = weak; if let Err(err) = ioctl::eviocsff(self.file.as_raw_fd(), &effect) { error!( "Failed to modify effect of gamepad {:?}, error: {}", self.file, err ); return; } }; let time = libc::timeval { tv_sec: 0, tv_usec: 0, }; let ev = input_event { type_: EV_FF, code: self.effect as u16, value: 1, time, }; let size = mem::size_of::(); let s = unsafe { slice::from_raw_parts(&ev as *const _ as *const u8, size) }; match self.file.write(s) { Ok(s) if s == size => (), Ok(_) => unreachable!(), Err(e) => error!("Failed to set ff state: {}", e), } } } impl Drop for Device { fn drop(&mut self) { #[cfg(target_os = "linux")] let effect = self.effect as ::libc::c_ulong; #[cfg(not(target_os = "linux"))] let effect = self.effect as ::libc::c_int; if let Err(err) = unsafe { ioctl::eviocrmff(self.file.as_raw_fd(), effect) } { if err != Errno::ENODEV { error!( "Failed to remove effect of gamepad {:?}: {}", self.file, err ) } }; } } const EV_FF: u16 = 0x15; const FF_RUMBLE: u16 = 0x50; ================================================ FILE: gilrs-core/src/platform/linux/gamepad.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use super::ff::Device as FfDevice; use super::ioctl; use super::ioctl::{input_absinfo, input_event}; use super::udev::*; use crate::utils; use crate::{AxisInfo, Event, EventType}; use crate::{PlatformError, PowerInfo}; use libc as c; use uuid::Uuid; use vec_map::VecMap; use inotify::{EventMask, Inotify, WatchMask}; use nix::errno::Errno; use nix::sys::epoll::{Epoll, EpollCreateFlags, EpollEvent, EpollFlags, EpollTimeout}; use nix::sys::eventfd::{EfdFlags, EventFd}; use std::collections::VecDeque; use std::error; use std::ffi::OsStr; use std::ffi::{CStr, CString}; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::fs::File; use std::mem::{self, MaybeUninit}; use std::ops::Index; use std::os::raw::c_char; use std::os::unix::ffi::OsStrExt; use std::os::unix::io::{BorrowedFd, RawFd}; use std::path::{Path, PathBuf}; use std::str; use std::sync::mpsc; use std::sync::mpsc::{Receiver, Sender}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; const HOTPLUG_DATA: u64 = u64::MAX; #[derive(Debug)] pub struct Gilrs { gamepads: Vec, epoll: Epoll, hotplug_rx: Receiver, to_check: VecDeque, discovery_backend: DiscoveryBackend, } #[derive(Debug, Clone, Copy)] enum DiscoveryBackend { Udev, Inotify, } const INPUT_DIR_PATH: &str = "/dev/input"; impl Gilrs { pub(crate) fn new() -> Result { let mut gamepads = Vec::new(); let epoll = Epoll::new(EpollCreateFlags::empty()) .map_err(|e| errno_to_platform_error(e, "creating epoll fd"))?; let mut hotplug_event = EventFd::from_value_and_flags(1, EfdFlags::EFD_NONBLOCK) .map_err(|e| errno_to_platform_error(e, "creating eventfd"))?; epoll .add( &hotplug_event, EpollEvent::new(EpollFlags::EPOLLIN | EpollFlags::EPOLLET, HOTPLUG_DATA), ) .map_err(|e| errno_to_platform_error(e, "adding evevntfd do epoll"))?; if Path::new("/.flatpak-info").exists() || std::env::var("GILRS_DISABLE_UDEV").is_ok() { log::debug!("Looks like we're in an environment without udev. Falling back to inotify"); let (hotplug_tx, hotplug_rx) = mpsc::channel(); let mut inotify = Inotify::init().map_err(|err| PlatformError::Other(Box::new(err)))?; let input_dir = Path::new(INPUT_DIR_PATH); inotify .watches() .add( input_dir, WatchMask::CREATE | WatchMask::DELETE | WatchMask::MOVE | WatchMask::ATTRIB, ) .map_err(|err| PlatformError::Other(Box::new(err)))?; for entry in input_dir .read_dir() .map_err(|err| PlatformError::Other(Box::new(err)))? .flatten() { let file_name = match entry.file_name().into_string() { Ok(file_name) => file_name, Err(_) => continue, }; let (gamepad_path, syspath) = match get_gamepad_path(&file_name) { Some((gamepad_path, syspath)) => (gamepad_path, syspath), None => continue, }; let devpath = CString::new(gamepad_path.to_str().unwrap()).unwrap(); if let Some(gamepad) = Gamepad::open(&devpath, &syspath, DiscoveryBackend::Inotify) { let idx = gamepads.len(); gamepad .register_fd(&epoll, idx as u64) .map_err(|e| errno_to_platform_error(e, "registering gamepad in epoll"))?; gamepads.push(gamepad); } } std::thread::Builder::new() .name("gilrs".to_owned()) .spawn(move || { let mut buffer = [0u8; 1024]; debug!("Started gilrs inotify thread"); loop { let events = match inotify.read_events_blocking(&mut buffer) { Ok(events) => events, Err(err) => { error!("Failed to check for changes to joysticks: {err}"); return; } }; for event in events { if !handle_inotify(&hotplug_tx, event, &mut hotplug_event) { return; } } } }) .expect("failed to spawn thread"); return Ok(Gilrs { gamepads, epoll, hotplug_rx, to_check: VecDeque::new(), discovery_backend: DiscoveryBackend::Inotify, }); } let udev = match Udev::new() { Some(udev) => udev, None => { return Err(PlatformError::Other(Box::new(Error::UdevCtx))); } }; let en = match udev.enumerate() { Some(en) => en, None => { return Err(PlatformError::Other(Box::new(Error::UdevEnumerate))); } }; unsafe { en.add_match_property(cstr_new(b"ID_INPUT_JOYSTICK\0"), cstr_new(b"1\0")) } unsafe { en.add_match_subsystem(cstr_new(b"input\0")) } en.scan_devices(); for dev in en.iter() { if let Some(dev) = Device::from_syspath(&udev, &dev) { let devpath = match dev.devnode() { Some(devpath) => devpath, None => continue, }; let syspath = Path::new(OsStr::from_bytes(dev.syspath().to_bytes())); if let Some(gamepad) = Gamepad::open(devpath, syspath, DiscoveryBackend::Udev) { let idx = gamepads.len(); gamepad .register_fd(&epoll, idx as u64) .map_err(|e| errno_to_platform_error(e, "registering gamepad in epoll"))?; gamepads.push(gamepad); } } } let (hotplug_tx, hotplug_rx) = mpsc::channel(); std::thread::Builder::new() .name("gilrs".to_owned()) .spawn(move || { let udev = match Udev::new() { Some(udev) => udev, None => { error!("Failed to create udev for hot plug thread!"); return; } }; let monitor = match Monitor::new(&udev) { Some(m) => m, None => { error!("Failed to create udev monitor for hot plug thread!"); return; } }; handle_hotplug(hotplug_tx, monitor, hotplug_event) }) .expect("failed to spawn thread"); Ok(Gilrs { gamepads, epoll, hotplug_rx, to_check: VecDeque::new(), discovery_backend: DiscoveryBackend::Udev, }) } pub(crate) fn next_event(&mut self) -> Option { self.next_event_impl(Some(Duration::new(0, 0))) } pub(crate) fn next_event_blocking(&mut self, timeout: Option) -> Option { self.next_event_impl(timeout) } fn next_event_impl(&mut self, timeout: Option) -> Option { let mut check_hotplug = false; if self.to_check.is_empty() { let mut events = [EpollEvent::new(EpollFlags::empty(), 0); 16]; let timeout = if let Some(timeout) = timeout { EpollTimeout::try_from(timeout).expect("timeout too large") } else { EpollTimeout::NONE }; let n = match self.epoll.wait(&mut events, timeout) { Ok(n) => n, Err(e) => { error!("epoll failed: {}", e); return None; } }; if n == 0 { return None; } for event in events { if event.events().contains(EpollFlags::EPOLLIN) { if event.data() == HOTPLUG_DATA { check_hotplug = true; } else { self.to_check.push_back(event.data() as usize); } } } } if check_hotplug { if let Some(event) = self.handle_hotplug() { return Some(event); } } while let Some(idx) = self.to_check.front().copied() { let gamepad = match self.gamepads.get_mut(idx) { Some(gp) => gp, None => { warn!("Somehow got invalid index from event"); self.to_check.pop_front(); return None; } }; if !gamepad.is_connected { self.to_check.pop_front(); continue; } match gamepad.event() { Some((event, time)) => { return Some(Event { id: idx, event, time, }); } None => { self.to_check.pop_front(); continue; } }; } None } pub fn gamepad(&self, id: usize) -> Option<&Gamepad> { self.gamepads.get(id) } pub fn last_gamepad_hint(&self) -> usize { self.gamepads.len() } fn handle_hotplug(&mut self) -> Option { while let Ok(event) = self.hotplug_rx.try_recv() { match event { HotplugEvent::New { devpath, syspath } => { // We already know this gamepad, ignore it: let gamepad_path_str = devpath.clone().to_string_lossy().into_owned(); if self .gamepads .iter() .any(|gamepad| gamepad.devpath == gamepad_path_str && gamepad.is_connected) { continue; } if let Some(gamepad) = Gamepad::open(&devpath, &syspath, self.discovery_backend) { return if let Some(id) = self .gamepads .iter() .position(|gp| gp.uuid() == gamepad.uuid && !gp.is_connected) { if let Err(e) = gamepad.register_fd(&self.epoll, id as u64) { error!("Failed to add gamepad to epoll: {}", e); } self.gamepads[id] = gamepad; Some(Event::new(id, EventType::Connected)) } else { if let Err(e) = gamepad.register_fd(&self.epoll, self.gamepads.len() as u64) { error!("Failed to add gamepad to epoll: {}", e); } self.gamepads.push(gamepad); Some(Event::new(self.gamepads.len() - 1, EventType::Connected)) }; } } HotplugEvent::Removed(devpath) => { if let Some(id) = self .gamepads .iter() .position(|gp| devpath == gp.devpath && gp.is_connected) { let gamepad_fd = unsafe { BorrowedFd::borrow_raw(self.gamepads[id].fd) }; if let Err(e) = self.epoll.delete(gamepad_fd) { error!("Failed to remove disconnected gamepad from epoll: {}", e); } self.gamepads[id].disconnect(); return Some(Event::new(id, EventType::Disconnected)); } else { debug!("Could not find disconnected gamepad {devpath:?}"); } } } } None } } enum HotplugEvent { New { devpath: CString, syspath: PathBuf }, Removed(String), } fn handle_inotify( sender: &Sender, event: inotify::Event<&std::ffi::OsStr>, event_fd: &mut EventFd, ) -> bool { let name = match event.name.and_then(|name| name.to_str()) { Some(name) => name, None => return true, }; let (gamepad_path, syspath) = match get_gamepad_path(name) { Some((gamepad_path, syspath)) => (gamepad_path, syspath), None => return true, }; let mut sent = false; if !(event.mask & (EventMask::CREATE | EventMask::MOVED_TO | EventMask::ATTRIB)).is_empty() { if sender .send(HotplugEvent::New { devpath: CString::new(gamepad_path.to_str().unwrap()).unwrap(), syspath, }) .is_err() { debug!("All receivers dropped, ending hot plug loop."); return false; } sent = true; } else if !(event.mask & (EventMask::DELETE | EventMask::MOVED_FROM)).is_empty() { if sender .send(HotplugEvent::Removed( gamepad_path.to_string_lossy().to_string(), )) .is_err() { debug!("All receivers dropped, ending hot plug loop."); return false; } sent = true; } if sent { if let Err(e) = event_fd.write(0u64) { error!( "Failed to notify other thread about new hotplug events: {}", e ); } } true } fn get_gamepad_path(name: &str) -> Option<(PathBuf, PathBuf)> { let event_id = name.strip_prefix("event")?; if event_id.is_empty() || event_id .chars() .any(|character| !character.is_ascii_digit()) { return None; } let gamepad_path = Path::new(INPUT_DIR_PATH).join(name); let syspath = Path::new("/sys/class/input/").join(name); Some((gamepad_path, syspath)) } fn handle_hotplug(sender: Sender, monitor: Monitor, event: EventFd) { loop { if !monitor.wait_hotplug_available() { continue; } let dev = monitor.device(); unsafe { if let Some(val) = dev.property_value(cstr_new(b"ID_INPUT_JOYSTICK\0")) { if val != cstr_new(b"1\0") { continue; } } else { continue; } let action = match dev.action() { Some(a) => a, None => continue, }; let mut sent = false; if action == cstr_new(b"add\0") { if let Some(devpath) = dev.devnode() { let syspath = Path::new(OsStr::from_bytes(dev.syspath().to_bytes())); if sender .send(HotplugEvent::New { devpath: devpath.into(), syspath: syspath.to_path_buf(), }) .is_err() { debug!("All receivers dropped, ending hot plug loop."); break; } sent = true; } } else if action == cstr_new(b"remove\0") { if let Some(devnode) = dev.devnode() { if let Ok(str) = devnode.to_str() { if sender.send(HotplugEvent::Removed(str.to_owned())).is_err() { debug!("All receivers dropped, ending hot plug loop."); break; } sent = true; } else { warn!("Received event with devnode that is not valid utf8: {devnode:?}") } } } if sent { if let Err(e) = event.write(0) { error!( "Failed to notify other thread about new hotplug events: {}", e ); } } } } } #[derive(Debug, Clone)] struct AxesInfo { info: VecMap, } impl AxesInfo { fn new(fd: i32) -> Self { let mut map = VecMap::new(); unsafe { let mut abs_bits = [0u8; (ABS_MAX / 8) as usize + 1]; ioctl::eviocgbit( fd, u32::from(EV_ABS), abs_bits.len() as i32, abs_bits.as_mut_ptr(), ); for axis in Gamepad::find_axes(&abs_bits) { let mut info = input_absinfo::default(); ioctl::eviocgabs(fd, u32::from(axis.code), &mut info); map.insert( axis.code as usize, AxisInfo { min: info.minimum, max: info.maximum, deadzone: Some(info.flat as u32), }, ); } } AxesInfo { info: map } } } impl Index for AxesInfo { type Output = AxisInfo; fn index(&self, i: u16) -> &Self::Output { &self.info[i as usize] } } #[derive(Debug)] pub struct Gamepad { fd: RawFd, axes_info: AxesInfo, ff_supported: bool, devpath: String, name: String, uuid: Uuid, vendor_id: u16, product_id: u16, bt_capacity_fd: RawFd, bt_status_fd: RawFd, axes_values: VecMap, buttons_values: VecMap, events: Vec, axes: Vec, buttons: Vec, is_connected: bool, } impl Gamepad { fn open(path: &CStr, syspath: &Path, discovery_backend: DiscoveryBackend) -> Option { if unsafe { !c::strstr(path.as_ptr(), c"js".as_ptr() as *const c_char).is_null() } { trace!("Device {:?} is js interface, ignoring.", path); return None; } let fd = unsafe { c::open(path.as_ptr(), c::O_RDWR | c::O_NONBLOCK) }; if fd < 0 { log!( match discovery_backend { DiscoveryBackend::Inotify => log::Level::Debug, _ => log::Level::Error, }, "Failed to open {:?}", path ); return None; } let input_id = match Self::get_input_id(fd) { Some(input_id) => input_id, None => { error!("Failed to get id of device {:?}", path); unsafe { c::close(fd); } return None; } }; let name = Self::get_name(fd).unwrap_or_else(|| { error!("Failed to get name of device {:?}", path); "Unknown".into() }); let axesi = AxesInfo::new(fd); let ff_supported = Self::test_ff(fd); let (cap, status) = Self::battery_fd(syspath); let mut gamepad = Gamepad { fd, axes_info: axesi, ff_supported, devpath: path.to_string_lossy().into_owned(), name, uuid: create_uuid(input_id), vendor_id: input_id.vendor, product_id: input_id.product, bt_capacity_fd: cap, bt_status_fd: status, axes_values: VecMap::new(), buttons_values: VecMap::new(), events: Vec::new(), axes: Vec::new(), buttons: Vec::new(), is_connected: true, }; gamepad.collect_axes_and_buttons(); if !gamepad.is_gamepad() { log!( match discovery_backend { DiscoveryBackend::Inotify => log::Level::Debug, _ => log::Level::Warn, }, "{:?} doesn't have at least 1 button and 2 axes, ignoring.", path ); return None; } info!("Gamepad {} ({}) connected.", gamepad.devpath, gamepad.name); debug!( "Gamepad {}: uuid: {}, ff_supported: {}, axes: {:?}, buttons: {:?}, axes_info: {:?}", gamepad.devpath, gamepad.uuid, gamepad.ff_supported, gamepad.axes, gamepad.buttons, gamepad.axes_info ); Some(gamepad) } fn register_fd(&self, epoll: &Epoll, data: u64) -> Result<(), Errno> { let fd = unsafe { BorrowedFd::borrow_raw(self.fd) }; epoll.add(fd, EpollEvent::new(EpollFlags::EPOLLIN, data)) } fn collect_axes_and_buttons(&mut self) { let mut key_bits = [0u8; (KEY_MAX / 8) as usize + 1]; let mut abs_bits = [0u8; (ABS_MAX / 8) as usize + 1]; unsafe { ioctl::eviocgbit( self.fd, u32::from(EV_KEY), key_bits.len() as i32, key_bits.as_mut_ptr(), ); ioctl::eviocgbit( self.fd, u32::from(EV_ABS), abs_bits.len() as i32, abs_bits.as_mut_ptr(), ); } self.buttons = Self::find_buttons(&key_bits, false); self.axes = Self::find_axes(&abs_bits); } fn get_name(fd: i32) -> Option { unsafe { let mut namebuff: [MaybeUninit; 128] = MaybeUninit::uninit().assume_init(); if ioctl::eviocgname(fd, &mut namebuff).is_err() { None } else { Some( CStr::from_ptr(namebuff.as_ptr() as *const c_char) .to_string_lossy() .into_owned(), ) } } } fn get_input_id(fd: i32) -> Option { unsafe { let mut iid = MaybeUninit::::uninit(); if ioctl::eviocgid(fd, iid.as_mut_ptr()).is_err() { return None; } Some(iid.assume_init()) } } fn test_ff(fd: i32) -> bool { unsafe { let mut ff_bits = [0u8; (FF_MAX / 8) as usize + 1]; if ioctl::eviocgbit( fd, u32::from(EV_FF), ff_bits.len() as i32, ff_bits.as_mut_ptr(), ) >= 0 { utils::test_bit(FF_SQUARE, &ff_bits) && utils::test_bit(FF_TRIANGLE, &ff_bits) && utils::test_bit(FF_SINE, &ff_bits) && utils::test_bit(FF_GAIN, &ff_bits) } else { false } } } fn is_gamepad(&self) -> bool { // TODO: improve it (for example check for buttons in range) !self.buttons.is_empty() && self.axes.len() >= 2 } fn find_buttons(key_bits: &[u8], only_gamepad_btns: bool) -> Vec { let mut buttons = Vec::with_capacity(16); for bit in BTN_MISC..BTN_MOUSE { if utils::test_bit(bit, key_bits) { buttons.push(EvCode::new(EV_KEY, bit)); } } for bit in BTN_JOYSTICK..(key_bits.len() as u16 * 8) { if utils::test_bit(bit, key_bits) { buttons.push(EvCode::new(EV_KEY, bit)); } } if !only_gamepad_btns { for bit in 0..BTN_MISC { if utils::test_bit(bit, key_bits) { buttons.push(EvCode::new(EV_KEY, bit)); } } for bit in BTN_MOUSE..BTN_JOYSTICK { if utils::test_bit(bit, key_bits) { buttons.push(EvCode::new(EV_KEY, bit)); } } } buttons } fn find_axes(abs_bits: &[u8]) -> Vec { let mut axes = Vec::with_capacity(8); for bit in 0..(abs_bits.len() * 8) { if utils::test_bit(bit as u16, abs_bits) { axes.push(EvCode::new(EV_ABS, bit as u16)); } } axes } fn battery_fd(syspath: &Path) -> (i32, i32) { use std::fs::{self}; use std::os::unix::io::IntoRawFd; // Returned syspath points to /input/inputXX/eventXX. First "device" is // symlink to inputXX, second to actual device root. let syspath = syspath.join("device/device/power_supply"); if let Ok(mut read_dir) = fs::read_dir(syspath) { if let Some(Ok(bat_entry)) = read_dir.next() { if let Ok(cap) = File::open(bat_entry.path().join("capacity")) { if let Ok(status) = File::open(bat_entry.path().join("status")) { return (cap.into_raw_fd(), status.into_raw_fd()); } } } } (-1, -1) } fn event(&mut self) -> Option<(EventType, SystemTime)> { let mut skip = false; // Skip all unknown events and return Option on first know event or when there is no more // events to read. Returning None on unknown event breaks iterators. loop { let event = self.next_event()?; if skip { if event.type_ == EV_SYN && event.code == SYN_REPORT { skip = false; self.compare_state(); } continue; } let ev = match event.type_ { EV_SYN if event.code == SYN_DROPPED => { skip = true; None } EV_KEY => { self.buttons_values .insert(event.code as usize, event.value == 1); match event.value { 0 => Some(EventType::ButtonReleased(event.into())), 1 => Some(EventType::ButtonPressed(event.into())), _ => None, } } EV_ABS => { self.axes_values.insert(event.code as usize, event.value); Some(EventType::AxisValueChanged(event.value, event.into())) } _ => { trace!("Skipping event {:?}", event); None } }; if let Some(ev) = ev { let dur = Duration::new(event.time.tv_sec as u64, event.time.tv_usec as u32 * 1000); return Some((ev, UNIX_EPOCH + dur)); } } } fn next_event(&mut self) -> Option { if !self.events.is_empty() { self.events.pop() } else { unsafe { let mut event_buf: [MaybeUninit; 12] = MaybeUninit::uninit().assume_init(); let size = mem::size_of::(); let n = c::read( self.fd, event_buf.as_mut_ptr() as *mut c::c_void, size * event_buf.len(), ); if n == -1 || n == 0 { // Nothing to read (non-blocking IO) None } else if n % size as isize != 0 { error!("Unexpected read of size {}", n); None } else { let n = n as usize / size; trace!("Got {} new events", n); for ev in event_buf[1..n].iter().rev() { self.events.push(ev.assume_init()); } Some(event_buf[0].assume_init()) } } } } fn compare_state(&mut self) { let mut absinfo = input_absinfo::default(); for axis in self.axes.iter().cloned() { let value = unsafe { ioctl::eviocgabs(self.fd, u32::from(axis.code), &mut absinfo); absinfo.value }; if self .axes_values .get(axis.code as usize) .cloned() .unwrap_or(0) != value { self.events.push(input_event { type_: EV_ABS, code: axis.code, value, ..Default::default() }); } } let mut buf = [0u8; KEY_MAX as usize / 8 + 1]; unsafe { let _ = ioctl::eviocgkey(self.fd, &mut buf); } for btn in self.buttons.iter().cloned() { let val = utils::test_bit(btn.code, &buf); if self .buttons_values .get(btn.code as usize) .cloned() .unwrap_or(false) != val { self.events.push(input_event { type_: EV_KEY, code: btn.code, value: val as i32, ..Default::default() }); } } } fn disconnect(&mut self) { unsafe { if self.fd >= 0 { c::close(self.fd); } } self.fd = -2; self.devpath.clear(); self.is_connected = false; } pub fn is_connected(&self) -> bool { self.is_connected } pub fn power_info(&self) -> PowerInfo { if self.bt_capacity_fd > -1 && self.bt_status_fd > -1 { unsafe { let mut buff = [0u8; 15]; c::lseek(self.bt_capacity_fd, 0, c::SEEK_SET); c::lseek(self.bt_status_fd, 0, c::SEEK_SET); let len = c::read( self.bt_capacity_fd, buff.as_mut_ptr() as *mut c::c_void, buff.len(), ); if len > 0 { let len = len as usize; let cap = match str::from_utf8_unchecked(&buff[..(len - 1)]).parse() { Ok(cap) => cap, Err(_) => { error!( "Failed to parse battery capacity: {}", str::from_utf8_unchecked(&buff[..(len - 1)]) ); return PowerInfo::Unknown; } }; let len = c::read( self.bt_status_fd, buff.as_mut_ptr() as *mut c::c_void, buff.len(), ); if len > 0 { let len = len as usize; return match str::from_utf8_unchecked(&buff[..(len - 1)]) { "Charging" => PowerInfo::Charging(cap), "Discharging" => PowerInfo::Discharging(cap), "Full" | "Not charging" => PowerInfo::Charged, s => { error!("Unknown battery status value: {}", s); PowerInfo::Unknown } }; } } } PowerInfo::Unknown } else if self.fd > -1 { PowerInfo::Wired } else { PowerInfo::Unknown } } pub fn is_ff_supported(&self) -> bool { self.ff_supported } pub fn name(&self) -> &str { &self.name } pub fn uuid(&self) -> Uuid { self.uuid } pub fn vendor_id(&self) -> Option { Some(self.vendor_id) } pub fn product_id(&self) -> Option { Some(self.product_id) } pub fn ff_device(&self) -> Option { if self.is_ff_supported() { FfDevice::new(&self.devpath).ok() } else { None } } pub fn buttons(&self) -> &[EvCode] { &self.buttons } pub fn axes(&self) -> &[EvCode] { &self.axes } pub(crate) fn axis_info(&self, nec: EvCode) -> Option<&AxisInfo> { if nec.kind != EV_ABS { None } else { self.axes_info.info.get(nec.code as usize) } } } impl Drop for Gamepad { fn drop(&mut self) { unsafe { if self.fd >= 0 { c::close(self.fd); } if self.bt_capacity_fd >= 0 { c::close(self.bt_capacity_fd); } if self.bt_status_fd >= 0 { c::close(self.bt_status_fd); } } } } impl PartialEq for Gamepad { fn eq(&self, other: &Self) -> bool { self.uuid == other.uuid } } fn create_uuid(iid: ioctl::input_id) -> Uuid { let bus = (u32::from(iid.bustype)).to_be(); let vendor = iid.vendor.to_be(); let product = iid.product.to_be(); let version = iid.version.to_be(); Uuid::from_fields( bus, vendor, 0, &[ (product >> 8) as u8, product as u8, 0, 0, (version >> 8) as u8, version as u8, 0, 0, ], ) } unsafe fn cstr_new(bytes: &[u8]) -> &CStr { CStr::from_bytes_with_nul_unchecked(bytes) } #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] pub struct EvCode { kind: u16, code: u16, } impl EvCode { fn new(kind: u16, code: u16) -> Self { EvCode { kind, code } } pub fn into_u32(self) -> u32 { (u32::from(self.kind) << 16) | u32::from(self.code) } } impl From for crate::EvCode { fn from(f: input_event) -> Self { crate::EvCode(EvCode { kind: f.type_, code: f.code, }) } } impl Display for EvCode { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self.kind { EV_SYN => f.write_str("SYN")?, EV_KEY => f.write_str("KEY")?, EV_REL => f.write_str("REL")?, EV_ABS => f.write_str("ABS")?, EV_MSC => f.write_str("MSC")?, EV_SW => f.write_str("SW")?, kind => f.write_fmt(format_args!("EV_TYPE_{}", kind))?, } f.write_fmt(format_args!("({})", self.code)) } } #[derive(Debug, Copy, Clone)] #[allow(clippy::enum_variant_names)] enum Error { UdevCtx, UdevEnumerate, Errno(Errno, &'static str), } impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match *self { Error::UdevCtx => f.write_str("Failed to create udev context"), Error::UdevEnumerate => f.write_str("Failed to create udev enumerate object"), Error::Errno(e, ctx) => f.write_fmt(format_args!("{} failed: {}", ctx, e)), } } } impl error::Error for Error {} fn errno_to_platform_error(errno: Errno, ctx: &'static str) -> PlatformError { PlatformError::Other(Box::new(Error::Errno(errno, ctx))) } const KEY_MAX: u16 = 0x2ff; #[allow(dead_code)] const EV_MAX: u16 = 0x1f; const EV_SYN: u16 = 0x00; const EV_KEY: u16 = 0x01; const EV_REL: u16 = 0x02; const EV_ABS: u16 = 0x03; const EV_MSC: u16 = 0x04; const EV_SW: u16 = 0x05; const ABS_MAX: u16 = 0x3f; const EV_FF: u16 = 0x15; const SYN_REPORT: u16 = 0x00; const SYN_DROPPED: u16 = 0x03; const BTN_MISC: u16 = 0x100; const BTN_MOUSE: u16 = 0x110; const BTN_JOYSTICK: u16 = 0x120; const BTN_SOUTH: u16 = 0x130; const BTN_EAST: u16 = 0x131; #[allow(dead_code)] const BTN_C: u16 = 0x132; const BTN_NORTH: u16 = 0x133; const BTN_WEST: u16 = 0x134; #[allow(dead_code)] const BTN_Z: u16 = 0x135; const BTN_TL: u16 = 0x136; const BTN_TR: u16 = 0x137; const BTN_TL2: u16 = 0x138; const BTN_TR2: u16 = 0x139; const BTN_SELECT: u16 = 0x13a; const BTN_START: u16 = 0x13b; const BTN_MODE: u16 = 0x13c; const BTN_THUMBL: u16 = 0x13d; const BTN_THUMBR: u16 = 0x13e; const BTN_DPAD_UP: u16 = 0x220; const BTN_DPAD_DOWN: u16 = 0x221; const BTN_DPAD_LEFT: u16 = 0x222; const BTN_DPAD_RIGHT: u16 = 0x223; const ABS_X: u16 = 0x00; const ABS_Y: u16 = 0x01; const ABS_Z: u16 = 0x02; const ABS_RX: u16 = 0x03; const ABS_RY: u16 = 0x04; const ABS_RZ: u16 = 0x05; const ABS_HAT0X: u16 = 0x10; const ABS_HAT0Y: u16 = 0x11; const ABS_HAT1X: u16 = 0x12; const ABS_HAT1Y: u16 = 0x13; const ABS_HAT2X: u16 = 0x14; const ABS_HAT2Y: u16 = 0x15; const FF_MAX: u16 = FF_GAIN; const FF_SQUARE: u16 = 0x58; const FF_TRIANGLE: u16 = 0x59; const FF_SINE: u16 = 0x5a; const FF_GAIN: u16 = 0x60; pub mod native_ev_codes { use super::*; pub const BTN_SOUTH: EvCode = EvCode { kind: EV_KEY, code: super::BTN_SOUTH, }; pub const BTN_EAST: EvCode = EvCode { kind: EV_KEY, code: super::BTN_EAST, }; pub const BTN_C: EvCode = EvCode { kind: EV_KEY, code: super::BTN_C, }; pub const BTN_NORTH: EvCode = EvCode { kind: EV_KEY, code: super::BTN_NORTH, }; pub const BTN_WEST: EvCode = EvCode { kind: EV_KEY, code: super::BTN_WEST, }; pub const BTN_Z: EvCode = EvCode { kind: EV_KEY, code: super::BTN_Z, }; pub const BTN_LT: EvCode = EvCode { kind: EV_KEY, code: super::BTN_TL, }; pub const BTN_RT: EvCode = EvCode { kind: EV_KEY, code: super::BTN_TR, }; pub const BTN_LT2: EvCode = EvCode { kind: EV_KEY, code: super::BTN_TL2, }; pub const BTN_RT2: EvCode = EvCode { kind: EV_KEY, code: super::BTN_TR2, }; pub const BTN_SELECT: EvCode = EvCode { kind: EV_KEY, code: super::BTN_SELECT, }; pub const BTN_START: EvCode = EvCode { kind: EV_KEY, code: super::BTN_START, }; pub const BTN_MODE: EvCode = EvCode { kind: EV_KEY, code: super::BTN_MODE, }; pub const BTN_LTHUMB: EvCode = EvCode { kind: EV_KEY, code: super::BTN_THUMBL, }; pub const BTN_RTHUMB: EvCode = EvCode { kind: EV_KEY, code: super::BTN_THUMBR, }; pub const BTN_DPAD_UP: EvCode = EvCode { kind: EV_KEY, code: super::BTN_DPAD_UP, }; pub const BTN_DPAD_DOWN: EvCode = EvCode { kind: EV_KEY, code: super::BTN_DPAD_DOWN, }; pub const BTN_DPAD_LEFT: EvCode = EvCode { kind: EV_KEY, code: super::BTN_DPAD_LEFT, }; pub const BTN_DPAD_RIGHT: EvCode = EvCode { kind: EV_KEY, code: super::BTN_DPAD_RIGHT, }; pub const AXIS_LSTICKX: EvCode = EvCode { kind: EV_ABS, code: super::ABS_X, }; pub const AXIS_LSTICKY: EvCode = EvCode { kind: EV_ABS, code: super::ABS_Y, }; pub const AXIS_LEFTZ: EvCode = EvCode { kind: EV_ABS, code: super::ABS_Z, }; pub const AXIS_RSTICKX: EvCode = EvCode { kind: EV_ABS, code: super::ABS_RX, }; pub const AXIS_RSTICKY: EvCode = EvCode { kind: EV_ABS, code: super::ABS_RY, }; pub const AXIS_RIGHTZ: EvCode = EvCode { kind: EV_ABS, code: super::ABS_RZ, }; pub const AXIS_DPADX: EvCode = EvCode { kind: EV_ABS, code: super::ABS_HAT0X, }; pub const AXIS_DPADY: EvCode = EvCode { kind: EV_ABS, code: super::ABS_HAT0Y, }; pub const AXIS_RT: EvCode = EvCode { kind: EV_ABS, code: super::ABS_HAT1X, }; pub const AXIS_LT: EvCode = EvCode { kind: EV_ABS, code: super::ABS_HAT1Y, }; pub const AXIS_RT2: EvCode = EvCode { kind: EV_ABS, code: super::ABS_HAT2X, }; pub const AXIS_LT2: EvCode = EvCode { kind: EV_ABS, code: super::ABS_HAT2Y, }; } #[cfg(test)] mod tests { use super::super::ioctl; use super::create_uuid; use uuid::Uuid; #[test] fn sdl_uuid() { let x = Uuid::parse_str("030000005e0400008e02000020200000").unwrap(); let y = create_uuid(ioctl::input_id { bustype: 0x3, vendor: 0x045e, product: 0x028e, version: 0x2020, }); assert_eq!(x, y); } } ================================================ FILE: gilrs-core/src/platform/linux/ioctl.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. // Some ioctls are exported by ioctl crate only for x86_64, so we have to define them anyway. // Diffing linux/input.h across different architectures (i686, x86_64 and arm) didn't show any // difference, so it looks like conditional compilation is not needed. #![allow(dead_code)] use nix::{ioctl_read, ioctl_read_buf, ioctl_write_int, ioctl_write_ptr, request_code_read}; use std::mem::MaybeUninit; #[cfg(target_env = "musl")] pub type IoctlRequest = libc::c_int; #[cfg(not(target_env = "musl"))] pub type IoctlRequest = libc::c_ulong; ioctl_read!(eviocgid, b'E', 0x02, /*struct*/ input_id); ioctl_write_int!(eviocrmff, b'E', 0x81); ioctl_write_ptr!(eviocsff, b'E', 0x80, ff_effect); ioctl_read_buf!(eviocgname, b'E', 0x06, MaybeUninit); ioctl_read_buf!(eviocgkey, b'E', 0x18, u8); pub unsafe fn eviocgbit(fd: libc::c_int, ev: u32, len: libc::c_int, buf: *mut u8) -> libc::c_int { ::nix::libc::ioctl( fd, request_code_read!(b'E', 0x20 + ev, len) as IoctlRequest, buf, ) } pub unsafe fn eviocgabs(fd: ::libc::c_int, abs: u32, buf: *mut input_absinfo) -> libc::c_int { ::nix::libc::ioctl( fd, request_code_read!(b'E', 0x40 + abs, ::std::mem::size_of::()) as IoctlRequest, buf, ) } #[derive(Copy, Clone)] #[repr(C)] pub struct input_event { pub time: libc::timeval, pub type_: u16, pub code: u16, pub value: i32, } impl ::std::default::Default for input_event { fn default() -> Self { unsafe { ::std::mem::zeroed() } } } impl ::std::fmt::Debug for input_event { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { write!( f, "input_event {{ time: {{ tv_sec: {}, tv_usec: {} }}, type_: {}, code: {}, value: {}", self.time.tv_sec, self.time.tv_usec, self.type_, self.code, self.value ) } } #[derive(Copy, Clone)] #[repr(C)] pub struct input_id { pub bustype: u16, pub vendor: u16, pub product: u16, pub version: u16, } #[derive(Copy, Clone, Default, PartialEq, Eq, Debug)] #[repr(C)] pub struct input_absinfo { pub value: i32, pub minimum: i32, pub maximum: i32, pub fuzz: i32, pub flat: i32, pub resolution: i32, } #[derive(Copy, Clone, Default)] #[repr(C)] pub struct ff_replay { pub length: u16, pub delay: u16, } #[derive(Copy, Clone, Default)] #[repr(C)] pub struct ff_trigger { pub button: u16, pub interval: u16, } #[derive(Copy, Clone)] #[repr(C)] pub struct ff_envelope { pub attack_length: u16, pub attack_level: u16, pub fade_length: u16, pub fade_level: u16, } #[derive(Copy, Clone)] #[repr(C)] pub struct ff_constant_effect { pub level: i16, pub envelope: ff_envelope, } #[derive(Copy, Clone)] #[repr(C)] pub struct ff_ramp_effect { pub start_level: i16, pub end_level: i16, pub envelope: ff_envelope, } #[derive(Copy, Clone)] #[repr(C)] pub struct ff_condition_effect { pub right_saturation: u16, pub left_saturation: u16, pub right_coeff: i16, pub left_coeff: i16, pub deadband: u16, pub center: i16, } #[derive(Copy, Clone)] #[repr(C)] pub struct ff_periodic_effect { pub waveform: u16, pub period: u16, pub magnitude: i16, pub offset: i16, pub phase: u16, pub envelope: ff_envelope, pub custom_len: u32, pub custom_data: *mut i16, } #[derive(Copy, Clone)] #[repr(C)] pub struct ff_rumble_effect { pub strong_magnitude: u16, pub weak_magnitude: u16, } #[derive(Copy, Clone)] #[repr(C)] pub struct ff_effect { pub type_: u16, pub id: i16, pub direction: u16, pub trigger: ff_trigger, pub replay: ff_replay, // FIXME this is actually a union #[cfg(target_pointer_width = "64")] pub u: [u64; 4], #[cfg(target_pointer_width = "32")] pub u: [u32; 7], } ================================================ FILE: gilrs-core/src/platform/linux/mod.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. // Copyright 2016 GilRs Developers mod ff; mod gamepad; mod ioctl; mod udev; pub use self::ff::Device as FfDevice; pub use self::gamepad::{native_ev_codes, EvCode, Gamepad, Gilrs}; pub const IS_Y_AXIS_REVERSED: bool = true; ================================================ FILE: gilrs-core/src/platform/linux/udev.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use libc as c; use libudev_sys as ud; use std::ffi::{CStr, CString}; use std::os::raw::c_char; use std::ptr; #[derive(Debug)] pub struct Udev(*mut ud::udev); impl Udev { pub fn new() -> Option { let u = unsafe { ud::udev_new() }; if u.is_null() { None } else { Some(Udev(u)) } } pub fn enumerate(&self) -> Option { let en = unsafe { ud::udev_enumerate_new(self.0) }; if en.is_null() { None } else { let en = Enumerate(en); Some(en) } } } impl Drop for Udev { fn drop(&mut self) { unsafe { ud::udev_unref(self.0); } } } impl Clone for Udev { fn clone(&self) -> Self { Udev(unsafe { ud::udev_ref(self.0) }) } } pub struct Enumerate(*mut ud::udev_enumerate); impl Enumerate { pub fn scan_devices(&self) { // TODO: Check for error let _ = unsafe { ud::udev_enumerate_scan_devices(self.0) }; } pub fn add_match_property(&self, key: &CStr, val: &CStr) { // TODO: Check for error unsafe { ud::udev_enumerate_add_match_property(self.0, key.as_ptr(), val.as_ptr()); } } pub fn add_match_subsystem(&self, subsystem: &CStr) { // TODO: Check for error unsafe { ud::udev_enumerate_add_match_subsystem(self.0, subsystem.as_ptr()); } } pub fn iter(&self) -> DeviceIterator { DeviceIterator(unsafe { ud::udev_enumerate_get_list_entry(self.0) }) } } impl Drop for Enumerate { fn drop(&mut self) { unsafe { ud::udev_enumerate_unref(self.0); } } } pub struct DeviceIterator(*mut ud::udev_list_entry); impl Iterator for DeviceIterator { type Item = CString; fn next(&mut self) -> Option { if self.0.is_null() { None } else { let p_name = unsafe { ud::udev_list_entry_get_name(self.0) }; let name = if p_name.is_null() { return None; } else { unsafe { CStr::from_ptr(p_name).to_owned() } }; self.0 = unsafe { ud::udev_list_entry_get_next(self.0) }; Some(name) } } } pub struct Device(*mut ud::udev_device); impl Device { pub fn from_syspath(udev: &Udev, path: &CStr) -> Option { let dev = unsafe { ud::udev_device_new_from_syspath(udev.0, path.as_ptr()) }; if dev.is_null() { None } else { Some(Device(dev)) } } pub fn syspath(&self) -> &CStr { // Always returns cstring unsafe { CStr::from_ptr(ud::udev_device_get_syspath(self.0)) } } pub fn devnode(&self) -> Option<&CStr> { unsafe { let s = ud::udev_device_get_devnode(self.0); if s.is_null() { None } else { Some(CStr::from_ptr(s)) } } } #[allow(dead_code)] pub fn properties(&self) -> PropertyIterator { let prop = unsafe { ud::udev_device_get_properties_list_entry(self.0) }; PropertyIterator(prop) } pub fn action(&self) -> Option<&CStr> { unsafe { let s = ud::udev_device_get_action(self.0); if s.is_null() { None } else { Some(CStr::from_ptr(s)) } } } pub fn property_value(&self, key: &CStr) -> Option<&CStr> { unsafe { let s = ud::udev_device_get_property_value(self.0, key.as_ptr()); if s.is_null() { None } else { Some(CStr::from_ptr(s)) } } } } impl Clone for Device { fn clone(&self) -> Self { unsafe { Device(ud::udev_device_ref(self.0)) } } } impl Drop for Device { fn drop(&mut self) { unsafe { ud::udev_device_unref(self.0); } } } #[allow(dead_code)] pub struct PropertyIterator(*mut ud::udev_list_entry); impl Iterator for PropertyIterator { type Item = (String, String); fn next(&mut self) -> Option<(String, String)> { if self.0.is_null() { None } else { let p_name = unsafe { ud::udev_list_entry_get_name(self.0) }; let p_val = unsafe { ud::udev_list_entry_get_value(self.0) }; let name = if p_name.is_null() { return None; } else { unsafe { CStr::from_ptr(p_name).to_string_lossy().into_owned() } }; let value = if p_val.is_null() { return None; } else { unsafe { CStr::from_ptr(p_val).to_string_lossy().into_owned() } }; self.0 = unsafe { ud::udev_list_entry_get_next(self.0) }; Some((name, value)) } } } #[derive(Debug)] pub struct Monitor(*mut ud::udev_monitor); impl Monitor { pub fn new(udev: &Udev) -> Option { unsafe { let monitor = ud::udev_monitor_new_from_netlink(udev.0, c"udev".as_ptr() as *const c_char); if monitor.is_null() { None } else { ud::udev_monitor_filter_add_match_subsystem_devtype( monitor, c"input".as_ptr() as *const c_char, ptr::null(), ); ud::udev_monitor_enable_receiving(monitor); Some(Monitor(monitor)) } } } pub fn wait_hotplug_available(&self) -> bool { unsafe { let mut fds = c::pollfd { fd: ud::udev_monitor_get_fd(self.0), events: c::POLLIN, revents: 0, }; (c::poll(&mut fds, 1, -1) == 1) && (fds.revents & c::POLLIN != 0) } } pub fn device(&self) -> Device { Device(unsafe { ud::udev_monitor_receive_device(self.0) }) } } impl Drop for Monitor { fn drop(&mut self) { unsafe { ud::udev_monitor_unref(self.0); } } } ================================================ FILE: gilrs-core/src/platform/macos/ff.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::time::Duration; #[derive(Debug)] /// Represents gamepad. Reexported as FfDevice pub struct Device; impl Device { /// Sets magnitude for strong and weak ff motors. pub fn set_ff_state(&mut self, _strong: u16, _weak: u16, _min_duration: Duration) {} } ================================================ FILE: gilrs-core/src/platform/macos/gamepad.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use super::io_kit::*; use super::FfDevice; use crate::{AxisInfo, Event, EventType, PlatformError, PowerInfo}; use objc2_core_foundation::{kCFRunLoopDefaultMode, CFRetained, CFRunLoop, Type}; use objc2_io_kit::{ kHIDPage_GenericDesktop, kHIDPage_VendorDefinedStart, kHIDUsage_GD_GamePad, kHIDUsage_GD_Joystick, kHIDUsage_GD_MultiAxisController, IOHIDDevice, IOHIDElement, IOHIDValue, IOReturn, }; use uuid::Uuid; use vec_map::VecMap; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::os::raw::c_void; use std::ptr::NonNull; use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::thread; use std::time::Duration; #[derive(Debug)] pub struct Gilrs { gamepads: Vec, device_infos: Arc>>, rx: Receiver<(Event, Option)>, } impl Gilrs { pub(crate) fn new() -> Result { let gamepads = Vec::new(); let device_infos = Arc::new(Mutex::new(Vec::new())); let (tx, rx) = mpsc::channel(); Self::spawn_thread(tx, device_infos.clone()); Ok(Gilrs { gamepads, device_infos, rx, }) } fn spawn_thread( tx: Sender<(Event, Option)>, device_infos: Arc>>, ) { thread::Builder::new() .name("gilrs".to_owned()) .spawn(move || { let manager = match new_manager() { Some(manager) => manager, None => { error!("Failed to create IOHIDManager object"); return; } }; let rl = CFRunLoop::current().unwrap(); // SAFETY: We pass the current thread's runloop, so the // callback will be run on this thread below. unsafe { manager.schedule_with_run_loop(&rl, kCFRunLoopDefaultMode.unwrap()) }; // SAFETY: The contexts pointer is a valid pointer. let context = &(tx.clone(), device_infos.clone()) as *const Context as *mut c_void; unsafe { manager.register_device_matching_callback(Some(device_matching_cb), context) }; // SAFETY: Same as above. let context = &(tx.clone(), device_infos.clone()) as *const Context as *mut c_void; unsafe { manager.register_device_removal_callback(Some(device_removal_cb), context) }; // SAFETY: Same as above. let context = &(tx, device_infos) as *const Context as *mut c_void; unsafe { manager.register_input_value_callback(Some(input_value_cb), context) }; CFRunLoop::run(); // SAFETY: There are no threading requirements from this. unsafe { manager.unschedule_from_run_loop(&rl, kCFRunLoopDefaultMode.unwrap()) }; }) .expect("failed to spawn thread"); } pub(crate) fn next_event(&mut self) -> Option { let event = self.rx.try_recv().ok(); self.handle_event(event) } pub(crate) fn next_event_blocking(&mut self, timeout: Option) -> Option { let event = if let Some(timeout) = timeout { self.rx.recv_timeout(timeout).ok() } else { self.rx.recv().ok() }; self.handle_event(event) } fn handle_event(&mut self, event: Option<(Event, Option)>) -> Option { match event { Some((event, Some(device))) => { if event.event == EventType::Connected { if self.gamepads.get(event.id).is_some() { self.gamepads[event.id].is_connected = true; } else { match Gamepad::open(&device.0) { Some(gamepad) => { self.gamepads.push(gamepad); } None => { error!("Failed to open gamepad: {:?}", event.id); return None; } }; } } Some(event) } Some((event, None)) => { if event.event == EventType::Disconnected { match self.gamepads.get_mut(event.id) { Some(gamepad) => { match self.device_infos.lock().unwrap().get_mut(event.id) { Some(device_info) => device_info.is_connected = false, None => { error!("Failed to find device_info: {:?}", event.id); return None; } }; gamepad.is_connected = false; } None => { error!("Failed to find gamepad: {:?}", event.id); return None; } } } Some(event) } None => None, } } pub fn gamepad(&self, id: usize) -> Option<&Gamepad> { self.gamepads.get(id) } /// Returns index greater than index of last connected gamepad. pub fn last_gamepad_hint(&self) -> usize { self.gamepads.len() } } #[derive(Debug)] #[allow(dead_code)] pub struct Gamepad { name: String, vendor: Option, product: Option, uuid: Uuid, entry_id: u64, location_id: u32, page: u32, usage: u32, axes_info: VecMap, axes: Vec, hats: Vec, buttons: Vec, is_connected: bool, } impl Gamepad { fn open(device: &IOHIDDevice) -> Option { let io_service = match IOService::new(device.service()) { Some(io_service) => io_service, None => { error!("Failed to get device service"); return None; } }; let entry_id = match io_service.get_registry_entry_id() { Some(entry_id) => entry_id, None => { error!("Failed to get entry id of device"); return None; } }; let location_id = match device.get_location_id() { Some(location_id) => location_id, None => { error!("Failed to get location id of device"); return None; } }; let page = match device.get_page() { Some(page) => { if page >= kHIDPage_VendorDefinedStart { error!("Device HID page is Vendor Defined. {device:?}"); return None; } if page == kHIDPage_GenericDesktop { page } else { error!("Failed to get valid device. Expecting kHIDPage_GenericDesktop. Got 0x{:X?}", page); return None; } } None => { error!("Failed to get page of device"); return None; } }; let usage = match device.get_usage() { Some(usage) => { if usage == kHIDUsage_GD_GamePad || usage == kHIDUsage_GD_Joystick || usage == kHIDUsage_GD_MultiAxisController { usage } else { error!("Failed to get valid device: {:?}", usage); return None; } } None => { error!("Failed to get usage of device"); return None; } }; let name = device.get_name().unwrap_or_else(|| { warn!("Failed to get name of device"); "Unknown".into() }); let uuid = Self::create_uuid(&device).unwrap_or_default(); let mut gamepad = Gamepad { name, vendor: device.get_vendor_id(), product: device.get_product_id(), uuid, entry_id, location_id, page, usage, axes_info: VecMap::with_capacity(8), axes: Vec::with_capacity(8), hats: Vec::with_capacity(4), buttons: Vec::with_capacity(16), is_connected: true, }; gamepad.collect_axes_and_buttons(&device_elements(&device)); Some(gamepad) } fn create_uuid(device: &IOHIDDevice) -> Option { // SDL always uses USB bus for UUID let bustype = u32::to_be(0x03); let vendor_id = match device.get_vendor_id() { Some(vendor_id) => vendor_id.to_be(), None => { warn!("Failed to get vendor id of device"); 0 } }; let product_id = match device.get_product_id() { Some(product_id) => product_id.to_be(), None => { warn!("Failed to get product id of device"); 0 } }; let version = match device.get_version() { Some(version) => version.to_be(), None => { warn!("Failed to get version of device"); 0 } }; if vendor_id == 0 && product_id == 0 && version == 0 { None } else { Some(Uuid::from_fields( bustype, vendor_id, 0, &[ (product_id >> 8) as u8, product_id as u8, 0, 0, (version >> 8) as u8, version as u8, 0, 0, ], )) } } pub fn name(&self) -> &str { &self.name } pub fn vendor_id(&self) -> Option { self.vendor } pub fn product_id(&self) -> Option { self.product } pub fn uuid(&self) -> Uuid { self.uuid } pub fn power_info(&self) -> PowerInfo { PowerInfo::Unknown } pub fn is_ff_supported(&self) -> bool { false } /// Creates Ffdevice corresponding to this gamepad. pub fn ff_device(&self) -> Option { Some(FfDevice) } pub fn buttons(&self) -> &[EvCode] { &self.buttons } pub fn axes(&self) -> &[EvCode] { &self.axes } pub(crate) fn axis_info(&self, nec: EvCode) -> Option<&AxisInfo> { self.axes_info.get(nec.usage as usize) } pub fn is_connected(&self) -> bool { self.is_connected } fn collect_axes_and_buttons(&mut self, elements: &Vec>) { let mut cookies = Vec::new(); self.collect_axes(elements, &mut cookies); self.axes.sort_by_key(|axis| axis.usage); self.hats.sort_by_key(|axis| axis.usage); // Because "hat is axis" is a gilrs thing, we want to ensure that all hats are at the end of // the axis vector, so the SDL mappings still work. self.axes.extend(&self.hats); self.collect_buttons(elements, &mut cookies); self.buttons.sort_by_key(|button| button.usage); } fn collect_axes(&mut self, elements: &Vec>, cookies: &mut Vec) { for element in elements { let type_ = element.r#type(); let cookie = element.cookie(); let page = element.usage_page(); let usage = element.usage(); if element_is_collection(type_) { let children = element_children(element); self.collect_axes(&children, cookies); } else if element_is_axis(type_, page, usage) && !cookies.contains(&cookie) { cookies.push(cookie); self.axes_info.insert( usage as usize, AxisInfo { min: element.logical_min() as _, max: element.logical_max() as _, deadzone: None, }, ); self.axes.push(EvCode::new(page, usage)); } else if element_is_hat(type_, page, usage) && !cookies.contains(&cookie) { cookies.push(cookie); self.axes_info.insert( usage as usize, AxisInfo { min: -1, max: 1, deadzone: None, }, ); self.hats.push(EvCode::new(page, usage)); // All hat switches are translated into *two* axes self.axes_info.insert( (usage + 1) as usize, // "+ 1" is assumed for usage of 2nd hat switch axis AxisInfo { min: -1, max: 1, deadzone: None, }, ); self.hats.push(EvCode::new(page, usage + 1)); } } } fn collect_buttons( &mut self, elements: &Vec>, cookies: &mut Vec, ) { for element in elements { let type_ = element.r#type(); let cookie = element.cookie(); let page = element.usage_page(); let usage = element.usage(); if element_is_collection(type_) { let children = element_children(element); self.collect_buttons(&children, cookies); } else if element_is_button(type_, page, usage) && !cookies.contains(&cookie) { cookies.push(cookie); self.buttons.push(EvCode::new(page, usage)); } } } } #[derive(Debug)] struct DeviceInfo { entry_id: u64, location_id: u32, is_connected: bool, } #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct EvCode { page: u32, usage: u32, } impl EvCode { fn new(page: u32, usage: u32) -> Self { EvCode { page, usage } } pub fn into_u32(self) -> u32 { (self.page << 16) | self.usage } } impl From for crate::EvCode { fn from(e: IOHIDElement) -> Self { crate::EvCode(EvCode { page: e.usage_page(), usage: e.usage(), }) } } impl Display for EvCode { fn fmt(&self, f: &mut Formatter) -> FmtResult { match self.page { PAGE_GENERIC_DESKTOP => f.write_str("GENERIC_DESKTOP")?, PAGE_BUTTON => f.write_str("BUTTON")?, page => f.write_fmt(format_args!("PAGE_{}", page))?, } f.write_fmt(format_args!("({})", self.usage)) } } pub mod native_ev_codes { use super::*; pub const AXIS_LSTICKX: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_LSTICKX, }; pub const AXIS_LSTICKY: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_LSTICKY, }; pub const AXIS_LEFTZ: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_LEFTZ, }; pub const AXIS_RSTICKX: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_RSTICKX, }; pub const AXIS_RSTICKY: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_RSTICKY, }; pub const AXIS_RIGHTZ: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_RIGHTZ, }; pub const AXIS_DPADX: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_DPADX, }; pub const AXIS_DPADY: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_DPADY, }; pub const AXIS_RT: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_RT, }; pub const AXIS_LT: EvCode = EvCode { page: super::PAGE_GENERIC_DESKTOP, usage: super::USAGE_AXIS_LT, }; pub const AXIS_RT2: EvCode = EvCode { page: super::PAGE_SIMULATION, usage: super::USAGE_AXIS_RT2, }; pub const AXIS_LT2: EvCode = EvCode { page: super::PAGE_SIMULATION, usage: super::USAGE_AXIS_LT2, }; pub const BTN_SOUTH: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_SOUTH, }; pub const BTN_EAST: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_EAST, }; pub const BTN_C: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_C, }; pub const BTN_NORTH: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_NORTH, }; pub const BTN_WEST: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_WEST, }; pub const BTN_Z: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_Z, }; pub const BTN_LT: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_LT, }; pub const BTN_RT: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_RT, }; pub const BTN_LT2: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_LT2, }; pub const BTN_RT2: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_RT2, }; pub const BTN_SELECT: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_SELECT, }; pub const BTN_START: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_START, }; pub const BTN_MODE: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_MODE, }; pub const BTN_LTHUMB: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_LTHUMB, }; pub const BTN_RTHUMB: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_RTHUMB, }; pub const BTN_DPAD_UP: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_DPAD_UP, }; pub const BTN_DPAD_DOWN: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_DPAD_DOWN, }; pub const BTN_DPAD_LEFT: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_DPAD_LEFT, }; pub const BTN_DPAD_RIGHT: EvCode = EvCode { page: super::PAGE_BUTTON, usage: super::USAGE_BTN_DPAD_RIGHT, }; } type Context = (Sender<(Event, Option)>, Arc>>); extern "C-unwind" fn device_matching_cb( context: *mut c_void, _result: IOReturn, _sender: *mut c_void, device: NonNull, ) { // SAFETY: Validity of the pointer is upheld by the caller. let device = unsafe { device.as_ref() }; // SAFETY: The context is the one we passed in `Gilrs::spawn_thread`. let (tx, device_infos): &Context = unsafe { &*(context as *mut _) }; let io_service = match IOService::new(device.service()) { Some(io_service) => io_service, None => { error!("Failed to get device service"); return; } }; let entry_id = match io_service.get_registry_entry_id() { Some(entry_id) => entry_id, None => { error!("Failed to get entry id of device"); return; } }; // Filter devices which will not succed in open(). If devices are added to the // DeviceInfo vec, it will cause a mismatch of IDs as they're derived from // the length of the DeviceInfo Vec, but devices which fail to open() will not // be pushed into the Gilrs inner gamepads vec, and panics will ensue. // // Try to open the device early, and if it fails, do not add it to device_infos match Gamepad::open(&device) { Some(gamepad) => drop(gamepad), None => { warn!("Failed to open device {device:?}. Skipping."); return; } } let mut device_infos = device_infos.lock().unwrap(); let id = match device_infos .iter() .position(|info| info.entry_id == entry_id && info.is_connected) { Some(id) => { info!("Device is already registered: {:?}", entry_id); id } None => { let location_id = match device.get_location_id() { Some(location_id) => location_id, None => { error!("Failed to get location id of device"); return; } }; device_infos.push(DeviceInfo { entry_id, location_id, is_connected: true, }); device_infos.len() - 1 } }; let _ = tx.send(( Event::new(id, EventType::Connected), Some(Device(device.retain())), )); } #[allow(clippy::type_complexity)] unsafe extern "C-unwind" fn device_removal_cb( context: *mut c_void, _result: IOReturn, _sender: *mut c_void, device: NonNull, ) { // SAFETY: Validity of the pointer is upheld by the caller. let device = unsafe { device.as_ref() }; // SAFETY: The context is the one we passed in `Gilrs::spawn_thread`. let (tx, device_infos): &Context = unsafe { &*(context as *mut _) }; let location_id = match device.get_location_id() { Some(location_id) => location_id, None => { error!("Failed to get location id of device"); return; } }; let device_infos = device_infos.lock().unwrap(); let id = match device_infos .iter() .position(|info| info.location_id == location_id && info.is_connected) { Some(id) => id, None => { warn!("Failed to find device: {:?}", location_id); return; } }; let _ = tx.send((Event::new(id, EventType::Disconnected), None)); } #[allow(clippy::type_complexity)] unsafe extern "C-unwind" fn input_value_cb( context: *mut c_void, _result: IOReturn, sender: *mut c_void, value: NonNull, ) { // SAFETY: Validity of the pointer is upheld by the caller. let value = unsafe { value.as_ref() }; // SAFETY: The context is the one we passed in `Gilrs::spawn_thread`. let (tx, device_infos): &Context = unsafe { &*(context as *mut _) }; // SAFETY: TODO. let device = match unsafe { sender.cast::().as_ref() } { Some(device) => device, None => { error!("Failed to get device"); return; } }; let io_service = match device.get_service() { Some(io_service) => io_service, None => { error!("Failed to get device service"); return; } }; let entry_id = match io_service.get_registry_entry_id() { Some(entry_id) => entry_id, None => { error!("Failed to get entry id of device"); return; } }; let device_infos = device_infos.lock().unwrap(); let id = match device_infos .iter() .position(|info| info.entry_id == entry_id && info.is_connected) { Some(id) => id, None => { warn!("Failed to find device: {:?}", entry_id); return; } }; let element = value.element(); let type_ = element.r#type(); let page = element.usage_page(); let usage = element.usage(); if element_is_axis(type_, page, usage) { let event = Event::new( id, EventType::AxisValueChanged( value.integer_value() as i32, crate::EvCode(EvCode { page, usage }), ), ); let _ = tx.send((event, None)); } else if element_is_button(type_, page, usage) { if value.integer_value() == 0 { let event = Event::new( id, EventType::ButtonReleased(crate::EvCode(EvCode { page, usage })), ); let _ = tx.send((event, None)); } else { let event = Event::new( id, EventType::ButtonPressed(crate::EvCode(EvCode { page, usage })), ); let _ = tx.send((event, None)); } } else if element_is_hat(type_, page, usage) { // Hat switch values are reported with a range of usually 8 numbers (sometimes 4). The logic // below uses the reported min/max values of that range to map that onto a range of 0-7 for // the directions (and any other value indicates the center position). Lucky for us, they // always start with "up" as the lowest number and proceed clockwise. See similar handling // here https://github.com/spurious/SDL-mirror/blob/094b2f68dd7fc9af167f905e10625e103a131459/src/joystick/darwin/SDL_sysjoystick.c#L976-L1028 // // up // 7 0 1 // \ | / // left 6 - ? - 2 right (After mapping) // / | \ // 5 4 3 // down let range = element.logical_max() - element.logical_min() + 1; let shifted_value = value.integer_value() - element.logical_min(); let dpad_value = match range { 4 => shifted_value * 2, // 4-position hat switch - scale it up to 8 8 => shifted_value, // 8-position hat switch - no adjustment necessary _ => -1, // Neither 4 nor 8 positions, we don't know what to do - default to centered }; // At this point, the value should be normalized to the 0-7 directional values (or center // for any other value). The dpad is a hat switch on macOS, but on other platforms dpads are // either buttons or a pair of axes that get converted to button events by the // `axis_dpad_to_button` filter. We will emulate axes here and let that filter do the // button conversion, because it is safer and easier than making separate logic for button // conversion that may diverge in subtle ways from the axis conversion logic. The most // practical outcome of this conversion is that there are extra "released" axis events for // the unused axis. For example, pressing just "up" will also give you a "released" event // for either the left or right button, even if it wasn't pressed before pressing "up". let x_axis_value = match dpad_value { 5..=7 => -1, // left 1..=3 => 1, // right _ => 0, }; // Since we're emulating an inverted macOS gamepad axis, down is positive and up is negative let y_axis_value = match dpad_value { 3..=5 => 1, // down 0 | 1 | 7 => -1, // up _ => 0, }; let x_axis_event = Event::new( id, EventType::AxisValueChanged( x_axis_value, crate::EvCode(EvCode { page, usage: USAGE_AXIS_DPADX, }), ), ); let y_axis_event = Event::new( id, EventType::AxisValueChanged( y_axis_value, crate::EvCode(EvCode { page, usage: USAGE_AXIS_DPADY, }), ), ); let _ = tx.send((x_axis_event, None)); let _ = tx.send((y_axis_event, None)); } } ================================================ FILE: gilrs-core/src/platform/macos/io_kit.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. #![allow(non_upper_case_globals)] use objc2_core_foundation::{ kCFAllocatorDefault, CFArray, CFDictionary, CFNumber, CFRetained, CFString, CFStringBuiltInEncodings, CFType, }; use objc2_io_kit::{ io_service_t, kHIDPage_Button, kHIDPage_Consumer, kHIDPage_GenericDesktop, kHIDPage_Simulation, kHIDUsage_Button_1, kHIDUsage_GD_DPadDown, kHIDUsage_GD_DPadLeft, kHIDUsage_GD_DPadRight, kHIDUsage_GD_DPadUp, kHIDUsage_GD_Dial, kHIDUsage_GD_GamePad, kHIDUsage_GD_Hatswitch, kHIDUsage_GD_Joystick, kHIDUsage_GD_MultiAxisController, kHIDUsage_GD_Rx, kHIDUsage_GD_Ry, kHIDUsage_GD_Rz, kHIDUsage_GD_Select, kHIDUsage_GD_Slider, kHIDUsage_GD_Start, kHIDUsage_GD_SystemMainMenu, kHIDUsage_GD_Wheel, kHIDUsage_GD_X, kHIDUsage_GD_Y, kHIDUsage_GD_Z, kHIDUsage_Sim_Accelerator, kHIDUsage_Sim_Brake, kHIDUsage_Sim_Rudder, kHIDUsage_Sim_Throttle, kIOHIDDeviceUsageKey, kIOHIDDeviceUsagePageKey, kIOHIDLocationIDKey, kIOHIDOptionsTypeNone, kIOHIDPrimaryUsageKey, kIOHIDPrimaryUsagePageKey, kIOHIDProductIDKey, kIOHIDProductKey, kIOHIDVendorIDKey, kIOHIDVersionNumberKey, kIOReturnSuccess, IOHIDDevice, IOHIDElement, IOHIDElementType, IOHIDManager, IOObjectRelease, IOObjectRetain, IORegistryEntryGetRegistryEntryID, IO_OBJECT_NULL, }; use std::ffi::CStr; pub fn new_manager() -> Option> { let manager = IOHIDManager::new(None, kIOHIDOptionsTypeNone); let matchers = CFArray::from_retained_objects(&[ create_hid_device_matcher(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick), create_hid_device_matcher(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad), create_hid_device_matcher(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController), ]); // SAFETY: The matchers are of the correct type. unsafe { manager.set_device_matching_multiple(Some(matchers.as_opaque())) }; let ret = manager.open(kIOHIDOptionsTypeNone); if ret != kIOReturnSuccess { None } else { Some(manager) } } #[derive(Debug, Clone)] pub struct Device(pub CFRetained); // SAFETY: TODO, unsure? unsafe impl Sync for Device {} unsafe impl Send for Device {} pub trait DeviceExt: Properties { fn device(&self) -> &IOHIDDevice; fn get_name(&self) -> Option { self.get_string_property(kIOHIDProductKey) .map(|name| name.to_string()) } fn get_location_id(&self) -> Option { self.get_number_property(kIOHIDLocationIDKey) .and_then(|location_id| location_id.as_i32().map(|location_id| location_id as u32)) } fn get_vendor_id(&self) -> Option { self.get_number_property(kIOHIDVendorIDKey) .and_then(|vendor_id| vendor_id.as_i32().map(|vendor_id| vendor_id as u16)) } fn get_product_id(&self) -> Option { self.get_number_property(kIOHIDProductIDKey) .and_then(|product_id| product_id.as_i32().map(|product_id| product_id as u16)) } fn get_version(&self) -> Option { self.get_number_property(kIOHIDVersionNumberKey) .and_then(|version| version.as_i32().map(|version| version as u16)) } fn get_page(&self) -> Option { self.get_number_property(kIOHIDPrimaryUsagePageKey) .and_then(|page| page.as_i32().map(|page| page as u32)) } fn get_usage(&self) -> Option { self.get_number_property(kIOHIDPrimaryUsageKey) .and_then(|usage| usage.as_i32().map(|usage| usage as u32)) } fn get_service(&self) -> Option { IOService::new(self.device().service()) } } pub fn device_elements(device: &IOHIDDevice) -> Vec> { // SAFETY: We pass `None` as the dictionary, which means we don't have to worry about // type-safety there. let elements = unsafe { device.matching_elements(None, kIOHIDOptionsTypeNone) }; let Some(elements) = elements else { return vec![]; }; // SAFETY: `IOHIDDeviceCopyMatchingElements` is documented to return CFArray of IOHIDElement. let elements = unsafe { elements.cast_unchecked::() }; elements.into_iter().collect() } impl DeviceExt for IOHIDDevice { fn device(&self) -> &IOHIDDevice { self } } impl Properties for IOHIDDevice { fn get_property(&self, key: &CStr) -> Option> { debug_assert!(key.to_str().is_ok()); // SAFETY: The key is a valid C string with UTF-8 contents. let key = unsafe { CFString::with_c_string( kCFAllocatorDefault, key.as_ptr(), CFStringBuiltInEncodings::EncodingUTF8.0, )? }; self.property(&key) } } pub fn element_is_collection(type_: IOHIDElementType) -> bool { type_ == IOHIDElementType::Collection } pub fn element_is_axis(type_: IOHIDElementType, page: u32, usage: u32) -> bool { match type_ { IOHIDElementType::Input_Misc | IOHIDElementType::Input_Button | IOHIDElementType::Input_Axis => match page { kHIDPage_GenericDesktop => { matches!( usage, kHIDUsage_GD_X | kHIDUsage_GD_Y | kHIDUsage_GD_Z | kHIDUsage_GD_Rx | kHIDUsage_GD_Ry | kHIDUsage_GD_Rz | kHIDUsage_GD_Slider | kHIDUsage_GD_Dial | kHIDUsage_GD_Wheel ) } kHIDPage_Simulation => matches!( usage, kHIDUsage_Sim_Rudder | kHIDUsage_Sim_Throttle | kHIDUsage_Sim_Accelerator | kHIDUsage_Sim_Brake ), _ => false, }, _ => false, } } pub fn element_is_button(type_: IOHIDElementType, page: u32, usage: u32) -> bool { match type_ { IOHIDElementType::Input_Misc | IOHIDElementType::Input_Button | IOHIDElementType::Input_Axis => match page { kHIDPage_GenericDesktop => matches!( usage, kHIDUsage_GD_DPadUp | kHIDUsage_GD_DPadDown | kHIDUsage_GD_DPadRight | kHIDUsage_GD_DPadLeft | kHIDUsage_GD_Start | kHIDUsage_GD_Select | kHIDUsage_GD_SystemMainMenu ), kHIDPage_Button | kHIDPage_Consumer => true, _ => false, }, _ => false, } } pub fn element_is_hat(type_: IOHIDElementType, page: u32, usage: u32) -> bool { match type_ { IOHIDElementType::Input_Misc | IOHIDElementType::Input_Button | IOHIDElementType::Input_Axis => match page { kHIDPage_GenericDesktop => matches!(usage, USAGE_AXIS_DPADX | USAGE_AXIS_DPADY), _ => false, }, _ => false, } } pub fn element_children(element: &IOHIDElement) -> Vec> { let elements = element.children(); let Some(elements) = elements else { return vec![]; }; // SAFETY: `IOHIDElementGetChildren` is documented to return CFArray of IOHIDElement. let elements = unsafe { elements.cast_unchecked::() }; elements.into_iter().collect() } impl Properties for IOHIDElement { fn get_property(&self, key: &CStr) -> Option> { debug_assert!(key.to_str().is_ok()); // SAFETY: The key is a valid C string with UTF-8 contents. let key = unsafe { CFString::with_c_string( kCFAllocatorDefault, key.as_ptr(), CFStringBuiltInEncodings::EncodingUTF8.0, )? }; self.property(&key) } } #[repr(C)] #[derive(Debug)] pub(crate) struct IOService(io_service_t); impl IOService { pub fn new(io_service: io_service_t) -> Option { if io_service == IO_OBJECT_NULL { return None; } // We pair this retain with a release in `Drop`. let result = IOObjectRetain(io_service); if result == kIOReturnSuccess { Some(IOService(io_service)) } else { None } } pub fn get_registry_entry_id(&self) -> Option { IOObjectRetain(self.0); let mut entry_id = 0; // SAFETY: `&mut entry_id` is a valid pointer. let result = unsafe { IORegistryEntryGetRegistryEntryID(self.0, &mut entry_id) }; IOObjectRelease(self.0); if result == kIOReturnSuccess { Some(entry_id) } else { None } } } impl Drop for IOService { fn drop(&mut self) { IOObjectRelease(self.0 as _); } } pub trait Properties { fn get_property(&self, key: &CStr) -> Option>; fn get_number_property(&self, key: &CStr) -> Option> { self.get_property(key) .and_then(|value| value.downcast::().ok()) } fn get_string_property(&self, key: &CStr) -> Option> { self.get_property(key) .and_then(|value| value.downcast::().ok()) } } fn create_hid_device_matcher( page: u32, usage: u32, ) -> CFRetained> { let page_key = CFString::from_static_str(kIOHIDDeviceUsagePageKey.to_str().unwrap()); let page_value = CFNumber::new_i32(page as i32); let usage_key = CFString::from_static_str(kIOHIDDeviceUsageKey.to_str().unwrap()); let usage_value = CFNumber::new_i32(usage as i32); CFDictionary::from_slices(&[&*page_key, &*usage_key], &[&*page_value, &*usage_value]) } // Revisions: // - MacOS Version: Sequoia 15.5 (Xbox One Elite Series 2 Controller) (20th of July 2025) // Usage Pages pub const PAGE_GENERIC_DESKTOP: u32 = kHIDPage_GenericDesktop; pub const PAGE_SIMULATION: u32 = kHIDPage_Simulation; pub const PAGE_BUTTON: u32 = kHIDPage_Button; // GenericDesktop Page (0x01) pub const USAGE_AXIS_LSTICKX: u32 = kHIDUsage_GD_X; pub const USAGE_AXIS_LSTICKY: u32 = kHIDUsage_GD_Y; pub const USAGE_AXIS_LEFTZ: u32 = 0; // unconfirmed pub const USAGE_AXIS_RSTICKX: u32 = kHIDUsage_GD_Z; pub const USAGE_AXIS_RSTICKY: u32 = kHIDUsage_GD_Rz; pub const USAGE_AXIS_RIGHTZ: u32 = 0; // unconfirmed pub const USAGE_AXIS_DPADX: u32 = kHIDUsage_GD_Hatswitch; pub const USAGE_AXIS_DPADY: u32 = kHIDUsage_GD_Hatswitch + 1; pub const USAGE_AXIS_RT: u32 = 0; // unconfirmed pub const USAGE_AXIS_LT: u32 = 0; // unconfirmed pub const USAGE_AXIS_RT2: u32 = kHIDUsage_Sim_Accelerator; pub const USAGE_AXIS_LT2: u32 = kHIDUsage_Sim_Brake; // Button Page (0x09) pub const USAGE_BTN_SOUTH: u32 = kHIDUsage_Button_1; pub const USAGE_BTN_EAST: u32 = kHIDUsage_Button_1 + 1; pub const USAGE_BTN_WEST: u32 = kHIDUsage_Button_1 + 3; pub const USAGE_BTN_NORTH: u32 = kHIDUsage_Button_1 + 4; pub const USAGE_BTN_LT: u32 = kHIDUsage_Button_1 + 6; pub const USAGE_BTN_RT: u32 = kHIDUsage_Button_1 + 7; pub const USAGE_BTN_LT2: u32 = kHIDUsage_Button_1 + 8; // unconfirmed pub const USAGE_BTN_RT2: u32 = kHIDUsage_Button_1 + 9; // unconfirmed pub const USAGE_BTN_SELECT: u32 = kHIDUsage_Button_1 + 10; pub const USAGE_BTN_START: u32 = kHIDUsage_Button_1 + 11; pub const USAGE_BTN_MODE: u32 = kHIDUsage_Button_1 + 12; pub const USAGE_BTN_LTHUMB: u32 = kHIDUsage_Button_1 + 13; pub const USAGE_BTN_RTHUMB: u32 = kHIDUsage_Button_1 + 14; pub const USAGE_BTN_DPAD_UP: u32 = kHIDUsage_Button_1 + 15; // unconfirmed pub const USAGE_BTN_DPAD_DOWN: u32 = kHIDUsage_Button_1 + 16; // unconfirmed pub const USAGE_BTN_DPAD_LEFT: u32 = kHIDUsage_Button_1 + 17; // unconfirmed pub const USAGE_BTN_DPAD_RIGHT: u32 = kHIDUsage_Button_1 + 18; // unconfirmed pub const USAGE_BTN_C: u32 = kHIDUsage_Button_1 + 19; // unconfirmed pub const USAGE_BTN_Z: u32 = kHIDUsage_Button_1 + 20; // unconfirmed ================================================ FILE: gilrs-core/src/platform/macos/mod.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. mod ff; mod gamepad; mod io_kit; pub use self::ff::Device as FfDevice; pub use self::gamepad::{native_ev_codes, EvCode, Gamepad, Gilrs}; // True, if Y axis of sticks points downwards. pub const IS_Y_AXIS_REVERSED: bool = true; ================================================ FILE: gilrs-core/src/platform/mod.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. //! Module which exports the platform-specific types. //! //! Each backend has to provide: //! //! * A `FfDevice` (a struct which handles force feedback) //! * A `Gilrs` context //! * A `Gamepad` struct //! * A static `str` which specifies the name of the SDL input mapping //! * A constant which define whether Y axis of sticks points upwards or downwards //! * A module with the platform-specific constants for common gamepad buttons //! called `native_ev_codes` #![allow(clippy::module_inception)] pub use self::platform::*; #[cfg(target_os = "linux")] #[path = "linux/mod.rs"] mod platform; #[cfg(target_os = "macos")] #[path = "macos/mod.rs"] mod platform; #[cfg(all(not(feature = "xinput"), not(feature = "wgi")))] compile_error!( "Windows needs one of the features `gilrs/xinput` or `gilrs/wgi` enabled. \nEither don't use \ 'default-features = false' or add one of the features back." ); #[cfg(all(feature = "wgi", feature = "xinput"))] compile_error!("features `gilrs/xinput` and `gilrs/wgi` are mutually exclusive"); #[cfg(all(target_os = "windows", feature = "xinput", not(feature = "wgi")))] #[path = "windows_xinput/mod.rs"] mod platform; #[cfg(all(target_os = "windows", feature = "wgi"))] #[path = "windows_wgi/mod.rs"] mod platform; #[cfg(target_arch = "wasm32")] #[path = "wasm/mod.rs"] mod platform; #[cfg(all( not(any(target_os = "linux")), not(target_os = "macos"), not(target_os = "windows"), not(target_arch = "wasm32") ))] #[path = "default/mod.rs"] mod platform; ================================================ FILE: gilrs-core/src/platform/wasm/ff.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::time::Duration; #[derive(Debug)] pub struct Device; impl Device { pub fn set_ff_state(&mut self, _strong: u16, _weak: u16, _min_duration: Duration) {} } ================================================ FILE: gilrs-core/src/platform/wasm/gamepad.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::collections::VecDeque; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::time::Duration; use js_sys::RegExp; use uuid::Uuid; use wasm_bindgen::JsCast; use web_sys::{DomException, Gamepad as WebGamepad, GamepadButton, GamepadMappingType}; use super::FfDevice; use crate::platform::native_ev_codes::{BTN_LT2, BTN_RT2}; use crate::{AxisInfo, Event, EventType, PlatformError, PowerInfo}; #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; #[derive(Debug)] pub struct Gilrs { event_cache: VecDeque, gamepads: Vec, new_web_gamepads: Vec, next_event_error_logged: bool, } impl Gilrs { pub(crate) fn new() -> Result { let window = web_sys::window().ok_or_else(|| PlatformError::Other(Box::new(Error::NoWindow)))?; if !window.is_secure_context() { warn!("Context is not secure, gamepad API may not be available.") } Ok({ Gilrs { event_cache: VecDeque::new(), gamepads: Vec::new(), new_web_gamepads: Vec::new(), next_event_error_logged: false, } }) } pub(crate) fn next_event(&mut self) -> Option { // Don't duplicate the work of checking the diff between the old and new gamepads if // there are still events to return if !self.event_cache.is_empty() { return self.event_cache.pop_front(); } let gamepads = match web_sys::window() .expect("no window") .navigator() .get_gamepads() { Ok(x) => { self.next_event_error_logged = false; x } Err(js) => { if !self.next_event_error_logged { self.next_event_error_logged = true; let exception: DomException = match js.dyn_into() { Ok(x) => x, Err(e) => { error!("getGamepads() failed with unknown error: {:?}", e); return None; } }; error!("getGamepads(): {}", exception.message()); } return None; } }; // Gather all non-null gamepads for maybe_js_gamepad in gamepads { if !maybe_js_gamepad.is_null() { self.new_web_gamepads .push(WebGamepad::from(maybe_js_gamepad)); } } // Update existing gamepads for (id, gamepad) in self.gamepads.iter_mut().enumerate() { let maybe_js_gamepad_index = self .new_web_gamepads .iter() .position(|x| gamepad.gamepad.index() == x.index()); if let Some(js_gamepad_index) = maybe_js_gamepad_index { gamepad.gamepad = self.new_web_gamepads.swap_remove(js_gamepad_index); if !gamepad.connected { self.event_cache .push_back(Event::new(id, EventType::Connected)); gamepad.connected = true; } let buttons = gamepad.gamepad.buttons(); for btn_index in 0..gamepad .mapping .buttons() .len() .min(buttons.length() as usize) { let (old_pressed, old_value) = gamepad.mapping.buttons()[btn_index]; let ev_code = crate::EvCode(gamepad.button_code(btn_index)); let button_object = GamepadButton::from(buttons.get(btn_index as u32)); let new_pressed = button_object.pressed(); let new_value = button_object.value(); if [BTN_LT2, BTN_RT2].contains(&ev_code.0) && old_value != new_value { // Treat left and right triggers as axes so we get non-binary values. // Button Pressed/Changed events are generated from the axis changed // events later. let value = (new_value * i32::MAX as f64) as i32; self.event_cache .push_back(Event::new(id, EventType::AxisValueChanged(value, ev_code))); } else { match (old_pressed, new_pressed) { (false, true) => self .event_cache .push_back(Event::new(id, EventType::ButtonPressed(ev_code))), (true, false) => self .event_cache .push_back(Event::new(id, EventType::ButtonReleased(ev_code))), _ => (), } } gamepad.mapping.buttons_mut()[btn_index] = (new_pressed, new_value); } let axes = gamepad.gamepad.axes(); for axis_index in 0..gamepad.mapping.axes().len().min(axes.length() as usize) { let old_value = gamepad.mapping.axes()[axis_index]; let new_value = axes .get(axis_index as u32) .as_f64() .expect("axes() should be an array of f64"); if old_value != new_value { let ev_code = crate::EvCode(gamepad.axis_code(axis_index)); let value = (new_value * i32::MAX as f64) as i32; self.event_cache .push_back(Event::new(id, EventType::AxisValueChanged(value, ev_code))); } gamepad.mapping.axes_mut()[axis_index] = new_value; } } else { // Create a disconnect event if gamepad.connected { self.event_cache .push_back(Event::new(id, EventType::Disconnected)); gamepad.connected = false; } } } // Add new gamepads for js_gamepad in self.new_web_gamepads.drain(..) { let id = self.gamepads.len(); self.gamepads.push(Gamepad::new(js_gamepad)); // Create a connected event let event = Event::new(id, EventType::Connected); self.event_cache.push_back(event); } self.event_cache.pop_front() } pub(crate) fn next_event_blocking(&mut self, _timeout: Option) -> Option { unimplemented!("next_event_blocking is not supported on web. Use next_event.") } pub fn gamepad(&self, id: usize) -> Option<&Gamepad> { self.gamepads.get(id) } pub fn last_gamepad_hint(&self) -> usize { self.gamepads.len() } } #[derive(Debug)] #[allow(clippy::large_enum_variant)] enum Mapping { Standard { buttons: [(bool, f64); 17], axes: [f64; 4], }, NoMapping { buttons: Vec<(bool, f64)>, axes: Vec, }, } impl Mapping { fn buttons(&self) -> &[(bool, f64)] { match self { Mapping::Standard { buttons, .. } => buttons, Mapping::NoMapping { buttons, .. } => buttons, } } fn buttons_mut(&mut self) -> &mut [(bool, f64)] { match self { Mapping::Standard { buttons, .. } => &mut *buttons, Mapping::NoMapping { buttons, .. } => &mut *buttons, } } fn axes(&self) -> &[f64] { match self { Mapping::Standard { axes, .. } => axes, Mapping::NoMapping { axes, .. } => axes, } } fn axes_mut(&mut self) -> &mut [f64] { match self { Mapping::Standard { axes, .. } => &mut *axes, Mapping::NoMapping { axes, .. } => &mut *axes, } } } #[derive(Debug)] pub struct Gamepad { uuid: Uuid, gamepad: WebGamepad, name: String, vendor: Option, product: Option, mapping: Mapping, connected: bool, } impl Gamepad { fn new(gamepad: WebGamepad) -> Gamepad { let name = gamepad.id(); // This regular expression extracts the vendor and product ID from the gamepad "id". // Firefox: // 054c-05c4-Sony Computer Entertainment Wireless Controller // Chrome: // Sony Computer Entertainment Wireless Controller (STANDARD GAMEPAD Vendor: 054c Product: 05c4) let regexp = RegExp::new( r"(?:^([a-f0-9]{4})-([a-f0-9]{4})-)|(?:Vendor: ([a-f0-9]{4}) Product: ([a-f0-9]{4})\)$)", "", ); let (vendor, product) = if let Some(matches) = regexp.exec(&name) { let parse_hex = |index| { matches .get(index) .as_string() .and_then(|id| u16::from_str_radix(&id, 16).ok()) }; ( parse_hex(1).or_else(|| parse_hex(3)), parse_hex(2).or_else(|| parse_hex(4)), ) } else { (None, None) }; let buttons = gamepad.buttons(); let button_iter = { { buttons.iter().map(GamepadButton::from) } }; let axes = gamepad.axes(); let axis_iter = { { axes.iter() .map(|val| val.as_f64().expect("axes() should be an array of f64")) } }; let mapping = match gamepad.mapping() { GamepadMappingType::Standard => { let mut buttons = [(false, 0.0); 17]; let mut axes = [0.0; 4]; for (index, button) in button_iter.enumerate().take(buttons.len()) { buttons[index] = (button.pressed(), button.value()); } for (index, axis) in axis_iter.enumerate().take(axes.len()) { axes[index] = axis; } Mapping::Standard { buttons, axes } } _ => { let buttons = button_iter .map(|button| (button.pressed(), button.value())) .collect(); let axes = axis_iter.collect(); Mapping::NoMapping { buttons, axes } } }; Gamepad { uuid: Uuid::nil(), gamepad, name, vendor, product, mapping, connected: true, } } pub fn name(&self) -> &str { &self.name } pub fn uuid(&self) -> Uuid { self.uuid } pub fn vendor_id(&self) -> Option { self.vendor } pub fn product_id(&self) -> Option { self.product } pub fn is_connected(&self) -> bool { self.gamepad.connected() } pub fn power_info(&self) -> PowerInfo { PowerInfo::Unknown } pub fn is_ff_supported(&self) -> bool { false } pub fn ff_device(&self) -> Option { None } pub fn buttons(&self) -> &[EvCode] { &native_ev_codes::BUTTONS } pub fn axes(&self) -> &[EvCode] { &native_ev_codes::AXES } fn button_code(&self, index: usize) -> EvCode { self.buttons() .get(index) .copied() .unwrap_or(EvCode(index as u8 + 31)) } fn axis_code(&self, index: usize) -> EvCode { self.axes() .get(index) .copied() .unwrap_or_else(|| EvCode((index + self.mapping.buttons().len()) as u8 + 31)) } pub(crate) fn axis_info(&self, _nec: EvCode) -> Option<&AxisInfo> { if self.buttons().contains(&_nec) { return Some(&AxisInfo { min: 0, max: i32::MAX, deadzone: None, }); } Some(&AxisInfo { min: i32::MIN, max: i32::MAX, deadzone: None, }) } } #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct EvCode(u8); impl EvCode { pub fn into_u32(self) -> u32 { self.0 as u32 } } impl Display for EvCode { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { self.0.fmt(f) } } #[derive(Debug, Copy, Clone)] enum Error { NoWindow, } impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match *self { Error::NoWindow => f.write_str("window is not available"), } } } impl std::error::Error for Error {} pub mod native_ev_codes { use super::EvCode; pub const AXIS_LSTICKX: EvCode = EvCode(0); pub const AXIS_LSTICKY: EvCode = EvCode(1); pub const AXIS_LEFTZ: EvCode = EvCode(2); pub const AXIS_RSTICKX: EvCode = EvCode(3); pub const AXIS_RSTICKY: EvCode = EvCode(4); pub const AXIS_RIGHTZ: EvCode = EvCode(5); pub const AXIS_DPADX: EvCode = EvCode(6); pub const AXIS_DPADY: EvCode = EvCode(7); pub const AXIS_RT: EvCode = EvCode(8); pub const AXIS_LT: EvCode = EvCode(9); pub const AXIS_RT2: EvCode = EvCode(10); pub const AXIS_LT2: EvCode = EvCode(11); pub const BTN_SOUTH: EvCode = EvCode(12); pub const BTN_EAST: EvCode = EvCode(13); pub const BTN_C: EvCode = EvCode(14); pub const BTN_NORTH: EvCode = EvCode(15); pub const BTN_WEST: EvCode = EvCode(16); pub const BTN_Z: EvCode = EvCode(17); pub const BTN_LT: EvCode = EvCode(18); pub const BTN_RT: EvCode = EvCode(19); pub const BTN_LT2: EvCode = EvCode(20); pub const BTN_RT2: EvCode = EvCode(21); pub const BTN_SELECT: EvCode = EvCode(22); pub const BTN_START: EvCode = EvCode(23); pub const BTN_MODE: EvCode = EvCode(24); pub const BTN_LTHUMB: EvCode = EvCode(25); pub const BTN_RTHUMB: EvCode = EvCode(26); pub const BTN_DPAD_UP: EvCode = EvCode(27); pub const BTN_DPAD_DOWN: EvCode = EvCode(28); pub const BTN_DPAD_LEFT: EvCode = EvCode(29); pub const BTN_DPAD_RIGHT: EvCode = EvCode(30); pub(super) static BUTTONS: [EvCode; 17] = [ BTN_SOUTH, BTN_EAST, BTN_WEST, BTN_NORTH, BTN_LT, BTN_RT, BTN_LT2, BTN_RT2, BTN_SELECT, BTN_START, BTN_LTHUMB, BTN_RTHUMB, BTN_DPAD_UP, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT, BTN_MODE, ]; pub(super) static AXES: [EvCode; 4] = [AXIS_LSTICKX, AXIS_LSTICKY, AXIS_RSTICKX, AXIS_RSTICKY]; } ================================================ FILE: gilrs-core/src/platform/wasm/mod.rs ================================================ mod ff; mod gamepad; pub use self::ff::Device as FfDevice; pub use self::gamepad::{native_ev_codes, EvCode, Gamepad, Gilrs}; pub const IS_Y_AXIS_REVERSED: bool = true; ================================================ FILE: gilrs-core/src/platform/windows_wgi/ff.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use std::time::Duration; use windows::Gaming::Input::Gamepad as WgiGamepad; use windows::Gaming::Input::GamepadVibration; #[derive(Debug)] pub struct Device { id: u32, wgi_gamepad: Option, } impl Device { pub(crate) fn new(id: u32, wgi_gamepad: Option) -> Self { Device { id, wgi_gamepad } } pub fn set_ff_state(&mut self, strong: u16, weak: u16, _min_duration: Duration) { if let Some(wgi_gamepad) = &self.wgi_gamepad { if let Err(err) = wgi_gamepad.SetVibration(GamepadVibration { LeftMotor: (strong as f64) / (u16::MAX as f64), RightMotor: (weak as f64) / (u16::MAX as f64), LeftTrigger: 0.0, RightTrigger: 0.0, }) { error!( "Failed to change FF state – unknown error. ID = {}, error = {:?}.", self.id, err ); } } } } ================================================ FILE: gilrs-core/src/platform/windows_wgi/gamepad.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use super::FfDevice; use crate::native_ev_codes as nec; use crate::{utils, AxisInfo, Event, EventType, PlatformError, PowerInfo}; #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::sync::mpsc::{self, Receiver, Sender, TryRecvError}; use std::thread; use std::thread::JoinHandle; use std::time::{Duration, Instant, SystemTime}; use uuid::Uuid; use windows::core::HSTRING; use windows::Devices::Power::BatteryReport; use windows::Foundation::EventHandler; use windows::Gaming::Input::{ GameControllerSwitchPosition, Gamepad as WgiGamepad, GamepadButtons, GamepadReading, RawGameController, }; use windows::System::Power::BatteryStatus; const SDL_HARDWARE_BUS_USB: u32 = 0x03; // const SDL_HARDWARE_BUS_BLUETOOTH: u32 = 0x05; // The general consensus is that standard xbox controllers poll at ~125 hz which // means 8 ms between updates. // Seems like a good target for how often we update the background thread. const EVENT_THREAD_SLEEP_TIME: u64 = 8; const WGI_TO_GILRS_BUTTON_MAP: [(GamepadButtons, crate::EvCode); 14] = [ (GamepadButtons::DPadUp, nec::BTN_DPAD_UP), (GamepadButtons::DPadDown, nec::BTN_DPAD_DOWN), (GamepadButtons::DPadLeft, nec::BTN_DPAD_LEFT), (GamepadButtons::DPadRight, nec::BTN_DPAD_RIGHT), (GamepadButtons::Menu, nec::BTN_START), (GamepadButtons::View, nec::BTN_SELECT), (GamepadButtons::LeftThumbstick, nec::BTN_LTHUMB), (GamepadButtons::RightThumbstick, nec::BTN_RTHUMB), (GamepadButtons::LeftShoulder, nec::BTN_LT), (GamepadButtons::RightShoulder, nec::BTN_RT), (GamepadButtons::A, nec::BTN_SOUTH), (GamepadButtons::B, nec::BTN_EAST), (GamepadButtons::X, nec::BTN_WEST), (GamepadButtons::Y, nec::BTN_NORTH), ]; /// This is similar to `gilrs_core::Event` but has a raw_game_controller that still needs to be /// converted to a gilrs gamepad id. #[derive(Debug)] struct WgiEvent { raw_game_controller: RawGameController, event: EventType, pub time: SystemTime, } impl WgiEvent { fn new(raw_game_controller: RawGameController, event: EventType) -> Self { let time = utils::time_now(); WgiEvent { raw_game_controller, event, time, } } } #[derive(Debug)] pub struct Gilrs { gamepads: Vec, rx: Receiver, join_handle: Option>, stop_tx: Sender<()>, } impl Gilrs { pub(crate) fn new() -> Result { let raw_game_controllers = RawGameController::RawGameControllers() .map_err(|e| PlatformError::Other(Box::new(e)))?; let count = raw_game_controllers .Size() .map_err(|e| PlatformError::Other(Box::new(e)))?; // Intentionally avoiding using RawGameControllers.into_iter() as it triggers a crash when // the app is run through steam. // https://gitlab.com/gilrs-project/gilrs/-/issues/132 let gamepads = (0..count) .map(|i| { let controller = raw_game_controllers .GetAt(i) .map_err(|e| PlatformError::Other(Box::new(e)))?; Ok(Gamepad::new(i, controller)) }) .collect::, _>>()?; let (tx, rx) = mpsc::channel(); let (stop_tx, stop_rx) = mpsc::channel(); let join_handle = Some(Self::spawn_thread(tx, stop_rx)); Ok(Gilrs { gamepads, rx, join_handle, stop_tx, }) } fn spawn_thread(tx: Sender, stop_rx: Receiver<()>) -> JoinHandle<()> { let added_tx = tx.clone(); let added_handler = EventHandler::::new(move |_, g| { if let Some(g) = g.as_ref() { added_tx .send(WgiEvent::new(g.clone(), EventType::Connected)) .expect("should be able to send to main thread"); } Ok(()) }); let controller_added_token = RawGameController::RawGameControllerAdded(&added_handler).unwrap(); let removed_tx = tx.clone(); let removed_handler = EventHandler::::new(move |_, g| { if let Some(g) = g.as_ref() { removed_tx .send(WgiEvent::new(g.clone(), EventType::Disconnected)) .expect("should be able to send to main thread"); } Ok(()) }); let controller_removed_token = RawGameController::RawGameControllerRemoved(&removed_handler).unwrap(); std::thread::Builder::new() .name("gilrs".to_owned()) .spawn(move || { let mut controllers: Vec = Vec::new(); // To avoid allocating every update, store old and new readings for every controller // and swap their memory let mut readings: Vec<(HSTRING, Reading, Reading)> = Vec::new(); let mut last_failed_get_id: Option = None; loop { match stop_rx.try_recv() { Ok(_) => break, Err(TryRecvError::Disconnected) => { warn!("stop_rx channel disconnected prematurely"); break; } Err(TryRecvError::Empty) => {} } controllers.clear(); // Avoiding using RawGameControllers().into_iter() here due to it causing an // unhandled exception when the app is running through steam. // https://gitlab.com/gilrs-project/gilrs/-/issues/132 if let Ok(raw_game_controllers) = RawGameController::RawGameControllers() { let count = raw_game_controllers.Size().unwrap_or_default(); for index in 0..count { if let Ok(controller) = raw_game_controllers.GetAt(index) { controllers.push(controller); } } } for controller in controllers.iter() { let id: HSTRING = match controller.NonRoamableId() { Ok(id) => id, Err(e) => { if last_failed_get_id.map_or(true, |x| x.elapsed().as_secs() > 59) { error!( "Failed to get gamepad id: {e}! Skipping reading events \ for this gamepad." ); last_failed_get_id = Some(Instant::now()); } continue; } }; // Find readings for this controller or insert new ones. let index = match readings.iter().position(|(other_id, ..)| id == *other_id) { None => { let reading = match WgiGamepad::FromGameController(controller) { Ok(wgi_gamepad) => { Reading::Gamepad(wgi_gamepad.GetCurrentReading().unwrap()) } _ => Reading::Raw(RawGamepadReading::new(controller).unwrap()), }; readings.push((id, reading.clone(), reading)); readings.len() - 1 } Some(i) => i, }; let (_, old_reading, new_reading) = &mut readings[index]; // Make last update's reading the old reading and get a new one. std::mem::swap(old_reading, new_reading); if let Err(e) = new_reading.update(controller) { if e.code().is_err() { error!("Reading::update() function failed with {e}"); } } // Skip if this is the same reading as the last one. if old_reading.time() == new_reading.time() { continue; } Reading::send_events_for_differences( old_reading, new_reading, controller, &tx, ); } thread::sleep(Duration::from_millis(EVENT_THREAD_SLEEP_TIME)); } if let Err(e) = RawGameController::RemoveRawGameControllerAdded(controller_added_token) { error!("Failed to remove RawGameControllerAdded event handler: {e}"); } if let Err(e) = RawGameController::RemoveRawGameControllerRemoved(controller_removed_token) { error!("Failed to remove RawGameControllerRemoved event handler: {e}"); } }) .expect("failed to spawn thread") } pub(crate) fn next_event(&mut self) -> Option { self.rx .try_recv() .ok() .map(|wgi_event: WgiEvent| self.handle_event(wgi_event)) } pub(crate) fn next_event_blocking(&mut self, timeout: Option) -> Option { if let Some(timeout) = timeout { self.rx .recv_timeout(timeout) .ok() .map(|wgi_event: WgiEvent| self.handle_event(wgi_event)) } else { self.rx .recv() .ok() .map(|wgi_event: WgiEvent| self.handle_event(wgi_event)) } } fn handle_event(&mut self, wgi_event: WgiEvent) -> Event { // Find the index of the gamepad in our vec or insert it let id = self .gamepads .iter() .position( |gamepad| match wgi_event.raw_game_controller.NonRoamableId() { Ok(id) => id == gamepad.non_roamable_id, _ => false, }, ) .unwrap_or_else(|| { self.gamepads.push(Gamepad::new( self.gamepads.len() as u32, wgi_event.raw_game_controller, )); self.gamepads.len() - 1 }); match wgi_event.event { EventType::Connected => self.gamepads[id].is_connected = true, EventType::Disconnected => self.gamepads[id].is_connected = false, _ => (), } Event { id, event: wgi_event.event, time: wgi_event.time, } } pub fn gamepad(&self, id: usize) -> Option<&Gamepad> { self.gamepads.get(id) } pub fn last_gamepad_hint(&self) -> usize { self.gamepads.len() } } impl Drop for Gilrs { fn drop(&mut self) { if let Err(e) = self.stop_tx.send(()) { warn!("Failed to send stop signal to thread: {e:?}"); } if let Err(e) = self.join_handle.take().unwrap().join() { warn!("Failed to join thread: {e:?}"); } } } #[derive(Debug, Clone)] struct RawGamepadReading { axes: Vec, buttons: Vec, switches: Vec, time: u64, } impl RawGamepadReading { fn new(raw_game_controller: &RawGameController) -> windows::core::Result { let axis_count = raw_game_controller.AxisCount()? as usize; let button_count = raw_game_controller.ButtonCount()? as usize; let switch_count = raw_game_controller.SwitchCount()? as usize; let mut new = Self { axes: vec![0.0; axis_count], buttons: vec![false; button_count], switches: vec![GameControllerSwitchPosition::default(); switch_count], time: 0, }; new.time = raw_game_controller.GetCurrentReading( &mut new.buttons, &mut new.switches, &mut new.axes, )?; Ok(new) } fn update(&mut self, raw_game_controller: &RawGameController) -> windows::core::Result<()> { self.time = raw_game_controller.GetCurrentReading( &mut self.buttons, &mut self.switches, &mut self.axes, )?; Ok(()) } } /// Treats switches like a two axes similar to a Directional pad. /// Returns a tuple containing the values of the x and y axis. /// Value's range is -1 to 1. fn direction_from_switch(switch: GameControllerSwitchPosition) -> (i32, i32) { match switch { GameControllerSwitchPosition::Up => (0, 1), GameControllerSwitchPosition::Down => (0, -1), GameControllerSwitchPosition::Right => (1, 0), GameControllerSwitchPosition::Left => (-1, 0), GameControllerSwitchPosition::UpLeft => (-1, 1), GameControllerSwitchPosition::UpRight => (1, 1), GameControllerSwitchPosition::DownLeft => (-1, -1), GameControllerSwitchPosition::DownRight => (1, -1), _ => (0, 0), } } #[derive(Clone)] enum Reading { Raw(RawGamepadReading), Gamepad(GamepadReading), } impl Reading { fn time(&self) -> u64 { match self { Reading::Raw(r) => r.time, Reading::Gamepad(r) => r.Timestamp, } } fn update(&mut self, controller: &RawGameController) -> windows::core::Result<()> { match self { Reading::Raw(raw_reading) => { raw_reading.update(controller)?; } Reading::Gamepad(gamepad_reading) => { let gamepad: WgiGamepad = WgiGamepad::FromGameController(controller)?; *gamepad_reading = gamepad.GetCurrentReading()?; } } Ok(()) } fn send_events_for_differences( old: &Self, new: &Self, controller: &RawGameController, tx: &Sender, ) { match (old, new) { // WGI RawGameController (Reading::Raw(old), Reading::Raw(new)) => { // Axis changes for index in 0..new.axes.len() { if old.axes.get(index) != new.axes.get(index) { // https://github.com/libsdl-org/SDL/blob/6af17369ca773155bd7f39b8801725c4a6d52e4f/src/joystick/windows/SDL_windows_gaming_input.c#L863 let value = ((new.axes[index] * 65535.0) - 32768.0) as i32; let event_type = EventType::AxisValueChanged( value, crate::EvCode(EvCode { kind: EvCodeKind::Axis, index: index as u32, }), ); tx.send(WgiEvent::new(controller.clone(), event_type)) .unwrap() } } for index in 0..new.buttons.len() { if old.buttons.get(index) != new.buttons.get(index) { let event_type = match new.buttons[index] { true => EventType::ButtonPressed(crate::EvCode(EvCode { kind: EvCodeKind::Button, index: index as u32, })), false => EventType::ButtonReleased(crate::EvCode(EvCode { kind: EvCodeKind::Button, index: index as u32, })), }; tx.send(WgiEvent::new(controller.clone(), event_type)) .unwrap() } } for index in 0..old.switches.len() { let (old_x, old_y) = direction_from_switch(old.switches[index]); let (new_x, new_y) = direction_from_switch(new.switches[index]); if old_x != new_x { let event_type = EventType::AxisValueChanged( new_x, crate::EvCode(EvCode { kind: EvCodeKind::Switch, index: (index * 2) as u32, }), ); tx.send(WgiEvent::new(controller.clone(), event_type)) .unwrap() } if old_y != new_y { let event_type = EventType::AxisValueChanged( -new_y, crate::EvCode(EvCode { kind: EvCodeKind::Switch, index: (index * 2) as u32 + 1, }), ); tx.send(WgiEvent::new(controller.clone(), event_type)) .unwrap() } } } // WGI Gamepad (Reading::Gamepad(old), Reading::Gamepad(new)) => { #[rustfmt::skip] let axes = [ (new.LeftTrigger, old.LeftTrigger, nec::AXIS_LT2, 1.0), (new.RightTrigger, old.RightTrigger, nec::AXIS_RT2, 1.0), (new.LeftThumbstickX, old.LeftThumbstickX, nec::AXIS_LSTICKX, 1.0), (new.LeftThumbstickY, old.LeftThumbstickY, nec::AXIS_LSTICKY, -1.0), (new.RightThumbstickX, old.RightThumbstickX, nec::AXIS_RSTICKX, 1.0), (new.RightThumbstickY, old.RightThumbstickY, nec::AXIS_RSTICKY, -1.0), ]; for (new, old, code, multiplier) in axes { if new != old { let _ = tx.send(WgiEvent::new( controller.clone(), EventType::AxisValueChanged( (multiplier * new * i32::MAX as f64) as i32, code, ), )); } } for (current_button, ev_code) in WGI_TO_GILRS_BUTTON_MAP { if (new.Buttons & current_button) != (old.Buttons & current_button) { let _ = match new.Buttons & current_button != GamepadButtons::None { true => tx.send(WgiEvent::new( controller.clone(), EventType::ButtonPressed(ev_code), )), false => tx.send(WgiEvent::new( controller.clone(), EventType::ButtonReleased(ev_code), )), }; } } } (a, b) => { warn!( "WGI Controller changed from gamepad: {} to gamepad: {}. Could not compare \ last update.", a.is_gamepad(), b.is_gamepad() ); #[cfg(debug_assertions)] panic!( "Controllers shouldn't change type between updates, likely programmer error" ); } } } fn is_gamepad(&self) -> bool { matches!(self, Reading::Gamepad(_)) } } #[derive(Debug)] pub struct Gamepad { id: u32, name: String, uuid: Uuid, is_connected: bool, /// This is the generic controller handle without any mappings /// https://learn.microsoft.com/en-us/uwp/api/windows.gaming.input.rawgamecontroller raw_game_controller: RawGameController, /// An ID for this device that will survive disconnects and restarts. /// [NonRoamableIds](https://learn.microsoft.com/en-us/uwp/api/windows.gaming.input.rawgamecontroller.nonroamableid) /// /// Changes if plugged into a different port and is not the same between different applications /// or PCs. non_roamable_id: HSTRING, /// If the controller has a [Gamepad](https://learn.microsoft.com/en-us/uwp/api/windows.gaming.input.gamepad?view=winrt-22621) /// mapping, this is used to access the mapped values. wgi_gamepad: Option, axes: Option>, buttons: Option>, } impl Gamepad { fn new(id: u32, raw_game_controller: RawGameController) -> Gamepad { let is_connected = true; let non_roamable_id = raw_game_controller.NonRoamableId().unwrap(); // See if we can cast this to a windows definition of a gamepad let wgi_gamepad = WgiGamepad::FromGameController(&raw_game_controller).ok(); let name = match raw_game_controller.DisplayName() { Ok(hstring) => hstring.to_string_lossy(), Err(_) => "unknown".to_string(), }; let uuid = match wgi_gamepad.is_some() { true => Uuid::nil(), false => { let vendor_id = raw_game_controller.HardwareVendorId().unwrap_or(0).to_be(); let product_id = raw_game_controller.HardwareProductId().unwrap_or(0).to_be(); let version = 0; // SDL uses the SDL_HARDWARE_BUS_BLUETOOTH bustype for IsWireless devices: // https://github.com/libsdl-org/SDL/blob/294ccba0a23b37fffef62189423444f93732e565/src/joystick/windows/SDL_windows_gaming_input.c#L335-L338 // In my testing though, it caused my controllers to not find mappings. // SDL only uses their WGI implementation for UWP apps so I guess it hasn't been // used enough for people to submit mappings with the different bustype. let bustype = SDL_HARDWARE_BUS_USB.to_be(); Uuid::from_fields( bustype, vendor_id, 0, &[ (product_id >> 8) as u8, product_id as u8, 0, 0, (version >> 8) as u8, version as u8, 0, 0, ], ) } }; let mut gamepad = Gamepad { id, name, uuid, is_connected, raw_game_controller, non_roamable_id, wgi_gamepad, axes: None, buttons: None, }; if gamepad.wgi_gamepad.is_none() { gamepad.collect_axes_and_buttons(); } gamepad } pub fn name(&self) -> &str { &self.name } pub fn uuid(&self) -> Uuid { self.uuid } pub fn vendor_id(&self) -> Option { self.raw_game_controller.HardwareVendorId().ok() } pub fn product_id(&self) -> Option { self.raw_game_controller.HardwareProductId().ok() } pub fn is_connected(&self) -> bool { self.is_connected } pub fn power_info(&self) -> PowerInfo { self.power_info_err().unwrap_or(PowerInfo::Unknown) } /// Using this function so we can easily map errors to unknown fn power_info_err(&self) -> windows::core::Result { if !self.raw_game_controller.IsWireless()? { return Ok(PowerInfo::Wired); } let report: BatteryReport = self.raw_game_controller.TryGetBatteryReport()?; let status: BatteryStatus = report.Status()?; let power_info = match status { BatteryStatus::Discharging | BatteryStatus::Charging => { let full = report.FullChargeCapacityInMilliwattHours()?.GetInt32()? as f32; let remaining = report.RemainingCapacityInMilliwattHours()?.GetInt32()? as f32; let percent: u8 = ((remaining / full) * 100.0) as u8; match status { _ if percent == 100 => PowerInfo::Charged, BatteryStatus::Discharging => PowerInfo::Discharging(percent), BatteryStatus::Charging => PowerInfo::Charging(percent), _ => unreachable!(), } } BatteryStatus::NotPresent => PowerInfo::Wired, BatteryStatus::Idle => PowerInfo::Charged, BatteryStatus(_) => PowerInfo::Unknown, }; Ok(power_info) } pub fn is_ff_supported(&self) -> bool { self.wgi_gamepad.is_some() && self .raw_game_controller .ForceFeedbackMotors() .ok() .map(|motors| motors.First()) .is_some() } pub fn ff_device(&self) -> Option { Some(FfDevice::new(self.id, self.wgi_gamepad.clone())) } pub fn buttons(&self) -> &[EvCode] { match &self.buttons { None => &native_ev_codes::BUTTONS, Some(buttons) => buttons, } } pub fn axes(&self) -> &[EvCode] { match &self.axes { None => &native_ev_codes::AXES, Some(axes) => axes, } } pub(crate) fn axis_info(&self, nec: EvCode) -> Option<&AxisInfo> { // If it isn't a Windows "Gamepad" then return what we want SDL mappings to be able to use if self.wgi_gamepad.is_none() { return match nec.kind { EvCodeKind::Button => None, EvCodeKind::Axis => Some(&AxisInfo { min: i16::MIN as i32, max: i16::MAX as i32, deadzone: None, }), EvCodeKind::Switch => Some(&AxisInfo { min: -1, max: 1, deadzone: None, }), }; } // For Windows Gamepads, the triggers are 0.0 to 1.0 and the thumbsticks are -1.0 to 1.0 // https://learn.microsoft.com/en-us/uwp/api/windows.gaming.input.gamepadreading#fields // Since Gilrs processes axis data as integers, the input has already been multiplied by // i32::MAX in the joy_value method. match nec { native_ev_codes::AXIS_LT2 | native_ev_codes::AXIS_RT2 => Some(&AxisInfo { min: 0, max: i32::MAX, deadzone: None, }), _ => Some(&AxisInfo { min: i32::MIN, max: i32::MAX, deadzone: None, }), } } fn collect_axes_and_buttons(&mut self) { let axis_count = self.raw_game_controller.AxisCount().unwrap() as u32; let button_count = self.raw_game_controller.ButtonCount().unwrap() as u32; let switch_count = self.raw_game_controller.SwitchCount().unwrap() as u32; self.buttons = Some( (0..button_count) .map(|index| EvCode { kind: EvCodeKind::Button, index, }) .collect(), ); self.axes = Some( (0..axis_count) .map(|index| EvCode { kind: EvCodeKind::Axis, index, }) .chain( // Treat switches as two axes (0..switch_count).flat_map(|index| { [ EvCode { kind: EvCodeKind::Switch, index: index * 2, }, EvCode { kind: EvCodeKind::Switch, index: (index * 2) + 1, }, ] }), ) .collect(), ); } } #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] enum EvCodeKind { Button = 0, Axis, Switch, } impl Display for EvCodeKind { fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { match self { EvCodeKind::Button => "Button", EvCodeKind::Axis => "Axis", EvCodeKind::Switch => "Switch", } .fmt(f) } } #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct EvCode { kind: EvCodeKind, index: u32, } impl EvCode { pub fn into_u32(self) -> u32 { ((self.kind as u32) << 16) | self.index } } impl Display for EvCode { fn fmt(&self, f: &mut Formatter) -> FmtResult { write!(f, "{}({})", self.kind, self.index) } } pub mod native_ev_codes { use super::{EvCode, EvCodeKind}; pub const AXIS_LSTICKX: EvCode = EvCode { kind: EvCodeKind::Axis, index: 0, }; pub const AXIS_LSTICKY: EvCode = EvCode { kind: EvCodeKind::Axis, index: 1, }; pub const AXIS_RSTICKX: EvCode = EvCode { kind: EvCodeKind::Axis, index: 2, }; pub const AXIS_LT2: EvCode = EvCode { kind: EvCodeKind::Axis, index: 3, }; pub const AXIS_RT2: EvCode = EvCode { kind: EvCodeKind::Axis, index: 4, }; pub const AXIS_RSTICKY: EvCode = EvCode { kind: EvCodeKind::Axis, index: 5, }; pub const AXIS_RT: EvCode = EvCode { kind: EvCodeKind::Axis, index: 6, }; pub const AXIS_LT: EvCode = EvCode { kind: EvCodeKind::Axis, index: 7, }; pub const AXIS_LEFTZ: EvCode = EvCode { kind: EvCodeKind::Axis, index: 8, }; pub const AXIS_RIGHTZ: EvCode = EvCode { kind: EvCodeKind::Axis, index: 9, }; pub const AXIS_DPADX: EvCode = EvCode { kind: EvCodeKind::Switch, index: 0, }; pub const AXIS_DPADY: EvCode = EvCode { kind: EvCodeKind::Switch, index: 1, }; pub const BTN_WEST: EvCode = EvCode { kind: EvCodeKind::Button, index: 0, }; pub const BTN_SOUTH: EvCode = EvCode { kind: EvCodeKind::Button, index: 1, }; pub const BTN_EAST: EvCode = EvCode { kind: EvCodeKind::Button, index: 2, }; pub const BTN_NORTH: EvCode = EvCode { kind: EvCodeKind::Button, index: 3, }; pub const BTN_LT: EvCode = EvCode { kind: EvCodeKind::Button, index: 4, }; pub const BTN_RT: EvCode = EvCode { kind: EvCodeKind::Button, index: 5, }; pub const BTN_LT2: EvCode = EvCode { kind: EvCodeKind::Button, index: 6, }; pub const BTN_RT2: EvCode = EvCode { kind: EvCodeKind::Button, index: 7, }; pub const BTN_SELECT: EvCode = EvCode { kind: EvCodeKind::Button, index: 8, }; pub const BTN_START: EvCode = EvCode { kind: EvCodeKind::Button, index: 9, }; pub const BTN_LTHUMB: EvCode = EvCode { kind: EvCodeKind::Button, index: 10, }; pub const BTN_RTHUMB: EvCode = EvCode { kind: EvCodeKind::Button, index: 11, }; pub const BTN_MODE: EvCode = EvCode { kind: EvCodeKind::Button, index: 12, }; pub const BTN_C: EvCode = EvCode { kind: EvCodeKind::Button, index: 13, }; pub const BTN_Z: EvCode = EvCode { kind: EvCodeKind::Button, index: 14, }; // The DPad for DS4 controllers is a hat/switch that gets mapped to the DPad native event // code buttons. These "buttons" don't exist on the DS4 controller, so it doesn't matter // what the index is, but if it overlaps with an existing button it will send the event // for the overlapping button as a dpad button instead of unknown. // By using a large index it should avoid this. pub const BTN_DPAD_UP: EvCode = EvCode { kind: EvCodeKind::Button, index: u32::MAX - 3, }; pub const BTN_DPAD_RIGHT: EvCode = EvCode { kind: EvCodeKind::Button, index: u32::MAX - 2, }; pub const BTN_DPAD_DOWN: EvCode = EvCode { kind: EvCodeKind::Button, index: u32::MAX - 1, }; pub const BTN_DPAD_LEFT: EvCode = EvCode { kind: EvCodeKind::Button, index: u32::MAX, }; pub(super) static BUTTONS: [EvCode; 14] = [ BTN_WEST, BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_LT, BTN_RT, BTN_SELECT, BTN_START, BTN_LTHUMB, BTN_RTHUMB, BTN_DPAD_UP, BTN_DPAD_RIGHT, BTN_DPAD_DOWN, BTN_DPAD_LEFT, ]; pub(super) static AXES: [EvCode; 6] = [ AXIS_LSTICKX, AXIS_LSTICKY, AXIS_RSTICKX, AXIS_LT2, AXIS_RT2, AXIS_RSTICKY, ]; } ================================================ FILE: gilrs-core/src/platform/windows_wgi/mod.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. mod ff; mod gamepad; pub use self::ff::Device as FfDevice; pub use self::gamepad::{native_ev_codes, EvCode, Gamepad, Gilrs}; pub const IS_Y_AXIS_REVERSED: bool = true; ================================================ FILE: gilrs-core/src/platform/windows_xinput/ff.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use rusty_xinput::{self, XInputHandle, XInputUsageError}; use std::sync::Arc; use std::time::Duration; #[derive(Debug)] pub struct Device { id: u32, xinput_handle: Arc, } impl Device { pub(crate) fn new(id: u32, xinput_handle: Arc) -> Self { Device { id, xinput_handle } } pub fn set_ff_state(&mut self, strong: u16, weak: u16, _min_duration: Duration) { match self.xinput_handle.set_state(self.id, strong, weak) { Ok(()) => (), Err(XInputUsageError::DeviceNotConnected) => { error!( "Failed to change FF state – gamepad with id {} is no longer connected.", self.id ); } Err(err) => { error!( "Failed to change FF state – unknown error. ID = {}, error = {:?}.", self.id, err ); } } } } ================================================ FILE: gilrs-core/src/platform/windows_xinput/gamepad.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. use super::FfDevice; use crate::{AxisInfo, Event, EventType, PlatformError, PowerInfo}; use std::error::Error as StdError; use std::fmt::{Display, Formatter, Result as FmtResult}; use std::sync::{ mpsc::{self, Receiver, Sender}, Arc, }; use std::time::Duration; use std::{mem, thread}; use rusty_xinput::{ BatteryLevel, BatteryType, XInputHandle, XInputLoadingFailure, XInputState, XInputUsageError, }; use uuid::Uuid; use winapi::um::xinput::{ XINPUT_GAMEPAD as XGamepad, XINPUT_GAMEPAD_A, XINPUT_GAMEPAD_B, XINPUT_GAMEPAD_BACK, XINPUT_GAMEPAD_DPAD_DOWN, XINPUT_GAMEPAD_DPAD_LEFT, XINPUT_GAMEPAD_DPAD_RIGHT, XINPUT_GAMEPAD_DPAD_UP, XINPUT_GAMEPAD_LEFT_SHOULDER, XINPUT_GAMEPAD_LEFT_THUMB, XINPUT_GAMEPAD_RIGHT_SHOULDER, XINPUT_GAMEPAD_RIGHT_THUMB, XINPUT_GAMEPAD_START, XINPUT_GAMEPAD_X, XINPUT_GAMEPAD_Y, XINPUT_STATE as XState, }; // Chosen by dice roll ;) const EVENT_THREAD_SLEEP_TIME: u64 = 10; const ITERATIONS_TO_CHECK_IF_CONNECTED: u64 = 100; const MAX_XINPUT_CONTROLLERS: usize = 4; #[derive(Debug)] pub struct Gilrs { gamepads: [Gamepad; MAX_XINPUT_CONTROLLERS], rx: Receiver, } impl Gilrs { pub(crate) fn new() -> Result { let xinput_handle = XInputHandle::load_default() .map_err(|e| PlatformError::Other(Box::new(Error::FailedToLoadDll(e))))?; let xinput_handle = Arc::new(xinput_handle); let gamepad_ids: [usize; MAX_XINPUT_CONTROLLERS] = std::array::from_fn(|idx| idx); // Map controller IDs to Gamepads let gamepads = gamepad_ids.map(|id| Gamepad::new(id as u32, xinput_handle.clone())); let mut connected: [bool; MAX_XINPUT_CONTROLLERS] = Default::default(); // Iterate through each controller ID and set connected state for id in 0..MAX_XINPUT_CONTROLLERS { connected[id] = gamepads[id].is_connected; } let (tx, rx) = mpsc::channel(); Self::spawn_thread(tx, connected, xinput_handle.clone()); // Coerce gamepads vector to slice Ok(Gilrs { gamepads, rx }) } pub(crate) fn next_event(&mut self) -> Option { let ev = self.rx.try_recv().ok(); self.handle_evevnt(ev); ev } pub(crate) fn next_event_blocking(&mut self, timeout: Option) -> Option { let ev = if let Some(tiemout) = timeout { self.rx.recv_timeout(tiemout).ok() } else { self.rx.recv().ok() }; self.handle_evevnt(ev); ev } fn handle_evevnt(&mut self, ev: Option) { if let Some(ev) = ev { match ev.event { EventType::Connected => self.gamepads[ev.id].is_connected = true, EventType::Disconnected => self.gamepads[ev.id].is_connected = false, _ => (), } } } pub fn gamepad(&self, id: usize) -> Option<&Gamepad> { self.gamepads.get(id) } pub fn last_gamepad_hint(&self) -> usize { self.gamepads.len() } fn spawn_thread( tx: Sender, connected: [bool; MAX_XINPUT_CONTROLLERS], xinput_handle: Arc, ) { std::thread::Builder::new() .name("gilrs".to_owned()) .spawn(move || unsafe { // Issue #70 fix - Maintain a prev_state per controller id. Otherwise the loop will compare the prev_state of a different controller. let mut prev_states: [XState; MAX_XINPUT_CONTROLLERS] = [mem::zeroed::(); MAX_XINPUT_CONTROLLERS]; let mut connected = connected; let mut counter = 0; loop { for id in 0..MAX_XINPUT_CONTROLLERS { if *connected.get_unchecked(id) || counter % ITERATIONS_TO_CHECK_IF_CONNECTED == 0 { match xinput_handle.get_state(id as u32) { Ok(XInputState { raw: state }) => { if !connected[id] { connected[id] = true; let _ = tx.send(Event::new(id, EventType::Connected)); } if state.dwPacketNumber != prev_states[id].dwPacketNumber { Self::compare_state( id, &state.Gamepad, &prev_states[id].Gamepad, &tx, ); prev_states[id] = state; } } Err(XInputUsageError::DeviceNotConnected) if connected[id] => { connected[id] = false; let _ = tx.send(Event::new(id, EventType::Disconnected)); } Err(XInputUsageError::DeviceNotConnected) => (), Err(e) => error!("Failed to get gamepad state: {:?}", e), } } } counter = counter.wrapping_add(1); thread::sleep(Duration::from_millis(EVENT_THREAD_SLEEP_TIME)); } }) .expect("failed to spawn thread"); } fn compare_state(id: usize, g: &XGamepad, pg: &XGamepad, tx: &Sender) { if g.bLeftTrigger != pg.bLeftTrigger { let _ = tx.send(Event::new( id, EventType::AxisValueChanged( g.bLeftTrigger as i32, crate::native_ev_codes::AXIS_LT2, ), )); } if g.bRightTrigger != pg.bRightTrigger { let _ = tx.send(Event::new( id, EventType::AxisValueChanged( g.bRightTrigger as i32, crate::native_ev_codes::AXIS_RT2, ), )); } if g.sThumbLX != pg.sThumbLX { let _ = tx.send(Event::new( id, EventType::AxisValueChanged( g.sThumbLX as i32, crate::native_ev_codes::AXIS_LSTICKX, ), )); } if g.sThumbLY != pg.sThumbLY { let _ = tx.send(Event::new( id, EventType::AxisValueChanged( g.sThumbLY as i32, crate::native_ev_codes::AXIS_LSTICKY, ), )); } if g.sThumbRX != pg.sThumbRX { let _ = tx.send(Event::new( id, EventType::AxisValueChanged( g.sThumbRX as i32, crate::native_ev_codes::AXIS_RSTICKX, ), )); } if g.sThumbRY != pg.sThumbRY { let _ = tx.send(Event::new( id, EventType::AxisValueChanged( g.sThumbRY as i32, crate::native_ev_codes::AXIS_RSTICKY, ), )); } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_DPAD_UP) { let _ = match g.wButtons & XINPUT_GAMEPAD_DPAD_UP != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_DPAD_UP), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_DPAD_UP), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_DPAD_DOWN) { let _ = match g.wButtons & XINPUT_GAMEPAD_DPAD_DOWN != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_DPAD_DOWN), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_DPAD_DOWN), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_DPAD_LEFT) { let _ = match g.wButtons & XINPUT_GAMEPAD_DPAD_LEFT != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_DPAD_LEFT), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_DPAD_LEFT), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_DPAD_RIGHT) { let _ = match g.wButtons & XINPUT_GAMEPAD_DPAD_RIGHT != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_DPAD_RIGHT), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_DPAD_RIGHT), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_START) { let _ = match g.wButtons & XINPUT_GAMEPAD_START != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_START), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_START), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_BACK) { let _ = match g.wButtons & XINPUT_GAMEPAD_BACK != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_SELECT), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_SELECT), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_LEFT_THUMB) { let _ = match g.wButtons & XINPUT_GAMEPAD_LEFT_THUMB != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_LTHUMB), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_LTHUMB), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_RIGHT_THUMB) { let _ = match g.wButtons & XINPUT_GAMEPAD_RIGHT_THUMB != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_RTHUMB), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_RTHUMB), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_LEFT_SHOULDER) { let _ = match g.wButtons & XINPUT_GAMEPAD_LEFT_SHOULDER != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_LT), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_LT), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_RIGHT_SHOULDER) { let _ = match g.wButtons & XINPUT_GAMEPAD_RIGHT_SHOULDER != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_RT), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_RT), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_A) { let _ = match g.wButtons & XINPUT_GAMEPAD_A != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_SOUTH), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_SOUTH), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_B) { let _ = match g.wButtons & XINPUT_GAMEPAD_B != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_EAST), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_EAST), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_X) { let _ = match g.wButtons & XINPUT_GAMEPAD_X != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_WEST), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_WEST), )), }; } if !is_mask_eq(g.wButtons, pg.wButtons, XINPUT_GAMEPAD_Y) { let _ = match g.wButtons & XINPUT_GAMEPAD_Y != 0 { true => tx.send(Event::new( id, EventType::ButtonPressed(crate::native_ev_codes::BTN_NORTH), )), false => tx.send(Event::new( id, EventType::ButtonReleased(crate::native_ev_codes::BTN_NORTH), )), }; } } } #[derive(Debug)] pub struct Gamepad { uuid: Uuid, id: u32, is_connected: bool, xinput_handle: Arc, } impl Gamepad { fn new(id: u32, xinput_handle: Arc) -> Gamepad { let is_connected = xinput_handle.get_state(id).is_ok(); Gamepad { uuid: Uuid::nil(), id, is_connected, xinput_handle, } } pub fn name(&self) -> &str { "Xbox Controller" } pub fn uuid(&self) -> Uuid { self.uuid } pub fn vendor_id(&self) -> Option { None } pub fn product_id(&self) -> Option { None } pub fn is_connected(&self) -> bool { self.is_connected } pub fn power_info(&self) -> PowerInfo { match self.xinput_handle.get_gamepad_battery_information(self.id) { Ok(binfo) => match binfo.battery_type { BatteryType::WIRED => PowerInfo::Wired, BatteryType::ALKALINE | BatteryType::NIMH => { let lvl = match binfo.battery_level { BatteryLevel::EMPTY => 0, BatteryLevel::LOW => 33, BatteryLevel::MEDIUM => 67, BatteryLevel::FULL => 100, lvl => { trace!("Unexpected battery level: {}", lvl.0); 100 } }; if lvl == 100 { PowerInfo::Charged } else { PowerInfo::Discharging(lvl) } } _ => PowerInfo::Unknown, }, Err(e) => { debug!("Failed to get battery info: {:?}", e); PowerInfo::Unknown } } } pub fn is_ff_supported(&self) -> bool { true } pub fn ff_device(&self) -> Option { Some(FfDevice::new(self.id, self.xinput_handle.clone())) } pub fn buttons(&self) -> &[EvCode] { &native_ev_codes::BUTTONS } pub fn axes(&self) -> &[EvCode] { &native_ev_codes::AXES } pub(crate) fn axis_info(&self, nec: EvCode) -> Option<&AxisInfo> { native_ev_codes::AXES_INFO .get(nec.0 as usize) .and_then(|o| o.as_ref()) } } #[inline(always)] fn is_mask_eq(l: u16, r: u16, mask: u16) -> bool { (l & mask != 0) == (r & mask != 0) } #[cfg(feature = "serde-serialize")] use serde::{Deserialize, Serialize}; #[cfg_attr(feature = "serde-serialize", derive(Serialize, Deserialize))] #[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)] pub struct EvCode(u8); impl EvCode { pub fn into_u32(self) -> u32 { self.0 as u32 } } impl Display for EvCode { fn fmt(&self, f: &mut Formatter) -> FmtResult { self.0.fmt(f) } } #[derive(Debug)] enum Error { FailedToLoadDll(XInputLoadingFailure), } impl StdError for Error {} impl Display for Error { fn fmt(&self, f: &mut Formatter) -> FmtResult { match self { Error::FailedToLoadDll(e) => { f.write_fmt(format_args!("Failed to load XInput DLL {:?}", e)) } } } } pub mod native_ev_codes { use winapi::um::xinput::{ XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE, XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE, XINPUT_GAMEPAD_TRIGGER_THRESHOLD, }; use super::EvCode; use crate::AxisInfo; pub const AXIS_LSTICKX: EvCode = EvCode(0); pub const AXIS_LSTICKY: EvCode = EvCode(1); pub const AXIS_LEFTZ: EvCode = EvCode(2); pub const AXIS_RSTICKX: EvCode = EvCode(3); pub const AXIS_RSTICKY: EvCode = EvCode(4); pub const AXIS_RIGHTZ: EvCode = EvCode(5); pub const AXIS_DPADX: EvCode = EvCode(6); pub const AXIS_DPADY: EvCode = EvCode(7); pub const AXIS_RT: EvCode = EvCode(8); pub const AXIS_LT: EvCode = EvCode(9); pub const AXIS_RT2: EvCode = EvCode(10); pub const AXIS_LT2: EvCode = EvCode(11); pub const BTN_SOUTH: EvCode = EvCode(12); pub const BTN_EAST: EvCode = EvCode(13); pub const BTN_C: EvCode = EvCode(14); pub const BTN_NORTH: EvCode = EvCode(15); pub const BTN_WEST: EvCode = EvCode(16); pub const BTN_Z: EvCode = EvCode(17); pub const BTN_LT: EvCode = EvCode(18); pub const BTN_RT: EvCode = EvCode(19); pub const BTN_LT2: EvCode = EvCode(20); pub const BTN_RT2: EvCode = EvCode(21); pub const BTN_SELECT: EvCode = EvCode(22); pub const BTN_START: EvCode = EvCode(23); pub const BTN_MODE: EvCode = EvCode(24); pub const BTN_LTHUMB: EvCode = EvCode(25); pub const BTN_RTHUMB: EvCode = EvCode(26); pub const BTN_DPAD_UP: EvCode = EvCode(27); pub const BTN_DPAD_DOWN: EvCode = EvCode(28); pub const BTN_DPAD_LEFT: EvCode = EvCode(29); pub const BTN_DPAD_RIGHT: EvCode = EvCode(30); pub(super) static BUTTONS: [EvCode; 15] = [ BTN_SOUTH, BTN_EAST, BTN_NORTH, BTN_WEST, BTN_LT, BTN_RT, BTN_SELECT, BTN_START, BTN_MODE, BTN_LTHUMB, BTN_RTHUMB, BTN_DPAD_UP, BTN_DPAD_DOWN, BTN_DPAD_LEFT, BTN_DPAD_RIGHT, ]; pub(super) static AXES: [EvCode; 6] = [ AXIS_LSTICKX, AXIS_LSTICKY, AXIS_RSTICKX, AXIS_RSTICKY, AXIS_RT2, AXIS_LT2, ]; pub(super) static AXES_INFO: [Option; 12] = [ // LeftStickX Some(AxisInfo { min: i16::MIN as i32, max: i16::MAX as i32, deadzone: Some(XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE as u32), }), // LeftStickY Some(AxisInfo { min: i16::MIN as i32, max: i16::MAX as i32, deadzone: Some(XINPUT_GAMEPAD_LEFT_THUMB_DEADZONE as u32), }), // LeftZ None, // RightStickX Some(AxisInfo { min: i16::MIN as i32, max: i16::MAX as i32, deadzone: Some(XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE as u32), }), // RightStickY Some(AxisInfo { min: i16::MIN as i32, max: i16::MAX as i32, deadzone: Some(XINPUT_GAMEPAD_RIGHT_THUMB_DEADZONE as u32), }), // RightZ None, // DPadX None, // DPadY None, // RightTrigger None, // LeftTrigger None, // RightTrigger2 Some(AxisInfo { min: u8::MIN as i32, max: u8::MAX as i32, deadzone: Some(XINPUT_GAMEPAD_TRIGGER_THRESHOLD as u32), }), // LeftTrigger2 Some(AxisInfo { min: u8::MIN as i32, max: u8::MAX as i32, deadzone: Some(XINPUT_GAMEPAD_TRIGGER_THRESHOLD as u32), }), ]; } ================================================ FILE: gilrs-core/src/platform/windows_xinput/mod.rs ================================================ // Copyright 2016-2018 Mateusz Sieczko and other GilRs Developers // // Licensed under the Apache License, Version 2.0, or the MIT license , at your option. This file may not be // copied, modified, or distributed except according to those terms. mod ff; mod gamepad; pub use self::ff::Device as FfDevice; pub use self::gamepad::{native_ev_codes, EvCode, Gamepad, Gilrs}; pub const IS_Y_AXIS_REVERSED: bool = false; ================================================ FILE: gilrs-core/src/utils.rs ================================================ use std::time::SystemTime; /// Returns true if nth bit in array is 1. #[allow(dead_code)] pub(crate) fn test_bit(n: u16, array: &[u8]) -> bool { (array[(n / 8) as usize] >> (n % 8)) & 1 != 0 } #[cfg(not(target_arch = "wasm32"))] pub fn time_now() -> SystemTime { SystemTime::now() } #[cfg(target_arch = "wasm32")] pub fn time_now() -> SystemTime { use js_sys::Date; use std::time::Duration; let offset = Duration::from_millis(Date::now() as u64); SystemTime::UNIX_EPOCH + offset } ================================================ FILE: rustfmt.toml ================================================ unstable_features = true condense_wildcard_suffixes = true error_on_line_overflow = false reorder_imports = true use_try_shorthand = true format_strings = true