From 0729074ce3f284682e2cd6d8dcf4427a53424866 Mon Sep 17 00:00:00 2001 From: Osspial Date: Thu, 20 Jun 2019 15:13:07 -0400 Subject: [PATCH 1/8] Overhaul device events API and add gamepad support on Windows (#804) * Initial implementation * Corrected RAWINPUT buffer sizing * Mostly complete XInput implementation * XInput triggers * Add preliminary CHANGELOG entry. * match unix common API to evl 2.0 * wayland: eventloop2.0 * make EventLoopProxy require T: 'static * Revamp device event API, as well as several misc. fixes on Windows: * When you have multiple windows, you no longer receive duplicate device events * Mouse Device Events now send X-button input * Mouse Device Events now send horizontal scroll wheel input * Add MouseEvent documentation and Device ID debug passthrough * Improve type safety on get_raw_input_data * Remove button_id field from MouseEvent::Button in favor of utton * Remove regex dependency on Windows * Remove axis filtering in XInput * Make gamepads not use lazy_static * Publicly expose gamepad rumble * Unstack DeviceEvent and fix examples/tests * Add HANDLE retrieval method to DeviceExtWindows * Add distinction between non-joystick axes and joystick axes. This helps with properly calculating the deadzone for controller joysticks. One potential issue is that the `Stick` variant isn't used for *all* joysticks, which could be potentially confusing - for example, raw input joysticks will never use the `Stick` variant because we don't understand the semantic meaning of raw input joystick axes. * Add ability to get gamepad port * Fix xinput controller hot swapping * Add functions for enumerating attached devices * Clamp input to [0.0, 1.0] on gamepad rumble * Expose gamepad rumble errors * Add method to check if device is still connected * Add docs * Rename AxisHint and ButtonHint to GamepadAxis and GamepadButton * Add CHANGELOG entry * Update CHANGELOG.md * Add HidId and MovedAbsolute * Fix xinput deprecation warnings * Add ability to retrieve gamepad battery level * Fix weird imports in gamepad example * Update CHANGELOG.md * Resolve francesca64 comments --- CHANGELOG.md | 14 + Cargo.toml | 3 + examples/cursor.rs | 10 +- examples/cursor_grab.rs | 14 +- examples/fullscreen.rs | 12 +- examples/gamepad.rs | 52 +++ examples/gamepad_rumble.rs | 60 +++ examples/handling_close.rs | 12 +- examples/multithreaded.rs | 26 +- examples/multiwindow.rs | 10 +- examples/resizable.rs | 12 +- src/event.rs | 155 ++----- src/event/device.rs | 363 +++++++++++++++ src/lib.rs | 1 + src/platform/windows.rs | 40 +- src/platform_impl/windows/event_loop.rs | 581 +++++++++++++++--------- src/platform_impl/windows/gamepad.rs | 89 ++++ src/platform_impl/windows/mod.rs | 183 ++++++-- src/platform_impl/windows/raw_input.rs | 517 +++++++++++++++++++-- src/platform_impl/windows/util.rs | 10 +- src/platform_impl/windows/window.rs | 13 +- src/platform_impl/windows/xinput.rs | 334 ++++++++++++++ src/util.rs | 29 ++ tests/send_objects.rs | 4 +- 24 files changed, 2073 insertions(+), 471 deletions(-) create mode 100644 examples/gamepad.rs create mode 100644 examples/gamepad_rumble.rs create mode 100644 src/event/device.rs create mode 100644 src/platform_impl/windows/gamepad.rs create mode 100644 src/platform_impl/windows/xinput.rs create mode 100644 src/util.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 43c609d072..37f3ab4727 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -150,6 +150,20 @@ and `WindowEvent::HoveredFile`. - On Windows, fix initial dimensions of a fullscreen window. - On Windows, Fix transparent borderless windows rendering wrong. +- Improve event API documentation. +- Overhaul device event API: + - **Breaking**: `Event::DeviceEvent` split into `MouseEvent`, `KeyboardEvent`, and `GamepadEvent`. + - **Breaking**: Remove `DeviceEvent::Text` variant. + - **Breaking**: `DeviceId` split into `MouseId`, `KeyboardId`, and `GamepadHandle`. + - **Breaking**: Removed device IDs from `WindowEvent` variants. + - Add `enumerate` function on device ID types to list all attached devices of that type. + - Add `is_connected` function on device ID types check if the specified device is still available. + - **Breaking**: On Windows, rename `DeviceIdExtWindows` to `DeviceExtWindows`. + - Add `handle` function to retrieve the underlying `HANDLE`. +- On Windows, fix duplicate device events getting sent if Winit managed multiple windows. +- On Windows, raw mouse events now report Mouse4 and Mouse5 presses and releases. +- Added gamepad support on Windows via raw input and XInput. + # Version 0.19.1 (2019-04-08) - On Wayland, added a `get_wayland_display` function to `EventsLoopExt`. diff --git a/Cargo.toml b/Cargo.toml index 99586de807..6987a01fc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ features = ["display_link"] [target.'cfg(any(target_os = "ios", target_os = "windows"))'.dependencies] bitflags = "1" +rusty-xinput = "1.0" [target.'cfg(target_os = "windows")'.dependencies.winapi] version = "0.3.6" @@ -58,6 +59,7 @@ features = [ "commctrl", "dwmapi", "errhandlingapi", + "hidpi", "hidusage", "libloaderapi", "objbase", @@ -73,6 +75,7 @@ features = [ "wingdi", "winnt", "winuser", + "xinput", ] [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd"))'.dependencies] diff --git a/examples/cursor.rs b/examples/cursor.rs index d5bd34a980..b8b6dec176 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -15,14 +15,10 @@ fn main() { event_loop.run(move |event, _, control_flow| match event { Event::WindowEvent { event: - WindowEvent::KeyboardInput { - input: - KeyboardInput { - state: ElementState::Pressed, - .. - }, + WindowEvent::KeyboardInput(KeyboardInput { + state: ElementState::Pressed, .. - }, + }), .. } => { println!("Setting cursor to \"{:?}\"", CURSORS[cursor_idx]); diff --git a/examples/cursor_grab.rs b/examples/cursor_grab.rs index 726aba56bd..5564225f12 100644 --- a/examples/cursor_grab.rs +++ b/examples/cursor_grab.rs @@ -17,16 +17,12 @@ fn main() { match event { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::KeyboardInput { - input: - KeyboardInput { - state: ElementState::Released, - virtual_keycode: Some(key), - modifiers, - .. - }, + WindowEvent::KeyboardInput(KeyboardInput { + state: ElementState::Released, + virtual_keycode: Some(key), + modifiers, .. - } => { + }) => { use winit::event::VirtualKeyCode::*; match key { Escape => *control_flow = ControlFlow::Exit, diff --git a/examples/fullscreen.rs b/examples/fullscreen.rs index d4b83bb0f7..ee21ffdd7a 100644 --- a/examples/fullscreen.rs +++ b/examples/fullscreen.rs @@ -35,15 +35,11 @@ fn main() { match event { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(virtual_code), - state, - .. - }, + WindowEvent::KeyboardInput(KeyboardInput { + virtual_keycode: Some(virtual_code), + state, .. - } => match (virtual_code, state) { + }) => match (virtual_code, state) { (VirtualKeyCode::Escape, _) => *control_flow = ControlFlow::Exit, (VirtualKeyCode::F, ElementState::Pressed) => { if window.fullscreen().is_some() { diff --git a/examples/gamepad.rs b/examples/gamepad.rs new file mode 100644 index 0000000000..42023a81f9 --- /dev/null +++ b/examples/gamepad.rs @@ -0,0 +1,52 @@ +use winit::{ + event::{ + device::{GamepadEvent, GamepadHandle}, + Event, WindowEvent, + }, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +fn main() { + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_title("The world's worst video game") + .build(&event_loop) + .unwrap(); + + println!("enumerating gamepads:"); + for gamepad in GamepadHandle::enumerate(&event_loop) { + println!( + " gamepad={:?}\tport={:?}\tbattery level={:?}", + gamepad, + gamepad.port(), + gamepad.battery_level() + ); + } + + let deadzone = 0.12; + + event_loop.run(move |event, _, control_flow| { + match event { + Event::GamepadEvent(gamepad_handle, event) => { + match event { + // Discard any Axis events that has a corresponding Stick event. + GamepadEvent::Axis { stick: true, .. } => (), + + // Discard any Stick event that falls inside the stick's deadzone. + GamepadEvent::Stick { + x_value, y_value, .. + } if (x_value.powi(2) + y_value.powi(2)).sqrt() < deadzone => (), + + _ => println!("[{:?}] {:#?}", gamepad_handle, event), + } + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} diff --git a/examples/gamepad_rumble.rs b/examples/gamepad_rumble.rs new file mode 100644 index 0000000000..8ae6ccc8c9 --- /dev/null +++ b/examples/gamepad_rumble.rs @@ -0,0 +1,60 @@ +use std::time::Instant; +use winit::event_loop::EventLoop; + +#[derive(Debug, Clone)] +enum Rumble { + None, + Left, + Right, +} + +fn main() { + let event_loop = EventLoop::new(); + + // You should generally use `GamepadEvent::Added/Removed` to detect gamepads, as doing that will + // allow you to more easily support gamepad hotswapping. However, we're using `enumerate` here + // because it makes this example more concise. + let gamepads = winit::event::device::GamepadHandle::enumerate(&event_loop).collect::>(); + + let rumble_patterns = &[ + (0.5, Rumble::None), + (2.0, Rumble::Left), + (0.5, Rumble::None), + (2.0, Rumble::Right), + ]; + let mut rumble_iter = rumble_patterns.iter().cloned().cycle(); + + let mut active_pattern = rumble_iter.next().unwrap(); + let mut timeout = active_pattern.0; + let mut timeout_start = Instant::now(); + + event_loop.run(move |_, _, _| { + if timeout <= active_pattern.0 { + let t = (timeout / active_pattern.0) * std::f64::consts::PI; + let intensity = t.sin(); + + for g in &gamepads { + let result = match active_pattern.1 { + Rumble::Left => g.rumble(intensity, 0.0), + Rumble::Right => g.rumble(0.0, intensity), + Rumble::None => Ok(()), + }; + + if let Err(e) = result { + println!("Rumble failed: {:?}", e); + } + } + + timeout = (Instant::now() - timeout_start).as_millis() as f64 / 1000.0; + } else { + active_pattern = rumble_iter.next().unwrap(); + println!( + "Rumbling {:?} for {:?} seconds", + active_pattern.1, active_pattern.0 + ); + + timeout = 0.0; + timeout_start = Instant::now(); + } + }); +} diff --git a/examples/handling_close.rs b/examples/handling_close.rs index d67c0046a7..639e2b7482 100644 --- a/examples/handling_close.rs +++ b/examples/handling_close.rs @@ -41,15 +41,11 @@ fn main() { // closing the window. How to close the window is detailed in the handler for // the Y key. } - WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(virtual_code), - state: Released, - .. - }, + WindowEvent::KeyboardInput(KeyboardInput { + virtual_keycode: Some(virtual_code), + state: Released, .. - } => { + }) => { match virtual_code { Y => { if close_requested { diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index 8ce3607799..15e386422f 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -49,16 +49,12 @@ fn main() { ); } } - WindowEvent::KeyboardInput { - input: - KeyboardInput { - state: ElementState::Released, - virtual_keycode: Some(key), - modifiers, - .. - }, + WindowEvent::KeyboardInput(KeyboardInput { + state: ElementState::Released, + virtual_keycode: Some(key), + modifiers, .. - } => { + }) => { window.set_title(&format!("{:?}", key)); let state = !modifiers.shift; use VirtualKeyCode::*; @@ -148,15 +144,11 @@ fn main() { Event::WindowEvent { event, window_id } => match event { WindowEvent::CloseRequested | WindowEvent::Destroyed - | WindowEvent::KeyboardInput { - input: - KeyboardInput { - state: ElementState::Released, - virtual_keycode: Some(VirtualKeyCode::Escape), - .. - }, + | WindowEvent::KeyboardInput(KeyboardInput { + state: ElementState::Released, + virtual_keycode: Some(VirtualKeyCode::Escape), .. - } => { + }) => { window_senders.remove(&window_id); } _ => { diff --git a/examples/multiwindow.rs b/examples/multiwindow.rs index 61dc1fd7ec..de00a49a87 100644 --- a/examples/multiwindow.rs +++ b/examples/multiwindow.rs @@ -29,14 +29,10 @@ fn main() { *control_flow = ControlFlow::Exit; } } - WindowEvent::KeyboardInput { - input: - KeyboardInput { - state: ElementState::Pressed, - .. - }, + WindowEvent::KeyboardInput(KeyboardInput { + state: ElementState::Pressed, .. - } => { + }) => { let window = Window::new(&event_loop).unwrap(); windows.insert(window.id(), window); } diff --git a/examples/resizable.rs b/examples/resizable.rs index 8aa6f70631..f17d177df2 100644 --- a/examples/resizable.rs +++ b/examples/resizable.rs @@ -21,15 +21,11 @@ fn main() { match event { Event::WindowEvent { event, .. } => match event { WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit, - WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(VirtualKeyCode::Space), - state: ElementState::Released, - .. - }, + WindowEvent::KeyboardInput(KeyboardInput { + virtual_keycode: Some(VirtualKeyCode::Space), + state: ElementState::Released, .. - } => { + }) => { resizable = !resizable; println!("Resizable: {}", resizable); window.set_resizable(resizable); diff --git a/src/event.rs b/src/event.rs index ff94aa84e4..bd39b95ba2 100644 --- a/src/event.rs +++ b/src/event.rs @@ -9,11 +9,12 @@ use std::path::PathBuf; use crate::{ dpi::{LogicalPosition, LogicalSize}, - platform_impl, window::WindowId, }; -/// Describes a generic event. +pub mod device; + +/// A generic event. #[derive(Clone, Debug, PartialEq)] pub enum Event { /// Emitted when the OS sends an event to a winit window. @@ -21,12 +22,16 @@ pub enum Event { window_id: WindowId, event: WindowEvent, }, - /// Emitted when the OS sends an event to a device. - DeviceEvent { - device_id: DeviceId, - event: DeviceEvent, - }, - /// Emitted when an event is sent from [`EventLoopProxy::send_event`](crate::event_loop::EventLoopProxy::send_event) + + /// Emitted when a mouse device has generated input. + MouseEvent(device::MouseId, device::MouseEvent), + /// Emitted when a keyboard device has generated input. + KeyboardEvent(device::KeyboardId, device::KeyboardEvent), + HidEvent(device::HidId, device::HidEvent), + /// Emitted when a gamepad/joystick device has generated input. + GamepadEvent(device::GamepadHandle, device::GamepadEvent), + + /// Emitted when an event is sent from [`EventLoopProxy::send_event`](../event_loop/struct.EventLoopProxy.html#method.send_event) UserEvent(T), /// Emitted when new events arrive from the OS to be processed. NewEvents(StartCause), @@ -51,7 +56,10 @@ impl Event { match self { UserEvent(_) => Err(self), WindowEvent { window_id, event } => Ok(WindowEvent { window_id, event }), - DeviceEvent { device_id, event } => Ok(DeviceEvent { device_id, event }), + MouseEvent(id, event) => Ok(MouseEvent(id, event)), + KeyboardEvent(id, event) => Ok(KeyboardEvent(id, event)), + HidEvent(id, event) => Ok(HidEvent(id, event)), + GamepadEvent(id, event) => Ok(GamepadEvent(id, event)), NewEvents(cause) => Ok(NewEvents(cause)), EventsCleared => Ok(EventsCleared), LoopDestroyed => Ok(LoopDestroyed), @@ -61,8 +69,8 @@ impl Event { } } -/// Describes the reason the event loop is resuming. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +/// The reason the event loop is resuming. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum StartCause { /// Sent if the time specified by `ControlFlow::WaitUntil` has been reached. Contains the /// moment the timeout was requested and the requested resume time. The actual resume time is @@ -87,7 +95,7 @@ pub enum StartCause { Init, } -/// Describes an event from a `Window`. +/// An event from a `Window`. #[derive(Clone, Debug, PartialEq)] pub enum WindowEvent { /// The size of the window has changed. Contains the client area's new dimensions. @@ -129,15 +137,10 @@ pub enum WindowEvent { Focused(bool), /// An event from the keyboard has been received. - KeyboardInput { - device_id: DeviceId, - input: KeyboardInput, - }, + KeyboardInput(KeyboardInput), /// The cursor has moved on the window. CursorMoved { - device_id: DeviceId, - /// (x,y) coords in pixels relative to the top-left corner of the window. Because the range of this data is /// limited by the display area and it may have been transformed by the OS to implement effects such as cursor /// acceleration, it should not be used to implement non-cursor-like interactions such as 3D camera control. @@ -146,14 +149,13 @@ pub enum WindowEvent { }, /// The cursor has entered the window. - CursorEntered { device_id: DeviceId }, + CursorEntered, /// The cursor has left the window. - CursorLeft { device_id: DeviceId }, + CursorLeft, /// A mouse wheel movement or touchpad scroll occurred. MouseWheel { - device_id: DeviceId, delta: MouseScrollDelta, phase: TouchPhase, modifiers: ModifiersState, @@ -161,7 +163,6 @@ pub enum WindowEvent { /// An mouse button press has been received. MouseInput { - device_id: DeviceId, state: ElementState, button: MouseButton, modifiers: ModifiersState, @@ -172,18 +173,7 @@ pub enum WindowEvent { /// At the moment, only supported on Apple forcetouch-capable macbooks. /// The parameters are: pressure level (value between 0 and 1 representing how hard the touchpad /// is being pressed) and stage (integer representing the click level). - TouchpadPressure { - device_id: DeviceId, - pressure: f32, - stage: i64, - }, - - /// Motion on some analog axis. May report data redundant to other, more specific events. - AxisMotion { - device_id: DeviceId, - axis: AxisId, - value: f64, - }, + TouchpadPressure { pressure: f32, stage: i64 }, /// The OS or application has requested that the window be redrawn. RedrawRequested, @@ -203,80 +193,7 @@ pub enum WindowEvent { HiDpiFactorChanged(f64), } -/// Identifier of an input device. -/// -/// Whenever you receive an event arising from a particular input device, this event contains a `DeviceId` which -/// identifies its origin. Note that devices may be virtual (representing an on-screen cursor and keyboard focus) or -/// physical. Virtual devices typically aggregate inputs from multiple physical devices. -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DeviceId(pub(crate) platform_impl::DeviceId); - -impl DeviceId { - /// Returns a dummy `DeviceId`, useful for unit testing. The only guarantee made about the return - /// value of this function is that it will always be equal to itself and to future values returned - /// by this function. No other guarantees are made. This may be equal to a real `DeviceId`. - /// - /// **Passing this into a winit function will result in undefined behavior.** - pub unsafe fn dummy() -> Self { - DeviceId(platform_impl::DeviceId::dummy()) - } -} - -/// Represents raw hardware events that are not associated with any particular window. -/// -/// Useful for interactions that diverge significantly from a conventional 2D GUI, such as 3D camera or first-person -/// game controls. Many physical actions, such as mouse movement, can produce both device and window events. Because -/// window events typically arise from virtual devices (corresponding to GUI cursors and keyboard focus) the device IDs -/// may not match. -/// -/// Note that these events are delivered regardless of input focus. -#[derive(Clone, Debug, PartialEq)] -pub enum DeviceEvent { - Added, - Removed, - - /// Change in physical position of a pointing device. - /// - /// This represents raw, unfiltered physical motion. Not to be confused with `WindowEvent::CursorMoved`. - MouseMotion { - /// (x, y) change in position in unspecified units. - /// - /// Different devices may use different units. - delta: (f64, f64), - }, - - /// Physical scroll event - MouseWheel { - delta: MouseScrollDelta, - }, - - /// Motion on some analog axis. This event will be reported for all arbitrary input devices - /// that winit supports on this platform, including mouse devices. If the device is a mouse - /// device then this will be reported alongside the MouseMotion event. - Motion { - axis: AxisId, - value: f64, - }, - - Button { - button: ButtonId, - state: ElementState, - }, - - Key(KeyboardInput), - - /// Keyboard modifiers have changed - #[doc(hidden)] - ModifiersChanged { - modifiers: ModifiersState, - }, - - Text { - codepoint: char, - }, -} - -/// Describes a keyboard input event. +/// A keyboard input event. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct KeyboardInput { @@ -302,13 +219,16 @@ pub struct KeyboardInput { pub modifiers: ModifiersState, } -/// Describes touch-screen input state. +/// Touch input state. #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TouchPhase { Started, Moved, Ended, + /// The touch has been cancelled by the OS. + /// + /// This can occur in a variety of situations, such as the window losing focus. Cancelled, } @@ -330,7 +250,6 @@ pub enum TouchPhase { /// device against their face. #[derive(Debug, Clone, Copy, PartialEq)] pub struct Touch { - pub device_id: DeviceId, pub phase: TouchPhase, pub location: LogicalPosition, /// Describes how hard the screen was pressed. May be `None` if the platform @@ -341,6 +260,8 @@ pub struct Touch { /// - Only available on **iOS** 9.0+ and **Windows** 8+. pub force: Option, /// Unique identifier of a finger. + /// + /// This may get reused by the system after the touch ends. pub id: u64, } @@ -409,16 +330,16 @@ pub type AxisId = u32; /// Identifier for a specific button on some device. pub type ButtonId = u32; -/// Describes the input state of a key. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +/// The input state of a key or button. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum ElementState { Pressed, Released, } -/// Describes a button of a mouse controller. -#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] +/// A button on a mouse. +#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum MouseButton { Left, @@ -427,7 +348,7 @@ pub enum MouseButton { Other(u8), } -/// Describes a difference in the mouse scroll wheel state. +/// A difference in the mouse scroll wheel state. #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum MouseScrollDelta { @@ -446,7 +367,7 @@ pub enum MouseScrollDelta { PixelDelta(LogicalPosition), } -/// Symbolic name for a keyboard key. +/// Symbolic name of a keyboard key. #[derive(Debug, Hash, Ord, PartialOrd, PartialEq, Eq, Clone, Copy)] #[repr(u32)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] @@ -645,7 +566,7 @@ pub enum VirtualKeyCode { Cut, } -/// Represents the current state of the keyboard modifiers +/// The current state of the keyboard modifiers /// /// Each field of this struct represents a modifier and is `true` if this modifier is active. #[derive(Default, Debug, Hash, PartialEq, Eq, Clone, Copy)] diff --git a/src/event/device.rs b/src/event/device.rs new file mode 100644 index 0000000000..0ac6abc1da --- /dev/null +++ b/src/event/device.rs @@ -0,0 +1,363 @@ +//! Raw hardware events that are not associated with any particular window. +//! +//! Useful for interactions that diverge significantly from a conventional 2D GUI, such as 3D camera or first-person +//! game controls. Many physical actions, such as mouse movement, can produce both device and window events. Because +//! window events typically arise from virtual devices (corresponding to GUI cursors and keyboard focus) the device IDs +//! may not match. +//! +//! All attached devices are guaranteed to emit an `Added` event upon the initialization of the event loop. +//! +//! Note that device events are always delivered regardless of window focus. + +use crate::{ + dpi::PhysicalPosition, + event::{AxisId, ButtonId, ElementState, KeyboardInput, MouseButton, ModifiersState}, + event_loop::EventLoop, + platform_impl, +}; +use std::{fmt, io}; + +/// A hint suggesting the type of button that was pressed. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum GamepadButton { + Start, + Select, + + /// The north face button. + /// + /// * Nintendo: X + /// * Playstation: Triangle + /// * XBox: Y + North, + /// The south face button. + /// + /// * Nintendo: B + /// * Playstation: X + /// * XBox: A + South, + /// The east face button. + /// + /// * Nintendo: A + /// * Playstation: Circle + /// * XBox: B + East, + /// The west face button. + /// + /// * Nintendo: Y + /// * Playstation: Square + /// * XBox: X + West, + + LeftStick, + RightStick, + + LeftTrigger, + RightTrigger, + + LeftShoulder, + RightShoulder, + + DPadUp, + DPadDown, + DPadLeft, + DPadRight, +} + +/// A hint suggesting the type of axis that moved. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum GamepadAxis { + LeftStickX, + LeftStickY, + + RightStickX, + RightStickY, + + LeftTrigger, + RightTrigger, +} + +/// A given joystick's side on the gamepad. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum Side { + Left, + Right, +} + +/// Raw mouse events. +/// +/// See the module-level docs for more information. +#[derive(Debug, Copy, Clone, PartialEq)] +pub enum MouseEvent { + /// A mouse device has been added. + Added, + /// A mouse device has been removed. + Removed, + /// A mouse button has been pressed or released. + Button { + state: ElementState, + button: MouseButton, + }, + /// Relative change in physical position of a pointing device. + /// + /// This represents raw, unfiltered physical motion, NOT the position of the mouse. Accordingly, + /// the values provided here are the change in position of the mouse since the previous + /// `MovedRelative` event. + MovedRelative(f64, f64), + /// Change in absolute position of a pointing device. + /// + /// The `PhysicalPosition` value is the new position of the cursor relative to the desktop. This + /// generally doesn't get output by standard mouse devices, but can get output from tablet devices. + MovedAbsolute(PhysicalPosition), + /// Change in rotation of mouse wheel. + Wheel(f64, f64), +} + +/// Raw keyboard events. +/// +/// See the module-level docs for more information. +#[derive(Debug, Copy, Clone, PartialEq, Hash)] +pub enum KeyboardEvent { + /// A keyboard device has been added. + Added, + /// A keyboard device has been removed. + Removed, + /// A key has been pressed or released. + Input(KeyboardInput), + ModifiersChanged(ModifiersState), +} + +/// Raw HID event. +/// +/// See the module-level docs for more information. +#[derive(Debug, Clone, PartialEq, Hash)] +pub enum HidEvent { + /// A Human Interface Device device has been added. + Added, + /// A Human Interface Device device has been removed. + Removed, + /// A raw data packet has been received from the Human Interface Device. + Data(Box<[u8]>), +} + +/// Gamepad/joystick events. +/// +/// These can be generated by any of a variety of game controllers, including (but not limited to) +/// gamepads, joysicks, and HOTAS devices. +#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] +pub enum GamepadEvent { + /// A gamepad/joystick device has been added. + Added, + /// A gamepad/joystick device has been removed. + Removed, + /// An analog axis value on the gamepad/joystick has changed. + /// + /// Winit does NOT provide [deadzone filtering](https://www.quora.com/What-does-the-term-joystick-deadzone-mean), + /// and such filtering may have to be provided by API users for joystick axes. + Axis { + axis_id: AxisId, + /// A hint regarding the physical axis that moved. + /// + /// On traditional gamepads (such as an X360 controller) this can be assumed to have a + /// non-`None` value; however, other joystick devices with more varied layouts generally won't + /// provide a value here. + /// + /// TODO: DISCUSS CONTROLLER MAPPING ONCE WE FIGURE OUT WHAT WE'RE DOING THERE. + axis: Option, + value: f64, + /// Whether or not this axis has also produced a [`GamepadEvent::Stick`] event. + stick: bool, + }, + /// A two-axis joystick's value has changed. + /// + /// This is mainly provided to assist with deadzone calculation, as proper deadzones should be + /// calculated via the combined distance of each joystick axis from the center of the joystick, + /// rather than per-axis. + /// + /// Note that this is only guaranteed to be emitted for traditionally laid out gamepads. More + /// complex joysticks generally don't report specifics of their layout to the operating system, + /// preventing Winit from automatically aggregating their axis input into two-axis stick events. + Stick { + /// The X axis' ID. + x_id: AxisId, + /// The Y axis' ID. + y_id: AxisId, + x_value: f64, + y_value: f64, + /// Which joystick side produced this event. + side: Side, + }, + Button { + button_id: ButtonId, + /// A hint regarding the location of the button. + /// + /// The caveats on the `Axis.hint` field also apply here. + button: Option, + state: ElementState, + }, +} + +/// Error reported if a rumble attempt unexpectedly failed. +#[derive(Debug)] +pub enum RumbleError { + /// The device is no longer connected. + DeviceNotConnected, + /// An unknown OS error has occured. + OsError(io::Error), +} + +/// A typed identifier for a mouse device. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MouseId(pub(crate) platform_impl::MouseId); +/// A typed identifier for a keyboard device. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct KeyboardId(pub(crate) platform_impl::KeyboardId); +/// A typed if for a Human Interface Device. +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct HidId(pub(crate) platform_impl::HidId); +/// A handle to a gamepad/joystick device. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct GamepadHandle(pub(crate) platform_impl::GamepadHandle); + +impl MouseId { + /// Returns a dummy `MouseId`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `MouseId`. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + MouseId(platform_impl::MouseId::dummy()) + } + + /// Enumerate all attached mouse devices. + pub fn enumerate(event_loop: &EventLoop) -> impl '_ + Iterator { + platform_impl::MouseId::enumerate(&event_loop.event_loop) + } + + /// Check to see if this mouse device is still connected. + pub fn is_connected(&self) -> bool { + self.0.is_connected() + } +} + +impl KeyboardId { + /// Returns a dummy `KeyboardId`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `KeyboardId`. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + KeyboardId(platform_impl::KeyboardId::dummy()) + } + + /// Enumerate all attached keyboard devices. + pub fn enumerate(event_loop: &EventLoop) -> impl '_ + Iterator { + platform_impl::KeyboardId::enumerate(&event_loop.event_loop) + } + + /// Check to see if this keyboard device is still connected. + pub fn is_connected(&self) -> bool { + self.0.is_connected() + } +} + +impl HidId { + /// Returns a dummy `HidId`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `HidId`. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + HidId(platform_impl::HidId::dummy()) + } + + /// Enumerate all attached keyboard devices. + pub fn enumerate(event_loop: &EventLoop) -> impl '_ + Iterator { + platform_impl::HidId::enumerate(&event_loop.event_loop) + } + + /// Check to see if this keyboard device is still connected. + pub fn is_connected(&self) -> bool { + self.0.is_connected() + } +} + +impl GamepadHandle { + /// Returns a dummy `GamepadHandle`, useful for unit testing. The only guarantee made about the return + /// value of this function is that it will always be equal to itself and to future values returned + /// by this function. No other guarantees are made. This may be equal to a real `GamepadHandle`. + /// + /// **Passing this into a winit function will result in undefined behavior.** + pub unsafe fn dummy() -> Self { + GamepadHandle(platform_impl::GamepadHandle::dummy()) + } + + /// Enumerate all attached gamepad/joystick devices. + pub fn enumerate(event_loop: &EventLoop) -> impl '_ + Iterator { + platform_impl::GamepadHandle::enumerate(&event_loop.event_loop) + } + + /// Check to see if this gamepad/joystick device is still connected. + pub fn is_connected(&self) -> bool { + self.0.is_connected() + } + + /// Attempts to set the rumble values for an attached controller. Input values are automatically + /// bound to a [`0.0`, `1.0`] range. + /// + /// Certain gamepads assign different usages to the left and right motors - for example, X360 + /// controllers treat the left motor as a low-frequency rumble and the right motor as a + /// high-frequency rumble. However, this cannot necessarily be assumed for all gamepad devices. + /// + /// Note that, if the given gamepad does not support rumble, no error value gets thrown. + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), RumbleError> { + self.0.rumble(left_speed, right_speed) + } + + /// Gets the port number assigned to the gamepad. + pub fn port(&self) -> Option { + self.0.port() + } + + /// Gets the controller's battery level. + /// + /// If the controller doesn't report a battery level, this returns `None`. + pub fn battery_level(&self) -> Option { + self.0.battery_level() + } +} + +/// TODO: IS THIS THE RIGHT ABSTRACTION FOR ALL PLATFORMS? +/// This is exposed in its current form because it's what Microsoft does for XInput, and that's my +/// (@Osspial's) main point of reference. If you're implementing this on a different platform and +/// that platform exposes battery level differently, please bring it up in the tracking issue! +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum BatteryLevel { + Empty, + Low, + Medium, + Full, +} + +impl fmt::Debug for MouseId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(f) + } +} + +impl fmt::Debug for KeyboardId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(f) + } +} + +impl fmt::Debug for HidId { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(f) + } +} + +impl fmt::Debug for GamepadHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + self.0.fmt(f) + } +} diff --git a/src/lib.rs b/src/lib.rs index 92b35b410c..7ac09a5949 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -138,6 +138,7 @@ pub mod event_loop; mod icon; pub mod monitor; mod platform_impl; +mod util; pub mod window; pub mod platform; diff --git a/src/platform/windows.rs b/src/platform/windows.rs index 3ab9b1dde6..12105af3a8 100644 --- a/src/platform/windows.rs +++ b/src/platform/windows.rs @@ -6,7 +6,7 @@ use libc; use winapi::shared::windef::HWND; use crate::{ - event::DeviceId, + event::device::{GamepadHandle, KeyboardId, MouseId}, event_loop::EventLoop, monitor::MonitorHandle, platform_impl::EventLoop as WindowsEventLoop, @@ -149,17 +149,49 @@ impl MonitorHandleExtWindows for MonitorHandle { } } -/// Additional methods on `DeviceId` that are specific to Windows. -pub trait DeviceIdExtWindows { +/// Additional methods on device types that are specific to Windows. +pub trait DeviceExtWindows { /// Returns an identifier that persistently refers to this specific device. /// /// Will return `None` if the device is no longer available. fn persistent_identifier(&self) -> Option; + + /// Returns the handle of the device - `HANDLE`. + fn handle(&self) -> *mut c_void; +} + +impl DeviceExtWindows for MouseId { + #[inline] + fn persistent_identifier(&self) -> Option { + self.0.persistent_identifier() + } + + #[inline] + fn handle(&self) -> *mut c_void { + self.0.handle() as _ + } +} + +impl DeviceExtWindows for KeyboardId { + #[inline] + fn persistent_identifier(&self) -> Option { + self.0.persistent_identifier() + } + + #[inline] + fn handle(&self) -> *mut c_void { + self.0.handle() as _ + } } -impl DeviceIdExtWindows for DeviceId { +impl DeviceExtWindows for GamepadHandle { #[inline] fn persistent_identifier(&self) -> Option { self.0.persistent_identifier() } + + #[inline] + fn handle(&self) -> *mut c_void { + self.0.handle() as _ + } } diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 0968db83a1..48ebf09573 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -17,7 +17,7 @@ use parking_lot::Mutex; use std::{ any::Any, cell::RefCell, - collections::VecDeque, + collections::{HashMap, VecDeque}, marker::PhantomData, mem, panic, ptr, rc::Rc, @@ -45,8 +45,11 @@ use winapi::{ }; use crate::{ - dpi::{LogicalPosition, LogicalSize, PhysicalSize}, - event::{DeviceEvent, Event, Force, KeyboardInput, StartCause, Touch, TouchPhase, WindowEvent}, + dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}, + event::{ + device::{GamepadEvent, HidEvent, KeyboardEvent, MouseEvent}, + Event, Force, KeyboardInput, MouseButton, StartCause, Touch, TouchPhase, WindowEvent, + }, event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}, platform_impl::platform::{ dpi::{ @@ -55,11 +58,12 @@ use crate::{ drop_handler::FileDropHandler, event::{self, handle_extended_keys, process_key_params, vkey_to_winit_vkey}, monitor, - raw_input::{get_raw_input_data, get_raw_mouse_button_state}, + gamepad::Gamepad, + raw_input::{self, get_raw_input_data, get_raw_mouse_button_state, RawInputData}, util, window::adjust_size, window_state::{CursorFlags, WindowFlags, WindowState}, - wrap_device_id, WindowId, DEVICE_ID, + GamepadHandle, HidId, KeyboardId, MouseId, WindowId, }, window::{Fullscreen, WindowId as RootWindowId}, }; @@ -99,24 +103,32 @@ lazy_static! { pub(crate) struct SubclassInput { pub window_state: Arc>, - pub event_loop_runner: EventLoopRunnerShared, + pub shared_data: Rc>, pub file_drop_handler: FileDropHandler, } impl SubclassInput { unsafe fn send_event(&self, event: Event) { - self.event_loop_runner.send_event(event); + self.shared_data.runner_shared.send_event(event); } } struct ThreadMsgTargetSubclassInput { - event_loop_runner: EventLoopRunnerShared, + shared_data: Rc>, user_event_receiver: Receiver, } +#[derive(Debug)] +pub(crate) enum DeviceId { + Mouse(MouseId), + Keyboard(KeyboardId), + Hid(HidId), + Gamepad(GamepadHandle, Gamepad), +} + impl ThreadMsgTargetSubclassInput { unsafe fn send_event(&self, event: Event) { - self.event_loop_runner.send_event(event); + self.shared_data.runner_shared.send_event(event); } } @@ -129,7 +141,7 @@ pub struct EventLoopWindowTarget { thread_id: DWORD, trigger_newevents_on_redraw: Arc, thread_msg_target: HWND, - pub(crate) runner_shared: EventLoopRunnerShared, + pub(crate) shared_data: Rc>, } macro_rules! main_thread_check { @@ -167,12 +179,15 @@ impl EventLoop { pub fn new_dpi_unaware_any_thread() -> EventLoop { let thread_id = unsafe { processthreadsapi::GetCurrentThreadId() }; - let runner_shared = Rc::new(ELRShared { - runner: RefCell::new(None), - buffer: RefCell::new(VecDeque::new()), + let shared_data = Rc::new(SubclassSharedData { + runner_shared: ELRShared { + runner: RefCell::new(None), + buffer: RefCell::new(VecDeque::new()), + }, + active_device_ids: RefCell::new(HashMap::default()), }); let (thread_msg_target, thread_msg_sender) = - thread_event_target_window(runner_shared.clone()); + thread_event_target_window(shared_data.clone()); EventLoop { thread_msg_sender, @@ -181,7 +196,7 @@ impl EventLoop { thread_id, trigger_newevents_on_redraw: Arc::new(AtomicBool::new(true)), thread_msg_target, - runner_shared, + shared_data, }, _marker: PhantomData, }, @@ -212,10 +227,10 @@ impl EventLoop { }) }; { - let runner_shared = self.window_target.p.runner_shared.clone(); - let mut runner_ref = runner_shared.runner.borrow_mut(); + let shared_data = self.window_target.p.shared_data.clone(); + let mut runner_ref = shared_data.runner_shared.runner.borrow_mut(); loop { - let event = runner_shared.buffer.borrow_mut().pop_front(); + let event = shared_data.runner_shared.buffer.borrow_mut().pop_front(); match event { Some(e) => { runner.process_event(e); @@ -230,6 +245,7 @@ impl EventLoop { () => { self.window_target .p + .shared_data .runner_shared .runner .borrow_mut() @@ -280,7 +296,13 @@ impl EventLoop { } runner!().call_event_handler(Event::LoopDestroyed); - *self.window_target.p.runner_shared.runner.borrow_mut() = None; + *self + .window_target + .p + .shared_data + .runner_shared + .runner + .borrow_mut() = None; } pub fn create_proxy(&self) -> EventLoopProxy { @@ -289,6 +311,70 @@ impl EventLoop { event_send: self.thread_msg_sender.clone(), } } + + fn devices( + &self, + f: impl FnMut(&DeviceId) -> Option, + ) -> impl '_ + Iterator { + // Flush WM_INPUT and WM_INPUT_DEVICE_CHANGE events so that the active_device_ids list is + // accurate. This is essential to make this function work if called before calling `run` or + // `run_return`. + unsafe { + let mut msg = mem::zeroed(); + loop { + let result = winuser::PeekMessageW( + &mut msg, + self.window_target.p.thread_msg_target, + winuser::WM_INPUT_DEVICE_CHANGE, + winuser::WM_INPUT, + 1, + ); + if 0 == result { + break; + } + winuser::TranslateMessage(&mut msg); + winuser::DispatchMessageW(&mut msg); + } + } + + self.window_target + .p + .shared_data + .active_device_ids + .borrow() + .values() + .filter_map(f) + .collect::>() + .into_iter() + } + + pub fn mouses(&self) -> impl '_ + Iterator { + self.devices(|d| match d { + DeviceId::Mouse(id) => Some(id.clone().into()), + _ => None, + }) + } + + pub fn keyboards(&self) -> impl '_ + Iterator { + self.devices(|d| match d { + DeviceId::Keyboard(id) => Some(id.clone().into()), + _ => None, + }) + } + + pub fn hids(&self) -> impl '_ + Iterator { + self.devices(|d| match d { + DeviceId::Hid(id) => Some(id.clone().into()), + _ => None, + }) + } + + pub fn gamepads(&self) -> impl '_ + Iterator { + self.devices(|d| match d { + DeviceId::Gamepad(handle, _) => Some(handle.clone().into()), + _ => None, + }) + } } impl EventLoopWindowTarget { @@ -317,7 +403,10 @@ fn main_thread_id() -> DWORD { unsafe { MAIN_THREAD_ID } } -pub(crate) type EventLoopRunnerShared = Rc>; +pub(crate) struct SubclassSharedData { + pub runner_shared: ELRShared, + pub active_device_ids: RefCell>, +} pub(crate) struct ELRShared { runner: RefCell>>, buffer: RefCell>>, @@ -825,7 +914,7 @@ lazy_static! { }; } -fn thread_event_target_window(event_loop_runner: EventLoopRunnerShared) -> (HWND, Sender) { +fn thread_event_target_window(shared_data: Rc>) -> (HWND, Sender) { unsafe { let window = winuser::CreateWindowExW( winuser::WS_EX_NOACTIVATE | winuser::WS_EX_TRANSPARENT | winuser::WS_EX_LAYERED, @@ -853,7 +942,7 @@ fn thread_event_target_window(event_loop_runner: EventLoopRunnerShared) -> let (tx, rx) = mpsc::channel(); let subclass_input = ThreadMsgTargetSubclassInput { - event_loop_runner, + shared_data, user_event_receiver: rx, }; let input_ptr = Box::into_raw(Box::new(subclass_input)); @@ -865,6 +954,9 @@ fn thread_event_target_window(event_loop_runner: EventLoopRunnerShared) -> ); assert_eq!(subclass_result, 1); + // Set up raw input + raw_input::register_for_raw_input(window); + (window, tx) } } @@ -926,14 +1018,14 @@ unsafe extern "system" fn public_window_callback( match msg { winuser::WM_ENTERSIZEMOVE => { - let mut runner = subclass_input.event_loop_runner.runner.borrow_mut(); + let mut runner = subclass_input.shared_data.runner_shared.runner.borrow_mut(); if let Some(ref mut runner) = *runner { runner.in_modal_loop = true; } 0 } winuser::WM_EXITSIZEMOVE => { - let mut runner = subclass_input.event_loop_runner.runner.borrow_mut(); + let mut runner = subclass_input.shared_data.runner_shared.runner.borrow_mut(); if let Some(ref mut runner) = *runner { runner.in_modal_loop = false; } @@ -982,7 +1074,7 @@ unsafe extern "system" fn public_window_callback( _ if msg == *REQUEST_REDRAW_NO_NEWEVENTS_MSG_ID => { use crate::event::WindowEvent::RedrawRequested; - let mut runner = subclass_input.event_loop_runner.runner.borrow_mut(); + let mut runner = subclass_input.shared_data.runner_shared.runner.borrow_mut(); subclass_input.window_state.lock().queued_out_of_band_redraw = false; if let Some(ref mut runner) = *runner { // This check makes sure that calls to `request_redraw()` during `EventsCleared` @@ -1171,9 +1263,7 @@ unsafe extern "system" fn public_window_callback( if mouse_was_outside_window { subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: CursorEntered { - device_id: DEVICE_ID, - }, + event: CursorEntered, }); // Calling TrackMouseEvent in order to receive mouse leave events. @@ -1193,7 +1283,6 @@ unsafe extern "system" fn public_window_callback( subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: CursorMoved { - device_id: DEVICE_ID, position, modifiers: event::get_key_mods(), }, @@ -1213,9 +1302,7 @@ unsafe extern "system" fn public_window_callback( subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: CursorLeft { - device_id: DEVICE_ID, - }, + event: CursorLeft, }); 0 @@ -1231,7 +1318,6 @@ unsafe extern "system" fn public_window_callback( subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: WindowEvent::MouseWheel { - device_id: DEVICE_ID, delta: LineDelta(0.0, value), phase: TouchPhase::Moved, modifiers: event::get_key_mods(), @@ -1251,7 +1337,6 @@ unsafe extern "system" fn public_window_callback( subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: WindowEvent::MouseWheel { - device_id: DEVICE_ID, delta: LineDelta(value, 0.0), phase: TouchPhase::Moved, modifiers: event::get_key_mods(), @@ -1269,15 +1354,12 @@ unsafe extern "system" fn public_window_callback( if let Some((scancode, vkey)) = process_key_params(wparam, lparam) { subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: Pressed, - scancode, - virtual_keycode: vkey, - modifiers: event::get_key_mods(), - }, - }, + event: WindowEvent::KeyboardInput(KeyboardInput { + state: Pressed, + scancode, + virtual_keycode: vkey, + modifiers: event::get_key_mods(), + }), }); // Windows doesn't emit a delete character by default, but in order to make it // consistent with the other platforms we'll emit a delete character here. @@ -1297,31 +1379,27 @@ unsafe extern "system" fn public_window_callback( if let Some((scancode, vkey)) = process_key_params(wparam, lparam) { subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), - event: WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: Released, - scancode, - virtual_keycode: vkey, - modifiers: event::get_key_mods(), - }, - }, + event: WindowEvent::KeyboardInput(KeyboardInput { + state: Released, + scancode, + virtual_keycode: vkey, + modifiers: event::get_key_mods(), + }), }); } 0 } winuser::WM_LBUTTONDOWN => { - use crate::event::{ElementState::Pressed, MouseButton::Left, WindowEvent::MouseInput}; + use crate::event::{ElementState::Pressed, WindowEvent::MouseInput}; capture_mouse(window, &mut *subclass_input.window_state.lock()); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Pressed, - button: Left, + button: MouseButton::Left, modifiers: event::get_key_mods(), }, }); @@ -1329,18 +1407,15 @@ unsafe extern "system" fn public_window_callback( } winuser::WM_LBUTTONUP => { - use crate::event::{ - ElementState::Released, MouseButton::Left, WindowEvent::MouseInput, - }; + use crate::event::{ElementState::Released, WindowEvent::MouseInput}; release_mouse(&mut *subclass_input.window_state.lock()); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Released, - button: Left, + button: MouseButton::Left, modifiers: event::get_key_mods(), }, }); @@ -1348,18 +1423,15 @@ unsafe extern "system" fn public_window_callback( } winuser::WM_RBUTTONDOWN => { - use crate::event::{ - ElementState::Pressed, MouseButton::Right, WindowEvent::MouseInput, - }; + use crate::event::{ElementState::Pressed, WindowEvent::MouseInput}; capture_mouse(window, &mut *subclass_input.window_state.lock()); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Pressed, - button: Right, + button: MouseButton::Right, modifiers: event::get_key_mods(), }, }); @@ -1367,18 +1439,15 @@ unsafe extern "system" fn public_window_callback( } winuser::WM_RBUTTONUP => { - use crate::event::{ - ElementState::Released, MouseButton::Right, WindowEvent::MouseInput, - }; + use crate::event::{ElementState::Released, WindowEvent::MouseInput}; release_mouse(&mut *subclass_input.window_state.lock()); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Released, - button: Right, + button: MouseButton::Right, modifiers: event::get_key_mods(), }, }); @@ -1386,18 +1455,15 @@ unsafe extern "system" fn public_window_callback( } winuser::WM_MBUTTONDOWN => { - use crate::event::{ - ElementState::Pressed, MouseButton::Middle, WindowEvent::MouseInput, - }; + use crate::event::{ElementState::Pressed, WindowEvent::MouseInput}; capture_mouse(window, &mut *subclass_input.window_state.lock()); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Pressed, - button: Middle, + button: MouseButton::Middle, modifiers: event::get_key_mods(), }, }); @@ -1405,18 +1471,15 @@ unsafe extern "system" fn public_window_callback( } winuser::WM_MBUTTONUP => { - use crate::event::{ - ElementState::Released, MouseButton::Middle, WindowEvent::MouseInput, - }; + use crate::event::{ElementState::Released, WindowEvent::MouseInput}; release_mouse(&mut *subclass_input.window_state.lock()); subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Released, - button: Middle, + button: MouseButton::Middle, modifiers: event::get_key_mods(), }, }); @@ -1424,9 +1487,7 @@ unsafe extern "system" fn public_window_callback( } winuser::WM_XBUTTONDOWN => { - use crate::event::{ - ElementState::Pressed, MouseButton::Other, WindowEvent::MouseInput, - }; + use crate::event::{ElementState::Pressed, WindowEvent::MouseInput}; let xbutton = winuser::GET_XBUTTON_WPARAM(wparam); capture_mouse(window, &mut *subclass_input.window_state.lock()); @@ -1434,9 +1495,8 @@ unsafe extern "system" fn public_window_callback( subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Pressed, - button: Other(xbutton as u8), + button: MouseButton::Other(xbutton as u8), modifiers: event::get_key_mods(), }, }); @@ -1444,9 +1504,7 @@ unsafe extern "system" fn public_window_callback( } winuser::WM_XBUTTONUP => { - use crate::event::{ - ElementState::Released, MouseButton::Other, WindowEvent::MouseInput, - }; + use crate::event::{ElementState::Released, WindowEvent::MouseInput}; let xbutton = winuser::GET_XBUTTON_WPARAM(wparam); release_mouse(&mut *subclass_input.window_state.lock()); @@ -1454,129 +1512,14 @@ unsafe extern "system" fn public_window_callback( subclass_input.send_event(Event::WindowEvent { window_id: RootWindowId(WindowId(window)), event: MouseInput { - device_id: DEVICE_ID, state: Released, - button: Other(xbutton as u8), + button: MouseButton::Other(xbutton as u8), modifiers: event::get_key_mods(), }, }); 0 } - winuser::WM_INPUT_DEVICE_CHANGE => { - let event = match wparam as _ { - winuser::GIDC_ARRIVAL => DeviceEvent::Added, - winuser::GIDC_REMOVAL => DeviceEvent::Removed, - _ => unreachable!(), - }; - - subclass_input.send_event(Event::DeviceEvent { - device_id: wrap_device_id(lparam as _), - event, - }); - - 0 - } - - winuser::WM_INPUT => { - use crate::event::{ - DeviceEvent::{Button, Key, Motion, MouseMotion, MouseWheel}, - ElementState::{Pressed, Released}, - MouseScrollDelta::LineDelta, - }; - - if let Some(data) = get_raw_input_data(lparam as _) { - let device_id = wrap_device_id(data.header.hDevice as _); - - if data.header.dwType == winuser::RIM_TYPEMOUSE { - let mouse = data.data.mouse(); - - if util::has_flag(mouse.usFlags, winuser::MOUSE_MOVE_RELATIVE) { - let x = mouse.lLastX as f64; - let y = mouse.lLastY as f64; - - if x != 0.0 { - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Motion { axis: 0, value: x }, - }); - } - - if y != 0.0 { - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Motion { axis: 1, value: y }, - }); - } - - if x != 0.0 || y != 0.0 { - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: MouseMotion { delta: (x, y) }, - }); - } - } - - if util::has_flag(mouse.usButtonFlags, winuser::RI_MOUSE_WHEEL) { - let delta = mouse.usButtonData as SHORT / winuser::WHEEL_DELTA; - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: MouseWheel { - delta: LineDelta(0.0, delta as f32), - }, - }); - } - - let button_state = get_raw_mouse_button_state(mouse.usButtonFlags); - // Left, middle, and right, respectively. - for (index, state) in button_state.iter().enumerate() { - if let Some(state) = *state { - // This gives us consistency with X11, since there doesn't - // seem to be anything else reasonable to do for a mouse - // button ID. - let button = (index + 1) as _; - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Button { button, state }, - }); - } - } - } else if data.header.dwType == winuser::RIM_TYPEKEYBOARD { - let keyboard = data.data.keyboard(); - - let pressed = keyboard.Message == winuser::WM_KEYDOWN - || keyboard.Message == winuser::WM_SYSKEYDOWN; - let released = keyboard.Message == winuser::WM_KEYUP - || keyboard.Message == winuser::WM_SYSKEYUP; - - if pressed || released { - let state = if pressed { Pressed } else { Released }; - - let scancode = keyboard.MakeCode as _; - let extended = util::has_flag(keyboard.Flags, winuser::RI_KEY_E0 as _) - | util::has_flag(keyboard.Flags, winuser::RI_KEY_E1 as _); - if let Some((vkey, scancode)) = - handle_extended_keys(keyboard.VKey as _, scancode, extended) - { - let virtual_keycode = vkey_to_winit_vkey(vkey); - - subclass_input.send_event(Event::DeviceEvent { - device_id, - event: Key(KeyboardInput { - scancode, - state, - virtual_keycode, - modifiers: event::get_key_mods(), - }), - }); - } - } - } - } - - commctrl::DefSubclassProc(window, msg, wparam, lparam) - } - winuser::WM_TOUCH => { let pcount = LOWORD(wparam as DWORD) as usize; let mut inputs = Vec::with_capacity(pcount); @@ -1618,7 +1561,6 @@ unsafe extern "system" fn public_window_callback( location, force: None, // WM_TOUCH doesn't support pressure information id: input.dwID as u64, - device_id: DEVICE_ID, }), }); } @@ -1758,7 +1700,6 @@ unsafe extern "system" fn public_window_callback( location, force, id: pointer_info.pointerId as u64, - device_id: DEVICE_ID, }), }); } @@ -1972,7 +1913,7 @@ unsafe extern "system" fn thread_event_target_callback( ); }; let in_modal_loop = { - let runner = subclass_input.event_loop_runner.runner.borrow_mut(); + let runner = subclass_input.shared_data.runner_shared.runner.borrow_mut(); if let Some(ref runner) = *runner { runner.in_modal_loop } else { @@ -2006,7 +1947,7 @@ unsafe extern "system" fn thread_event_target_callback( } } - let mut runner = subclass_input.event_loop_runner.runner.borrow_mut(); + let mut runner = subclass_input.shared_data.runner_shared.runner.borrow_mut(); if let Some(ref mut runner) = *runner { runner.events_cleared(); match runner.control_flow { @@ -2026,6 +1967,230 @@ unsafe extern "system" fn thread_event_target_callback( } 0 } + winuser::WM_INPUT_DEVICE_CHANGE => { + use super::raw_input::RawDeviceInfo; + + let handle = lparam as HANDLE; + + match wparam as _ { + winuser::GIDC_ARRIVAL => { + if let Some(handle_info) = raw_input::get_raw_input_device_info(handle) { + let device: DeviceId; + let event: Event; + + match handle_info { + RawDeviceInfo::Mouse(_) => { + let mouse_id = MouseId(handle); + device = DeviceId::Mouse(mouse_id); + event = Event::MouseEvent(mouse_id.into(), MouseEvent::Added); + } + RawDeviceInfo::Keyboard(_) => { + let keyboard_id = KeyboardId(handle); + device = DeviceId::Keyboard(keyboard_id); + event = + Event::KeyboardEvent(keyboard_id.into(), KeyboardEvent::Added); + } + RawDeviceInfo::Hid(_) => match Gamepad::new(handle) { + Some(gamepad) => { + let gamepad_handle = GamepadHandle { + handle, + shared_data: gamepad.shared_data(), + }; + + device = DeviceId::Gamepad(gamepad_handle.clone(), gamepad); + event = Event::GamepadEvent( + gamepad_handle.into(), + GamepadEvent::Added, + ); + } + None => { + let hid_id = HidId(handle); + device = DeviceId::Hid(hid_id.into()); + event = Event::HidEvent(hid_id.into(), HidEvent::Added); + } + }, + } + + subclass_input + .shared_data + .active_device_ids + .borrow_mut() + .insert(handle, device); + subclass_input.send_event(event); + } + } + winuser::GIDC_REMOVAL => { + let removed_device = subclass_input + .shared_data + .active_device_ids + .borrow_mut() + .remove(&handle); + if let Some(device_id) = removed_device { + let event = match device_id { + DeviceId::Mouse(mouse_id) => { + Event::MouseEvent(mouse_id.into(), MouseEvent::Removed) + } + DeviceId::Keyboard(keyboard_id) => { + Event::KeyboardEvent(keyboard_id.into(), KeyboardEvent::Removed) + } + DeviceId::Hid(hid_id) => { + Event::HidEvent(hid_id.into(), HidEvent::Removed) + } + DeviceId::Gamepad(gamepad_handle, _) => { + Event::GamepadEvent(gamepad_handle.into(), GamepadEvent::Removed) + } + }; + subclass_input.send_event(event); + } + } + _ => unreachable!(), + } + + 0 + } + + winuser::WM_INPUT => { + use crate::event::ElementState::{Pressed, Released}; + + match get_raw_input_data(lparam as _) { + Some(RawInputData::Mouse { + device_handle, + raw_mouse, + }) => { + let mouse_handle = MouseId(device_handle).into(); + + if util::has_flag(raw_mouse.usFlags, winuser::MOUSE_MOVE_ABSOLUTE) { + let x = raw_mouse.lLastX as f64; + let y = raw_mouse.lLastY as f64; + + if x != 0.0 || y != 0.0 { + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::MovedAbsolute(PhysicalPosition { x, y }), + )); + } + } else if util::has_flag(raw_mouse.usFlags, winuser::MOUSE_MOVE_RELATIVE) { + let x = raw_mouse.lLastX as f64; + let y = raw_mouse.lLastY as f64; + + if x != 0.0 || y != 0.0 { + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::MovedRelative(x, y), + )); + } + } + + if util::has_flag(raw_mouse.usButtonFlags, winuser::RI_MOUSE_WHEEL) { + // TODO: HOW IS RAW WHEEL DELTA HANDLED ON OTHER PLATFORMS? + let delta = raw_mouse.usButtonData as SHORT / winuser::WHEEL_DELTA; + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::Wheel(0.0, delta as f64), + )); + } + // Check if there's horizontal wheel movement. + if util::has_flag(raw_mouse.usButtonFlags, 0x0800) { + // TODO: HOW IS RAW WHEEL DELTA HANDLED ON OTHER PLATFORMS? + let delta = raw_mouse.usButtonData as SHORT / winuser::WHEEL_DELTA; + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::Wheel(delta as f64, 0.0), + )); + } + + let button_state = get_raw_mouse_button_state(raw_mouse.usButtonFlags); + for (index, state) in button_state + .iter() + .cloned() + .enumerate() + .filter_map(|(i, state)| state.map(|s| (i, s))) + { + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::Button { + state, + button: match index { + 0 => MouseButton::Left, + 1 => MouseButton::Middle, + 2 => MouseButton::Right, + _ => MouseButton::Other(index as u8 - 2), + }, + }, + )); + } + } + Some(RawInputData::Keyboard { + device_handle, + raw_keyboard, + }) => { + let keyboard_id = KeyboardId(device_handle).into(); + + let pressed = raw_keyboard.Message == winuser::WM_KEYDOWN + || raw_keyboard.Message == winuser::WM_SYSKEYDOWN; + let released = raw_keyboard.Message == winuser::WM_KEYUP + || raw_keyboard.Message == winuser::WM_SYSKEYUP; + + if pressed || released { + let state = if pressed { Pressed } else { Released }; + + let scancode = raw_keyboard.MakeCode as _; + let extended = util::has_flag(raw_keyboard.Flags, winuser::RI_KEY_E0 as _) + | util::has_flag(raw_keyboard.Flags, winuser::RI_KEY_E1 as _); + if let Some((vkey, scancode)) = + handle_extended_keys(raw_keyboard.VKey as _, scancode, extended) + { + let virtual_keycode = vkey_to_winit_vkey(vkey); + + subclass_input.send_event(Event::KeyboardEvent( + keyboard_id, + KeyboardEvent::Input(KeyboardInput { + scancode, + state, + virtual_keycode, + modifiers: event::get_key_mods(), + }), + )); + } + } + } + Some(RawInputData::Hid { + device_handle, + mut raw_hid, + }) => { + let mut gamepad_handle_opt: Option = None; + let mut gamepad_events = vec![]; + + { + let mut devices = subclass_input.shared_data.active_device_ids.borrow_mut(); + let device_id = devices.get_mut(&device_handle); + if let Some(DeviceId::Gamepad(gamepad_handle, ref mut gamepad)) = device_id + { + gamepad.update_state(&mut raw_hid.raw_input); + gamepad_events = gamepad.get_gamepad_events(); + gamepad_handle_opt = Some(gamepad_handle.clone().into()); + } + } + + if let Some(gamepad_handle) = gamepad_handle_opt { + for gamepad_event in gamepad_events { + subclass_input.send_event(Event::GamepadEvent( + gamepad_handle.clone(), + gamepad_event, + )); + } + } else { + subclass_input.send_event(Event::HidEvent( + HidId(device_handle).into(), + HidEvent::Data(raw_hid.raw_input), + )); + } + } + None => (), + } + + commctrl::DefSubclassProc(window, msg, wparam, lparam) + } _ if msg == *USER_EVENT_MSG_ID => { if let Ok(event) = subclass_input.user_event_receiver.recv() { subclass_input.send_event(Event::UserEvent(event)); diff --git a/src/platform_impl/windows/gamepad.rs b/src/platform_impl/windows/gamepad.rs new file mode 100644 index 0000000000..4867105099 --- /dev/null +++ b/src/platform_impl/windows/gamepad.rs @@ -0,0 +1,89 @@ +use std::sync::Weak; + +use winapi::um::winnt::HANDLE; + +use crate::{ + event::device::{BatteryLevel, GamepadEvent, RumbleError}, + platform_impl::platform::{ + raw_input::{get_raw_input_device_name, RawGamepad}, + xinput::{self, XInputGamepad, XInputGamepadShared}, + }, +}; + +#[derive(Debug)] +pub enum GamepadType { + Raw(RawGamepad), + XInput(XInputGamepad), +} + +#[derive(Clone)] +pub enum GamepadShared { + Raw(()), + XInput(Weak), + Dummy, +} + +#[derive(Debug)] +pub struct Gamepad { + handle: HANDLE, + backend: GamepadType, +} + +impl Gamepad { + pub fn new(handle: HANDLE) -> Option { + // TODO: Verify that this is an HID device + let name = get_raw_input_device_name(handle)?; + xinput::id_from_name(&name) + .and_then(XInputGamepad::new) + .map(GamepadType::XInput) + .or_else(|| RawGamepad::new(handle).map(GamepadType::Raw)) + .map(|backend| Gamepad { handle, backend }) + } + + pub unsafe fn update_state(&mut self, raw_input_report: &mut [u8]) -> Option<()> { + match self.backend { + GamepadType::Raw(ref mut gamepad) => gamepad.update_state(raw_input_report), + GamepadType::XInput(ref mut gamepad) => gamepad.update_state(), + } + } + + pub fn get_gamepad_events(&self) -> Vec { + match self.backend { + GamepadType::Raw(ref gamepad) => gamepad.get_gamepad_events(), + GamepadType::XInput(ref gamepad) => gamepad.get_gamepad_events(), + } + } + + pub fn shared_data(&self) -> GamepadShared { + match self.backend { + GamepadType::Raw(_) => GamepadShared::Raw(()), + GamepadType::XInput(ref gamepad) => GamepadShared::XInput(gamepad.shared_data()), + } + } +} + +impl GamepadShared { + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), RumbleError> { + match self { + GamepadShared::Raw(_) | GamepadShared::Dummy => Ok(()), + GamepadShared::XInput(ref data) => data + .upgrade() + .map(|r| r.rumble(left_speed, right_speed)) + .unwrap_or(Err(RumbleError::DeviceNotConnected)), + } + } + + pub fn port(&self) -> Option { + match self { + GamepadShared::Raw(_) | GamepadShared::Dummy => None, + GamepadShared::XInput(ref data) => data.upgrade().map(|r| r.port()), + } + } + + pub fn battery_level(&self) -> Option { + match self { + GamepadShared::Raw(_) | GamepadShared::Dummy => None, + GamepadShared::XInput(ref data) => data.upgrade().and_then(|r| r.battery_level()), + } + } +} diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index 34e9327d96..6fa07e16f6 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -1,14 +1,35 @@ #![cfg(target_os = "windows")] -use winapi::{self, shared::windef::HWND}; +#[macro_use] +mod util; +mod dpi; +mod drop_handler; +mod event; +mod event_loop; +mod gamepad; +mod icon; +mod monitor; +mod raw_input; +mod window; +mod window_state; +mod xinput; + +use std::{ + cmp::{Eq, Ord, Ordering, PartialEq, PartialOrd}, + fmt, + hash::{Hash, Hasher}, + ptr, +}; +use winapi::{self, shared::windef::HWND, um::winnt::HANDLE}; pub use self::{ event_loop::{EventLoop, EventLoopProxy, EventLoopWindowTarget}, + gamepad::GamepadShared, monitor::{MonitorHandle, VideoMode}, window::Window, }; -use crate::{event::DeviceId as RootDeviceId, window::Icon}; +use crate::{event::device, window::Icon}; #[derive(Clone, Default)] pub struct PlatformSpecificWindowBuilderAttributes { @@ -27,54 +48,148 @@ unsafe impl Send for Cursor {} unsafe impl Sync for Cursor {} #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct DeviceId(u32); +pub struct WindowId(HWND); +unsafe impl Send for WindowId {} +unsafe impl Sync for WindowId {} -impl DeviceId { +impl WindowId { pub unsafe fn dummy() -> Self { - DeviceId(0) + WindowId(ptr::null_mut()) } } -impl DeviceId { - pub fn persistent_identifier(&self) -> Option { - if self.0 != 0 { - raw_input::get_raw_input_device_name(self.0 as _) - } else { - None +macro_rules! device_id { + ($name:ident, $enumerate:ident) => { + #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] + pub(crate) struct $name(HANDLE); + + unsafe impl Send for $name {} + unsafe impl Sync for $name {} + + impl $name { + pub unsafe fn dummy() -> Self { + Self(ptr::null_mut()) + } + + pub fn persistent_identifier(&self) -> Option { + raw_input::get_raw_input_device_name(self.0) + } + + pub fn is_connected(&self) -> bool { + raw_input::get_raw_input_device_info(self.0).is_some() + } + + #[inline(always)] + pub fn handle(&self) -> HANDLE { + self.0 + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.$enumerate() + } } - } + + impl From<$name> for device::$name { + fn from(platform_id: $name) -> Self { + Self(platform_id) + } + } + }; } -// Constant device ID, to be removed when this backend is updated to report real device IDs. -const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId(0)); +device_id!(MouseId, mouses); +device_id!(KeyboardId, keyboards); +device_id!(HidId, hids); -fn wrap_device_id(id: u32) -> RootDeviceId { - RootDeviceId(DeviceId(id)) +#[derive(Clone)] +pub(crate) struct GamepadHandle { + handle: HANDLE, + shared_data: GamepadShared, } pub type OsError = std::io::Error; -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct WindowId(HWND); -unsafe impl Send for WindowId {} -unsafe impl Sync for WindowId {} +unsafe impl Send for GamepadHandle where GamepadShared: Send {} +unsafe impl Sync for GamepadHandle where GamepadShared: Sync {} -impl WindowId { +impl GamepadHandle { pub unsafe fn dummy() -> Self { - use std::ptr::null_mut; + Self { + handle: ptr::null_mut(), + shared_data: GamepadShared::Dummy, + } + } + + pub fn persistent_identifier(&self) -> Option { + raw_input::get_raw_input_device_name(self.handle) + } + + pub fn is_connected(&self) -> bool { + raw_input::get_raw_input_device_info(self.handle).is_some() + } + + #[inline(always)] + pub fn handle(&self) -> HANDLE { + self.handle + } + + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), device::RumbleError> { + self.shared_data.rumble(left_speed, right_speed) + } + + pub fn port(&self) -> Option { + self.shared_data.port() + } - WindowId(null_mut()) + pub fn battery_level(&self) -> Option { + self.shared_data.battery_level() + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.gamepads() } } -#[macro_use] -mod util; -mod dpi; -mod drop_handler; -mod event; -mod event_loop; -mod icon; -mod monitor; -mod raw_input; -mod window; -mod window_state; +impl From for device::GamepadHandle { + fn from(platform_id: GamepadHandle) -> Self { + Self(platform_id) + } +} + +impl fmt::Debug for GamepadHandle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + f.debug_tuple("GamepadHandle").field(&self.handle).finish() + } +} + +impl Eq for GamepadHandle {} +impl PartialEq for GamepadHandle { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + self.handle == other.handle + } +} + +impl Ord for GamepadHandle { + #[inline(always)] + fn cmp(&self, other: &Self) -> Ordering { + self.handle.cmp(&other.handle) + } +} +impl PartialOrd for GamepadHandle { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + self.handle.partial_cmp(&other.handle) + } +} + +impl Hash for GamepadHandle { + #[inline(always)] + fn hash(&self, state: &mut H) { + self.handle.hash(state); + } +} diff --git a/src/platform_impl/windows/raw_input.rs b/src/platform_impl/windows/raw_input.rs index 73b136a82f..22ad22ae7a 100644 --- a/src/platform_impl/windows/raw_input.rs +++ b/src/platform_impl/windows/raw_input.rs @@ -1,27 +1,43 @@ use std::{ + cmp::max, + fmt, mem::{self, size_of}, - ptr, + ptr, slice, }; use winapi::{ ctypes::wchar_t, shared::{ - hidusage::{HID_USAGE_GENERIC_KEYBOARD, HID_USAGE_GENERIC_MOUSE, HID_USAGE_PAGE_GENERIC}, - minwindef::{TRUE, UINT, USHORT}, + hidpi::{ + HidP_GetButtonCaps, HidP_GetCaps, HidP_GetScaledUsageValue, HidP_GetUsageValue, + HidP_GetUsagesEx, HidP_GetValueCaps, HidP_Input, HIDP_STATUS_SUCCESS, HIDP_VALUE_CAPS, + PHIDP_PREPARSED_DATA, + }, + hidusage::{ + HID_USAGE_GENERIC_GAMEPAD, HID_USAGE_GENERIC_JOYSTICK, HID_USAGE_GENERIC_KEYBOARD, + HID_USAGE_GENERIC_MOUSE, HID_USAGE_PAGE_GENERIC, + }, + minwindef::{INT, TRUE, UINT, USHORT}, windef::HWND, }, um::{ - winnt::HANDLE, + winnt::{HANDLE, PCHAR}, winuser::{ self, HRAWINPUT, RAWINPUT, RAWINPUTDEVICE, RAWINPUTDEVICELIST, RAWINPUTHEADER, - RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDI_DEVICEINFO, RIDI_DEVICENAME, RID_DEVICE_INFO, - RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, RID_INPUT, - RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, + RIDEV_DEVNOTIFY, RIDEV_INPUTSINK, RIDI_DEVICEINFO, RIDI_DEVICENAME, RIDI_PREPARSEDDATA, + RID_DEVICE_INFO, RID_DEVICE_INFO_HID, RID_DEVICE_INFO_KEYBOARD, RID_DEVICE_INFO_MOUSE, + RID_INPUT, RIM_TYPEHID, RIM_TYPEKEYBOARD, RIM_TYPEMOUSE, }, }, }; -use crate::{event::ElementState, platform_impl::platform::util}; +use crate::{ + event::{ + device::{GamepadAxis, GamepadEvent}, + ElementState, + }, + platform_impl::platform::util, +}; #[allow(dead_code)] pub fn get_raw_input_device_list() -> Option> { @@ -52,7 +68,6 @@ pub fn get_raw_input_device_list() -> Option> { Some(buffer) } -#[allow(dead_code)] pub enum RawDeviceInfo { Mouse(RID_DEVICE_INFO_MOUSE), Keyboard(RID_DEVICE_INFO_KEYBOARD), @@ -72,28 +87,27 @@ impl From for RawDeviceInfo { } } -#[allow(dead_code)] pub fn get_raw_input_device_info(handle: HANDLE) -> Option { let mut info: RID_DEVICE_INFO = unsafe { mem::zeroed() }; let info_size = size_of::() as UINT; info.cbSize = info_size; - let mut minimum_size = 0; + let mut data_size = info_size; let status = unsafe { winuser::GetRawInputDeviceInfoW( handle, RIDI_DEVICEINFO, &mut info as *mut _ as _, - &mut minimum_size, + &mut data_size, ) - }; + } as INT; - if status == UINT::max_value() || status == 0 { + if status <= 0 { return None; } - debug_assert_eq!(info_size, status); + debug_assert_eq!(info_size, status as _); Some(info.into()) } @@ -130,6 +144,43 @@ pub fn get_raw_input_device_name(handle: HANDLE) -> Option { Some(util::wchar_to_string(&name)) } +pub fn get_raw_input_pre_parse_info(handle: HANDLE) -> Option> { + let mut minimum_size = 0; + let status = unsafe { + winuser::GetRawInputDeviceInfoW( + handle, + RIDI_PREPARSEDDATA, + ptr::null_mut(), + &mut minimum_size, + ) + }; + + if status != 0 { + return None; + } + + let mut buf: Vec = Vec::with_capacity(minimum_size as _); + + let status = unsafe { + winuser::GetRawInputDeviceInfoW( + handle, + RIDI_PREPARSEDDATA, + buf.as_ptr() as _, + &mut minimum_size, + ) + }; + + if status == UINT::max_value() || status == 0 { + return None; + } + + debug_assert_eq!(minimum_size, status); + + unsafe { buf.set_len(minimum_size as _) }; + + Some(buf) +} + pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { let device_size = size_of::() as UINT; @@ -140,12 +191,12 @@ pub fn register_raw_input_devices(devices: &[RAWINPUTDEVICE]) -> bool { success == TRUE } -pub fn register_all_mice_and_keyboards_for_raw_input(window_handle: HWND) -> bool { - // RIDEV_DEVNOTIFY: receive hotplug events - // RIDEV_INPUTSINK: receive events even if we're not in the foreground +pub fn register_for_raw_input(window_handle: HWND) -> bool { + // `RIDEV_DEVNOTIFY`: receive hotplug events + // `RIDEV_INPUTSINK`: receive events even if we're not in the foreground let flags = RIDEV_DEVNOTIFY | RIDEV_INPUTSINK; - let devices: [RAWINPUTDEVICE; 2] = [ + let devices: [RAWINPUTDEVICE; 5] = [ RAWINPUTDEVICE { usUsagePage: HID_USAGE_PAGE_GENERIC, usUsage: HID_USAGE_GENERIC_MOUSE, @@ -158,27 +209,182 @@ pub fn register_all_mice_and_keyboards_for_raw_input(window_handle: HWND) -> boo dwFlags: flags, hwndTarget: window_handle, }, + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_JOYSTICK, + dwFlags: flags, + hwndTarget: window_handle, + }, + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: HID_USAGE_GENERIC_GAMEPAD, + dwFlags: flags, + hwndTarget: window_handle, + }, + RAWINPUTDEVICE { + usUsagePage: HID_USAGE_PAGE_GENERIC, + usUsage: 0x08, // multi-axis + dwFlags: flags, + hwndTarget: window_handle, + }, ]; register_raw_input_devices(&devices) } -pub fn get_raw_input_data(handle: HRAWINPUT) -> Option { - let mut data: RAWINPUT = unsafe { mem::zeroed() }; - let mut data_size = size_of::() as UINT; +pub enum RawInputData { + Mouse { + device_handle: HANDLE, + raw_mouse: winuser::RAWMOUSE, + }, + Keyboard { + device_handle: HANDLE, + raw_keyboard: winuser::RAWKEYBOARD, + }, + Hid { + device_handle: HANDLE, + raw_hid: RawHidData, + }, +} + +pub struct RawHidData { + pub hid_input_size: u32, + pub hid_input_count: u32, + pub raw_input: Box<[u8]>, +} + +pub fn get_raw_input_data(handle: HRAWINPUT) -> Option { + let mut data_size = 0; let header_size = size_of::() as UINT; - let status = unsafe { + // There are two classes of data this function can output: + // - Raw mouse and keyboard data + // - Raw HID data + // The first class (mouse and keyboard) is always going to write data formatted like the + // `RAWINPUT` struct, with no other data, and can be placed on the stack into `RAWINPUT`. + // The second class (raw HID data) writes the struct, and then a buffer of data appended to + // the end. That data needs to be heap-allocated so we can store all of it. + unsafe { winuser::GetRawInputData( handle, RID_INPUT, - &mut data as *mut _ as _, + ptr::null_mut(), &mut data_size, header_size, ) }; - if status == UINT::max_value() || status == 0 { + let (status, data): (INT, RawInputData); + + if data_size <= size_of::() as UINT { + // Since GetRawInputData is going to write... well, a buffer that's `RAWINPUT` bytes long + // and structured like `RAWINPUT`, we're just going to cut to the chase and write directly into + // a `RAWINPUT` struct. + let mut rawinput_data: RAWINPUT = unsafe { mem::zeroed() }; + + status = unsafe { + winuser::GetRawInputData( + handle, + RID_INPUT, + &mut rawinput_data as *mut RAWINPUT as *mut _, + &mut data_size, + header_size, + ) + } as INT; + + assert_ne!(-1, status); + + let device_handle = rawinput_data.header.hDevice; + + data = match rawinput_data.header.dwType { + winuser::RIM_TYPEMOUSE => { + let raw_mouse = unsafe { rawinput_data.data.mouse().clone() }; + RawInputData::Mouse { + device_handle, + raw_mouse, + } + } + winuser::RIM_TYPEKEYBOARD => { + let raw_keyboard = unsafe { rawinput_data.data.keyboard().clone() }; + RawInputData::Keyboard { + device_handle, + raw_keyboard, + } + } + winuser::RIM_TYPEHID => { + let hid_data = unsafe { rawinput_data.data.hid() }; + let buf_len = hid_data.dwSizeHid as usize * hid_data.dwCount as usize; + let data = unsafe { slice::from_raw_parts(hid_data.bRawData.as_ptr(), buf_len) }; + RawInputData::Hid { + device_handle, + raw_hid: RawHidData { + hid_input_size: hid_data.dwSizeHid, + hid_input_count: hid_data.dwCount, + raw_input: Box::from(data), + }, + } + } + _ => unreachable!(), + }; + } else { + let mut buf = vec![0u8; data_size as usize]; + + status = unsafe { + winuser::GetRawInputData( + handle, + RID_INPUT, + buf.as_mut_ptr() as *mut _, + &mut data_size, + header_size, + ) + } as INT; + + let rawinput_data = buf.as_ptr() as *const RAWINPUT; + + let device_handle = unsafe { (&*rawinput_data).header.hDevice }; + + data = match unsafe { *rawinput_data }.header.dwType { + winuser::RIM_TYPEMOUSE => { + let raw_mouse = unsafe { (&*rawinput_data).data.mouse().clone() }; + RawInputData::Mouse { + device_handle, + raw_mouse, + } + } + winuser::RIM_TYPEKEYBOARD => { + let raw_keyboard = unsafe { (&*rawinput_data).data.keyboard().clone() }; + RawInputData::Keyboard { + device_handle, + raw_keyboard, + } + } + winuser::RIM_TYPEHID => { + let hid_data: winuser::RAWHID = unsafe { (&*rawinput_data).data.hid().clone() }; + + let hid_data_index = { + let hid_data_start = + unsafe { &((&*rawinput_data).data.hid().bRawData) } as *const _; + hid_data_start as usize - buf.as_ptr() as usize + }; + + buf.drain(..hid_data_index); + + RawInputData::Hid { + device_handle, + raw_hid: RawHidData { + hid_input_size: hid_data.dwSizeHid, + hid_input_count: hid_data.dwCount, + raw_input: buf.into_boxed_slice(), + }, + } + } + _ => unreachable!(), + }; + + assert_ne!(-1, status); + } + + if status == 0 { return None; } @@ -200,7 +406,7 @@ fn button_flags_to_element_state( } } -pub fn get_raw_mouse_button_state(button_flags: USHORT) -> [Option; 3] { +pub fn get_raw_mouse_button_state(button_flags: USHORT) -> [Option; 5] { [ button_flags_to_element_state( button_flags, @@ -217,5 +423,264 @@ pub fn get_raw_mouse_button_state(button_flags: USHORT) -> [Option winuser::RI_MOUSE_RIGHT_BUTTON_DOWN, winuser::RI_MOUSE_RIGHT_BUTTON_UP, ), + button_flags_to_element_state( + button_flags, + winuser::RI_MOUSE_BUTTON_4_DOWN, + winuser::RI_MOUSE_BUTTON_4_UP, + ), + button_flags_to_element_state( + button_flags, + winuser::RI_MOUSE_BUTTON_5_DOWN, + winuser::RI_MOUSE_BUTTON_5_UP, + ), ] } + +pub struct Axis { + caps: HIDP_VALUE_CAPS, + value: f64, + prev_value: f64, + axis: Option, +} + +impl fmt::Debug for Axis { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + #[derive(Debug)] + struct Axis { + value: f64, + prev_value: f64, + axis: Option, + } + + let axis_proxy = Axis { + value: self.value, + prev_value: self.prev_value, + axis: self.axis, + }; + + axis_proxy.fmt(f) + } +} + +#[derive(Debug)] +pub struct RawGamepad { + handle: HANDLE, + pre_parsed_data: Vec, + button_count: usize, + pub button_state: Vec, + pub prev_button_state: Vec, + axis_count: usize, + pub axis_state: Vec, +} + +// Reference: https://chromium.googlesource.com/chromium/chromium/+/trunk/content/browser/gamepad/raw_input_data_fetcher_win.cc +impl RawGamepad { + pub fn new(handle: HANDLE) -> Option { + let pre_parsed_data = get_raw_input_pre_parse_info(handle)?; + let data_ptr = pre_parsed_data.as_ptr() as PHIDP_PREPARSED_DATA; + let mut caps = unsafe { mem::zeroed() }; + let status = unsafe { HidP_GetCaps(data_ptr, &mut caps) }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + let mut button_caps_len = caps.NumberInputButtonCaps; + let mut button_caps = Vec::with_capacity(button_caps_len as _); + let status = unsafe { + HidP_GetButtonCaps( + HidP_Input, + button_caps.as_mut_ptr(), + &mut button_caps_len, + data_ptr, + ) + }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + unsafe { button_caps.set_len(button_caps_len as _) }; + let mut button_count = 0; + for button_cap in button_caps { + let range = unsafe { button_cap.u.Range() }; + button_count = max(button_count, range.UsageMax); + } + let button_state = vec![false; button_count as usize]; + let mut axis_caps_len = caps.NumberInputValueCaps; + let mut axis_caps = Vec::with_capacity(axis_caps_len as _); + let status = unsafe { + HidP_GetValueCaps( + HidP_Input, + axis_caps.as_mut_ptr(), + &mut axis_caps_len, + data_ptr, + ) + }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + unsafe { axis_caps.set_len(axis_caps_len as _) }; + let mut axis_state = Vec::with_capacity(axis_caps_len as _); + let mut axis_count = 0; + for (axis_index, axis_cap) in axis_caps.drain(0..).enumerate() { + axis_state.push(Axis { + caps: axis_cap, + value: 0.0, + prev_value: 0.0, + axis: None, + }); + axis_count = max(axis_count, axis_index + 1); + } + Some(RawGamepad { + handle, + pre_parsed_data, + button_count: button_count as usize, + button_state: button_state.clone(), + prev_button_state: button_state, + axis_count, + axis_state, + }) + } + + fn pre_parsed_data_ptr(&mut self) -> PHIDP_PREPARSED_DATA { + self.pre_parsed_data.as_mut_ptr() as PHIDP_PREPARSED_DATA + } + + fn update_button_state(&mut self, raw_input_report: &mut [u8]) -> Option<()> { + let pre_parsed_data_ptr = self.pre_parsed_data_ptr(); + self.prev_button_state = + mem::replace(&mut self.button_state, vec![false; self.button_count]); + let mut usages_len = 0; + // This is the officially documented way to get the required length, but it nonetheless returns + // `HIDP_STATUS_BUFFER_TOO_SMALL`... + unsafe { + HidP_GetUsagesEx( + HidP_Input, + 0, + ptr::null_mut(), + &mut usages_len, + pre_parsed_data_ptr, + raw_input_report.as_mut_ptr() as PCHAR, + raw_input_report.len() as _, + ) + }; + let mut usages = Vec::with_capacity(usages_len as _); + let status = unsafe { + HidP_GetUsagesEx( + HidP_Input, + 0, + usages.as_mut_ptr(), + &mut usages_len, + pre_parsed_data_ptr, + raw_input_report.as_mut_ptr() as PCHAR, + raw_input_report.len() as _, + ) + }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + unsafe { usages.set_len(usages_len as _) }; + for usage in usages { + if usage.UsagePage != 0xFF << 8 { + let button_index = (usage.Usage - 1) as usize; + self.button_state[button_index] = true; + } + } + Some(()) + } + + fn update_axis_state(&mut self, raw_input_report: &mut [u8]) -> Option<()> { + let pre_parsed_data_ptr = self.pre_parsed_data_ptr(); + for axis in &mut self.axis_state { + let (status, axis_value) = if axis.caps.LogicalMin < 0 { + let mut scaled_axis_value = 0; + let status = unsafe { + HidP_GetScaledUsageValue( + HidP_Input, + axis.caps.UsagePage, + 0, + axis.caps.u.Range().UsageMin, + &mut scaled_axis_value, + pre_parsed_data_ptr, + raw_input_report.as_mut_ptr() as PCHAR, + raw_input_report.len() as _, + ) + }; + (status, scaled_axis_value as f64) + } else { + let mut axis_value = 0; + let status = unsafe { + HidP_GetUsageValue( + HidP_Input, + axis.caps.UsagePage, + 0, + axis.caps.u.Range().UsageMin, + &mut axis_value, + pre_parsed_data_ptr, + raw_input_report.as_mut_ptr() as PCHAR, + raw_input_report.len() as _, + ) + }; + (status, axis_value as f64) + }; + if status != HIDP_STATUS_SUCCESS { + return None; + } + axis.prev_value = axis.value; + axis.value = util::normalize_symmetric( + axis_value, + axis.caps.LogicalMin as f64, + axis.caps.LogicalMax as f64, + ); + } + Some(()) + } + + pub unsafe fn update_state(&mut self, raw_input_report: &mut [u8]) -> Option<()> { + self.update_button_state(raw_input_report)?; + self.update_axis_state(raw_input_report)?; + Some(()) + } + + pub fn get_changed_buttons(&self) -> impl '_ + Iterator { + self.button_state + .iter() + .zip(self.prev_button_state.iter()) + .enumerate() + .filter(|&(_, (button, prev_button))| button != prev_button) + .map(|(index, (button, _))| { + let state = if *button { + ElementState::Pressed + } else { + ElementState::Released + }; + GamepadEvent::Button { + button_id: index as _, + button: None, + state, + } + }) + } + + pub fn get_changed_axes(&self) -> impl '_ + Iterator { + self.axis_state + .iter() + .enumerate() + .filter(|&(_, axis)| axis.value != axis.prev_value) + .map(|(index, axis)| GamepadEvent::Axis { + axis_id: index as _, + axis: axis.axis, + value: axis.value, + stick: false, + }) + } + + pub fn get_gamepad_events(&self) -> Vec { + self.get_changed_axes() + .chain(self.get_changed_buttons()) + .collect() + } + + // pub fn rumble(&mut self, _left_speed: u16, _right_speed: u16) { + // // Even though I can't read German, this is still the most useful resource I found: + // // https://zfx.info/viewtopic.php?t=3574&f=7 + // // I'm not optimistic about it being possible to implement this. + // } +} diff --git a/src/platform_impl/windows/util.rs b/src/platform_impl/windows/util.rs index 96347849cf..0040cb3816 100644 --- a/src/platform_impl/windows/util.rs +++ b/src/platform_impl/windows/util.rs @@ -1,6 +1,5 @@ use std::{ io, mem, - ops::BitAnd, os::raw::c_void, ptr, slice, sync::atomic::{AtomicBool, Ordering}, @@ -21,6 +20,8 @@ use winapi::{ }, }; +pub use crate::util::*; + // Helper function to dynamically load function pointer. // `library` and `function` must be zero-terminated. pub(super) fn get_function_impl(library: &str, function: &str) -> Option<*const c_void> { @@ -51,13 +52,6 @@ macro_rules! get_function { }; } -pub fn has_flag(bitset: T, flag: T) -> bool -where - T: Copy + PartialEq + BitAnd, -{ - bitset & flag == flag -} - pub fn wchar_to_string(wchar: &[wchar_t]) -> String { String::from_utf16_lossy(wchar).to_string() } diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 783ad5e7a8..b98b02752f 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -41,9 +41,7 @@ use crate::{ REQUEST_REDRAW_NO_NEWEVENTS_MSG_ID, }, icon::{self, IconType, WinIcon}, - monitor, - raw_input::register_all_mice_and_keyboards_for_raw_input, - util, + monitor, util, window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, PlatformSpecificWindowBuilderAttributes, WindowId, }, @@ -86,12 +84,12 @@ impl Window { panic!("OleInitialize failed! Result was: `RPC_E_CHANGED_MODE`"); } - let file_drop_runner = event_loop.runner_shared.clone(); + let shared_data = event_loop.shared_data.clone(); let file_drop_handler = FileDropHandler::new( win.window.0, Box::new(move |event| { if let Ok(e) = event.map_nonuser_event() { - file_drop_runner.send_event(e) + shared_data.runner_shared.send_event(e) } }), ); @@ -107,7 +105,7 @@ impl Window { let subclass_input = event_loop::SubclassInput { window_state: win.window_state.clone(), - event_loop_runner: event_loop.runner_shared.clone(), + shared_data: event_loop.shared_data.clone(), file_drop_handler, }; @@ -839,9 +837,6 @@ unsafe fn init( WindowWrapper(handle) }; - // Set up raw input - register_all_mice_and_keyboards_for_raw_input(real_window.0); - // Register for touch events if applicable { let digitizer = winuser::GetSystemMetrics(winuser::SM_DIGITIZER) as u32; diff --git a/src/platform_impl/windows/xinput.rs b/src/platform_impl/windows/xinput.rs new file mode 100644 index 0000000000..9984c8a512 --- /dev/null +++ b/src/platform_impl/windows/xinput.rs @@ -0,0 +1,334 @@ +use std::{ + io, mem, + sync::{Arc, Weak}, +}; + +use rusty_xinput::*; +use winapi::{ + shared::minwindef::{DWORD, WORD}, + um::xinput::*, +}; + +use crate::{ + event::{ + device::{BatteryLevel, GamepadAxis, GamepadButton, GamepadEvent, RumbleError, Side}, + ElementState, + }, + platform_impl::platform::util, +}; + +lazy_static! { + static ref XINPUT_HANDLE: Option = XInputHandle::load_default().ok(); +} + +static BUTTONS: &[(WORD, u32, GamepadButton)] = &[ + (XINPUT_GAMEPAD_DPAD_UP, 12, GamepadButton::DPadUp), + (XINPUT_GAMEPAD_DPAD_DOWN, 13, GamepadButton::DPadDown), + (XINPUT_GAMEPAD_DPAD_LEFT, 14, GamepadButton::DPadLeft), + (XINPUT_GAMEPAD_DPAD_RIGHT, 15, GamepadButton::DPadRight), + (XINPUT_GAMEPAD_START, 9, GamepadButton::Start), + (XINPUT_GAMEPAD_BACK, 8, GamepadButton::Select), + (XINPUT_GAMEPAD_LEFT_THUMB, 10, GamepadButton::LeftStick), + (XINPUT_GAMEPAD_RIGHT_THUMB, 11, GamepadButton::RightStick), + (XINPUT_GAMEPAD_LEFT_SHOULDER, 4, GamepadButton::LeftShoulder), + ( + XINPUT_GAMEPAD_RIGHT_SHOULDER, + 5, + GamepadButton::RightShoulder, + ), + (XINPUT_GAMEPAD_A, 0, GamepadButton::South), + (XINPUT_GAMEPAD_B, 1, GamepadButton::East), + (XINPUT_GAMEPAD_X, 2, GamepadButton::West), + (XINPUT_GAMEPAD_Y, 3, GamepadButton::North), +]; + +pub fn id_from_name(name: &str) -> Option { + // A device name looks like \\?\HID#VID_046D&PID_C21D&IG_00#8&6daf3b6&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030} + // The IG_00 substring indicates that this is an XInput gamepad, and that the ID is 00 + let pat = "IG_0"; + name.find(pat) + .and_then(|i| name[i + pat.len()..].chars().next()) + .and_then(|c| match c { + '0' => Some(0), + '1' => Some(1), + '2' => Some(2), + '3' => Some(3), + _ => None, + }) +} + +#[derive(Debug)] +pub struct XInputGamepad { + shared: Arc, + prev_state: Option, + state: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct XInputGamepadShared { + port: DWORD, +} + +impl XInputGamepad { + pub fn new(port: DWORD) -> Option { + XINPUT_HANDLE.as_ref().map(|_| XInputGamepad { + shared: Arc::new(XInputGamepadShared { port }), + prev_state: None, + state: None, + }) + } + + pub fn update_state(&mut self) -> Option<()> { + let state = XINPUT_HANDLE + .as_ref() + .and_then(|h| h.get_state(self.shared.port).ok()); + if state.is_some() { + self.prev_state = mem::replace(&mut self.state, state); + Some(()) + } else { + None + } + } + + fn check_trigger_digital( + events: &mut Vec, + value: bool, + prev_value: Option, + side: Side, + ) { + const LEFT_TRIGGER_ID: u32 = /*BUTTONS.len() as _*/ 16; + const RIGHT_TRIGGER_ID: u32 = LEFT_TRIGGER_ID + 1; + if Some(value) != prev_value { + let state = if value { + ElementState::Pressed + } else { + ElementState::Released + }; + let (button_id, button) = match side { + Side::Left => (LEFT_TRIGGER_ID, Some(GamepadButton::LeftTrigger)), + Side::Right => (RIGHT_TRIGGER_ID, Some(GamepadButton::RightTrigger)), + }; + events.push(GamepadEvent::Button { + button_id, + button, + state, + }); + } + } + + pub fn get_changed_buttons(&self, events: &mut Vec) { + let (buttons, left_trigger, right_trigger) = match self.state.as_ref() { + Some(state) => ( + state.raw.Gamepad.wButtons, + state.left_trigger_bool(), + state.right_trigger_bool(), + ), + None => return, + }; + let (prev_buttons, prev_left, prev_right) = self + .prev_state + .as_ref() + .map(|state| { + ( + state.raw.Gamepad.wButtons, + Some(state.left_trigger_bool()), + Some(state.right_trigger_bool()), + ) + }) + .unwrap_or_else(|| (0, None, None)); + /* + A = buttons + B = prev_buttons + C = changed + P = pressed + R = released + A B C C A P C B R + (0 0) 0 (0 0) 0 (0 0) 0 + (0 1) 1 (1 1) 1 (1 0) 0 + (1 0) 1 (1 0) 0 (1 1) 1 + (1 1) 0 (0 1) 0 (0 1) 0 + */ + let changed = buttons ^ prev_buttons; + let pressed = changed & buttons; + let released = changed & prev_buttons; + for &(flag, button_id, button) in BUTTONS { + let button = Some(button); + if util::has_flag(pressed, flag) { + events.push(GamepadEvent::Button { + button_id, + button, + state: ElementState::Pressed, + }); + } else if util::has_flag(released, flag) { + events.push(GamepadEvent::Button { + button_id, + button, + state: ElementState::Released, + }); + } + } + Self::check_trigger_digital(events, left_trigger, prev_left, Side::Left); + Self::check_trigger_digital(events, right_trigger, prev_right, Side::Right); + } + + fn check_trigger( + events: &mut Vec, + value: u8, + prev_value: Option, + side: Side, + ) { + const LEFT_TRIGGER_ID: u32 = 4; + const RIGHT_TRIGGER_ID: u32 = LEFT_TRIGGER_ID + 1; + if Some(value) != prev_value { + let (axis_id, axis) = match side { + Side::Left => (LEFT_TRIGGER_ID, Some(GamepadAxis::LeftTrigger)), + Side::Right => (RIGHT_TRIGGER_ID, Some(GamepadAxis::RightTrigger)), + }; + events.push(GamepadEvent::Axis { + axis_id, + axis, + value: value as f64 / u8::max_value() as f64, + stick: false, + }); + } + } + + fn check_stick( + events: &mut Vec, + value: (i16, i16), + prev_value: Option<(i16, i16)>, + stick: Side, + ) { + let (id, axis) = match stick { + Side::Left => ((0, 1), (GamepadAxis::LeftStickX, GamepadAxis::LeftStickY)), + Side::Right => ((2, 3), (GamepadAxis::RightStickX, GamepadAxis::RightStickY)), + }; + let prev_x = prev_value.map(|prev| prev.0); + let prev_y = prev_value.map(|prev| prev.1); + + let value_f64 = |value_int: i16| match value_int.signum() { + 0 => 0.0, + 1 => value_int as f64 / i16::max_value() as f64, + -1 => value_int as f64 / (i16::min_value() as f64).abs(), + _ => unreachable!(), + }; + + let value_f64 = (value_f64(value.0), value_f64(value.1)); + if prev_x != Some(value.0) { + events.push(GamepadEvent::Axis { + axis_id: id.0, + axis: Some(axis.0), + value: value_f64.0, + stick: true, + }); + } + if prev_y != Some(value.1) { + events.push(GamepadEvent::Axis { + axis_id: id.1, + axis: Some(axis.1), + value: value_f64.1, + stick: true, + }); + } + if prev_x != Some(value.0) || prev_y != Some(value.1) { + events.push(GamepadEvent::Stick { + x_id: id.0, + y_id: id.1, + x_value: value_f64.0, + y_value: value_f64.1, + side: stick, + }) + } + } + + pub fn get_changed_axes(&self, events: &mut Vec) { + let state = match self.state { + Some(ref state) => state, + None => return, + }; + let left_stick = state.left_stick_raw(); + let right_stick = state.right_stick_raw(); + let left_trigger = state.left_trigger(); + let right_trigger = state.right_trigger(); + + let prev_state = self.prev_state.as_ref(); + let prev_left_stick = prev_state.map(|state| state.left_stick_raw()); + let prev_right_stick = prev_state.map(|state| state.right_stick_raw()); + let prev_left_trigger = prev_state.map(|state| state.left_trigger()); + let prev_right_trigger = prev_state.map(|state| state.right_trigger()); + + Self::check_stick(events, left_stick, prev_left_stick, Side::Left); + Self::check_stick(events, right_stick, prev_right_stick, Side::Right); + Self::check_trigger(events, left_trigger, prev_left_trigger, Side::Left); + Self::check_trigger(events, right_trigger, prev_right_trigger, Side::Right); + } + + pub fn get_gamepad_events(&self) -> Vec { + let mut events = Vec::new(); + self.get_changed_axes(&mut events); + self.get_changed_buttons(&mut events); + events + } + + pub fn shared_data(&self) -> Weak { + Arc::downgrade(&self.shared) + } +} + +impl Drop for XInputGamepad { + fn drop(&mut self) { + // For some reason, if you don't attempt to retrieve the xinput gamepad state at least once + // after the gamepad was disconnected, all future attempts to read from a given port (even + // if a controller was plugged back into said port) will fail! I don't know why that happens, + // but this fixes it, so 🤷. + XINPUT_HANDLE + .as_ref() + .and_then(|h| h.get_state(self.shared.port).ok()); + } +} + +impl XInputGamepadShared { + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), RumbleError> { + let left_speed = (left_speed.max(0.0).min(1.0) * u16::max_value() as f64) as u16; + let right_speed = (right_speed.max(0.0).min(1.0) * u16::max_value() as f64) as u16; + + let result = XINPUT_HANDLE + .as_ref() + .unwrap() + .set_state(self.port, left_speed, right_speed); + result.map_err(|e| match e { + XInputUsageError::XInputNotLoaded | XInputUsageError::InvalidControllerID => panic!( + "unexpected xinput error {:?}; this is a bug and should be reported", + e + ), + XInputUsageError::DeviceNotConnected => RumbleError::DeviceNotConnected, + XInputUsageError::UnknownError(code) => { + RumbleError::OsError(io::Error::from_raw_os_error(code as i32)) + } + }) + } + + pub fn port(&self) -> u8 { + self.port as _ + } + + pub fn battery_level(&self) -> Option { + use rusty_xinput::BatteryLevel as XBatteryLevel; + + let battery_info = XINPUT_HANDLE + .as_ref() + .unwrap() + .get_gamepad_battery_information(self.port) + .ok()?; + match battery_info.battery_type { + BatteryType::ALKALINE | BatteryType::NIMH => match battery_info.battery_level { + XBatteryLevel::EMPTY => Some(BatteryLevel::Empty), + XBatteryLevel::LOW => Some(BatteryLevel::Low), + XBatteryLevel::MEDIUM => Some(BatteryLevel::Medium), + XBatteryLevel::FULL => Some(BatteryLevel::Full), + _ => None, + }, + _ => None, + } + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000000..18e8c1941b --- /dev/null +++ b/src/util.rs @@ -0,0 +1,29 @@ +use std::ops::BitAnd; + +pub fn has_flag(bitset: T, flag: T) -> bool +where + T: Copy + PartialEq + BitAnd, +{ + bitset & flag == flag +} + +pub fn clamp(value: f64, min: f64, max: f64) -> f64 { + if value > max { + max + } else if value < min { + min + } else { + value + } +} + +pub fn normalize_asymmetric(value: f64, min: f64, max: f64) -> f64 { + let range = max - min; + let translated = value - min; + let scaled = translated / range; + clamp(scaled, 0.0, 1.0) +} + +pub fn normalize_symmetric(value: f64, min: f64, max: f64) -> f64 { + (2.0 * normalize_asymmetric(value, min, max)) - 1.0 +} diff --git a/tests/send_objects.rs b/tests/send_objects.rs index 252b271a4b..7221009813 100644 --- a/tests/send_objects.rs +++ b/tests/send_objects.rs @@ -22,6 +22,8 @@ fn window_send() { fn ids_send() { // ensures that the various `..Id` types implement `Send` needs_send::(); - needs_send::(); + needs_send::(); + needs_send::(); + needs_send::(); needs_send::(); } From e004bd2bb39d1f2f0bd86eeecf2351c9bf3d7d1f Mon Sep 17 00:00:00 2001 From: Christophe Massolin Date: Tue, 3 Mar 2020 15:56:11 +0100 Subject: [PATCH 2/8] Gamepad device events - Web/WASM (#1414) Add gamepad support for stdweb and web-sys, as well as web-specific gamepad examples. * [web] Fix compilation error from device api * [wasm] Apply device api changes * [wasm] Format and cleanup * [wasm32] Implement gamepad connections * [wasm] Harmonize * [Test] Made some tests with wasm-pack * Quick fix instant non supporting Hash trait * Fix on_received_character * [web_sys] Split add_event and add_window_event * [web] split device implementations * Update tests/web...still does not work * [tests/web] do not ignore index.html * [web/web_sys] split canvas and window * [tests/web] enable stack trace * [web] fix borrowmut * [web_sys] fix gamepad registration * [web] harmonize naming * [web_sys] create global emitter * [web] implement gamepad buttons * [web] implement gamepad axis * [web] cleanup * [web] update test * [web] move tests/web to examples/web * [web] axis does produce stick event * [web] Support Stick event * [web] implement gamepad to stdweb * [web] rename examples/web to examples/wasm * [web/web-sys] Move gamepad_manager from backend * [web/web_sys] implement EventLoop::gamepads * [web/web_sys] Drain gamepad events * [web/stdweb] apply web_sys changes * [web] update web/examples * [web] move gamepads code to gamepad_manager * [web] simplify and optimise * [web] replace EventCode to GamepadAxis and GamepadButton structs * [web] reuse gamepad events due to chrome issue * [web] rumble does not work * [web/stdweb] try debugging * [web] fix Chrome gamepad not updated * [web/stdweb] created an example * [examples] fix paths * fix warnings * [web/examples] update comments * [web/stdweb] add experimental support to vibrate() * [web] add CR --- .gitignore | 1 + Cargo.toml | 17 +- examples/web/gamepad/stdweb/Cargo.toml | 11 ++ examples/web/gamepad/stdweb/src/main.rs | 80 ++++++++ examples/web/gamepad/websys/.gitignore | 7 + examples/web/gamepad/websys/Cargo.toml | 20 ++ .../web/gamepad/websys/files/gamepad.html | 23 +++ examples/web/gamepad/websys/src/lib.rs | 78 ++++++++ examples/web/gamepad/websys/src/utils.rs | 10 + src/event.rs | 2 +- src/platform_impl/web/device.rs | 8 - .../web/device/gamepad/constants.rs | 37 ++++ .../web/device/gamepad/manager.rs | 167 ++++++++++++++++ .../web/device/gamepad/mapping.rs | 23 +++ src/platform_impl/web/device/gamepad/mod.rs | 99 ++++++++++ src/platform_impl/web/device/gamepad/utils.rs | 44 +++++ src/platform_impl/web/device/mod.rs | 161 +++++++++++++++ src/platform_impl/web/event_loop/global.rs | 81 ++++++++ src/platform_impl/web/event_loop/mod.rs | 19 +- src/platform_impl/web/event_loop/runner.rs | 15 ++ .../web/event_loop/window_target.rs | 114 +++++------ src/platform_impl/web/mod.rs | 3 +- src/platform_impl/web/stdweb/canvas.rs | 60 +++--- src/platform_impl/web/stdweb/gamepad.rs | 82 ++++++++ src/platform_impl/web/stdweb/mod.rs | 10 +- .../web/stdweb/{event.rs => utils.rs} | 50 ++++- src/platform_impl/web/stdweb/window.rs | 77 ++++++++ src/platform_impl/web/web_sys/canvas.rs | 185 ++++++++++-------- src/platform_impl/web/web_sys/gamepad.rs | 75 +++++++ src/platform_impl/web/web_sys/mod.rs | 21 +- .../web/web_sys/{event.rs => utils.rs} | 55 +++++- src/platform_impl/web/web_sys/window.rs | 88 +++++++++ src/platform_impl/web/window.rs | 1 + 33 files changed, 1514 insertions(+), 210 deletions(-) create mode 100644 examples/web/gamepad/stdweb/Cargo.toml create mode 100644 examples/web/gamepad/stdweb/src/main.rs create mode 100644 examples/web/gamepad/websys/.gitignore create mode 100644 examples/web/gamepad/websys/Cargo.toml create mode 100644 examples/web/gamepad/websys/files/gamepad.html create mode 100644 examples/web/gamepad/websys/src/lib.rs create mode 100644 examples/web/gamepad/websys/src/utils.rs delete mode 100644 src/platform_impl/web/device.rs create mode 100644 src/platform_impl/web/device/gamepad/constants.rs create mode 100644 src/platform_impl/web/device/gamepad/manager.rs create mode 100644 src/platform_impl/web/device/gamepad/mapping.rs create mode 100644 src/platform_impl/web/device/gamepad/mod.rs create mode 100644 src/platform_impl/web/device/gamepad/utils.rs create mode 100644 src/platform_impl/web/device/mod.rs create mode 100644 src/platform_impl/web/event_loop/global.rs create mode 100644 src/platform_impl/web/stdweb/gamepad.rs rename src/platform_impl/web/stdweb/{event.rs => utils.rs} (87%) create mode 100644 src/platform_impl/web/stdweb/window.rs create mode 100644 src/platform_impl/web/web_sys/gamepad.rs rename src/platform_impl/web/web_sys/{event.rs => utils.rs} (83%) create mode 100644 src/platform_impl/web/web_sys/window.rs diff --git a/.gitignore b/.gitignore index 7cd4f7f91d..1510490872 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ Cargo.lock target/ rls/ .vscode/ +.cargo/ util/ *~ *.wasm diff --git a/Cargo.toml b/Cargo.toml index 6987a01fc4..4e6e433f75 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,9 +106,24 @@ features = [ 'KeyboardEvent', 'MouseEvent', 'Node', + 'Navigator', 'PointerEvent', 'Window', - 'WheelEvent' + 'WheelEvent', + 'Gamepad', + 'GamepadAxisMoveEvent', + 'GamepadAxisMoveEventInit', + 'GamepadButton', + 'GamepadButtonEvent', + 'GamepadButtonEventInit', + 'GamepadEvent', + 'GamepadEventInit', + 'GamepadHand', + 'GamepadHapticActuator', + 'GamepadHapticActuatorType', + 'GamepadMappingType', + 'GamepadPose', + 'GamepadServiceTest' ] [target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] diff --git a/examples/web/gamepad/stdweb/Cargo.toml b/examples/web/gamepad/stdweb/Cargo.toml new file mode 100644 index 0000000000..83b442c6bb --- /dev/null +++ b/examples/web/gamepad/stdweb/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "stdweb-gamepad" +version = "0.1.0" +authors = ["furiouzz "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +winit = { path = "../../../../", features = [ "stdweb" ] } +stdweb = "0.4.20" \ No newline at end of file diff --git a/examples/web/gamepad/stdweb/src/main.rs b/examples/web/gamepad/stdweb/src/main.rs new file mode 100644 index 0000000000..7b545bc18d --- /dev/null +++ b/examples/web/gamepad/stdweb/src/main.rs @@ -0,0 +1,80 @@ +use winit::{ + event::{device::GamepadEvent, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +use stdweb::js; + +/** + * Build example (from examples/web/gamepad/stdweb): + * cargo web build + * Run example (from examples/web/gamepad/stdweb): + * cargo web start + * Development (from project root): + * npx nodemon --watch src --watch examples/web/gamepad/stdweb/src -e rs --exec 'cargo web check' + */ + +pub fn main() { + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_title("Gamepad tests") + .build(&event_loop) + .unwrap(); + + let deadzone = 0.12; + + event_loop.run(move |event, _, control_flow| match event { + Event::GamepadEvent(gamepad_handle, event) => match event { + GamepadEvent::Axis { + axis_id, + axis, + value, + stick, + } if value > deadzone => { + let string = format!("Axis {:#?} {:#?} {:#?} {:#?}", axis_id, axis, value, stick); + js! { console.log( @{string} ); } + } + + GamepadEvent::Stick { + x_id, + y_id, + x_value, + y_value, + side, + } if (x_value.powi(2) + y_value.powi(2)).sqrt() > deadzone => { + let string = format!( + "Stick {:#?} {:#?} {:#?} {:#?} {:#?}", + x_id, y_id, x_value, y_value, side + ); + js! { console.log( @{string} ); } + } + + GamepadEvent::Button { + button_id, + button, + state, + } => { + let string = format!("Button {:#?} {:#?} {:#?}", button_id, button, state); + js! { console.log( @{string} ); } + } + + GamepadEvent::Added => { + let string = format!("[{:?}] {:#?}", gamepad_handle, event); + js! { console.log( @{string} ); } + } + GamepadEvent::Removed => { + let string = format!("[{:?}] {:#?}", gamepad_handle, event); + js! { console.log( @{string} ); } + } + + _ => {} + }, + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + }); +} diff --git a/examples/web/gamepad/websys/.gitignore b/examples/web/gamepad/websys/.gitignore new file mode 100644 index 0000000000..8d63731034 --- /dev/null +++ b/examples/web/gamepad/websys/.gitignore @@ -0,0 +1,7 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log +.DS_Store \ No newline at end of file diff --git a/examples/web/gamepad/websys/Cargo.toml b/examples/web/gamepad/websys/Cargo.toml new file mode 100644 index 0000000000..c142f168d5 --- /dev/null +++ b/examples/web/gamepad/websys/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "websys-gamepad" +version = "0.0.1" +authors = ["The winit contributors", "Pierre Krieger "] +edition = "2018" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +winit = { path = "../../../../", features = [ "web-sys" ] } +wasm-bindgen = "0.2.45" +wasm-bindgen-test = "0.3.8" +web-sys = { version = "0.3.22", features = [ "console" ] } + +# The `console_error_panic_hook` crate provides better debugging of panics by +# logging them with `console.error`. This is great for development, but requires +# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for +# code size when deploying. +console_error_panic_hook = "0.1.6" \ No newline at end of file diff --git a/examples/web/gamepad/websys/files/gamepad.html b/examples/web/gamepad/websys/files/gamepad.html new file mode 100644 index 0000000000..a92272492d --- /dev/null +++ b/examples/web/gamepad/websys/files/gamepad.html @@ -0,0 +1,23 @@ + + + + + + + Gamepad + + + + + + + \ No newline at end of file diff --git a/examples/web/gamepad/websys/src/lib.rs b/examples/web/gamepad/websys/src/lib.rs new file mode 100644 index 0000000000..7c95db9a9e --- /dev/null +++ b/examples/web/gamepad/websys/src/lib.rs @@ -0,0 +1,78 @@ +mod utils; + +use wasm_bindgen::prelude::*; +use winit::{ + event::{device::GamepadEvent, Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +/** + * Build example (from examples/gamepad/websys): + * wasm-pack build --target web + * Run web server (from examples/gamepad/websys): + * npx http-server + * Open your browser at http://localhost:8000/files/${EXAMPLE}.html + * Development (from project root): + * npx nodemon --watch src --watch examples/web/gamepad/websys/src -e rs --exec 'cd examples/web/gamepad/websys && wasm-pack build --target web' + */ + +macro_rules! console_log { + ($($t:tt)*) => (web_sys::console::log_1(&format_args!($($t)*).to_string().into())) +} + +#[wasm_bindgen(start)] +pub fn example_gamepad() { + utils::set_panic_hook(); // needed for error stack trace + let event_loop = EventLoop::new(); + + let _window = WindowBuilder::new() + .with_title("Gamepad tests") + .build(&event_loop) + .unwrap(); + + let deadzone = 0.12; + + event_loop.run(move |event, _, control_flow| { + match event { + Event::GamepadEvent(gamepad_handle, event) => { + match event { + GamepadEvent::Axis { + axis_id, + axis, + value, + stick, + } if value > deadzone => { + console_log!("Axis {:#?} {:#?} {:#?} {:#?}", axis_id, axis, value, stick) + }, + + GamepadEvent::Stick { + x_id, y_id, x_value, y_value, side + } if (x_value.powi(2) + y_value.powi(2)).sqrt() > deadzone => { + console_log!("Stick {:#?} {:#?} {:#?} {:#?} {:#?}", x_id, y_id, x_value, y_value, side) + }, + + GamepadEvent::Button { + button_id, + button, + state, + } => { + console_log!("Button {:#?} {:#?} {:#?}", button_id, button, state) + }, + + GamepadEvent::Added => { + console_log!("[{:?}] {:#?}", gamepad_handle, event) + }, + GamepadEvent::Removed => console_log!("[{:?}] {:#?}", gamepad_handle, event), + + _ => {}, + } + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + _ => (), + } + }); +} diff --git a/examples/web/gamepad/websys/src/utils.rs b/examples/web/gamepad/websys/src/utils.rs new file mode 100644 index 0000000000..02aa5c19ef --- /dev/null +++ b/examples/web/gamepad/websys/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + // #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} \ No newline at end of file diff --git a/src/event.rs b/src/event.rs index bd39b95ba2..1df0d51a62 100644 --- a/src/event.rs +++ b/src/event.rs @@ -70,7 +70,7 @@ impl Event { } /// The reason the event loop is resuming. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StartCause { /// Sent if the time specified by `ControlFlow::WaitUntil` has been reached. Contains the /// moment the timeout was requested and the requested resume time. The actual resume time is diff --git a/src/platform_impl/web/device.rs b/src/platform_impl/web/device.rs deleted file mode 100644 index a2f00b69c4..0000000000 --- a/src/platform_impl/web/device.rs +++ /dev/null @@ -1,8 +0,0 @@ -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub struct Id(pub i32); - -impl Id { - pub unsafe fn dummy() -> Self { - Id(0) - } -} diff --git a/src/platform_impl/web/device/gamepad/constants.rs b/src/platform_impl/web/device/gamepad/constants.rs new file mode 100644 index 0000000000..c2c589adc1 --- /dev/null +++ b/src/platform_impl/web/device/gamepad/constants.rs @@ -0,0 +1,37 @@ +use crate::event::device::{GamepadAxis, GamepadButton}; + +pub(crate) static BUTTONS: [GamepadButton; 16] = [ + GamepadButton::South, + GamepadButton::East, + GamepadButton::West, + GamepadButton::North, + GamepadButton::LeftTrigger, + GamepadButton::RightTrigger, + GamepadButton::LeftShoulder, + GamepadButton::RightShoulder, + GamepadButton::Select, + GamepadButton::Start, + GamepadButton::LeftStick, + GamepadButton::RightStick, + GamepadButton::DPadUp, + GamepadButton::DPadDown, + GamepadButton::DPadLeft, + GamepadButton::DPadRight, +]; + +pub(crate) static AXES: [GamepadAxis; 6] = [ + GamepadAxis::LeftStickX, + GamepadAxis::LeftStickY, + GamepadAxis::RightStickX, + GamepadAxis::RightStickY, + GamepadAxis::LeftTrigger, + GamepadAxis::RightTrigger, +]; + +pub(crate) fn button_code(index: usize) -> Option { + BUTTONS.get(index).map(|ev| ev.clone()) +} + +pub(crate) fn axis_code(index: usize) -> Option { + AXES.get(index).map(|ev| ev.clone()) +} diff --git a/src/platform_impl/web/device/gamepad/manager.rs b/src/platform_impl/web/device/gamepad/manager.rs new file mode 100644 index 0000000000..ec98ade4f7 --- /dev/null +++ b/src/platform_impl/web/device/gamepad/manager.rs @@ -0,0 +1,167 @@ +use super::utils; +use crate::event::device; +use crate::platform_impl::platform::{backend, device::gamepad, GamepadHandle, event_loop::global}; +use std::collections::VecDeque; + +pub struct Manager { + pub(crate) gamepads: Vec, + pub(crate) events: VecDeque<(backend::gamepad::Gamepad, device::GamepadEvent)>, + pub(crate) global_window: Option, +} + +impl Manager { + pub fn new() -> Self { + Self { + gamepads: Vec::new(), + events: VecDeque::new(), + global_window: None, + } + } + + // Register global window to fetch gamepads. + // Due to Chrome issue, I prefer to use its gamepad list + pub fn set_global_window(&mut self, global_window: global::Shared) { + self.global_window.replace(global_window); + } + + // Get an updated raw gamepad and generate a new mapping + pub fn collect_gamepads(&self) -> Option> { + self.global_window.as_ref().map(|w| w.get_gamepads()) + } + + // Collect gamepad events (buttons/axes/sticks) + // dispatch to handler and update gamepads + pub fn collect_events(&mut self, mut handler: F) + where + F: 'static + FnMut((device::GamepadHandle, device::GamepadEvent)), + { + let opt_new_gamepads = self.collect_gamepads(); + if opt_new_gamepads.is_none() { + return; + } + + let new_gamepads = opt_new_gamepads.unwrap(); + let old_gamepads = &self.gamepads; + + let mut old_index = 0; + let mut new_index = 0; + + // Collect events + loop { + match (old_gamepads.get(old_index), new_gamepads.get(new_index)) { + (Some(old), Some(new)) if old.index() == new.index() => { + // Button events + let buttons = old.mapping.buttons().zip(new.mapping.buttons()).enumerate(); + for (btn_index, (old_button, new_button)) in buttons { + match (old_button, new_button) { + (false, true) => { + self.events.push_back((new.clone(), utils::gamepad_button(btn_index, true))) + } + (true, false) => { + self.events.push_back((new.clone(), utils::gamepad_button(btn_index, false))) + } + _ => (), + } + } + + // Axis events + let axes = old.mapping.axes().zip(new.mapping.axes()).enumerate(); + for (axis_index, (old_axis, new_axis)) in axes { + if old_axis != new_axis { + self.events.push_back((new.clone(), utils::gamepad_axis(axis_index, new_axis))) + } + } + + // Stick events + let mut old_axes = old.mapping.axes(); + let mut new_axes = new.mapping.axes(); + + let old_left = (old_axes.next(), old_axes.next()); + let new_left = (new_axes.next(), new_axes.next()); + if old_left != new_left { + if let (Some(x), Some(y)) = (new_left.0, new_left.1) { + self.events.push_back(( + new.clone(), + utils::gamepad_stick(0, 1, x, y, device::Side::Left), + )); + } + } + + let old_right = (old_axes.next(), old_axes.next()); + let new_right = (new_axes.next(), new_axes.next()); + if old_right != new_right { + if let (Some(x), Some(y)) = (new_right.0, new_right.1) { + self.events.push_back(( + new.clone(), + utils::gamepad_stick(2, 3, x, y, device::Side::Right), + )); + } + } + + // Increment indices + old_index += 1; + new_index += 1; + }, + + // Connect + (None, Some(new)) => { + self.events.push_back(( + new.clone(), + device::GamepadEvent::Added, + )); + new_index += 1; + }, + + // Connect + (Some(old), Some(new)) if old.index > new.index => { + self.events.push_back(( + new.clone(), + device::GamepadEvent::Added, + )); + new_index += 1; + }, + + // Disconnect + (Some(old), Some(_new)) => { + self.events.push_back(( + old.clone(), + device::GamepadEvent::Removed, + )); + old_index += 1; + }, + + // Disconnect + (Some(old), None) => { + self.events.push_back(( + old.clone(), + device::GamepadEvent::Removed, + )); + old_index += 1; + }, + + // Break loop + (None, None) => { + break + } + } + } + + // Dispatch events and drain events vec + loop { + if let Some((gamepad, event)) = self.events.pop_front() { + handler(( + device::GamepadHandle(GamepadHandle { + id: gamepad.index, + gamepad: gamepad::Shared::Raw(gamepad), + }), + event, + )); + } else { + break; + } + } + + // Update gamepads + self.gamepads = new_gamepads; + } +} diff --git a/src/platform_impl/web/device/gamepad/mapping.rs b/src/platform_impl/web/device/gamepad/mapping.rs new file mode 100644 index 0000000000..c424b73789 --- /dev/null +++ b/src/platform_impl/web/device/gamepad/mapping.rs @@ -0,0 +1,23 @@ +#[derive(Debug, Clone)] +pub enum Mapping { + Standard { buttons: [bool; 16], axes: [f64; 6] }, + NoMapping { buttons: Vec, axes: Vec }, +} + +impl Mapping { + pub(crate) fn buttons<'a>(&'a self) -> impl Iterator + 'a { + match self { + Mapping::Standard { buttons, .. } => buttons.iter(), + Mapping::NoMapping { buttons, .. } => buttons.iter(), + } + .cloned() + } + + pub(crate) fn axes<'a>(&'a self) -> impl Iterator + 'a { + match self { + Mapping::Standard { axes, .. } => axes.iter(), + Mapping::NoMapping { axes, .. } => axes.iter(), + } + .cloned() + } +} diff --git a/src/platform_impl/web/device/gamepad/mod.rs b/src/platform_impl/web/device/gamepad/mod.rs new file mode 100644 index 0000000000..7ed8e7abce --- /dev/null +++ b/src/platform_impl/web/device/gamepad/mod.rs @@ -0,0 +1,99 @@ +mod manager; +mod mapping; +mod utils; + +pub mod constants; +pub use manager::Manager; +pub use mapping::Mapping; + +use crate::event::device::{BatteryLevel, RumbleError}; +use crate::platform_impl::platform::backend; +use std::fmt; + +pub enum Shared { + Raw(backend::gamepad::Gamepad), + Dummy, +} + +impl Shared { + // An integer that is auto-incremented to be unique for each device + // currently connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/index + pub fn id(&self) -> i32 { + match self { + Shared::Raw(g) => g.index() as i32, + Shared::Dummy => -1, + } + } + + // A string containing some information about the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/id + pub fn info(&self) -> String { + match self { + Shared::Raw(g) => g.id(), + Shared::Dummy => String::new(), + } + } + + // A boolean indicating whether the gamepad is still connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/connected + pub fn connected(&self) -> bool { + match self { + Shared::Raw(g) => g.connected(), + Shared::Dummy => false, + } + } + + // [EXPERIMENTAL] An array containing GamepadHapticActuator objects, + // each of which represents haptic feedback hardware available on the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/hapticActuators + pub fn rumble(&self, left_speed: f64, _right_speed: f64) -> Result<(), RumbleError> { + match self { + Shared::Dummy => Ok(()), + Shared::Raw(g) => { + g.vibrate(left_speed, 1000f64); + Ok(()) + } + } + } + + pub fn is_dummy(&self) -> bool { + match self { + Shared::Dummy => true, + _ => false, + } + } + + pub fn port(&self) -> Option { + None + } + + pub fn battery_level(&self) -> Option { + None + } +} + +impl Clone for Shared { + fn clone(&self) -> Self { + match self { + Shared::Raw(g) => Shared::Raw(g.clone()), + Shared::Dummy => Shared::Dummy, + } + } +} + +impl Default for Shared { + fn default() -> Self { + Shared::Dummy + } +} + +impl fmt::Debug for Shared { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + if self.is_dummy() { + write!(f, "Gamepad (Dummy)") + } else { + write!(f, "Gamepad ({}#{})", self.id(), self.info()) + } + } +} diff --git a/src/platform_impl/web/device/gamepad/utils.rs b/src/platform_impl/web/device/gamepad/utils.rs new file mode 100644 index 0000000000..4bc5154c6a --- /dev/null +++ b/src/platform_impl/web/device/gamepad/utils.rs @@ -0,0 +1,44 @@ +use crate::event::{ElementState, device}; +use super::constants; + +pub fn gamepad_button(code: usize, pressed: bool) -> device::GamepadEvent { + let button_id = code as u32; + let button = constants::button_code(code); + + let state = if pressed { + ElementState::Pressed + } else { + ElementState::Released + }; + + device::GamepadEvent::Button { + button_id, + button, + state, + } +} + +pub fn gamepad_axis(code: usize, value: f64) -> device::GamepadEvent { + let axis_id = code as u32; + let axis = constants::axis_code(code); + + device::GamepadEvent::Axis { + axis_id, + axis, + value, + stick: true, + } +} + +pub fn gamepad_stick(x_code: usize, y_code: usize, x_value: f64, y_value: f64, side: device::Side) -> device::GamepadEvent { + let x_id = x_code as u32; + let y_id = y_code as u32; + + device::GamepadEvent::Stick { + x_id, + y_id, + x_value, + y_value, + side, + } +} diff --git a/src/platform_impl/web/device/mod.rs b/src/platform_impl/web/device/mod.rs new file mode 100644 index 0000000000..eb4108bade --- /dev/null +++ b/src/platform_impl/web/device/mod.rs @@ -0,0 +1,161 @@ +pub mod gamepad; + +use super::event_loop::EventLoop; +use crate::event::device; + +use std::{ + cmp::{Eq, Ordering, PartialEq, PartialOrd}, + hash::{Hash, Hasher}, +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct MouseId(pub i32); + +unsafe impl Send for MouseId {} +unsafe impl Sync for MouseId {} + +impl MouseId { + pub unsafe fn dummy() -> Self { + Self(0) + } + + pub fn is_connected(&self) -> bool { + false + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.mice() + } +} + +impl From for device::MouseId { + fn from(platform_id: MouseId) -> Self { + Self(platform_id) + } +} +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct KeyboardId(pub i32); + +unsafe impl Send for KeyboardId {} +unsafe impl Sync for KeyboardId {} + +impl KeyboardId { + pub unsafe fn dummy() -> Self { + Self(0) + } + + pub fn is_connected(&self) -> bool { + false + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.keyboards() + } +} + +impl From for device::KeyboardId { + fn from(platform_id: KeyboardId) -> Self { + Self(platform_id) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct HidId(pub i32); + +unsafe impl Send for HidId {} +unsafe impl Sync for HidId {} + +impl HidId { + pub unsafe fn dummy() -> Self { + Self(0) + } + + pub fn is_connected(&self) -> bool { + false + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.hids() + } +} + +impl From for device::HidId { + fn from(platform_id: HidId) -> Self { + Self(platform_id) + } +} + +#[derive(Clone, Debug)] +pub(crate) struct GamepadHandle { + pub(crate) id: i32, + pub(crate) gamepad: gamepad::Shared, +} + +unsafe impl Send for GamepadHandle {} +unsafe impl Sync for GamepadHandle {} + +impl GamepadHandle { + pub unsafe fn dummy() -> Self { + Self { + id: -1, + gamepad: gamepad::Shared::default(), + } + } + + pub fn is_connected(&self) -> bool { + self.gamepad.connected() + } + + pub fn enumerate<'a, T>( + event_loop: &'a EventLoop, + ) -> impl 'a + Iterator { + event_loop.gamepads() + } + + pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), device::RumbleError> { + self.gamepad.rumble(left_speed, right_speed) + } + + pub fn port(&self) -> Option { + self.gamepad.port() + } + + pub fn battery_level(&self) -> Option { + self.gamepad.battery_level() + } +} + +impl Eq for GamepadHandle {} + +impl PartialEq for GamepadHandle { + #[inline(always)] + fn eq(&self, othr: &Self) -> bool { + self.id == othr.id + } +} + +impl Ord for GamepadHandle { + #[inline(always)] + fn cmp(&self, othr: &Self) -> Ordering { + self.id.cmp(&othr.id) + } +} +impl PartialOrd for GamepadHandle { + #[inline(always)] + fn partial_cmp(&self, othr: &Self) -> Option { + self.id.partial_cmp(&othr.id) + } +} + +impl Hash for GamepadHandle { + #[inline(always)] + fn hash(&self, state: &mut H) { + self.id.hash(state) + } +} diff --git a/src/platform_impl/web/event_loop/global.rs b/src/platform_impl/web/event_loop/global.rs new file mode 100644 index 0000000000..d6e1907532 --- /dev/null +++ b/src/platform_impl/web/event_loop/global.rs @@ -0,0 +1,81 @@ +use super::super::device::{gamepad, GamepadHandle}; +use super::backend; +use crate::event::device; +use std::{cell::RefCell, rc::Rc, collections::HashSet}; + +#[derive(Debug)] +pub struct Window { + raw: RefCell>, + gamepads: Rc>>, +} + +#[derive(Debug)] +pub struct Shared(Rc); + +impl Shared { + pub fn new() -> Self { + Self(Rc::new(Window { + raw: RefCell::new(None), + gamepads: Rc::new(RefCell::new(HashSet::new())), + })) + } + + // Request window object and listen global events + pub fn register_events(&self) -> Result<(), crate::error::OsError> { + if (*self.0.raw.borrow()).is_none() { + let shared = backend::window::Shared::create()?; + let mut window = shared.0.borrow_mut(); + + let shared_gamepads = self.0.gamepads.clone(); + window.on_gamepad_connected(move |gamepad: backend::gamepad::Gamepad| { + let mut gamepads = shared_gamepads.borrow_mut(); + let index = gamepad.index(); + if !gamepads.contains(&index) { + gamepads.insert(index); + } + }); + + let shared_gamepads = self.0.gamepads.clone(); + window.on_gamepad_disconnected(move |gamepad: backend::gamepad::Gamepad| { + let mut gamepads = shared_gamepads.borrow_mut(); + let index = gamepad.index(); + if gamepads.contains(&index) { + gamepads.remove(&index); + } + }); + + self.0.raw.replace(Some(shared.clone())); + } + + Ok(()) + } + + // Google Chrome create an array of [null, null, null, null]. + // To fix that issue, I create my own list of gamepads + // by listening "gamepadconnected" and "gamepaddisconnected" + pub fn get_gamepads(&self) -> Vec { + let gamepads = self.0.gamepads.borrow_mut(); + backend::get_gamepads() + .filter(|g| gamepads.contains(&g.index())) + .collect() + } + + // Return gamepads handles required for EventLoop::gamepads() + pub fn get_gamepad_handles(&self) -> Vec { + self.get_gamepads() + .iter() + .map(|gamepad| { + device::GamepadHandle(GamepadHandle { + id: gamepad.index, + gamepad: gamepad::Shared::Raw(gamepad.clone()), + }) + }) + .collect() + } +} + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index 7124cee7e1..9be442a1cd 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -2,11 +2,12 @@ mod proxy; mod runner; mod state; mod window_target; +pub(crate) mod global; pub use self::proxy::Proxy; pub use self::window_target::WindowTarget; -use super::{backend, device, monitor, window}; +use super::{backend, monitor, window}; use crate::event::Event; use crate::event_loop as root; @@ -64,4 +65,20 @@ impl EventLoop { pub fn window_target(&self) -> &root::EventLoopWindowTarget { &self.elw } + + pub fn mice(&self) -> impl '_ + Iterator { + std::iter::empty() + } + + pub fn keyboards(&self) -> impl '_ + Iterator { + std::iter::empty() + } + + pub fn hids(&self) -> impl '_ + Iterator { + std::iter::empty() + } + + pub fn gamepads(&self) -> impl '_ + Iterator { + self.elw.p.collect_gamepads().into_iter() + } } diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index b3889fad2a..3b240e3cf0 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -2,6 +2,7 @@ use super::{backend, state::State}; use crate::event::{Event, StartCause, WindowEvent}; use crate::event_loop as root; use crate::window::WindowId; +use crate::platform_impl::platform::device::gamepad; use instant::{Duration, Instant}; use std::{ @@ -24,6 +25,7 @@ pub struct Execution { events: RefCell>>, id: RefCell, redraw_pending: RefCell>, + gamepad_manager: RefCell, } struct Runner { @@ -49,9 +51,14 @@ impl Shared { events: RefCell::new(VecDeque::new()), id: RefCell::new(0), redraw_pending: RefCell::new(HashSet::new()), + gamepad_manager: RefCell::new(gamepad::Manager::new()), })) } + pub fn set_global_window(&self, global_window: super::global::Shared) { + self.0.gamepad_manager.borrow_mut().set_global_window(global_window); + } + // Set the event callback to use for the event loop runner // This the event callback is a fairly thin layer over the user-provided callback that closes // over a RootEventLoopWindowTarget reference @@ -138,6 +145,14 @@ impl Shared { &mut control, ); } + // Collect all global events + let mut gamepad_manager = self.0.gamepad_manager.borrow_mut(); + let instance = self.clone(); + gamepad_manager.collect_events(move |(handle, event)| { + instance.handle_event(Event::GamepadEvent(handle, event), &mut control); + }); + + // Every events are cleared self.handle_event(Event::EventsCleared, &mut control); self.apply_control_flow(control); // If the event loop is closed, it has been closed this iteration and now the closing diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index d302edb87b..cefe9678d8 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -1,18 +1,20 @@ -use super::{backend, device, proxy::Proxy, runner, window}; +use super::{backend, proxy::Proxy, runner, window, global}; use crate::dpi::LogicalSize; -use crate::event::{DeviceId, ElementState, Event, KeyboardInput, TouchPhase, WindowEvent}; +use crate::event::{device, ElementState, Event, KeyboardInput, WindowEvent}; use crate::event_loop::ControlFlow; +use crate::platform_impl::platform::device::{KeyboardId, MouseId}; use crate::window::WindowId; -use std::clone::Clone; pub struct WindowTarget { pub(crate) runner: runner::Shared, + pub(crate) global_window: global::Shared, } impl Clone for WindowTarget { fn clone(&self) -> Self { WindowTarget { runner: self.runner.clone(), + global_window: self.global_window.clone(), } } } @@ -21,6 +23,7 @@ impl WindowTarget { pub fn new() -> Self { WindowTarget { runner: runner::Shared::new(), + global_window: global::Shared::new(), } } @@ -29,6 +32,7 @@ impl WindowTarget { } pub fn run(&self, event_handler: Box, &mut ControlFlow)>) { + self.runner.set_global_window(self.global_window.clone()); self.runner.set_listener(event_handler); } @@ -36,6 +40,14 @@ impl WindowTarget { window::Id(self.runner.generate_id()) } + pub fn collect_gamepads(&self) -> Vec { + self.global_window.get_gamepad_handles() + } + + pub fn register_global_events(&self) -> Result<(), crate::error::OsError> { + self.global_window.register_events() + } + pub fn register(&self, canvas: &mut backend::Canvas, id: window::Id) { let runner = self.runner.clone(); canvas.set_attribute("data-raw-handle", &id.0.to_string()); @@ -57,34 +69,28 @@ impl WindowTarget { let runner = self.runner.clone(); canvas.on_keyboard_press(move |scancode, virtual_keycode, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::KeyboardInput { - device_id: DeviceId(unsafe { device::Id::dummy() }), - input: KeyboardInput { - scancode, - state: ElementState::Pressed, - virtual_keycode, - modifiers, - }, - }, - }); + runner.send_event(Event::KeyboardEvent( + device::KeyboardId(unsafe { KeyboardId::dummy() }), + device::KeyboardEvent::Input(KeyboardInput { + scancode, + state: ElementState::Pressed, + virtual_keycode, + modifiers, + }), + )); }); let runner = self.runner.clone(); canvas.on_keyboard_release(move |scancode, virtual_keycode, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::KeyboardInput { - device_id: DeviceId(unsafe { device::Id::dummy() }), - input: KeyboardInput { - scancode, - state: ElementState::Released, - virtual_keycode, - modifiers, - }, - }, - }); + runner.send_event(Event::KeyboardEvent( + device::KeyboardId(unsafe { KeyboardId::dummy() }), + device::KeyboardEvent::Input(KeyboardInput { + scancode, + state: ElementState::Released, + virtual_keycode, + modifiers, + }), + )); }); let runner = self.runner.clone(); @@ -96,31 +102,26 @@ impl WindowTarget { }); let runner = self.runner.clone(); - canvas.on_cursor_leave(move |pointer_id| { + canvas.on_cursor_leave(move || { runner.send_event(Event::WindowEvent { window_id: WindowId(id), - event: WindowEvent::CursorLeft { - device_id: DeviceId(device::Id(pointer_id)), - }, + event: WindowEvent::CursorLeft, }); }); let runner = self.runner.clone(); - canvas.on_cursor_enter(move |pointer_id| { + canvas.on_cursor_enter(move || { runner.send_event(Event::WindowEvent { window_id: WindowId(id), - event: WindowEvent::CursorEntered { - device_id: DeviceId(device::Id(pointer_id)), - }, + event: WindowEvent::CursorEntered, }); }); let runner = self.runner.clone(); - canvas.on_cursor_move(move |pointer_id, position, modifiers| { + canvas.on_cursor_move(move |position, modifiers| { runner.send_event(Event::WindowEvent { window_id: WindowId(id), event: WindowEvent::CursorMoved { - device_id: DeviceId(device::Id(pointer_id)), position, modifiers, }, @@ -128,42 +129,33 @@ impl WindowTarget { }); let runner = self.runner.clone(); - canvas.on_mouse_press(move |pointer_id, button, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::MouseInput { - device_id: DeviceId(device::Id(pointer_id)), + canvas.on_mouse_press(move |pointer_id, button| { + runner.send_event(Event::MouseEvent( + device::MouseId(MouseId(pointer_id)), + device::MouseEvent::Button { state: ElementState::Pressed, button, - modifiers, }, - }); + )); }); let runner = self.runner.clone(); - canvas.on_mouse_release(move |pointer_id, button, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::MouseInput { - device_id: DeviceId(device::Id(pointer_id)), + canvas.on_mouse_release(move |pointer_id, button| { + runner.send_event(Event::MouseEvent( + device::MouseId(MouseId(pointer_id)), + device::MouseEvent::Button { state: ElementState::Released, button, - modifiers, }, - }); + )); }); let runner = self.runner.clone(); - canvas.on_mouse_wheel(move |pointer_id, delta, modifiers| { - runner.send_event(Event::WindowEvent { - window_id: WindowId(id), - event: WindowEvent::MouseWheel { - device_id: DeviceId(device::Id(pointer_id)), - delta, - phase: TouchPhase::Moved, - modifiers, - }, - }); + canvas.on_mouse_wheel(move |pointer_id, delta| { + runner.send_event(Event::MouseEvent( + device::MouseId(MouseId(pointer_id)), + device::MouseEvent::Wheel(delta.0, delta.1), + )); }); let runner = self.runner.clone(); diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index 029761b957..7c775d9a63 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -19,7 +19,6 @@ mod backend; #[cfg(not(any(feature = "web-sys", feature = "stdweb")))] compile_error!("Please select a feature to build for web: `web-sys`, `stdweb`"); -pub use self::device::Id as DeviceId; pub use self::error::OsError; pub use self::event_loop::{ EventLoop, Proxy as EventLoopProxy, WindowTarget as EventLoopWindowTarget, @@ -29,3 +28,5 @@ pub use self::window::{ Id as WindowId, PlatformSpecificBuilderAttributes as PlatformSpecificWindowBuilderAttributes, Window, }; + +pub(crate) use self::device::*; diff --git a/src/platform_impl/web/stdweb/canvas.rs b/src/platform_impl/web/stdweb/canvas.rs index a0ff5af2e8..8539f22293 100644 --- a/src/platform_impl/web/stdweb/canvas.rs +++ b/src/platform_impl/web/stdweb/canvas.rs @@ -1,7 +1,7 @@ -use super::event; +use super::utils; use crate::dpi::{LogicalPosition, LogicalSize}; use crate::error::OsError as RootOE; -use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; +use crate::event::{ModifiersState, MouseButton, ScanCode, VirtualKeyCode}; use crate::platform_impl::OsError; use std::cell::RefCell; @@ -129,9 +129,9 @@ impl Canvas { { self.on_keyboard_release = Some(self.add_user_event(move |event: KeyUpEvent| { handler( - event::scan_code(&event), - event::virtual_key_code(&event), - event::keyboard_modifiers(&event), + utils::scan_code(&event), + utils::virtual_key_code(&event), + utils::keyboard_modifiers(&event), ); })); } @@ -142,9 +142,9 @@ impl Canvas { { self.on_keyboard_press = Some(self.add_user_event(move |event: KeyDownEvent| { handler( - event::scan_code(&event), - event::virtual_key_code(&event), - event::keyboard_modifiers(&event), + utils::scan_code(&event), + utils::virtual_key_code(&event), + utils::keyboard_modifiers(&event), ); })); } @@ -159,75 +159,65 @@ impl Canvas { // viable/compatible alternative as of now. `beforeinput` is still widely // unsupported. self.on_received_character = Some(self.add_user_event(move |event: KeyPressEvent| { - handler(event::codepoint(&event)); + handler(utils::codepoint(&event)); })); } pub fn on_cursor_leave(&mut self, mut handler: F) where - F: 'static + FnMut(i32), + F: 'static + FnMut(), { - self.on_cursor_leave = Some(self.add_event(move |event: PointerOutEvent| { - handler(event.pointer_id()); + self.on_cursor_leave = Some(self.add_event(move |_event: PointerOutEvent| { + handler(); })); } pub fn on_cursor_enter(&mut self, mut handler: F) where - F: 'static + FnMut(i32), + F: 'static + FnMut(), { - self.on_cursor_enter = Some(self.add_event(move |event: PointerOverEvent| { - handler(event.pointer_id()); + self.on_cursor_enter = Some(self.add_event(move |_event: PointerOverEvent| { + handler(); })); } pub fn on_mouse_release(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseButton, ModifiersState), + F: 'static + FnMut(i32, MouseButton), { self.on_mouse_release = Some(self.add_user_event(move |event: PointerUpEvent| { - handler( - event.pointer_id(), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); + handler(event.pointer_id(), utils::mouse_button(&event)); })); } pub fn on_mouse_press(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseButton, ModifiersState), + F: 'static + FnMut(i32, MouseButton), { self.on_mouse_press = Some(self.add_user_event(move |event: PointerDownEvent| { - handler( - event.pointer_id(), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); + handler(event.pointer_id(), utils::mouse_button(&event)); })); } pub fn on_cursor_move(&mut self, mut handler: F) where - F: 'static + FnMut(i32, LogicalPosition, ModifiersState), + F: 'static + FnMut(LogicalPosition, ModifiersState), { self.on_cursor_move = Some(self.add_event(move |event: PointerMoveEvent| { handler( - event.pointer_id(), - event::mouse_position(&event), - event::mouse_modifiers(&event), + utils::mouse_position(&event), + utils::mouse_modifiers(&event), ); })); } pub fn on_mouse_wheel(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), + F: 'static + FnMut(i32, (f64, f64)), { self.on_mouse_wheel = Some(self.add_event(move |event: MouseWheelEvent| { - if let Some(delta) = event::mouse_scroll_delta(&event) { - handler(0, delta, event::mouse_modifiers(&event)); - } + let delta = utils::mouse_scroll_delta(&event); + handler(0, delta); })); } diff --git a/src/platform_impl/web/stdweb/gamepad.rs b/src/platform_impl/web/stdweb/gamepad.rs new file mode 100644 index 0000000000..0b54896b11 --- /dev/null +++ b/src/platform_impl/web/stdweb/gamepad.rs @@ -0,0 +1,82 @@ +use std::{cmp::PartialEq}; +use crate::platform_impl::platform::device; +use super::utils; +use stdweb::js; + +#[derive(Debug)] +pub struct Gamepad { + pub(crate) index: i32, + pub(crate) raw: stdweb::web::Gamepad, + pub(crate) mapping: device::gamepad::Mapping, +} + +impl Gamepad { + pub fn new(raw: stdweb::web::Gamepad) -> Self { + let mapping = utils::create_mapping(&raw); + + Self { + index: raw.index(), + raw, + mapping, + } + } + + // An integer that is auto-incremented to be unique for each device + // currently connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/index + pub fn index(&self) -> i32 { + self.raw.index() + } + + // A string containing some information about the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/id + pub fn id(&self) -> String { + self.raw.id() + } + + // A boolean indicating whether the gamepad is still connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/connected + pub fn connected(&self) -> bool { + self.raw.connected() + } + + // EXPERIMENTAL + #[allow(dead_code)] + pub fn vibrate(&self, value: f64, duration: f64) { + let index = self.index; + js! { + const gamepads = navigator.getGamepads(); + let gamepad = null; + for (let i = 0; i < gamepads.length; i++) { + if (gamepads[i] && gamepads[i].index == @{index}) { + gamepad = gamepads[i]; + break + } + } + if (!gamepad || !gamepad.hapticActuators) return; + for (let i = 0; i < gamepad.hapticActuators.length; i++) { + const actuator = gamepad.hapticActuators[i]; + if (actuator && actuator.type === "vibration") { + actuator.pulse(@{value}, @{duration}); + } + } + } + } +} + +impl Clone for Gamepad { + fn clone(&self) -> Self { + Self { + index: self.index, + raw: self.raw.clone(), + mapping: self.mapping.clone(), + } + } +} + +impl PartialEq for Gamepad { + #[inline(always)] + fn eq(&self, othr: &Self) -> bool { + self.raw.index() == othr.raw.index() + } +} diff --git a/src/platform_impl/web/stdweb/mod.rs b/src/platform_impl/web/stdweb/mod.rs index d49dbf02ac..9ed51c213e 100644 --- a/src/platform_impl/web/stdweb/mod.rs +++ b/src/platform_impl/web/stdweb/mod.rs @@ -1,6 +1,8 @@ mod canvas; -mod event; +pub mod gamepad; mod timeout; +mod utils; +pub mod window; pub use self::canvas::Canvas; pub use self::timeout::Timeout; @@ -50,3 +52,9 @@ pub fn is_fullscreen(canvas: &CanvasElement) -> bool { None => false, } } + +pub fn get_gamepads() -> impl Iterator { + stdweb::web::Gamepad::get_all() + .into_iter() + .filter_map(|gamepad| gamepad.map(|gamepad| gamepad::Gamepad::new(gamepad))) +} diff --git a/src/platform_impl/web/stdweb/event.rs b/src/platform_impl/web/stdweb/utils.rs similarity index 87% rename from src/platform_impl/web/stdweb/event.rs rename to src/platform_impl/web/stdweb/utils.rs index 14456dc054..bc4e0a029f 100644 --- a/src/platform_impl/web/stdweb/event.rs +++ b/src/platform_impl/web/stdweb/utils.rs @@ -1,7 +1,11 @@ use crate::dpi::LogicalPosition; -use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; +use crate::event::{ModifiersState, MouseButton, ScanCode, VirtualKeyCode}; +use crate::platform_impl::platform::device::gamepad; -use stdweb::web::event::{IKeyboardEvent, IMouseEvent, MouseWheelDeltaMode, MouseWheelEvent}; +use stdweb::web::{ + event::{IKeyboardEvent, IMouseEvent, MouseWheelEvent}, + Gamepad, GamepadMappingType, +}; use stdweb::{js, unstable::TryInto, JsSerialize}; pub fn mouse_button(event: &impl IMouseEvent) -> MouseButton { @@ -30,15 +34,10 @@ pub fn mouse_position(event: &impl IMouseEvent) -> LogicalPosition { } } -pub fn mouse_scroll_delta(event: &MouseWheelEvent) -> Option { +pub fn mouse_scroll_delta(event: &MouseWheelEvent) -> (f64, f64) { let x = event.delta_x(); let y = event.delta_y(); - - match event.delta_mode() { - MouseWheelDeltaMode::Line => Some(MouseScrollDelta::LineDelta(x as f32, y as f32)), - MouseWheelDeltaMode::Pixel => Some(MouseScrollDelta::PixelDelta(LogicalPosition { x, y })), - MouseWheelDeltaMode::Page => None, - } + (x, y) } pub fn scan_code(event: &T) -> ScanCode { @@ -227,3 +226,36 @@ pub fn codepoint(event: &impl IKeyboardEvent) -> char { // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key event.key().chars().next().unwrap() } + +pub fn create_mapping(raw: &Gamepad) -> gamepad::Mapping { + match raw.mapping() { + GamepadMappingType::Standard => { + let mut buttons = [false; 16]; + let mut axes = [0.0; 6]; + + for (index, button) in raw + .buttons() + .into_iter() + .enumerate() + .take(buttons.len()) + { + buttons[index] = button.pressed(); + } + + for (index, axis) in raw.axes().into_iter().enumerate().take(axes.len()) { + axes[index] = axis; + } + + gamepad::Mapping::Standard { buttons, axes } + } + _ => { + let buttons = raw + .buttons() + .into_iter() + .map(|button| button.pressed()) + .collect(); + let axes = raw.axes(); + gamepad::Mapping::NoMapping { buttons, axes } + } + } +} diff --git a/src/platform_impl/web/stdweb/window.rs b/src/platform_impl/web/stdweb/window.rs new file mode 100644 index 0000000000..4b97432fd1 --- /dev/null +++ b/src/platform_impl/web/stdweb/window.rs @@ -0,0 +1,77 @@ +use super::gamepad; +use crate::error::OsError as RootOE; +use std::{cell::RefCell, rc::Rc}; +use stdweb::web; +use stdweb::web::{IEventTarget, event::IGamepadEvent}; + +#[derive(Debug)] +pub struct Shared(pub Rc>); + +#[derive(Debug)] +pub struct Window { + raw: web::Window, + on_gamepad_connected: Option, + on_gamepad_disconnected: Option, +} + +impl Shared { + pub fn create() -> Result { + let global = Window::create()?; + Ok(Shared(Rc::new(RefCell::new(global)))) + } +} + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} + +impl Window { + pub fn create() -> Result { + let raw = stdweb::web::window(); + + Ok(Window { + raw, + on_gamepad_connected: None, + on_gamepad_disconnected: None, + }) + } + + pub fn on_gamepad_connected(&mut self, mut handler: F) + where + F: 'static + FnMut(gamepad::Gamepad), + { + self.on_gamepad_connected = Some(self.add_event( + move |event: stdweb::web::event::GamepadConnectedEvent| { + let gamepad = event.gamepad(); + handler(gamepad::Gamepad::new(gamepad)); + }, + )); + } + + pub fn on_gamepad_disconnected(&mut self, mut handler: F) + where + F: 'static + FnMut(gamepad::Gamepad), + { + self.on_gamepad_connected = Some(self.add_event( + move |event: stdweb::web::event::GamepadDisconnectedEvent| { + let gamepad = event.gamepad(); + handler(gamepad::Gamepad::new(gamepad)); + }, + )); + } + + fn add_event(&self, mut handler: F) -> web::EventListenerHandle + where + E: web::event::ConcreteEvent, + F: 'static + FnMut(E), + { + self.raw.add_event_listener(move |event: E| { + event.stop_propagation(); + event.cancel_bubble(); + + handler(event); + }) + } +} diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 0543055eb6..cc6a5b7fb3 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -1,14 +1,16 @@ -use super::event; +use super::utils; use crate::dpi::{LogicalPosition, LogicalSize}; use crate::error::OsError as RootOE; -use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; +use crate::event::{ModifiersState, MouseButton, ScanCode, VirtualKeyCode}; use crate::platform_impl::OsError; use std::cell::RefCell; use std::rc::Rc; use wasm_bindgen::{closure::Closure, JsCast}; -use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, PointerEvent, WheelEvent}; +use web_sys::{ + Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, PointerEvent, WheelEvent, +}; pub struct Canvas { raw: HtmlCanvasElement, @@ -107,46 +109,56 @@ impl Canvas { where F: 'static + FnMut(), { - self.on_blur = Some(self.add_event("blur", move |_: FocusEvent| { - handler(); - })); + self.on_blur = Some(self.add_event( + "blur", + move |_: FocusEvent| { + handler(); + }, + )); } pub fn on_focus(&mut self, mut handler: F) where F: 'static + FnMut(), { - self.on_focus = Some(self.add_event("focus", move |_: FocusEvent| { - handler(); - })); + self.on_focus = Some(self.add_event( + "focus", + move |_: FocusEvent| { + handler(); + }, + )); } pub fn on_keyboard_release(&mut self, mut handler: F) where F: 'static + FnMut(ScanCode, Option, ModifiersState), { - self.on_keyboard_release = - Some(self.add_user_event("keyup", move |event: KeyboardEvent| { + self.on_keyboard_release = Some(self.add_user_event( + "keyup", + move |event: KeyboardEvent| { handler( - event::scan_code(&event), - event::virtual_key_code(&event), - event::keyboard_modifiers(&event), + utils::scan_code(&event), + utils::virtual_key_code(&event), + utils::keyboard_modifiers(&event), ); - })); + }, + )); } pub fn on_keyboard_press(&mut self, mut handler: F) where F: 'static + FnMut(ScanCode, Option, ModifiersState), { - self.on_keyboard_press = - Some(self.add_user_event("keydown", move |event: KeyboardEvent| { + self.on_keyboard_press = Some(self.add_user_event( + "keydown", + move |event: KeyboardEvent| { handler( - event::scan_code(&event), - event::virtual_key_code(&event), - event::keyboard_modifiers(&event), + utils::scan_code(&event), + utils::virtual_key_code(&event), + utils::keyboard_modifiers(&event), ); - })); + }, + )); } pub fn on_received_character(&mut self, mut handler: F) @@ -161,83 +173,85 @@ impl Canvas { self.on_received_character = Some(self.add_user_event( "keypress", move |event: KeyboardEvent| { - handler(event::codepoint(&event)); + handler(utils::codepoint(&event)); }, )); } - pub fn on_cursor_leave(&mut self, mut handler: F) + pub fn on_mouse_release(&mut self, mut handler: F) where - F: 'static + FnMut(i32), + F: 'static + FnMut(i32, MouseButton), { - self.on_cursor_leave = Some(self.add_event("pointerout", move |event: PointerEvent| { - handler(event.pointer_id()); - })); + self.on_mouse_release = Some(self.add_event( + "pointerup", + move |event: PointerEvent| { + handler(event.pointer_id(), utils::mouse_button(&event)); + }, + )); } - pub fn on_cursor_enter(&mut self, mut handler: F) + pub fn on_mouse_press(&mut self, mut handler: F) where - F: 'static + FnMut(i32), + F: 'static + FnMut(i32, MouseButton), { - self.on_cursor_enter = Some(self.add_event("pointerover", move |event: PointerEvent| { - handler(event.pointer_id()); - })); + self.on_mouse_press = Some(self.add_event( + "pointerdown", + move |event: PointerEvent| { + handler(event.pointer_id(), utils::mouse_button(&event)); + }, + )); } - pub fn on_mouse_release(&mut self, mut handler: F) + pub fn on_mouse_wheel(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseButton, ModifiersState), + F: 'static + FnMut(i32, (f64, f64)), { - self.on_mouse_release = Some(self.add_user_event( - "pointerup", - move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); + self.on_mouse_wheel = Some(self.add_event( + "wheel", + move |event: WheelEvent| { + let delta = utils::mouse_scroll_delta(&event); + handler(0, delta); }, )); } - pub fn on_mouse_press(&mut self, mut handler: F) + pub fn on_cursor_leave(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseButton, ModifiersState), + F: 'static + FnMut(), { - self.on_mouse_press = Some(self.add_user_event( - "pointerdown", - move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); + self.on_cursor_leave = Some(self.add_event( + "pointerout", + move |_event: PointerEvent| { + handler(); }, )); } - pub fn on_cursor_move(&mut self, mut handler: F) + pub fn on_cursor_enter(&mut self, mut handler: F) where - F: 'static + FnMut(i32, LogicalPosition, ModifiersState), + F: 'static + FnMut(), { - self.on_cursor_move = Some(self.add_event("pointermove", move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_position(&event), - event::mouse_modifiers(&event), - ); - })); + self.on_cursor_enter = Some(self.add_event( + "pointerover", + move |_event: PointerEvent| { + handler(); + }, + )); } - pub fn on_mouse_wheel(&mut self, mut handler: F) + pub fn on_cursor_move(&mut self, mut handler: F) where - F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), + F: 'static + FnMut(LogicalPosition, ModifiersState), { - self.on_mouse_wheel = Some(self.add_event("wheel", move |event: WheelEvent| { - if let Some(delta) = event::mouse_scroll_delta(&event) { - handler(0, delta, event::mouse_modifiers(&event)); - } - })); + self.on_cursor_move = Some(self.add_event( + "pointermove", + move |event: PointerEvent| { + handler( + utils::mouse_position(&event), + utils::mouse_modifiers(&event), + ); + }, + )); } pub fn on_fullscreen_change(&mut self, mut handler: F) @@ -248,7 +262,11 @@ impl Canvas { Some(self.add_event("fullscreenchange", move |_: Event| handler())); } - fn add_event(&self, event_name: &str, mut handler: F) -> Closure + fn add_event( + &self, + event_name: &str, + mut handler: F, + ) -> Closure where E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, F: 'static + FnMut(E), @@ -273,7 +291,11 @@ impl Canvas { // The difference between add_event and add_user_event is that the latter has a special meaning // for browser security. A user event is a deliberate action by the user (like a mouse or key // press) and is the only time things like a fullscreen request may be successfully completed.) - fn add_user_event(&self, event_name: &str, mut handler: F) -> Closure + fn add_user_event( + &self, + event_name: &str, + mut handler: F, + ) -> Closure where E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, F: 'static + FnMut(E), @@ -281,16 +303,19 @@ impl Canvas { let wants_fullscreen = self.wants_fullscreen.clone(); let canvas = self.raw.clone(); - self.add_event(event_name, move |event: E| { - handler(event); - - if *wants_fullscreen.borrow() { - canvas - .request_fullscreen() - .expect("Failed to enter fullscreen"); - *wants_fullscreen.borrow_mut() = false; - } - }) + self.add_event( + event_name, + move |event: E| { + handler(event); + + if *wants_fullscreen.borrow() { + canvas + .request_fullscreen() + .expect("Failed to enter fullscreen"); + *wants_fullscreen.borrow_mut() = false; + } + }, + ) } pub fn request_fullscreen(&self) { diff --git a/src/platform_impl/web/web_sys/gamepad.rs b/src/platform_impl/web/web_sys/gamepad.rs new file mode 100644 index 0000000000..922bd645e0 --- /dev/null +++ b/src/platform_impl/web/web_sys/gamepad.rs @@ -0,0 +1,75 @@ +use super::utils; +use crate::platform_impl::platform::device; +use std::cmp::PartialEq; + +#[derive(Debug)] +pub struct Gamepad { + pub(crate) index: i32, + pub(crate) raw: web_sys::Gamepad, + pub(crate) mapping: device::gamepad::Mapping, +} + +impl Gamepad { + pub fn new(raw: web_sys::Gamepad) -> Self { + let mapping = utils::create_mapping(&raw); + + Self { + index: raw.index() as i32, + raw, + mapping, + } + } + + // An integer that is auto-incremented to be unique for each device + // currently connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/index + pub fn index(&self) -> i32 { + self.raw.index() as i32 + } + + // A string containing some information about the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/id + pub fn id(&self) -> String { + self.raw.id() + } + + // A boolean indicating whether the gamepad is still connected to the system. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/connected + pub fn connected(&self) -> bool { + self.raw.connected() + } + + // An array containing GamepadHapticActuator objects, + // each of which represents haptic feedback hardware available on the controller. + // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/hapticActuators + pub fn vibrate(&self, value: f64, duration: f64) { + for actuator in self.raw.haptic_actuators().values() { + actuator.ok().and_then(|a| { + let actuator: web_sys::GamepadHapticActuator = a.into(); + match actuator.type_() { + web_sys::GamepadHapticActuatorType::Vibration => { + actuator.pulse(value, duration).ok() + } + _ => None, + } + }); + } + } +} + +impl Clone for Gamepad { + fn clone(&self) -> Self { + Self { + index: self.index, + raw: self.raw.clone(), + mapping: self.mapping.clone(), + } + } +} + +impl PartialEq for Gamepad { + #[inline(always)] + fn eq(&self, othr: &Self) -> bool { + self.raw.index() == othr.raw.index() + } +} diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 205519d1ea..a795bd9fb1 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -1,9 +1,11 @@ mod canvas; -mod event; +pub mod gamepad; mod timeout; +mod utils; +pub mod window; -pub use self::canvas::Canvas; -pub use self::timeout::Timeout; +pub use canvas::Canvas; +pub use timeout::Timeout; use crate::dpi::LogicalSize; use crate::platform::web::WindowExtWebSys; @@ -68,3 +70,16 @@ pub fn is_fullscreen(canvas: &HtmlCanvasElement) -> bool { None => false, } } + +pub fn get_gamepads() -> impl Iterator { + let mut gamepads: Vec = Vec::new(); + let web_gamepads = web_sys::window().unwrap().navigator().get_gamepads().ok().unwrap(); + for index in 0..web_gamepads.length() { + let jsvalue = web_gamepads.get(index); + if !jsvalue.is_null() { + let gamepad: web_sys::Gamepad = jsvalue.into(); + gamepads.push(gamepad::Gamepad::new(gamepad)); + } + } + gamepads.into_iter() +} diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/utils.rs similarity index 83% rename from src/platform_impl/web/web_sys/event.rs rename to src/platform_impl/web/web_sys/utils.rs index af557b9941..e2b896de83 100644 --- a/src/platform_impl/web/web_sys/event.rs +++ b/src/platform_impl/web/web_sys/utils.rs @@ -1,8 +1,9 @@ use crate::dpi::LogicalPosition; -use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; +use crate::event::{ModifiersState, MouseButton, ScanCode, VirtualKeyCode}; +use crate::platform_impl::platform; use std::convert::TryInto; -use web_sys::{KeyboardEvent, MouseEvent, WheelEvent}; +use web_sys::{Gamepad, GamepadButton, GamepadMappingType, KeyboardEvent, MouseEvent, WheelEvent}; pub fn mouse_button(event: &MouseEvent) -> MouseButton { match event.button() { @@ -29,15 +30,10 @@ pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { } } -pub fn mouse_scroll_delta(event: &WheelEvent) -> Option { +pub fn mouse_scroll_delta(event: &WheelEvent) -> (f64, f64) { let x = event.delta_x(); let y = event.delta_y(); - - match event.delta_mode() { - WheelEvent::DOM_DELTA_LINE => Some(MouseScrollDelta::LineDelta(x as f32, y as f32)), - WheelEvent::DOM_DELTA_PIXEL => Some(MouseScrollDelta::PixelDelta(LogicalPosition { x, y })), - _ => None, - } + (x, y) } pub fn scan_code(event: &KeyboardEvent) -> ScanCode { @@ -225,3 +221,44 @@ pub fn codepoint(event: &KeyboardEvent) -> char { // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key event.key().chars().next().unwrap() } + +pub fn create_mapping(raw: &Gamepad) -> platform::device::gamepad::Mapping { + match raw.mapping() { + GamepadMappingType::Standard => { + let mut buttons = [false; 16]; + let mut axes = [0.0; 6]; + + let gbuttons = raw.buttons(); + for index in 0..buttons.len() { + let button: GamepadButton = gbuttons.get(index as u32).into(); + buttons[index] = button.pressed(); + } + + let gaxes = raw.axes(); + for index in 0..axes.len() { + let axe: f64 = gaxes.get(index as u32).as_f64().unwrap_or(0.0); + axes[index] = axe; + } + + platform::device::gamepad::Mapping::Standard { buttons, axes } + } + _ => { + let mut buttons: Vec = Vec::new(); + let mut axes: Vec = Vec::new(); + + let gbuttons = raw.buttons(); + for index in 0..gbuttons.length() { + let button: GamepadButton = gbuttons.get(index as u32).into(); + buttons.push(button.pressed()); + } + + let gaxes = raw.axes(); + for index in 0..gaxes.length() { + let axe: f64 = gaxes.get(index as u32).as_f64().unwrap_or(0.0); + axes.push(axe); + } + + platform::device::gamepad::Mapping::NoMapping { buttons, axes } + } + } +} diff --git a/src/platform_impl/web/web_sys/window.rs b/src/platform_impl/web/web_sys/window.rs new file mode 100644 index 0000000000..9bb4cee450 --- /dev/null +++ b/src/platform_impl/web/web_sys/window.rs @@ -0,0 +1,88 @@ +use super::gamepad; +use crate::error::OsError as RootOE; +use crate::platform_impl::OsError; +use std::{cell::RefCell, rc::Rc}; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::GamepadEvent; + +#[derive(Debug)] +pub struct Shared(pub Rc>); + +#[derive(Debug)] +pub struct Window { + raw: web_sys::Window, + on_gamepad_connected: Option>, + on_gamepad_disconnected: Option>, +} + +impl Shared { + pub fn create() -> Result { + let global = Window::create()?; + Ok(Shared(Rc::new(RefCell::new(global)))) + } +} + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} + +impl Window { + pub fn create() -> Result { + let raw = + web_sys::window().ok_or(os_error!(OsError("Failed to obtain window".to_owned())))?; + + Ok(Window { + raw, + on_gamepad_connected: None, + on_gamepad_disconnected: None, + }) + } + + pub fn on_gamepad_connected(&mut self, mut handler: F) + where + F: 'static + FnMut(gamepad::Gamepad), + { + self.on_gamepad_connected = Some(self.add_event( + "gamepadconnected", + move |event: GamepadEvent| { + let gamepad = event + .gamepad() + .expect("[gamepadconnected] expected gamepad"); + handler(gamepad::Gamepad::new(gamepad)); + }, + )) + } + + pub fn on_gamepad_disconnected(&mut self, mut handler: F) + where + F: 'static + FnMut(gamepad::Gamepad), + { + self.on_gamepad_disconnected = Some(self.add_event( + "gamepaddisconnected", + move |event: GamepadEvent| { + let gamepad = event + .gamepad() + .expect("[gamepaddisconnected] expected gamepad"); + handler(gamepad::Gamepad::new(gamepad)); + }, + )) + } + + fn add_event(&self, event_name: &str, mut handler: F) -> Closure + where + E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, + F: 'static + FnMut(E), + { + let closure = Closure::wrap(Box::new(move |event: E| { + handler(event); + }) as Box); + + self.raw + .add_event_listener_with_callback(event_name, &closure.as_ref().unchecked_ref()) + .expect("Failed to add event listener with callback"); + + closure + } +} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 8752d8b639..74a7c4fc42 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -34,6 +34,7 @@ impl Window { let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); + target.register_global_events()?; target.register(&mut canvas, id); let window = Window { From 79b168cf3ff8c38dd213a91d6f6f04eaad2906fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20R=C3=B8yset?= Date: Fri, 25 Jun 2021 16:41:43 +0200 Subject: [PATCH 3/8] Revert "Gamepad device events - Web/WASM (#1414)" This reverts commit e004bd2bb39d1f2f0bd86eeecf2351c9bf3d7d1f. --- Cargo.toml | 17 +- examples/web/gamepad/stdweb/Cargo.toml | 11 -- examples/web/gamepad/stdweb/src/main.rs | 80 --------- examples/web/gamepad/websys/.gitignore | 7 - examples/web/gamepad/websys/Cargo.toml | 20 --- .../web/gamepad/websys/files/gamepad.html | 23 --- examples/web/gamepad/websys/src/lib.rs | 78 -------- examples/web/gamepad/websys/src/utils.rs | 10 -- src/event.rs | 2 +- src/platform_impl/web/device.rs | 8 + .../web/device/gamepad/constants.rs | 37 ---- .../web/device/gamepad/manager.rs | 167 ------------------ .../web/device/gamepad/mapping.rs | 23 --- src/platform_impl/web/device/gamepad/mod.rs | 99 ----------- src/platform_impl/web/device/gamepad/utils.rs | 44 ----- src/platform_impl/web/device/mod.rs | 161 ----------------- src/platform_impl/web/event_loop/global.rs | 81 --------- src/platform_impl/web/event_loop/mod.rs | 17 -- src/platform_impl/web/event_loop/runner.rs | 1 - .../web/event_loop/window_target.rs | 43 +++-- src/platform_impl/web/web_sys/canvas.rs | 22 +-- .../web/web_sys/{utils.rs => event.rs} | 44 +---- src/platform_impl/web/web_sys/gamepad.rs | 75 -------- src/platform_impl/web/web_sys/mod.rs | 2 - src/platform_impl/web/web_sys/window.rs | 88 --------- src/platform_impl/web/window.rs | 1 - 26 files changed, 48 insertions(+), 1113 deletions(-) delete mode 100644 examples/web/gamepad/stdweb/Cargo.toml delete mode 100644 examples/web/gamepad/stdweb/src/main.rs delete mode 100644 examples/web/gamepad/websys/.gitignore delete mode 100644 examples/web/gamepad/websys/Cargo.toml delete mode 100644 examples/web/gamepad/websys/files/gamepad.html delete mode 100644 examples/web/gamepad/websys/src/lib.rs delete mode 100644 examples/web/gamepad/websys/src/utils.rs create mode 100644 src/platform_impl/web/device.rs delete mode 100644 src/platform_impl/web/device/gamepad/constants.rs delete mode 100644 src/platform_impl/web/device/gamepad/manager.rs delete mode 100644 src/platform_impl/web/device/gamepad/mapping.rs delete mode 100644 src/platform_impl/web/device/gamepad/mod.rs delete mode 100644 src/platform_impl/web/device/gamepad/utils.rs delete mode 100644 src/platform_impl/web/device/mod.rs delete mode 100644 src/platform_impl/web/event_loop/global.rs rename src/platform_impl/web/web_sys/{utils.rs => event.rs} (86%) delete mode 100644 src/platform_impl/web/web_sys/gamepad.rs delete mode 100644 src/platform_impl/web/web_sys/window.rs diff --git a/Cargo.toml b/Cargo.toml index 202e921318..bae24cd0c3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,24 +115,9 @@ features = [ 'MediaQueryListEvent', 'MouseEvent', 'Node', - 'Navigator', 'PointerEvent', 'Window', - 'WheelEvent', - 'Gamepad', - 'GamepadAxisMoveEvent', - 'GamepadAxisMoveEventInit', - 'GamepadButton', - 'GamepadButtonEvent', - 'GamepadButtonEventInit', - 'GamepadEvent', - 'GamepadEventInit', - 'GamepadHand', - 'GamepadHapticActuator', - 'GamepadHapticActuatorType', - 'GamepadMappingType', - 'GamepadPose', - 'GamepadServiceTest' + 'WheelEvent' ] [target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] diff --git a/examples/web/gamepad/stdweb/Cargo.toml b/examples/web/gamepad/stdweb/Cargo.toml deleted file mode 100644 index 83b442c6bb..0000000000 --- a/examples/web/gamepad/stdweb/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "stdweb-gamepad" -version = "0.1.0" -authors = ["furiouzz "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -winit = { path = "../../../../", features = [ "stdweb" ] } -stdweb = "0.4.20" \ No newline at end of file diff --git a/examples/web/gamepad/stdweb/src/main.rs b/examples/web/gamepad/stdweb/src/main.rs deleted file mode 100644 index 7b545bc18d..0000000000 --- a/examples/web/gamepad/stdweb/src/main.rs +++ /dev/null @@ -1,80 +0,0 @@ -use winit::{ - event::{device::GamepadEvent, Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, -}; - -use stdweb::js; - -/** - * Build example (from examples/web/gamepad/stdweb): - * cargo web build - * Run example (from examples/web/gamepad/stdweb): - * cargo web start - * Development (from project root): - * npx nodemon --watch src --watch examples/web/gamepad/stdweb/src -e rs --exec 'cargo web check' - */ - -pub fn main() { - let event_loop = EventLoop::new(); - - let _window = WindowBuilder::new() - .with_title("Gamepad tests") - .build(&event_loop) - .unwrap(); - - let deadzone = 0.12; - - event_loop.run(move |event, _, control_flow| match event { - Event::GamepadEvent(gamepad_handle, event) => match event { - GamepadEvent::Axis { - axis_id, - axis, - value, - stick, - } if value > deadzone => { - let string = format!("Axis {:#?} {:#?} {:#?} {:#?}", axis_id, axis, value, stick); - js! { console.log( @{string} ); } - } - - GamepadEvent::Stick { - x_id, - y_id, - x_value, - y_value, - side, - } if (x_value.powi(2) + y_value.powi(2)).sqrt() > deadzone => { - let string = format!( - "Stick {:#?} {:#?} {:#?} {:#?} {:#?}", - x_id, y_id, x_value, y_value, side - ); - js! { console.log( @{string} ); } - } - - GamepadEvent::Button { - button_id, - button, - state, - } => { - let string = format!("Button {:#?} {:#?} {:#?}", button_id, button, state); - js! { console.log( @{string} ); } - } - - GamepadEvent::Added => { - let string = format!("[{:?}] {:#?}", gamepad_handle, event); - js! { console.log( @{string} ); } - } - GamepadEvent::Removed => { - let string = format!("[{:?}] {:#?}", gamepad_handle, event); - js! { console.log( @{string} ); } - } - - _ => {} - }, - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), - }); -} diff --git a/examples/web/gamepad/websys/.gitignore b/examples/web/gamepad/websys/.gitignore deleted file mode 100644 index 8d63731034..0000000000 --- a/examples/web/gamepad/websys/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -/target -**/*.rs.bk -Cargo.lock -bin/ -pkg/ -wasm-pack.log -.DS_Store \ No newline at end of file diff --git a/examples/web/gamepad/websys/Cargo.toml b/examples/web/gamepad/websys/Cargo.toml deleted file mode 100644 index c142f168d5..0000000000 --- a/examples/web/gamepad/websys/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "websys-gamepad" -version = "0.0.1" -authors = ["The winit contributors", "Pierre Krieger "] -edition = "2018" - -[lib] -crate-type = ["cdylib", "rlib"] - -[dependencies] -winit = { path = "../../../../", features = [ "web-sys" ] } -wasm-bindgen = "0.2.45" -wasm-bindgen-test = "0.3.8" -web-sys = { version = "0.3.22", features = [ "console" ] } - -# The `console_error_panic_hook` crate provides better debugging of panics by -# logging them with `console.error`. This is great for development, but requires -# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for -# code size when deploying. -console_error_panic_hook = "0.1.6" \ No newline at end of file diff --git a/examples/web/gamepad/websys/files/gamepad.html b/examples/web/gamepad/websys/files/gamepad.html deleted file mode 100644 index a92272492d..0000000000 --- a/examples/web/gamepad/websys/files/gamepad.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - Gamepad - - - - - - - \ No newline at end of file diff --git a/examples/web/gamepad/websys/src/lib.rs b/examples/web/gamepad/websys/src/lib.rs deleted file mode 100644 index 7c95db9a9e..0000000000 --- a/examples/web/gamepad/websys/src/lib.rs +++ /dev/null @@ -1,78 +0,0 @@ -mod utils; - -use wasm_bindgen::prelude::*; -use winit::{ - event::{device::GamepadEvent, Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, -}; - -/** - * Build example (from examples/gamepad/websys): - * wasm-pack build --target web - * Run web server (from examples/gamepad/websys): - * npx http-server - * Open your browser at http://localhost:8000/files/${EXAMPLE}.html - * Development (from project root): - * npx nodemon --watch src --watch examples/web/gamepad/websys/src -e rs --exec 'cd examples/web/gamepad/websys && wasm-pack build --target web' - */ - -macro_rules! console_log { - ($($t:tt)*) => (web_sys::console::log_1(&format_args!($($t)*).to_string().into())) -} - -#[wasm_bindgen(start)] -pub fn example_gamepad() { - utils::set_panic_hook(); // needed for error stack trace - let event_loop = EventLoop::new(); - - let _window = WindowBuilder::new() - .with_title("Gamepad tests") - .build(&event_loop) - .unwrap(); - - let deadzone = 0.12; - - event_loop.run(move |event, _, control_flow| { - match event { - Event::GamepadEvent(gamepad_handle, event) => { - match event { - GamepadEvent::Axis { - axis_id, - axis, - value, - stick, - } if value > deadzone => { - console_log!("Axis {:#?} {:#?} {:#?} {:#?}", axis_id, axis, value, stick) - }, - - GamepadEvent::Stick { - x_id, y_id, x_value, y_value, side - } if (x_value.powi(2) + y_value.powi(2)).sqrt() > deadzone => { - console_log!("Stick {:#?} {:#?} {:#?} {:#?} {:#?}", x_id, y_id, x_value, y_value, side) - }, - - GamepadEvent::Button { - button_id, - button, - state, - } => { - console_log!("Button {:#?} {:#?} {:#?}", button_id, button, state) - }, - - GamepadEvent::Added => { - console_log!("[{:?}] {:#?}", gamepad_handle, event) - }, - GamepadEvent::Removed => console_log!("[{:?}] {:#?}", gamepad_handle, event), - - _ => {}, - } - } - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => (), - } - }); -} diff --git a/examples/web/gamepad/websys/src/utils.rs b/examples/web/gamepad/websys/src/utils.rs deleted file mode 100644 index 02aa5c19ef..0000000000 --- a/examples/web/gamepad/websys/src/utils.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub fn set_panic_hook() { - // When the `console_error_panic_hook` feature is enabled, we can call the - // `set_panic_hook` function at least once during initialization, and then - // we will get better error messages if our code ever panics. - // - // For more details see - // https://github.com/rustwasm/console_error_panic_hook#readme - // #[cfg(feature = "console_error_panic_hook")] - console_error_panic_hook::set_once(); -} \ No newline at end of file diff --git a/src/event.rs b/src/event.rs index de7ab077e9..721ec5b095 100644 --- a/src/event.rs +++ b/src/event.rs @@ -191,7 +191,7 @@ impl<'a, T> Event<'a, T> { } /// The reason the event loop is resuming. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum StartCause { /// Sent if the time specified by `ControlFlow::WaitUntil` has been reached. Contains the /// moment the timeout was requested and the requested resume time. The actual resume time is diff --git a/src/platform_impl/web/device.rs b/src/platform_impl/web/device.rs new file mode 100644 index 0000000000..a2f00b69c4 --- /dev/null +++ b/src/platform_impl/web/device.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(pub i32); + +impl Id { + pub unsafe fn dummy() -> Self { + Id(0) + } +} diff --git a/src/platform_impl/web/device/gamepad/constants.rs b/src/platform_impl/web/device/gamepad/constants.rs deleted file mode 100644 index c2c589adc1..0000000000 --- a/src/platform_impl/web/device/gamepad/constants.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::event::device::{GamepadAxis, GamepadButton}; - -pub(crate) static BUTTONS: [GamepadButton; 16] = [ - GamepadButton::South, - GamepadButton::East, - GamepadButton::West, - GamepadButton::North, - GamepadButton::LeftTrigger, - GamepadButton::RightTrigger, - GamepadButton::LeftShoulder, - GamepadButton::RightShoulder, - GamepadButton::Select, - GamepadButton::Start, - GamepadButton::LeftStick, - GamepadButton::RightStick, - GamepadButton::DPadUp, - GamepadButton::DPadDown, - GamepadButton::DPadLeft, - GamepadButton::DPadRight, -]; - -pub(crate) static AXES: [GamepadAxis; 6] = [ - GamepadAxis::LeftStickX, - GamepadAxis::LeftStickY, - GamepadAxis::RightStickX, - GamepadAxis::RightStickY, - GamepadAxis::LeftTrigger, - GamepadAxis::RightTrigger, -]; - -pub(crate) fn button_code(index: usize) -> Option { - BUTTONS.get(index).map(|ev| ev.clone()) -} - -pub(crate) fn axis_code(index: usize) -> Option { - AXES.get(index).map(|ev| ev.clone()) -} diff --git a/src/platform_impl/web/device/gamepad/manager.rs b/src/platform_impl/web/device/gamepad/manager.rs deleted file mode 100644 index ec98ade4f7..0000000000 --- a/src/platform_impl/web/device/gamepad/manager.rs +++ /dev/null @@ -1,167 +0,0 @@ -use super::utils; -use crate::event::device; -use crate::platform_impl::platform::{backend, device::gamepad, GamepadHandle, event_loop::global}; -use std::collections::VecDeque; - -pub struct Manager { - pub(crate) gamepads: Vec, - pub(crate) events: VecDeque<(backend::gamepad::Gamepad, device::GamepadEvent)>, - pub(crate) global_window: Option, -} - -impl Manager { - pub fn new() -> Self { - Self { - gamepads: Vec::new(), - events: VecDeque::new(), - global_window: None, - } - } - - // Register global window to fetch gamepads. - // Due to Chrome issue, I prefer to use its gamepad list - pub fn set_global_window(&mut self, global_window: global::Shared) { - self.global_window.replace(global_window); - } - - // Get an updated raw gamepad and generate a new mapping - pub fn collect_gamepads(&self) -> Option> { - self.global_window.as_ref().map(|w| w.get_gamepads()) - } - - // Collect gamepad events (buttons/axes/sticks) - // dispatch to handler and update gamepads - pub fn collect_events(&mut self, mut handler: F) - where - F: 'static + FnMut((device::GamepadHandle, device::GamepadEvent)), - { - let opt_new_gamepads = self.collect_gamepads(); - if opt_new_gamepads.is_none() { - return; - } - - let new_gamepads = opt_new_gamepads.unwrap(); - let old_gamepads = &self.gamepads; - - let mut old_index = 0; - let mut new_index = 0; - - // Collect events - loop { - match (old_gamepads.get(old_index), new_gamepads.get(new_index)) { - (Some(old), Some(new)) if old.index() == new.index() => { - // Button events - let buttons = old.mapping.buttons().zip(new.mapping.buttons()).enumerate(); - for (btn_index, (old_button, new_button)) in buttons { - match (old_button, new_button) { - (false, true) => { - self.events.push_back((new.clone(), utils::gamepad_button(btn_index, true))) - } - (true, false) => { - self.events.push_back((new.clone(), utils::gamepad_button(btn_index, false))) - } - _ => (), - } - } - - // Axis events - let axes = old.mapping.axes().zip(new.mapping.axes()).enumerate(); - for (axis_index, (old_axis, new_axis)) in axes { - if old_axis != new_axis { - self.events.push_back((new.clone(), utils::gamepad_axis(axis_index, new_axis))) - } - } - - // Stick events - let mut old_axes = old.mapping.axes(); - let mut new_axes = new.mapping.axes(); - - let old_left = (old_axes.next(), old_axes.next()); - let new_left = (new_axes.next(), new_axes.next()); - if old_left != new_left { - if let (Some(x), Some(y)) = (new_left.0, new_left.1) { - self.events.push_back(( - new.clone(), - utils::gamepad_stick(0, 1, x, y, device::Side::Left), - )); - } - } - - let old_right = (old_axes.next(), old_axes.next()); - let new_right = (new_axes.next(), new_axes.next()); - if old_right != new_right { - if let (Some(x), Some(y)) = (new_right.0, new_right.1) { - self.events.push_back(( - new.clone(), - utils::gamepad_stick(2, 3, x, y, device::Side::Right), - )); - } - } - - // Increment indices - old_index += 1; - new_index += 1; - }, - - // Connect - (None, Some(new)) => { - self.events.push_back(( - new.clone(), - device::GamepadEvent::Added, - )); - new_index += 1; - }, - - // Connect - (Some(old), Some(new)) if old.index > new.index => { - self.events.push_back(( - new.clone(), - device::GamepadEvent::Added, - )); - new_index += 1; - }, - - // Disconnect - (Some(old), Some(_new)) => { - self.events.push_back(( - old.clone(), - device::GamepadEvent::Removed, - )); - old_index += 1; - }, - - // Disconnect - (Some(old), None) => { - self.events.push_back(( - old.clone(), - device::GamepadEvent::Removed, - )); - old_index += 1; - }, - - // Break loop - (None, None) => { - break - } - } - } - - // Dispatch events and drain events vec - loop { - if let Some((gamepad, event)) = self.events.pop_front() { - handler(( - device::GamepadHandle(GamepadHandle { - id: gamepad.index, - gamepad: gamepad::Shared::Raw(gamepad), - }), - event, - )); - } else { - break; - } - } - - // Update gamepads - self.gamepads = new_gamepads; - } -} diff --git a/src/platform_impl/web/device/gamepad/mapping.rs b/src/platform_impl/web/device/gamepad/mapping.rs deleted file mode 100644 index c424b73789..0000000000 --- a/src/platform_impl/web/device/gamepad/mapping.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[derive(Debug, Clone)] -pub enum Mapping { - Standard { buttons: [bool; 16], axes: [f64; 6] }, - NoMapping { buttons: Vec, axes: Vec }, -} - -impl Mapping { - pub(crate) fn buttons<'a>(&'a self) -> impl Iterator + 'a { - match self { - Mapping::Standard { buttons, .. } => buttons.iter(), - Mapping::NoMapping { buttons, .. } => buttons.iter(), - } - .cloned() - } - - pub(crate) fn axes<'a>(&'a self) -> impl Iterator + 'a { - match self { - Mapping::Standard { axes, .. } => axes.iter(), - Mapping::NoMapping { axes, .. } => axes.iter(), - } - .cloned() - } -} diff --git a/src/platform_impl/web/device/gamepad/mod.rs b/src/platform_impl/web/device/gamepad/mod.rs deleted file mode 100644 index 7ed8e7abce..0000000000 --- a/src/platform_impl/web/device/gamepad/mod.rs +++ /dev/null @@ -1,99 +0,0 @@ -mod manager; -mod mapping; -mod utils; - -pub mod constants; -pub use manager::Manager; -pub use mapping::Mapping; - -use crate::event::device::{BatteryLevel, RumbleError}; -use crate::platform_impl::platform::backend; -use std::fmt; - -pub enum Shared { - Raw(backend::gamepad::Gamepad), - Dummy, -} - -impl Shared { - // An integer that is auto-incremented to be unique for each device - // currently connected to the system. - // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/index - pub fn id(&self) -> i32 { - match self { - Shared::Raw(g) => g.index() as i32, - Shared::Dummy => -1, - } - } - - // A string containing some information about the controller. - // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/id - pub fn info(&self) -> String { - match self { - Shared::Raw(g) => g.id(), - Shared::Dummy => String::new(), - } - } - - // A boolean indicating whether the gamepad is still connected to the system. - // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/connected - pub fn connected(&self) -> bool { - match self { - Shared::Raw(g) => g.connected(), - Shared::Dummy => false, - } - } - - // [EXPERIMENTAL] An array containing GamepadHapticActuator objects, - // each of which represents haptic feedback hardware available on the controller. - // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/hapticActuators - pub fn rumble(&self, left_speed: f64, _right_speed: f64) -> Result<(), RumbleError> { - match self { - Shared::Dummy => Ok(()), - Shared::Raw(g) => { - g.vibrate(left_speed, 1000f64); - Ok(()) - } - } - } - - pub fn is_dummy(&self) -> bool { - match self { - Shared::Dummy => true, - _ => false, - } - } - - pub fn port(&self) -> Option { - None - } - - pub fn battery_level(&self) -> Option { - None - } -} - -impl Clone for Shared { - fn clone(&self) -> Self { - match self { - Shared::Raw(g) => Shared::Raw(g.clone()), - Shared::Dummy => Shared::Dummy, - } - } -} - -impl Default for Shared { - fn default() -> Self { - Shared::Dummy - } -} - -impl fmt::Debug for Shared { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { - if self.is_dummy() { - write!(f, "Gamepad (Dummy)") - } else { - write!(f, "Gamepad ({}#{})", self.id(), self.info()) - } - } -} diff --git a/src/platform_impl/web/device/gamepad/utils.rs b/src/platform_impl/web/device/gamepad/utils.rs deleted file mode 100644 index 4bc5154c6a..0000000000 --- a/src/platform_impl/web/device/gamepad/utils.rs +++ /dev/null @@ -1,44 +0,0 @@ -use crate::event::{ElementState, device}; -use super::constants; - -pub fn gamepad_button(code: usize, pressed: bool) -> device::GamepadEvent { - let button_id = code as u32; - let button = constants::button_code(code); - - let state = if pressed { - ElementState::Pressed - } else { - ElementState::Released - }; - - device::GamepadEvent::Button { - button_id, - button, - state, - } -} - -pub fn gamepad_axis(code: usize, value: f64) -> device::GamepadEvent { - let axis_id = code as u32; - let axis = constants::axis_code(code); - - device::GamepadEvent::Axis { - axis_id, - axis, - value, - stick: true, - } -} - -pub fn gamepad_stick(x_code: usize, y_code: usize, x_value: f64, y_value: f64, side: device::Side) -> device::GamepadEvent { - let x_id = x_code as u32; - let y_id = y_code as u32; - - device::GamepadEvent::Stick { - x_id, - y_id, - x_value, - y_value, - side, - } -} diff --git a/src/platform_impl/web/device/mod.rs b/src/platform_impl/web/device/mod.rs deleted file mode 100644 index eb4108bade..0000000000 --- a/src/platform_impl/web/device/mod.rs +++ /dev/null @@ -1,161 +0,0 @@ -pub mod gamepad; - -use super::event_loop::EventLoop; -use crate::event::device; - -use std::{ - cmp::{Eq, Ordering, PartialEq, PartialOrd}, - hash::{Hash, Hasher}, -}; - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct MouseId(pub i32); - -unsafe impl Send for MouseId {} -unsafe impl Sync for MouseId {} - -impl MouseId { - pub unsafe fn dummy() -> Self { - Self(0) - } - - pub fn is_connected(&self) -> bool { - false - } - - pub fn enumerate<'a, T>( - event_loop: &'a EventLoop, - ) -> impl 'a + Iterator { - event_loop.mice() - } -} - -impl From for device::MouseId { - fn from(platform_id: MouseId) -> Self { - Self(platform_id) - } -} -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct KeyboardId(pub i32); - -unsafe impl Send for KeyboardId {} -unsafe impl Sync for KeyboardId {} - -impl KeyboardId { - pub unsafe fn dummy() -> Self { - Self(0) - } - - pub fn is_connected(&self) -> bool { - false - } - - pub fn enumerate<'a, T>( - event_loop: &'a EventLoop, - ) -> impl 'a + Iterator { - event_loop.keyboards() - } -} - -impl From for device::KeyboardId { - fn from(platform_id: KeyboardId) -> Self { - Self(platform_id) - } -} - -#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] -pub(crate) struct HidId(pub i32); - -unsafe impl Send for HidId {} -unsafe impl Sync for HidId {} - -impl HidId { - pub unsafe fn dummy() -> Self { - Self(0) - } - - pub fn is_connected(&self) -> bool { - false - } - - pub fn enumerate<'a, T>( - event_loop: &'a EventLoop, - ) -> impl 'a + Iterator { - event_loop.hids() - } -} - -impl From for device::HidId { - fn from(platform_id: HidId) -> Self { - Self(platform_id) - } -} - -#[derive(Clone, Debug)] -pub(crate) struct GamepadHandle { - pub(crate) id: i32, - pub(crate) gamepad: gamepad::Shared, -} - -unsafe impl Send for GamepadHandle {} -unsafe impl Sync for GamepadHandle {} - -impl GamepadHandle { - pub unsafe fn dummy() -> Self { - Self { - id: -1, - gamepad: gamepad::Shared::default(), - } - } - - pub fn is_connected(&self) -> bool { - self.gamepad.connected() - } - - pub fn enumerate<'a, T>( - event_loop: &'a EventLoop, - ) -> impl 'a + Iterator { - event_loop.gamepads() - } - - pub fn rumble(&self, left_speed: f64, right_speed: f64) -> Result<(), device::RumbleError> { - self.gamepad.rumble(left_speed, right_speed) - } - - pub fn port(&self) -> Option { - self.gamepad.port() - } - - pub fn battery_level(&self) -> Option { - self.gamepad.battery_level() - } -} - -impl Eq for GamepadHandle {} - -impl PartialEq for GamepadHandle { - #[inline(always)] - fn eq(&self, othr: &Self) -> bool { - self.id == othr.id - } -} - -impl Ord for GamepadHandle { - #[inline(always)] - fn cmp(&self, othr: &Self) -> Ordering { - self.id.cmp(&othr.id) - } -} -impl PartialOrd for GamepadHandle { - #[inline(always)] - fn partial_cmp(&self, othr: &Self) -> Option { - self.id.partial_cmp(&othr.id) - } -} - -impl Hash for GamepadHandle { - #[inline(always)] - fn hash(&self, state: &mut H) { - self.id.hash(state) - } -} diff --git a/src/platform_impl/web/event_loop/global.rs b/src/platform_impl/web/event_loop/global.rs deleted file mode 100644 index d6e1907532..0000000000 --- a/src/platform_impl/web/event_loop/global.rs +++ /dev/null @@ -1,81 +0,0 @@ -use super::super::device::{gamepad, GamepadHandle}; -use super::backend; -use crate::event::device; -use std::{cell::RefCell, rc::Rc, collections::HashSet}; - -#[derive(Debug)] -pub struct Window { - raw: RefCell>, - gamepads: Rc>>, -} - -#[derive(Debug)] -pub struct Shared(Rc); - -impl Shared { - pub fn new() -> Self { - Self(Rc::new(Window { - raw: RefCell::new(None), - gamepads: Rc::new(RefCell::new(HashSet::new())), - })) - } - - // Request window object and listen global events - pub fn register_events(&self) -> Result<(), crate::error::OsError> { - if (*self.0.raw.borrow()).is_none() { - let shared = backend::window::Shared::create()?; - let mut window = shared.0.borrow_mut(); - - let shared_gamepads = self.0.gamepads.clone(); - window.on_gamepad_connected(move |gamepad: backend::gamepad::Gamepad| { - let mut gamepads = shared_gamepads.borrow_mut(); - let index = gamepad.index(); - if !gamepads.contains(&index) { - gamepads.insert(index); - } - }); - - let shared_gamepads = self.0.gamepads.clone(); - window.on_gamepad_disconnected(move |gamepad: backend::gamepad::Gamepad| { - let mut gamepads = shared_gamepads.borrow_mut(); - let index = gamepad.index(); - if gamepads.contains(&index) { - gamepads.remove(&index); - } - }); - - self.0.raw.replace(Some(shared.clone())); - } - - Ok(()) - } - - // Google Chrome create an array of [null, null, null, null]. - // To fix that issue, I create my own list of gamepads - // by listening "gamepadconnected" and "gamepaddisconnected" - pub fn get_gamepads(&self) -> Vec { - let gamepads = self.0.gamepads.borrow_mut(); - backend::get_gamepads() - .filter(|g| gamepads.contains(&g.index())) - .collect() - } - - // Return gamepads handles required for EventLoop::gamepads() - pub fn get_gamepad_handles(&self) -> Vec { - self.get_gamepads() - .iter() - .map(|gamepad| { - device::GamepadHandle(GamepadHandle { - id: gamepad.index, - gamepad: gamepad::Shared::Raw(gamepad.clone()), - }) - }) - .collect() - } -} - -impl Clone for Shared { - fn clone(&self) -> Self { - Shared(self.0.clone()) - } -} diff --git a/src/platform_impl/web/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs index ccdef374d6..24be4d3dcc 100644 --- a/src/platform_impl/web/event_loop/mod.rs +++ b/src/platform_impl/web/event_loop/mod.rs @@ -1,4 +1,3 @@ -pub(crate) mod global; mod proxy; mod runner; mod state; @@ -56,20 +55,4 @@ impl EventLoop { pub fn window_target(&self) -> &root::EventLoopWindowTarget { &self.elw } - - pub fn mice(&self) -> impl '_ + Iterator { - std::iter::empty() - } - - pub fn keyboards(&self) -> impl '_ + Iterator { - std::iter::empty() - } - - pub fn hids(&self) -> impl '_ + Iterator { - std::iter::empty() - } - - pub fn gamepads(&self) -> impl '_ + Iterator { - self.elw.p.collect_gamepads().into_iter() - } } diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index b692124a80..4c51a8a36a 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -1,7 +1,6 @@ use super::{super::ScaleChangeArgs, backend, state::State}; use crate::event::{Event, StartCause}; use crate::event_loop as root; -use crate::platform_impl::platform::device::gamepad; use crate::window::WindowId; use instant::{Duration, Instant}; diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs index 38b5c6aae7..367556a7a1 100644 --- a/src/platform_impl/web/event_loop/window_target.rs +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -13,14 +13,12 @@ use std::rc::Rc; pub struct WindowTarget { pub(crate) runner: runner::Shared, - pub(crate) global_window: global::Shared, } impl Clone for WindowTarget { fn clone(&self) -> Self { WindowTarget { runner: self.runner.clone(), - global_window: self.global_window.clone(), } } } @@ -29,7 +27,6 @@ impl WindowTarget { pub fn new() -> Self { WindowTarget { runner: runner::Shared::new(), - global_window: global::Shared::new(), } } @@ -115,18 +112,22 @@ impl WindowTarget { }); let runner = self.runner.clone(); - canvas.on_cursor_leave(move || { + canvas.on_cursor_leave(move |pointer_id| { runner.send_event(Event::WindowEvent { window_id: WindowId(id), - event: WindowEvent::CursorLeft, + event: WindowEvent::CursorLeft { + device_id: DeviceId(device::Id(pointer_id)), + }, }); }); let runner = self.runner.clone(); - canvas.on_cursor_enter(move || { + canvas.on_cursor_enter(move |pointer_id| { runner.send_event(Event::WindowEvent { window_id: WindowId(id), - event: WindowEvent::CursorEntered, + event: WindowEvent::CursorEntered { + device_id: DeviceId(device::Id(pointer_id)), + }, }); }); @@ -135,6 +136,7 @@ impl WindowTarget { runner.send_event(Event::WindowEvent { window_id: WindowId(id), event: WindowEvent::CursorMoved { + device_id: DeviceId(device::Id(pointer_id)), position, modifiers, }, @@ -174,22 +176,29 @@ impl WindowTarget { }); let runner = self.runner.clone(); - canvas.on_mouse_release(move |pointer_id, button| { - runner.send_event(Event::MouseEvent( - device::MouseId(MouseId(pointer_id)), - device::MouseEvent::Button { + canvas.on_mouse_release(move |pointer_id, button, modifiers| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(id), + event: WindowEvent::MouseInput { + device_id: DeviceId(device::Id(pointer_id)), state: ElementState::Released, button, + modifiers, }, - )); + }); }); let runner = self.runner.clone(); - canvas.on_mouse_wheel(move |pointer_id, delta| { - runner.send_event(Event::MouseEvent( - device::MouseId(MouseId(pointer_id)), - device::MouseEvent::Wheel(delta.0, delta.1), - )); + canvas.on_mouse_wheel(move |pointer_id, delta, modifiers| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(id), + event: WindowEvent::MouseWheel { + device_id: DeviceId(device::Id(pointer_id)), + delta, + phase: TouchPhase::Moved, + modifiers, + }, + }); }); let runner = self.runner.clone(); diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 8bcdcdc500..9725e8b6b8 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -143,9 +143,9 @@ impl Canvas { move |event: KeyboardEvent| { event.prevent_default(); handler( - utils::scan_code(&event), - utils::virtual_key_code(&event), - utils::keyboard_modifiers(&event), + event::scan_code(&event), + event::virtual_key_code(&event), + event::keyboard_modifiers(&event), ); }, )); @@ -170,9 +170,9 @@ impl Canvas { event.prevent_default(); } handler( - utils::scan_code(&event), - utils::virtual_key_code(&event), - utils::keyboard_modifiers(&event), + event::scan_code(&event), + event::virtual_key_code(&event), + event::keyboard_modifiers(&event), ); }, )); @@ -199,7 +199,7 @@ impl Canvas { pub fn on_cursor_leave(&mut self, handler: F) where - F: 'static + FnMut(i32, MouseButton), + F: 'static + FnMut(i32), { match &mut self.mouse_state { MouseState::HasPointerEvent(h) => h.on_cursor_leave(&self.common, handler), @@ -209,7 +209,7 @@ impl Canvas { pub fn on_cursor_enter(&mut self, handler: F) where - F: 'static + FnMut(i32, MouseButton), + F: 'static + FnMut(i32), { match &mut self.mouse_state { MouseState::HasPointerEvent(h) => h.on_cursor_enter(&self.common, handler), @@ -219,7 +219,7 @@ impl Canvas { pub fn on_mouse_release(&mut self, handler: F) where - F: 'static + FnMut(i32, (f64, f64)), + F: 'static + FnMut(i32, MouseButton, ModifiersState), { match &mut self.mouse_state { MouseState::HasPointerEvent(h) => h.on_mouse_release(&self.common, handler), @@ -247,9 +247,9 @@ impl Canvas { } } - pub fn on_cursor_move(&mut self, mut handler: F) + pub fn on_mouse_wheel(&mut self, mut handler: F) where - F: 'static + FnMut(LogicalPosition, ModifiersState), + F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), { self.on_mouse_wheel = Some(self.common.add_event("wheel", move |event: WheelEvent| { event.prevent_default(); diff --git a/src/platform_impl/web/web_sys/utils.rs b/src/platform_impl/web/web_sys/event.rs similarity index 86% rename from src/platform_impl/web/web_sys/utils.rs rename to src/platform_impl/web/web_sys/event.rs index 09c31de77b..fb6c52d3b0 100644 --- a/src/platform_impl/web/web_sys/utils.rs +++ b/src/platform_impl/web/web_sys/event.rs @@ -1,6 +1,5 @@ use crate::dpi::LogicalPosition; -use crate::event::{ModifiersState, MouseButton, ScanCode, VirtualKeyCode}; -use crate::platform_impl::platform; +use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; use std::convert::TryInto; use web_sys::{HtmlCanvasElement, KeyboardEvent, MouseEvent, WheelEvent}; @@ -247,44 +246,3 @@ pub fn codepoint(event: &KeyboardEvent) -> char { // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key event.key().chars().next().unwrap() } - -pub fn create_mapping(raw: &Gamepad) -> platform::device::gamepad::Mapping { - match raw.mapping() { - GamepadMappingType::Standard => { - let mut buttons = [false; 16]; - let mut axes = [0.0; 6]; - - let gbuttons = raw.buttons(); - for index in 0..buttons.len() { - let button: GamepadButton = gbuttons.get(index as u32).into(); - buttons[index] = button.pressed(); - } - - let gaxes = raw.axes(); - for index in 0..axes.len() { - let axe: f64 = gaxes.get(index as u32).as_f64().unwrap_or(0.0); - axes[index] = axe; - } - - platform::device::gamepad::Mapping::Standard { buttons, axes } - } - _ => { - let mut buttons: Vec = Vec::new(); - let mut axes: Vec = Vec::new(); - - let gbuttons = raw.buttons(); - for index in 0..gbuttons.length() { - let button: GamepadButton = gbuttons.get(index as u32).into(); - buttons.push(button.pressed()); - } - - let gaxes = raw.axes(); - for index in 0..gaxes.length() { - let axe: f64 = gaxes.get(index as u32).as_f64().unwrap_or(0.0); - axes.push(axe); - } - - platform::device::gamepad::Mapping::NoMapping { buttons, axes } - } - } -} diff --git a/src/platform_impl/web/web_sys/gamepad.rs b/src/platform_impl/web/web_sys/gamepad.rs deleted file mode 100644 index 922bd645e0..0000000000 --- a/src/platform_impl/web/web_sys/gamepad.rs +++ /dev/null @@ -1,75 +0,0 @@ -use super::utils; -use crate::platform_impl::platform::device; -use std::cmp::PartialEq; - -#[derive(Debug)] -pub struct Gamepad { - pub(crate) index: i32, - pub(crate) raw: web_sys::Gamepad, - pub(crate) mapping: device::gamepad::Mapping, -} - -impl Gamepad { - pub fn new(raw: web_sys::Gamepad) -> Self { - let mapping = utils::create_mapping(&raw); - - Self { - index: raw.index() as i32, - raw, - mapping, - } - } - - // An integer that is auto-incremented to be unique for each device - // currently connected to the system. - // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/index - pub fn index(&self) -> i32 { - self.raw.index() as i32 - } - - // A string containing some information about the controller. - // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/id - pub fn id(&self) -> String { - self.raw.id() - } - - // A boolean indicating whether the gamepad is still connected to the system. - // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/connected - pub fn connected(&self) -> bool { - self.raw.connected() - } - - // An array containing GamepadHapticActuator objects, - // each of which represents haptic feedback hardware available on the controller. - // https://developer.mozilla.org/en-US/docs/Web/API/Gamepad/hapticActuators - pub fn vibrate(&self, value: f64, duration: f64) { - for actuator in self.raw.haptic_actuators().values() { - actuator.ok().and_then(|a| { - let actuator: web_sys::GamepadHapticActuator = a.into(); - match actuator.type_() { - web_sys::GamepadHapticActuatorType::Vibration => { - actuator.pulse(value, duration).ok() - } - _ => None, - } - }); - } - } -} - -impl Clone for Gamepad { - fn clone(&self) -> Self { - Self { - index: self.index, - raw: self.raw.clone(), - mapping: self.mapping.clone(), - } - } -} - -impl PartialEq for Gamepad { - #[inline(always)] - fn eq(&self, othr: &Self) -> bool { - self.raw.index() == othr.raw.index() - } -} diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index 09c1757cd8..8057f7756a 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -4,8 +4,6 @@ mod event_handle; mod media_query_handle; mod scaling; mod timeout; -mod utils; -pub mod window; pub use self::canvas::Canvas; pub use self::scaling::ScaleChangeDetector; diff --git a/src/platform_impl/web/web_sys/window.rs b/src/platform_impl/web/web_sys/window.rs deleted file mode 100644 index 9bb4cee450..0000000000 --- a/src/platform_impl/web/web_sys/window.rs +++ /dev/null @@ -1,88 +0,0 @@ -use super::gamepad; -use crate::error::OsError as RootOE; -use crate::platform_impl::OsError; -use std::{cell::RefCell, rc::Rc}; -use wasm_bindgen::{closure::Closure, JsCast}; -use web_sys::GamepadEvent; - -#[derive(Debug)] -pub struct Shared(pub Rc>); - -#[derive(Debug)] -pub struct Window { - raw: web_sys::Window, - on_gamepad_connected: Option>, - on_gamepad_disconnected: Option>, -} - -impl Shared { - pub fn create() -> Result { - let global = Window::create()?; - Ok(Shared(Rc::new(RefCell::new(global)))) - } -} - -impl Clone for Shared { - fn clone(&self) -> Self { - Shared(self.0.clone()) - } -} - -impl Window { - pub fn create() -> Result { - let raw = - web_sys::window().ok_or(os_error!(OsError("Failed to obtain window".to_owned())))?; - - Ok(Window { - raw, - on_gamepad_connected: None, - on_gamepad_disconnected: None, - }) - } - - pub fn on_gamepad_connected(&mut self, mut handler: F) - where - F: 'static + FnMut(gamepad::Gamepad), - { - self.on_gamepad_connected = Some(self.add_event( - "gamepadconnected", - move |event: GamepadEvent| { - let gamepad = event - .gamepad() - .expect("[gamepadconnected] expected gamepad"); - handler(gamepad::Gamepad::new(gamepad)); - }, - )) - } - - pub fn on_gamepad_disconnected(&mut self, mut handler: F) - where - F: 'static + FnMut(gamepad::Gamepad), - { - self.on_gamepad_disconnected = Some(self.add_event( - "gamepaddisconnected", - move |event: GamepadEvent| { - let gamepad = event - .gamepad() - .expect("[gamepaddisconnected] expected gamepad"); - handler(gamepad::Gamepad::new(gamepad)); - }, - )) - } - - fn add_event(&self, event_name: &str, mut handler: F) -> Closure - where - E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, - F: 'static + FnMut(E), - { - let closure = Closure::wrap(Box::new(move |event: E| { - handler(event); - }) as Box); - - self.raw - .add_event_listener_with_callback(event_name, &closure.as_ref().unchecked_ref()) - .expect("Failed to add event listener with callback"); - - closure - } -} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index b99fdf5aa7..9e1b84256f 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -40,7 +40,6 @@ impl Window { let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); - target.register_global_events()?; target.register(&mut canvas, id); let runner = target.runner.clone(); From eb84dd813c260df5f51c36d5fb6b9ba31dbfd486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20R=C3=B8yset?= Date: Fri, 25 Jun 2021 17:41:20 +0200 Subject: [PATCH 4/8] Update the doc section on device events in lib.rs --- src/lib.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 353cd80d57..4108a58ac5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,9 +24,10 @@ //! Once a [`Window`] has been created, it will generate different *events*. A [`Window`] object can //! generate [`WindowEvent`]s when certain input events occur, such as a cursor moving over the //! window or a key getting pressed while the window is focused. Devices can generate -//! [`DeviceEvent`]s, which contain unfiltered event data that isn't specific to a certain window. +//! device events which contain unfiltered data that isn't specific to a certain window. Examples of such +//! events are [`GamepadEvent`], [`KeyboardEvent`], [`MouseEvent`] and [`HidEvent`]. //! Some user activity, like mouse movement, can generate both a [`WindowEvent`] *and* a -//! [`DeviceEvent`]. You can also create and handle your own custom [`UserEvent`]s, if desired. +//! device event. You can also create and handle your own custom [`UserEvent`]s, if desired. //! //! You can retrieve events by calling [`EventLoop::run`][event_loop_run]. This function will //! dispatch events for every [`Window`] that was created with that particular [`EventLoop`], and @@ -123,7 +124,10 @@ //! [window_id_fn]: window::Window::id //! [`Event`]: event::Event //! [`WindowEvent`]: event::WindowEvent -//! [`DeviceEvent`]: event::DeviceEvent +//! [`GamepadEvent`]: event::device::GamepadEvent +//! [`KeyboardEvent`]: event::device::KeyboardEvent +//! [`MouseEvent`]: event::device::MouseEvent +//! [`HidEvent`]: event::device::HidEvent //! [`UserEvent`]: event::Event::UserEvent //! [`LoopDestroyed`]: event::Event::LoopDestroyed //! [`platform`]: platform From ff6254b89624dfc55f063722ba4606a5ff8f80bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20R=C3=B8yset?= Date: Sat, 3 Jul 2021 22:22:43 +0200 Subject: [PATCH 5/8] Use correct pointer type Co-authored-by: Markus Siglreithmaier --- src/platform_impl/windows/raw_input.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform_impl/windows/raw_input.rs b/src/platform_impl/windows/raw_input.rs index 22ad22ae7a..e62bec897c 100644 --- a/src/platform_impl/windows/raw_input.rs +++ b/src/platform_impl/windows/raw_input.rs @@ -165,7 +165,7 @@ pub fn get_raw_input_pre_parse_info(handle: HANDLE) -> Option> { winuser::GetRawInputDeviceInfoW( handle, RIDI_PREPARSEDDATA, - buf.as_ptr() as _, + buf.as_mut_ptr() as _, &mut minimum_size, ) }; From ee96a89560646060d208c62c1f71e4db59729f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20R=C3=B8yset?= Date: Sat, 3 Jul 2021 22:36:54 +0200 Subject: [PATCH 6/8] Use `std`'s `clamp` function --- src/util.rs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/util.rs b/src/util.rs index 18e8c1941b..69e56224a1 100644 --- a/src/util.rs +++ b/src/util.rs @@ -7,21 +7,11 @@ where bitset & flag == flag } -pub fn clamp(value: f64, min: f64, max: f64) -> f64 { - if value > max { - max - } else if value < min { - min - } else { - value - } -} - pub fn normalize_asymmetric(value: f64, min: f64, max: f64) -> f64 { let range = max - min; let translated = value - min; let scaled = translated / range; - clamp(scaled, 0.0, 1.0) + scaled.clamp(0.0, 1.0) } pub fn normalize_symmetric(value: f64, min: f64, max: f64) -> f64 { From 048ffafc694a51613fb4a50812177461d44f0453 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20R=C3=B8yset?= Date: Sat, 3 Jul 2021 23:35:28 +0200 Subject: [PATCH 7/8] Refer to the correct field on `GamepadEvent::Axis` --- src/event/device.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/event/device.rs b/src/event/device.rs index 3fd7c08393..1b9352f3c3 100644 --- a/src/event/device.rs +++ b/src/event/device.rs @@ -190,7 +190,7 @@ pub enum GamepadEvent { button_id: ButtonId, /// A hint regarding the location of the button. /// - /// The caveats on the `Axis.hint` field also apply here. + /// The caveats on the `Axis.axis` field also apply here. button: Option, state: ElementState, }, From 0b5b01f6b10a7c24475ae3d4ae70e64b68d2ea02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20R=C3=B8yset?= Date: Sun, 4 Jul 2021 17:46:33 +0200 Subject: [PATCH 8/8] Allow absolute mouse motion to (0.0, 0.0) --- src/platform_impl/windows/event_loop.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index e3aa982b83..4a952791aa 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -2223,12 +2223,10 @@ unsafe extern "system" fn thread_event_target_callback( let x = raw_mouse.lLastX as f64; let y = raw_mouse.lLastY as f64; - if x != 0.0 || y != 0.0 { - subclass_input.send_event(Event::MouseEvent( - mouse_handle, - MouseEvent::MovedAbsolute(PhysicalPosition { x, y }), - )); - } + subclass_input.send_event(Event::MouseEvent( + mouse_handle, + MouseEvent::MovedAbsolute(PhysicalPosition { x, y }), + )); } else if util::has_flag(raw_mouse.usFlags, winuser::MOUSE_MOVE_RELATIVE) { let x = raw_mouse.lLastX as f64; let y = raw_mouse.lLastY as f64;