diff --git a/.gitignore b/.gitignore index 7cd4f7f91..151049087 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 6987a01fc..4e6e433f7 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 000000000..83b442c6b --- /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 000000000..7b545bc18 --- /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 000000000..8d6373103 --- /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 000000000..c142f168d --- /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 000000000..a92272492 --- /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 000000000..7c95db9a9 --- /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 000000000..02aa5c19e --- /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 bd39b95ba..1df0d51a6 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 a2f00b69c..000000000 --- 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 000000000..c2c589adc --- /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 000000000..ec98ade4f --- /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 000000000..c424b7378 --- /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 000000000..7ed8e7abc --- /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 000000000..4bc5154c6 --- /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 000000000..eb4108bad --- /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 000000000..d6e190753 --- /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 7124cee7e..9be442a1c 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 b3889fad2..3b240e3cf 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 d302edb87..cefe9678d 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 029761b95..7c775d9a6 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 a0ff5af2e..8539f2229 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 000000000..0b54896b1 --- /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 d49dbf02a..9ed51c213 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 14456dc05..bc4e0a029 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 000000000..4b97432fd --- /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 0543055eb..cc6a5b7fb 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) - where - F: 'static + FnMut(i32), - { - self.on_cursor_leave = Some(self.add_event("pointerout", move |event: PointerEvent| { - handler(event.pointer_id()); - })); - } - - pub fn on_cursor_enter(&mut self, mut handler: F) - where - F: 'static + FnMut(i32), - { - self.on_cursor_enter = Some(self.add_event("pointerover", move |event: PointerEvent| { - handler(event.pointer_id()); - })); - } - 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( + self.on_mouse_release = Some(self.add_event( "pointerup", move |event: PointerEvent| { - 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( + self.on_mouse_press = Some(self.add_event( "pointerdown", move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_button(&event), - event::mouse_modifiers(&event), - ); + handler(event.pointer_id(), utils::mouse_button(&event)); + }, + )); + } + + pub fn on_mouse_wheel(&mut self, mut handler: F) + where + F: 'static + FnMut(i32, (f64, f64)), + { + 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_cursor_leave(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_cursor_leave = Some(self.add_event( + "pointerout", + move |_event: PointerEvent| { + handler(); + }, + )); + } + + pub fn on_cursor_enter(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_cursor_enter = Some(self.add_event( + "pointerover", + move |_event: PointerEvent| { + handler(); }, )); } 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("pointermove", move |event: PointerEvent| { - handler( - event.pointer_id(), - event::mouse_position(&event), - event::mouse_modifiers(&event), - ); - })); - } - - pub fn on_mouse_wheel(&mut self, mut handler: F) - where - F: 'static + FnMut(i32, MouseScrollDelta, 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); + 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; - } - }) + 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 000000000..922bd645e --- /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 205519d1e..a795bd9fb 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 af557b994..e2b896de8 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 000000000..9bb4cee45 --- /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 8752d8b63..74a7c4fc4 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 {