winit-core:winit: add tablet input support

The API is integrated into the `WindowEvent::Pointer*` API and is
present in form of `TabletTool` variant on corresponding data entries.

For now implemented for Web, Windows, and with limitations for Wayland.

Fixes #99.

Co-authored-by: daxpedda <daxpedda@gmail.com>
This commit is contained in:
Kirill Chibisov 2025-10-07 21:42:36 +09:00 committed by GitHub
parent ffcdf80192
commit f046e778aa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 1206 additions and 204 deletions

View file

@ -10,6 +10,9 @@ disallowed-methods = [
{ allow-invalid = true, path = "web_sys::HtmlCanvasElement::set_width", reason = "Winit shouldn't touch the internal canvas size" }, { allow-invalid = true, path = "web_sys::HtmlCanvasElement::set_width", reason = "Winit shouldn't touch the internal canvas size" },
{ allow-invalid = true, path = "web_sys::HtmlCanvasElement::width", reason = "Winit shouldn't touch the internal canvas size" }, { allow-invalid = true, path = "web_sys::HtmlCanvasElement::width", reason = "Winit shouldn't touch the internal canvas size" },
{ allow-invalid = true, path = "web_sys::HtmlElement::style", reason = "cache this to reduce calls to JS" }, { allow-invalid = true, path = "web_sys::HtmlElement::style", reason = "cache this to reduce calls to JS" },
{ allow-invalid = true, path = "web_sys::MouseEvent::buttons", reason = "Use `backend::event::cursor_buttons()` to avoid wrong conversions" },
{ allow-invalid = true, path = "web_sys::MouseEvent::button", reason = "Use `backend::event::cursor_button()` to avoid wrong conversions" },
{ allow-invalid = true, path = "web_sys::PointerEvent::pointer_type", reason = "Use `WebPointerType` to emit warnings" },
{ allow-invalid = true, path = "web_sys::Window::document", reason = "cache this to reduce calls to JS" }, { allow-invalid = true, path = "web_sys::Window::document", reason = "cache this to reduce calls to JS" },
{ allow-invalid = true, path = "web_sys::Window::get_computed_style", reason = "cache this to reduce calls to JS" }, { allow-invalid = true, path = "web_sys::Window::get_computed_style", reason = "cache this to reduce calls to JS" },
{ allow-invalid = true, path = "web_sys::Window::navigator", reason = "cache this to reduce calls to JS" }, { allow-invalid = true, path = "web_sys::Window::navigator", reason = "cache this to reduce calls to JS" },

View file

@ -1,4 +1,7 @@
//! The event enums and assorted supporting types. //! The event enums and assorted supporting types.
use std::cell::LazyCell;
use std::cmp::Ordering;
use std::f64;
use std::path::PathBuf; use std::path::PathBuf;
use std::sync::{Mutex, Weak}; use std::sync::{Mutex, Weak};
@ -435,6 +438,7 @@ pub enum PointerKind {
/// ///
/// **macOS:** Unsupported. /// **macOS:** Unsupported.
Touch(FingerId), Touch(FingerId),
TabletTool(TabletToolKind),
Unknown, Unknown,
} }
@ -485,6 +489,13 @@ pub enum PointerSource {
/// force will be 0.5 when a button is pressed or 0.0 otherwise. /// force will be 0.5 when a button is pressed or 0.0 otherwise.
force: Option<Force>, force: Option<Force>,
}, },
TabletTool {
/// Describes as which tool kind the interaction happened.
kind: TabletToolKind,
/// Describes how the tool was held and used.
data: TabletToolData,
},
Unknown, Unknown,
} }
@ -493,6 +504,7 @@ impl From<PointerSource> for PointerKind {
match source { match source {
PointerSource::Mouse => Self::Mouse, PointerSource::Mouse => Self::Mouse,
PointerSource::Touch { finger_id, .. } => Self::Touch(finger_id), PointerSource::Touch { finger_id, .. } => Self::Touch(finger_id),
PointerSource::TabletTool { kind, .. } => Self::TabletTool(kind),
PointerSource::Unknown => Self::Unknown, PointerSource::Unknown => Self::Unknown,
} }
} }
@ -502,7 +514,7 @@ impl From<PointerSource> for PointerKind {
/// ///
/// **Wayland/X11:** [`Unknown`](Self::Unknown) device types are converted to known variants by the /// **Wayland/X11:** [`Unknown`](Self::Unknown) device types are converted to known variants by the
/// system. /// system.
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum ButtonSource { pub enum ButtonSource {
Mouse(MouseButton), Mouse(MouseButton),
/// See [`PointerSource::Touch`] for more details. /// See [`PointerSource::Touch`] for more details.
@ -514,6 +526,11 @@ pub enum ButtonSource {
finger_id: FingerId, finger_id: FingerId,
force: Option<Force>, force: Option<Force>,
}, },
TabletTool {
kind: TabletToolKind,
button: TabletToolButton,
data: TabletToolData,
},
Unknown(u16), Unknown(u16),
} }
@ -525,6 +542,7 @@ impl ButtonSource {
match self { match self {
ButtonSource::Mouse(mouse) => mouse, ButtonSource::Mouse(mouse) => mouse,
ButtonSource::Touch { .. } => MouseButton::Left, ButtonSource::Touch { .. } => MouseButton::Left,
ButtonSource::TabletTool { button, .. } => button.into(),
ButtonSource::Unknown(button) => match button { ButtonSource::Unknown(button) => match button {
0 => MouseButton::Left, 0 => MouseButton::Left,
1 => MouseButton::Middle, 1 => MouseButton::Middle,
@ -981,6 +999,7 @@ pub enum TouchPhase {
/// Describes the force of a touch event /// Describes the force of a touch event
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[doc(alias = "Pressure")]
pub enum Force { pub enum Force {
/// On iOS, the force is calibrated so that the same number corresponds to /// On iOS, the force is calibrated so that the same number corresponds to
/// roughly the same amount of pressure on the screen regardless of the /// roughly the same amount of pressure on the screen regardless of the
@ -991,7 +1010,7 @@ pub enum Force {
/// ///
/// The force reported by Apple Pencil is measured along the axis of the /// The force reported by Apple Pencil is measured along the axis of the
/// pencil. If you want a force perpendicular to the device, you need to /// pencil. If you want a force perpendicular to the device, you need to
/// calculate this value using the `altitude_angle` value. /// calculate this value using the [`TabletToolAngle::altitude`] value.
force: f64, force: f64,
/// The maximum possible force for a touch. /// The maximum possible force for a touch.
/// ///
@ -1012,9 +1031,17 @@ impl Force {
/// Instead of normalizing the force, you should prefer to handle /// Instead of normalizing the force, you should prefer to handle
/// [`Force::Calibrated`] so that the amount of force the user has to apply is /// [`Force::Calibrated`] so that the amount of force the user has to apply is
/// consistent across devices. /// consistent across devices.
pub fn normalized(&self) -> f64 { ///
/// Passing in a [`TabletToolAngle`], returns the perpendicular force.
pub fn normalized(&self, angle: Option<TabletToolAngle>) -> f64 {
match self { match self {
Force::Calibrated { force, max_possible_force } => force / max_possible_force, Force::Calibrated { force, max_possible_force } => {
let force = match angle {
Some(TabletToolAngle { altitude, .. }) => force / altitude.sin(),
None => *force,
};
force / max_possible_force
},
Force::Normalized(force) => *force, Force::Normalized(force) => *force,
} }
} }
@ -1026,6 +1053,264 @@ pub type AxisId = u32;
/// Identifier for a specific button on some device. /// Identifier for a specific button on some device.
pub type ButtonId = u32; pub type ButtonId = u32;
/// Tablet of the tablet tool.
#[derive(Default, Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
#[non_exhaustive]
pub enum TabletToolKind {
#[default]
Pen,
Eraser,
Brush,
Pencil,
Airbrush,
Finger,
Mouse,
Lens,
}
#[derive(Default, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct TabletToolData {
/// The force applied to the tool against the surface.
///
/// When the force information is not available, [`None`] is returned.
///
/// ## Platform-specific
///
/// **Web:** Has no mechanism to detect support, so this will always be [`Some`].
pub force: Option<Force>,
/// Represents normalized tangential pressure, also known as barrel pressure. In the range of
/// -1 to 1. 0 means no tangential pressure is applied. [`None`] means backend or device has no
/// support.
///
/// ## Platform-specific
///
/// **Web:** Has no mechanism to detect support, so this will always be [`Some`] with a value
/// of 0.
pub tangential_force: Option<f32>,
/// The clockwise rotation in degrees of a tool around its own major axis. E.g. twisting a pen
/// around its length. In the range of 0 to 359. [`None`] means backend or device has no
/// support.
///
/// ## Platform-specific
///
/// **Web:** Has no mechanism to detect support, so this will always be [`Some`] with a value
/// of 0.
pub twist: Option<u16>,
/// The plane angle in degrees. [`None`] means backend or device has no support.
///
/// ## Platform-specific
///
/// **Web:** Has no mechanism to detect support, so this will always be [`Some`] with default
/// values.
pub tilt: Option<TabletToolTilt>,
/// The angular position in radians. [`None`] means backend or device has no support.
///
/// ## Platform-specific
///
/// **Web:** Has no mechanism to detect device support, so this will always be [`Some`] with
/// default values unless browser support is lacking.
pub angle: Option<TabletToolAngle>,
}
impl TabletToolData {
/// Returns [`TabletToolTilt`] if present or calculates it from [`TabletToolAngle`].
pub fn tilt(self) -> Option<TabletToolTilt> {
if let Some(tilt) = self.tilt {
Some(tilt)
} else {
self.angle.map(TabletToolAngle::tilt)
}
}
/// Returns [`TabletToolAngle`] if present or calculates it from [`TabletToolTilt`].
pub fn angle(self) -> Option<TabletToolAngle> {
if let Some(angle) = self.angle {
Some(angle)
} else {
self.tilt.map(TabletToolTilt::angle)
}
}
}
/// The plane angle in degrees of a tool.
#[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct TabletToolTilt {
/// The plane angle in degrees between the surface Y-Z plane and the plane containing the tool
/// and the surface Y axis. Positive values are to the right. In the range of -90 to 90. 0
/// means the tool is perpendicular to the surface and is the default.
///
/// ![Tilt X](https://raw.githubusercontent.com/rust-windowing/winit/master/winit/docs/res/tool_tilt_x.webp)
///
/// <sub>
/// For image attribution, see the
/// <a href="https://github.com/rust-windowing/winit/blob/master/winit/docs/ATTRIBUTION.md">
/// ATTRIBUTION.md
/// </a>
/// file.
/// </sub>
pub x: i8,
/// The plane angle in degrees between the surface X-Z plane and the plane containing the tool
/// and the surface X axis. Positive values are towards the user. In the range of -90 to
/// 90. 0 means the tool is perpendicular to the surface and is the default.
///
/// ![Tilt Y](https://raw.githubusercontent.com/rust-windowing/winit/master/winit/docs/res/tool_tilt_y.webp)
///
/// <sub>
/// For image attribution, see the
/// <a href="https://github.com/rust-windowing/winit/blob/master/winit/docs/ATTRIBUTION.md">
/// ATTRIBUTION.md
/// </a>
/// file.
/// </sub>
pub y: i8,
}
impl TabletToolTilt {
pub fn angle(self) -> TabletToolAngle {
// See <https://www.w3.org/TR/2024/WD-pointerevents3-20240326/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle>.
use std::f64::consts::*;
const PI_0_5: f64 = FRAC_PI_2;
const PI_1_5: f64 = 3. * FRAC_PI_2;
const PI_2: f64 = 2. * PI;
let x = LazyCell::new(|| f64::from(self.x).to_radians());
let y = LazyCell::new(|| f64::from(self.y).to_radians());
let mut azimuth = 0.;
if self.x == 0 {
match self.y.cmp(&0) {
Ordering::Greater => azimuth = PI_0_5,
Ordering::Less => azimuth = PI_1_5,
Ordering::Equal => (),
}
} else if self.y == 0 {
if self.x < 0 {
azimuth = PI;
}
} else if self.x.abs() == 90 || self.y.abs() == 90 {
// not enough information to calculate azimuth
azimuth = 0.;
} else {
// Non-boundary case: neither tiltX nor tiltY is equal to 0 or +-90
azimuth = f64::atan2(y.tan(), x.tan());
if azimuth < 0. {
azimuth += PI_2;
}
}
let altitude;
if self.x.abs() == 90 || self.y.abs() == 90 {
altitude = 0.;
} else if self.x == 0 {
altitude = PI_0_5 - y.abs();
} else if self.y == 0 {
altitude = PI_0_5 - x.abs();
} else {
// Non-boundary case: neither tiltX nor tiltY is equal to 0 or +-90
altitude = f64::atan(1. / f64::sqrt(x.tan().powi(2) + y.tan().powi(2)));
}
TabletToolAngle { altitude, azimuth }
}
}
/// The angular position in radians of a tool.
#[derive(Clone, Copy, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct TabletToolAngle {
/// The altitude angle in radians between the tools perpendicular position to the surface and
/// the surface X-Y plane. In the range of 0, parallel to the surface, to π/2, perpendicular to
/// the surface. π/2 means the tool is perpendicular to the surface and is the default.
///
/// ![Altitude angle](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/tool_altitude.webp)
///
/// <sub>
/// For image attribution, see the
/// <a href="https://github.com/rust-windowing/winit/blob/master/docs/res/ATTRIBUTION.md">
/// ATTRIBUTION.md
/// </a>
/// file.
/// </sub>
pub altitude: f64,
/// The azimuth angle in radiants representing the rotation between the major axis of the tool
/// and the surface X-Y plane. In the range of 0, 3 o'clock, progressively increasing clockwise
/// to 2π. 0 means the tool is at 3 o'clock or is perpendicular to the surface (`altitude` of
/// π/2) and is the default.
///
/// ![Azimuth angle](https://raw.githubusercontent.com/rust-windowing/winit/master/docs/res/tool_azimuth.webp)
///
/// <sub>
/// For image attribution, see the
/// <a href="https://github.com/rust-windowing/winit/blob/master/docs/res/ATTRIBUTION.md">
/// ATTRIBUTION.md
/// </a>
/// file.
/// </sub>
pub azimuth: f64,
}
impl Default for TabletToolAngle {
fn default() -> Self {
Self { altitude: f64::consts::FRAC_2_PI, azimuth: 0. }
}
}
impl TabletToolAngle {
pub fn tilt(self) -> TabletToolTilt {
// See <https://www.w3.org/TR/2024/WD-pointerevents3-20240326/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle>.
use std::f64::consts::*;
const PI_0_5: f64 = FRAC_PI_2;
const PI_1_5: f64 = 3. * FRAC_PI_2;
const PI_2: f64 = 2. * PI;
let mut x = 0.;
let mut y = 0.;
if self.altitude == 0. {
if self.azimuth == 0. || self.azimuth == PI_2 {
x = FRAC_PI_2;
} else if self.azimuth == PI_0_5 {
y = FRAC_PI_2;
} else if self.azimuth == PI {
x = -FRAC_PI_2;
} else if self.azimuth == PI_1_5 {
y = -FRAC_PI_2;
} else if self.azimuth > 0. && self.azimuth < PI_0_5 {
x = FRAC_PI_2;
y = FRAC_PI_2;
} else if self.azimuth > PI_0_5 && self.azimuth < PI {
x = -FRAC_PI_2;
y = FRAC_PI_2;
} else if self.azimuth > PI && self.azimuth < PI_1_5 {
x = -FRAC_PI_2;
y = -FRAC_PI_2;
} else if self.azimuth > PI_1_5 && self.azimuth < PI_2 {
x = FRAC_PI_2;
y = -FRAC_PI_2;
}
}
if self.altitude != 0. {
let altitude = self.altitude.tan();
x = f64::atan(f64::cos(self.azimuth) / altitude);
y = f64::atan(f64::sin(self.azimuth) / altitude);
}
TabletToolTilt { x: x.to_degrees().round() as i8, y: y.to_degrees().round() as i8 }
}
}
/// Describes the input state of a key. /// Describes the input state of a key.
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@ -1058,6 +1343,28 @@ pub enum MouseButton {
Other(u16), Other(u16),
} }
/// Describes a button of a tool, e.g. a pen.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub enum TabletToolButton {
Contact,
Barrel,
Other(u16),
}
impl From<TabletToolButton> for MouseButton {
fn from(tool: TabletToolButton) -> Self {
match tool {
TabletToolButton::Contact => MouseButton::Left,
TabletToolButton::Barrel => MouseButton::Right,
TabletToolButton::Other(1) => MouseButton::Middle,
TabletToolButton::Other(3) => MouseButton::Back,
TabletToolButton::Other(4) => MouseButton::Forward,
TabletToolButton::Other(other) => MouseButton::Other(other),
}
}
}
/// Describes a difference in the mouse scroll wheel state. /// Describes a difference in the mouse scroll wheel state.
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
@ -1241,16 +1548,85 @@ mod tests {
}); });
} }
#[test]
fn test_tilt_angle_conversions() {
use std::f64::consts::*;
use event::{TabletToolAngle, TabletToolTilt};
// See <https://github.com/web-platform-tests/wpt/blob/5af3e9c2a2aba76ade00f0dbc3486e50a74a4506/pointerevents/pointerevent_tiltX_tiltY_to_azimuth_altitude.html#L11-L23>.
const TILT_TO_ANGLE: &[(TabletToolTilt, TabletToolAngle)] = &[
(TabletToolTilt { x: 0, y: 0 }, TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }),
(TabletToolTilt { x: 0, y: 90 }, TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }),
(TabletToolTilt { x: 0, y: -90 }, TabletToolAngle {
altitude: 0.,
azimuth: 3. * FRAC_PI_2,
}),
(TabletToolTilt { x: 90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: 0. }),
(TabletToolTilt { x: 90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }),
(TabletToolTilt { x: 90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }),
(TabletToolTilt { x: -90, y: 0 }, TabletToolAngle { altitude: 0., azimuth: PI }),
(TabletToolTilt { x: -90, y: 90 }, TabletToolAngle { altitude: 0., azimuth: 0. }),
(TabletToolTilt { x: -90, y: -90 }, TabletToolAngle { altitude: 0., azimuth: 0. }),
(TabletToolTilt { x: 0, y: 45 }, TabletToolAngle {
altitude: FRAC_PI_4,
azimuth: FRAC_PI_2,
}),
(TabletToolTilt { x: 0, y: -45 }, TabletToolAngle {
altitude: FRAC_PI_4,
azimuth: 3. * FRAC_PI_2,
}),
(TabletToolTilt { x: 45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }),
(TabletToolTilt { x: -45, y: 0 }, TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }),
];
for (tilt, angle) in TILT_TO_ANGLE {
assert_eq!(tilt.angle(), *angle, "{tilt:?}");
}
// See <https://github.com/web-platform-tests/wpt/blob/5af3e9c2a2aba76ade00f0dbc3486e50a74a4506/pointerevents/pointerevent_tiltX_tiltY_to_azimuth_altitude.html#L38-L46>.
const ANGLE_TO_TILT: &[(TabletToolAngle, TabletToolTilt)] = &[
(TabletToolAngle { altitude: 0., azimuth: 0. }, TabletToolTilt { x: 90, y: 0 }),
(TabletToolAngle { altitude: FRAC_PI_4, azimuth: 0. }, TabletToolTilt { x: 45, y: 0 }),
(TabletToolAngle { altitude: FRAC_PI_2, azimuth: 0. }, TabletToolTilt { x: 0, y: 0 }),
(TabletToolAngle { altitude: 0., azimuth: FRAC_PI_2 }, TabletToolTilt { x: 0, y: 90 }),
(TabletToolAngle { altitude: FRAC_PI_4, azimuth: FRAC_PI_2 }, TabletToolTilt {
x: 0,
y: 45,
}),
(TabletToolAngle { altitude: 0., azimuth: PI }, TabletToolTilt { x: -90, y: 0 }),
(TabletToolAngle { altitude: FRAC_PI_4, azimuth: PI }, TabletToolTilt { x: -45, y: 0 }),
(TabletToolAngle { altitude: 0., azimuth: 3. * FRAC_PI_2 }, TabletToolTilt {
x: 0,
y: -90,
}),
(TabletToolAngle { altitude: FRAC_PI_4, azimuth: 3. * FRAC_PI_2 }, TabletToolTilt {
x: 0,
y: -45,
}),
];
for (angle, tilt) in ANGLE_TO_TILT {
assert_eq!(angle.tilt(), *tilt, "{angle:?}");
}
}
#[test] #[test]
fn test_force_normalize() { fn test_force_normalize() {
let force = event::Force::Normalized(0.0); let force = event::Force::Normalized(0.0);
assert_eq!(force.normalized(), 0.0); assert_eq!(force.normalized(None), 0.0);
let force2 = event::Force::Calibrated { force: 5.0, max_possible_force: 2.5 }; let force2 = event::Force::Calibrated { force: 5.0, max_possible_force: 2.5 };
assert_eq!(force2.normalized(), 2.0); assert_eq!(force2.normalized(None), 2.0);
let force3 = event::Force::Calibrated { force: 5.0, max_possible_force: 2.5 }; let force3 = event::Force::Calibrated { force: 5.0, max_possible_force: 2.5 };
assert_eq!(force3.normalized(), 2.0); assert_eq!(
force3.normalized(Some(event::TabletToolAngle {
altitude: std::f64::consts::PI / 2.0,
azimuth: 0.
})),
2.0
);
} }
#[allow(clippy::clone_on_copy)] #[allow(clippy::clone_on_copy)]

View file

@ -461,6 +461,10 @@ impl WinitView {
let window = self.window().unwrap(); let window = self.window().unwrap();
let mut touch_events = Vec::new(); let mut touch_events = Vec::new();
for touch in touches { for touch in touches {
if let UITouchType::Pencil = touch.r#type() {
continue;
}
let logical_location = touch.locationInView(None); let logical_location = touch.locationInView(None);
let touch_type = touch.r#type(); let touch_type = touch.r#type();
let force = if let UITouchType::Pencil = touch_type { let force = if let UITouchType::Pencil = touch_type {

View file

@ -13,6 +13,7 @@ use sctk::seat::pointer::{ThemeSpec, ThemedPointer};
use sctk::seat::{Capability as SeatCapability, SeatHandler, SeatState}; use sctk::seat::{Capability as SeatCapability, SeatHandler, SeatState};
use tracing::warn; use tracing::warn;
use wayland_protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1; use wayland_protocols::wp::pointer_gestures::zv1::client::zwp_pointer_gesture_pinch_v1::ZwpPointerGesturePinchV1;
use wayland_protocols::wp::tablet::zv2::client::zwp_tablet_seat_v2::ZwpTabletSeatV2;
use winit_core::event::WindowEvent; use winit_core::event::WindowEvent;
use winit_core::keyboard::ModifiersState; use winit_core::keyboard::ModifiersState;
@ -50,6 +51,9 @@ pub struct WinitSeatState {
/// The text input bound on the seat. /// The text input bound on the seat.
text_input: Option<Arc<ZwpTextInputV3>>, text_input: Option<Arc<ZwpTextInputV3>>,
/// The tablet input bound on the seat.
tablet: Option<Arc<ZwpTabletSeatV2>>,
/// The relative pointer bound on the seat. /// The relative pointer bound on the seat.
relative_pointer: Option<ZwpRelativePointerV1>, relative_pointer: Option<ZwpRelativePointerV1>,
@ -156,6 +160,12 @@ impl SeatHandler for WinitState {
TextInputData::default(), TextInputData::default(),
))); )));
} }
if let Some(tablet_state) =
seat_state.tablet.is_none().then_some(self.tablet_state.as_ref()).flatten()
{
seat_state.tablet = Some(Arc::new(tablet_state.get_tablet_seat(&seat, queue_handle)));
}
} }
fn remove_capability( fn remove_capability(
@ -177,6 +187,11 @@ impl SeatHandler for WinitState {
text_input.destroy(); text_input.destroy();
} }
// NOTE: figure out when this should actually be destroyed.
if let Some(tablet) = seat_state.tablet.take() {
tablet.destroy();
}
match capability { match capability {
SeatCapability::Touch => { SeatCapability::Touch => {
if let Some(touch) = seat_state.touch.take() { if let Some(touch) = seat_state.touch.take() {

View file

@ -30,6 +30,7 @@ use crate::seat::{
}; };
use crate::types::kwin_blur::KWinBlurManager; use crate::types::kwin_blur::KWinBlurManager;
use crate::types::wp_fractional_scaling::FractionalScalingManager; use crate::types::wp_fractional_scaling::FractionalScalingManager;
use crate::types::wp_tablet_input_v2::TabletManager;
use crate::types::wp_viewporter::ViewporterState; use crate::types::wp_viewporter::ViewporterState;
use crate::types::xdg_activation::XdgActivationState; use crate::types::xdg_activation::XdgActivationState;
use crate::types::xdg_toplevel_icon_manager::XdgToplevelIconManagerState; use crate::types::xdg_toplevel_icon_manager::XdgToplevelIconManagerState;
@ -100,6 +101,9 @@ pub struct WinitState {
/// Relative pointer. /// Relative pointer.
pub relative_pointer: Option<RelativePointerState>, pub relative_pointer: Option<RelativePointerState>,
/// Tablet manager.
pub tablet_state: Option<TabletManager>,
/// Pointer constraints to handle pointer locking and confining. /// Pointer constraints to handle pointer locking and confining.
pub pointer_constraints: Option<Arc<PointerConstraintsState>>, pub pointer_constraints: Option<Arc<PointerConstraintsState>>,
@ -194,6 +198,7 @@ impl WinitState {
text_input_state: TextInputState::new(globals, queue_handle).ok(), text_input_state: TextInputState::new(globals, queue_handle).ok(),
relative_pointer: RelativePointerState::new(globals, queue_handle).ok(), relative_pointer: RelativePointerState::new(globals, queue_handle).ok(),
tablet_state: TabletManager::new(globals, queue_handle).ok(),
pointer_constraints: PointerConstraintsState::new(globals, queue_handle) pointer_constraints: PointerConstraintsState::new(globals, queue_handle)
.map(Arc::new) .map(Arc::new)
.ok(), .ok(),

View file

@ -3,6 +3,7 @@
pub mod cursor; pub mod cursor;
pub mod kwin_blur; pub mod kwin_blur;
pub mod wp_fractional_scaling; pub mod wp_fractional_scaling;
pub mod wp_tablet_input_v2;
pub mod wp_viewporter; pub mod wp_viewporter;
pub mod xdg_activation; pub mod xdg_activation;
pub mod xdg_toplevel_icon_manager; pub mod xdg_toplevel_icon_manager;

View file

@ -0,0 +1,343 @@
//! Handling of wp_tablet_input_v2.
use std::sync::Mutex;
use dpi::LogicalPosition;
use sctk::compositor::SurfaceData;
use sctk::globals::GlobalData;
use sctk::reexports::client::backend::smallvec::SmallVec;
use sctk::reexports::client::globals::{BindError, GlobalList};
use sctk::reexports::client::protocol::wl_seat::WlSeat;
use sctk::reexports::client::protocol::wl_surface::WlSurface;
use sctk::reexports::client::{
delegate_dispatch, event_created_child, Connection, Dispatch, Proxy, QueueHandle, WEnum,
};
use sctk::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_manager_v2::ZwpTabletManagerV2;
use sctk::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_pad_v2::ZwpTabletPadV2;
use sctk::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_seat_v2::{
self, ZwpTabletSeatV2,
};
use sctk::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_tool_v2::{
ButtonState, Event as ToolEvent, Type as ToolType, ZwpTabletToolV2,
};
use sctk::reexports::protocols::wp::tablet::zv2::client::zwp_tablet_v2::ZwpTabletV2;
use winit_core::event::{
ButtonSource, ElementState, Force, PointerKind, PointerSource, TabletToolButton,
TabletToolData as CoreTabletToolData, TabletToolKind, TabletToolTilt, WindowEvent,
};
use crate::state::WinitState;
/// KWin blur manager.
#[derive(Debug, Clone)]
pub struct TabletManager {
manager: ZwpTabletManagerV2,
}
impl TabletManager {
pub fn new(
globals: &GlobalList,
queue_handle: &QueueHandle<WinitState>,
) -> Result<Self, BindError> {
// Ignore v2 since we are not interested in its events.
let manager = globals.bind(queue_handle, 1..=1, GlobalData)?;
Ok(Self { manager })
}
pub fn get_tablet_seat(
&self,
seat: &WlSeat,
queue_handle: &QueueHandle<WinitState>,
) -> ZwpTabletSeatV2 {
self.manager.get_tablet_seat(seat, queue_handle, ())
}
}
impl Dispatch<ZwpTabletManagerV2, GlobalData, WinitState> for TabletManager {
fn event(
_: &mut WinitState,
_: &ZwpTabletManagerV2,
_: <ZwpTabletManagerV2 as Proxy>::Event,
_: &GlobalData,
_: &Connection,
_: &QueueHandle<WinitState>,
) {
unreachable!("no events defined for zwp_tablet_manager_v2");
}
}
impl Dispatch<ZwpTabletManagerV2, (), WinitState> for TabletManager {
fn event(
_: &mut WinitState,
_: &ZwpTabletManagerV2,
_: <ZwpTabletManagerV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WinitState>,
) {
unreachable!("no events defined for zwp_tablet_manager_v2");
}
}
impl Dispatch<ZwpTabletSeatV2, (), WinitState> for TabletManager {
event_created_child!(WinitState, ZwpTabletSeatV2, [
zwp_tablet_seat_v2::EVT_TABLET_ADDED_OPCODE => (ZwpTabletV2, Default::default()),
zwp_tablet_seat_v2::EVT_TOOL_ADDED_OPCODE => (ZwpTabletToolV2, Default::default()),
zwp_tablet_seat_v2::EVT_PAD_ADDED_OPCODE => (ZwpTabletPadV2, Default::default())
]);
fn event(
_: &mut WinitState,
_: &ZwpTabletSeatV2,
_: <ZwpTabletSeatV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WinitState>,
) {
}
}
impl Dispatch<ZwpTabletToolV2, TabletToolData, WinitState> for TabletManager {
fn event(
state: &mut WinitState,
_: &ZwpTabletToolV2,
event: <ZwpTabletToolV2 as Proxy>::Event,
data: &TabletToolData,
_: &Connection,
_: &QueueHandle<WinitState>,
) {
let mut data = data.inner.lock().unwrap();
match event {
ToolEvent::Type { tool_type: WEnum::Value(tool_type) } => {
data.ty = match tool_type {
ToolType::Pen => TabletToolKind::Pen,
ToolType::Eraser => TabletToolKind::Eraser,
ToolType::Brush => TabletToolKind::Brush,
ToolType::Pencil => TabletToolKind::Pencil,
ToolType::Airbrush => TabletToolKind::Airbrush,
ToolType::Finger => TabletToolKind::Finger,
ToolType::Mouse => TabletToolKind::Mouse,
ToolType::Lens => TabletToolKind::Lens,
_ => return,
};
},
ToolEvent::Capability { .. } => {},
ToolEvent::Done => (),
ToolEvent::ProximityIn { serial, surface, .. } => {
data.pending.push(TabletEvent::Enter { serial, surface });
},
ToolEvent::ProximityOut => data.pending.push(TabletEvent::Left),
ToolEvent::Down { serial } => {
let event = TabletEvent::Button {
state: ElementState::Pressed,
serial: Some(serial),
button: TabletToolButton::Contact,
};
data.pending.push(event);
},
ToolEvent::Up => {
let event = TabletEvent::Button {
state: ElementState::Released,
serial: None,
button: TabletToolButton::Contact,
};
data.pending.push(event);
},
ToolEvent::Tilt { tilt_x, tilt_y } => {
data.tool_state.tilt = Some(TabletToolTilt { x: tilt_x as i8, y: tilt_y as i8 });
},
ToolEvent::Motion { x, y } => {
data.position = (x, y).into();
data.pending.push(TabletEvent::Moved);
},
ToolEvent::Pressure { pressure } => {
data.tool_state.force = Some(Force::Normalized(pressure as f64 / u16::MAX as f64));
},
ToolEvent::Rotation { degrees } => {
data.tool_state.twist = Some(degrees as u16);
},
ToolEvent::Button { serial, button, state: WEnum::Value(state) } => {
let state = match state {
ButtonState::Released => ElementState::Released,
ButtonState::Pressed => ElementState::Pressed,
_ => return,
};
// Map similar to SDL.
let button = match button {
// BTN_STYLUS.
0x14b => TabletToolButton::Contact,
0x14c => TabletToolButton::Barrel,
// BTN_STYLUS3.
0x149 => TabletToolButton::Other(1),
// There's no defined conversion for any of that.
button => TabletToolButton::Other(button as u16),
};
let event = TabletEvent::Button { button, state, serial: Some(serial) };
data.pending.push(event);
},
ToolEvent::Frame { .. } => {
let kind = data.ty;
for event in std::mem::take(&mut data.pending) {
if let TabletEvent::Enter { surface, serial } = &event {
data.latest_enter_serial = Some(*serial);
data.surface = Some(surface.clone());
}
// Handle events only for top-level surface.
let surface = match data
.surface
.as_ref()
.map(|surface| (surface, surface.data::<SurfaceData>()))
{
Some((surface, Some(surface_data)))
if surface_data.parent_surface().is_none() =>
{
surface
},
_ => continue,
};
let window_id = crate::make_wid(surface);
// Ensure that window exists.
let window = match state.windows.get_mut().get_mut(&window_id) {
Some(window) => window.lock().unwrap(),
None => continue,
};
let position = data.position.to_physical(window.scale_factor());
let window_event = match event {
TabletEvent::Enter { .. } => WindowEvent::PointerEntered {
device_id: None,
position,
primary: true,
kind: PointerKind::TabletTool(kind),
},
TabletEvent::Moved => WindowEvent::PointerMoved {
device_id: None,
position,
primary: true,
source: PointerSource::TabletTool {
kind,
data: data.tool_state.clone(),
},
},
TabletEvent::Button { button, state, serial } => {
// Update serial if we have it.
if let Some(serial) = serial {
data.latest_button_serial = Some(serial);
}
WindowEvent::PointerButton {
device_id: None,
state,
position,
primary: true,
button: ButtonSource::TabletTool {
kind,
button,
data: data.tool_state.clone(),
},
}
},
TabletEvent::Left => WindowEvent::PointerLeft {
device_id: None,
position: Some(position),
primary: true,
kind: PointerKind::TabletTool(kind),
},
};
state.events_sink.push_window_event(window_event, window_id);
// Clear up the surface after we've processed `Left` event.
if matches!(event, TabletEvent::Left) {
data.surface = None;
data.latest_button_serial = None;
data.latest_enter_serial = None;
data.tool_state = Default::default();
}
}
},
_ => (),
}
}
}
#[derive(Debug, Default)]
struct TabletToolData {
inner: Mutex<TabletToolDataInner>,
}
#[derive(Debug, Default)]
pub(crate) struct TabletToolDataInner {
pub(crate) ty: TabletToolKind,
/// Core tablet tool data.
pub(crate) tool_state: CoreTabletToolData,
/// Pending events until the `frame` is received.
pub(crate) pending: SmallVec<[TabletEvent; 4]>,
/// Surface the tablet most recently entered.
pub(crate) surface: Option<WlSurface>,
/// Position relative to the surface.
pub(crate) position: LogicalPosition<f64>,
// NOTE: even though we don't utilize serials
// right now, track them anyway.
/// The serial of the latest enter event for the pointer
pub(crate) latest_enter_serial: Option<u32>,
/// The serial of the latest button event for the pointer
pub(crate) latest_button_serial: Option<u32>,
}
// Due to wayland using logical coordinates,
// delay the conversion to physical until the dispatch actually happens,
// so the scaling is applied at the time of actual dispatch, since it
// can technically change before the `frame` event.
#[derive(Debug, Clone)]
pub(crate) enum TabletEvent {
Enter { serial: u32, surface: WlSurface },
Left,
Moved,
Button { button: TabletToolButton, state: ElementState, serial: Option<u32> },
}
impl Dispatch<ZwpTabletV2, (), WinitState> for TabletManager {
fn event(
_: &mut WinitState,
_: &ZwpTabletV2,
_: <ZwpTabletV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WinitState>,
) {
}
}
impl Dispatch<ZwpTabletPadV2, (), WinitState> for TabletManager {
fn event(
_: &mut WinitState,
_: &ZwpTabletPadV2,
_: <ZwpTabletPadV2 as Proxy>::Event,
_: &(),
_: &Connection,
_: &QueueHandle<WinitState>,
) {
}
}
delegate_dispatch!(WinitState: [ZwpTabletManagerV2: GlobalData] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletManagerV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletSeatV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletV2: ()] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletToolV2: TabletToolData] => TabletManager);
delegate_dispatch!(WinitState: [ZwpTabletPadV2: ()] => TabletManager);

View file

@ -216,7 +216,7 @@ impl CursorHandler {
Cursor::Custom(cursor) => { Cursor::Custom(cursor) => {
let cursor = match cursor.cast_ref::<CustomCursor>() { let cursor = match cursor.cast_ref::<CustomCursor>() {
Some(cursor) => cursor, Some(cursor) => cursor,
None => todo!(), None => return,
}; };
if let SelectedCursor::Loading { cursor: old_cursor, .. } if let SelectedCursor::Loading { cursor: old_cursor, .. }

View file

@ -24,7 +24,7 @@ use crate::event_loop::ActiveEventLoop;
use crate::main_thread::MainThreadMarker; use crate::main_thread::MainThreadMarker;
use crate::monitor::MonitorHandler; use crate::monitor::MonitorHandler;
use crate::r#async::DispatchRunner; use crate::r#async::DispatchRunner;
use crate::web_sys::event::mouse_button_to_id; use crate::web_sys::event::ButtonsState;
use crate::window::Inner; use crate::window::Inner;
use crate::{backend, event, EventLoop, PollStrategy, WaitUntilStrategy}; use crate::{backend, event, EventLoop, PollStrategy, WaitUntilStrategy};
@ -318,8 +318,10 @@ impl Shared {
// chorded button event // chorded button event
let device_id = event::mkdid(event.pointer_id()); let device_id = event::mkdid(event.pointer_id());
if let Some(button) = backend::event::mouse_button(&event) { if let Some(button) = backend::event::raw_button(&event) {
let state = if backend::event::mouse_buttons(&event).contains(button.into()) { let state = if backend::event::pointer_buttons(&event)
.contains(ButtonsState::from_bits_retain(button))
{
ElementState::Pressed ElementState::Pressed
} else { } else {
ElementState::Released ElementState::Released
@ -327,10 +329,7 @@ impl Shared {
runner.send_event(Event::DeviceEvent { runner.send_event(Event::DeviceEvent {
device_id, device_id,
event: DeviceEvent::Button { event: DeviceEvent::Button { button: button.into(), state },
button: mouse_button_to_id(button).into(),
state,
},
}); });
return; return;
@ -338,14 +337,16 @@ impl Shared {
// pointer move event // pointer move event
let mut delta = backend::event::MouseDelta::init(&navigator, &event); let mut delta = backend::event::MouseDelta::init(&navigator, &event);
runner.send_events(backend::event::pointer_move_event(event).map(|event| { runner.send_events(backend::event::pointer_move_event(event).map(
|event: web_sys::PointerEvent| {
let delta = delta.delta(&event).to_physical(backend::scale_factor(&window)); let delta = delta.delta(&event).to_physical(backend::scale_factor(&window));
Event::DeviceEvent { Event::DeviceEvent {
device_id, device_id,
event: DeviceEvent::PointerMotion { delta: (delta.x, delta.y) }, event: DeviceEvent::PointerMotion { delta: (delta.x, delta.y) },
} }
})); },
));
}), }),
)); ));
let runner = self.clone(); let runner = self.clone();
@ -375,11 +376,11 @@ impl Shared {
return; return;
} }
let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); let button = backend::event::raw_button(&event).expect("no pointer button pressed");
runner.send_event(Event::DeviceEvent { runner.send_event(Event::DeviceEvent {
device_id: event::mkdid(event.pointer_id()), device_id: event::mkdid(event.pointer_id()),
event: DeviceEvent::Button { event: DeviceEvent::Button {
button: mouse_button_to_id(button).into(), button: button.into(),
state: ElementState::Pressed, state: ElementState::Pressed,
}, },
}); });
@ -394,11 +395,11 @@ impl Shared {
return; return;
} }
let button = backend::event::mouse_button(&event).expect("no mouse button pressed"); let button = backend::event::raw_button(&event).expect("no pointer button pressed");
runner.send_event(Event::DeviceEvent { runner.send_event(Event::DeviceEvent {
device_id: event::mkdid(event.pointer_id()), device_id: event::mkdid(event.pointer_id()),
event: DeviceEvent::Button { event: DeviceEvent::Button {
button: mouse_button_to_id(button).into(), button: button.into(),
state: ElementState::Released, state: ElementState::Released,
}, },
}); });

View file

@ -1,11 +1,16 @@
use std::cell::OnceCell; use std::cell::OnceCell;
use std::f64;
use dpi::{LogicalPosition, PhysicalPosition, Position}; use dpi::{LogicalPosition, PhysicalPosition, Position};
use smol_str::SmolStr; use smol_str::SmolStr;
use tracing::warn;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use web_sys::{KeyboardEvent, MouseEvent, Navigator, PointerEvent, WheelEvent}; use web_sys::{Event, KeyboardEvent, MouseEvent, Navigator, PointerEvent, WheelEvent};
use winit_core::event::{FingerId, MouseButton, MouseScrollDelta, PointerKind}; use winit_core::event::{
ButtonSource, FingerId, Force, MouseButton, MouseScrollDelta, PointerKind, PointerSource,
TabletToolAngle, TabletToolButton, TabletToolData, TabletToolKind, TabletToolTilt,
};
use winit_core::keyboard::{ use winit_core::keyboard::{
Key, KeyCode, KeyLocation, ModifiersState, NamedKey, NativeKey, NativeKeyCode, PhysicalKey, Key, KeyCode, KeyLocation, ModifiersState, NamedKey, NativeKey, NativeKeyCode, PhysicalKey,
}; };
@ -16,11 +21,12 @@ bitflags::bitflags! {
// https://www.w3.org/TR/pointerevents3/#the-buttons-property // https://www.w3.org/TR/pointerevents3/#the-buttons-property
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ButtonsState: u16 { pub struct ButtonsState: u16 {
const LEFT = 0b00001; const LEFT = 0b000001;
const RIGHT = 0b00010; const RIGHT = 0b000010;
const MIDDLE = 0b00100; const MIDDLE = 0b000100;
const BACK = 0b01000; const BACK = 0b001000;
const FORWARD = 0b10000; const FORWARD = 0b010000;
const ERASER = 0b100000;
} }
} }
@ -37,6 +43,15 @@ impl From<ButtonsState> for MouseButton {
} }
} }
impl From<ButtonSource> for ButtonsState {
fn from(value: ButtonSource) -> Self {
match value {
ButtonSource::TabletTool { button, .. } => button.into(),
other => ButtonsState::from(other.mouse_button()),
}
}
}
impl From<MouseButton> for ButtonsState { impl From<MouseButton> for ButtonsState {
fn from(value: MouseButton) -> Self { fn from(value: MouseButton) -> Self {
match value { match value {
@ -50,37 +65,133 @@ impl From<MouseButton> for ButtonsState {
} }
} }
pub fn mouse_buttons(event: &MouseEvent) -> ButtonsState { impl From<TabletToolButton> for ButtonsState {
fn from(tool: TabletToolButton) -> Self {
match tool {
TabletToolButton::Contact => ButtonsState::LEFT,
TabletToolButton::Barrel => ButtonsState::RIGHT,
TabletToolButton::Other(value) => Self::from_bits_retain(value),
}
}
}
pub fn pointer_buttons(event: &MouseEvent) -> ButtonsState {
#[allow(clippy::disallowed_methods)]
ButtonsState::from_bits_retain(event.buttons()) ButtonsState::from_bits_retain(event.buttons())
} }
pub fn mouse_button(event: &MouseEvent) -> Option<MouseButton> { pub fn raw_button(event: &MouseEvent) -> Option<u16> {
// https://www.w3.org/TR/pointerevents3/#the-button-property // https://www.w3.org/TR/pointerevents3/#the-button-property
match event.button() { #[allow(clippy::disallowed_methods)]
-1 => None, let button = event.button();
0 => Some(MouseButton::Left),
1 => Some(MouseButton::Middle), if button == -1 {
2 => Some(MouseButton::Right), None
3 => Some(MouseButton::Back), } else {
4 => Some(MouseButton::Forward), Some(button.try_into().expect("unexpected negative mouse button value"))
i => { }
Some(MouseButton::Other(i.try_into().expect("unexpected negative mouse button value"))) }
pub fn mouse_button(button: u16) -> MouseButton {
match button {
0 => MouseButton::Left,
1 => MouseButton::Middle,
2 => MouseButton::Right,
3 => MouseButton::Back,
4 => MouseButton::Forward,
other => MouseButton::Other(other),
}
}
pub fn tool_button(button: u16) -> TabletToolButton {
match button {
0 => TabletToolButton::Contact,
2 => TabletToolButton::Barrel,
other => TabletToolButton::Other(other),
}
}
#[derive(Clone, Copy)]
pub enum WebPointerType {
Mouse,
Touch,
Pen,
}
impl WebPointerType {
pub fn from_event(event: &PointerEvent) -> Option<Self> {
#[allow(clippy::disallowed_methods)]
let r#type = event.pointer_type();
match r#type.as_ref() {
"mouse" => Some(Self::Mouse),
"touch" => Some(Self::Touch),
"pen" => Some(Self::Pen),
r#type => {
warn!("found unknown pointer type: {type}");
None
}, },
} }
} }
}
pub fn mouse_button_to_id(button: MouseButton) -> u16 { pub fn pointer_kind(event: &PointerEvent, pointer_id: i32) -> PointerKind {
match button { match WebPointerType::from_event(event) {
MouseButton::Left => 0, Some(WebPointerType::Mouse) => PointerKind::Mouse,
MouseButton::Right => 1, Some(WebPointerType::Touch) => PointerKind::Touch(FingerId::from_raw(pointer_id as usize)),
MouseButton::Middle => 2, Some(WebPointerType::Pen) => {
MouseButton::Back => 3, PointerKind::TabletTool(if pointer_buttons(event).contains(ButtonsState::ERASER) {
MouseButton::Forward => 4, TabletToolKind::Eraser
MouseButton::Other(value) => value, } else {
TabletToolKind::Pen
})
},
None => PointerKind::Unknown,
} }
} }
pub fn mouse_position(event: &MouseEvent) -> LogicalPosition<f64> { pub fn pointer_source(event: &PointerEvent, kind: PointerKind) -> PointerSource {
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = PointerEvent, extends = MouseEvent, extends = Event)]
pub type PointerEventExt;
#[wasm_bindgen(method, getter, js_name = altitudeAngle)]
pub fn altitude_angle(this: &PointerEventExt) -> Option<f64>;
#[wasm_bindgen(method, getter, js_name = azimuthAngle)]
pub fn azimuth_angle(this: &PointerEventExt) -> f64;
}
let event: &PointerEventExt = event.unchecked_ref();
match kind {
PointerKind::Mouse => PointerSource::Mouse,
PointerKind::Touch(id) => PointerSource::Touch {
finger_id: id,
force: Some(Force::Normalized(event.pressure().into())),
},
PointerKind::TabletTool(tool) => {
let data = TabletToolData {
force: Some(Force::Normalized(event.pressure().into())),
tangential_force: Some(event.tangential_pressure()),
twist: Some(event.twist().try_into().expect("found invalid `twist`")),
tilt: Some(TabletToolTilt {
x: event.tilt_x().try_into().expect("found invalid `tiltX`"),
y: event.tilt_y().try_into().expect("found invalid `tiltY`"),
}),
angle: event
.altitude_angle()
.map(|altitude| TabletToolAngle { altitude, azimuth: event.azimuth_angle() }),
};
PointerSource::TabletTool { kind: tool, data }
},
PointerKind::Unknown => PointerSource::Unknown,
}
}
pub fn pointer_position(event: &MouseEvent) -> LogicalPosition<f64> {
#[wasm_bindgen] #[wasm_bindgen]
extern "C" { extern "C" {
type MouseEventExt; type MouseEventExt;
@ -113,7 +224,7 @@ impl MouseDelta {
Some(Engine::Chromium) => Self::Chromium, Some(Engine::Chromium) => Self::Chromium,
// Firefox has wrong movement values in coalesced events. // Firefox has wrong movement values in coalesced events.
Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko { Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko {
old_position: mouse_position(event), old_position: pointer_position(event),
old_delta: LogicalPosition::new( old_delta: LogicalPosition::new(
event.movement_x() as f64, event.movement_x() as f64,
event.movement_y() as f64, event.movement_y() as f64,
@ -129,7 +240,7 @@ impl MouseDelta {
PhysicalPosition::new(event.movement_x(), event.movement_y()).into() PhysicalPosition::new(event.movement_x(), event.movement_y()).into()
}, },
MouseDelta::Gecko { old_position, old_delta } => { MouseDelta::Gecko { old_position, old_delta } => {
let new_position = mouse_position(event); let new_position = pointer_position(event);
let x = new_position.x - old_position.x + old_delta.x; let x = new_position.x - old_position.x + old_delta.x;
let y = new_position.y - old_position.y + old_delta.y; let y = new_position.y - old_position.y + old_delta.y;
*old_position = new_position; *old_position = new_position;
@ -160,14 +271,6 @@ pub fn mouse_scroll_delta(
} }
} }
pub fn pointer_type(event: &PointerEvent, pointer_id: i32) -> PointerKind {
match event.pointer_type().as_str() {
"mouse" => PointerKind::Mouse,
"touch" => PointerKind::Touch(FingerId::from_raw(pointer_id as usize)),
_ => PointerKind::Unknown,
}
}
pub fn key_code(event: &KeyboardEvent) -> PhysicalKey { pub fn key_code(event: &KeyboardEvent) -> PhysicalKey {
// Use keyboard-types' parsing (it is based on the W3C standard). // Use keyboard-types' parsing (it is based on the W3C standard).
match event.code().parse() { match event.code().parse() {

View file

@ -3,14 +3,14 @@ use std::rc::Rc;
use dpi::PhysicalPosition; use dpi::PhysicalPosition;
use web_sys::PointerEvent; use web_sys::PointerEvent;
use winit_core::event::{ButtonSource, DeviceId, ElementState, Force, PointerKind, PointerSource}; use winit_core::event::{ButtonSource, DeviceId, ElementState, PointerKind, PointerSource};
use winit_core::keyboard::ModifiersState; use winit_core::keyboard::ModifiersState;
use super::canvas::Common; use super::canvas::Common;
use super::event; use super::event;
use super::event_handle::EventListenerHandle; use super::event_handle::EventListenerHandle;
use crate::event::mkdid; use crate::event::mkdid;
use crate::web_sys::event::mouse_button_to_id; use crate::web_sys::event::ButtonsState;
#[allow(dead_code)] #[allow(dead_code)]
pub(super) struct PointerHandler { pub(super) struct PointerHandler {
@ -46,8 +46,8 @@ impl PointerHandler {
let pointer_id = event.pointer_id(); let pointer_id = event.pointer_id();
let device_id = mkdid(pointer_id); let device_id = mkdid(pointer_id);
let position = let position =
event::mouse_position(&event).to_physical(super::scale_factor(&window)); event::pointer_position(&event).to_physical(super::scale_factor(&window));
let kind = event::pointer_type(&event, pointer_id); let kind = event::pointer_kind(&event, pointer_id);
handler(modifiers, device_id, event.is_primary(), position, kind); handler(modifiers, device_id, event.is_primary(), position, kind);
})); }));
} }
@ -64,8 +64,8 @@ impl PointerHandler {
let pointer_id = event.pointer_id(); let pointer_id = event.pointer_id();
let device_id = mkdid(pointer_id); let device_id = mkdid(pointer_id);
let position = let position =
event::mouse_position(&event).to_physical(super::scale_factor(&window)); event::pointer_position(&event).to_physical(super::scale_factor(&window));
let kind = event::pointer_type(&event, pointer_id); let kind = event::pointer_kind(&event, pointer_id);
handler(modifiers, device_id, event.is_primary(), position, kind); handler(modifiers, device_id, event.is_primary(), position, kind);
})); }));
} }
@ -80,24 +80,25 @@ impl PointerHandler {
Some(canvas_common.add_event("pointerup", move |event: PointerEvent| { Some(canvas_common.add_event("pointerup", move |event: PointerEvent| {
let modifiers = event::mouse_modifiers(&event); let modifiers = event::mouse_modifiers(&event);
let pointer_id = event.pointer_id(); let pointer_id = event.pointer_id();
let kind = event::pointer_type(&event, pointer_id); let kind = event::pointer_kind(&event, pointer_id);
let button = event::raw_button(&event).expect("no button pressed");
let button = event::mouse_button(&event).expect("no mouse button pressed"); let source = match event::pointer_source(&event, kind) {
PointerSource::Mouse => ButtonSource::Mouse(event::mouse_button(button)),
let source = match kind { PointerSource::Touch { finger_id, force } => {
PointerKind::Mouse => ButtonSource::Mouse(button), ButtonSource::Touch { finger_id, force }
PointerKind::Touch(finger_id) => ButtonSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
}, },
PointerKind::Unknown => ButtonSource::Unknown(mouse_button_to_id(button)), PointerSource::TabletTool { kind, data } => {
ButtonSource::TabletTool { kind, button: event::tool_button(button), data }
},
PointerSource::Unknown => ButtonSource::Unknown(button),
}; };
handler( handler(
modifiers, modifiers,
mkdid(pointer_id), mkdid(pointer_id),
event.is_primary(), event.is_primary(),
event::mouse_position(&event).to_physical(super::scale_factor(&window)), event::pointer_position(&event).to_physical(super::scale_factor(&window)),
source, source,
) )
})); }));
@ -125,11 +126,11 @@ impl PointerHandler {
let modifiers = event::mouse_modifiers(&event); let modifiers = event::mouse_modifiers(&event);
let pointer_id = event.pointer_id(); let pointer_id = event.pointer_id();
let kind = event::pointer_type(&event, pointer_id); let kind = event::pointer_kind(&event, pointer_id);
let button = event::mouse_button(&event).expect("no mouse button pressed"); let button = event::raw_button(&event).expect("no button pressed");
let source = match kind { let source = match event::pointer_source(&event, kind) {
PointerKind::Mouse => { PointerSource::Mouse => {
// Error is swallowed here since the error would occur every time the // Error is swallowed here since the error would occur every time the
// mouse is clicked when the cursor is // mouse is clicked when the cursor is
// grabbed, and there is probably not a // grabbed, and there is probably not a
@ -137,20 +138,29 @@ impl PointerHandler {
// care if it fails. // care if it fails.
let _e = canvas.set_pointer_capture(pointer_id); let _e = canvas.set_pointer_capture(pointer_id);
ButtonSource::Mouse(button) ButtonSource::Mouse(event::mouse_button(button))
}, },
PointerKind::Touch(finger_id) => ButtonSource::Touch { PointerSource::Touch { finger_id, force } => {
finger_id, ButtonSource::Touch { finger_id, force }
force: Some(Force::Normalized(event.pressure().into())),
}, },
PointerKind::Unknown => ButtonSource::Unknown(mouse_button_to_id(button)), PointerSource::TabletTool { kind, data } => {
// Error is swallowed here since the error would occur every time the
// mouse is clicked when the cursor is
// grabbed, and there is probably not a
// situation where this could fail, that we
// care if it fails.
let _e = canvas.set_pointer_capture(pointer_id);
ButtonSource::TabletTool { kind, button: event::tool_button(button), data }
},
PointerSource::Unknown => ButtonSource::Unknown(button),
}; };
handler( handler(
modifiers, modifiers,
mkdid(pointer_id), mkdid(pointer_id),
event.is_primary(), event.is_primary(),
event::mouse_position(&event).to_physical(super::scale_factor(&window)), event::pointer_position(&event).to_physical(super::scale_factor(&window)),
source, source,
) )
})); }));
@ -186,11 +196,11 @@ impl PointerHandler {
Some(canvas_common.add_event("pointermove", move |event: PointerEvent| { Some(canvas_common.add_event("pointermove", move |event: PointerEvent| {
let pointer_id = event.pointer_id(); let pointer_id = event.pointer_id();
let device_id = mkdid(pointer_id); let device_id = mkdid(pointer_id);
let kind = event::pointer_type(&event, pointer_id); let kind = event::pointer_kind(&event, pointer_id);
let primary = event.is_primary(); let primary = event.is_primary();
// chorded button event // chorded button event
if let Some(button) = event::mouse_button(&event) { if let Some(button) = event::raw_button(&event) {
if prevent_default.get() { if prevent_default.get() {
// prevent text selection // prevent text selection
event.prevent_default(); event.prevent_default();
@ -198,34 +208,36 @@ impl PointerHandler {
let _ = canvas.focus(); let _ = canvas.focus();
} }
let state = if event::mouse_buttons(&event).contains(button.into()) { let state = if event::pointer_buttons(&event)
.contains(ButtonsState::from_bits_retain(button))
{
ElementState::Pressed ElementState::Pressed
} else { } else {
ElementState::Released ElementState::Released
}; };
let button = match kind { let button = match event::pointer_source(&event, kind) {
PointerKind::Mouse => ButtonSource::Mouse(button), PointerSource::Mouse => ButtonSource::Mouse(event::mouse_button(button)),
PointerKind::Touch(finger_id) => { PointerSource::Touch { finger_id, force } => {
let button_id = mouse_button_to_id(button); if button != 0 {
tracing::error!("unexpected touch button id: {button}");
if button_id != 1 {
tracing::error!("unexpected touch button id: {button_id}");
} }
ButtonSource::Touch { ButtonSource::Touch { finger_id, force }
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
}
}, },
PointerKind::Unknown => todo!(), PointerSource::TabletTool { kind, data } => ButtonSource::TabletTool {
kind,
button: event::tool_button(button),
data,
},
PointerSource::Unknown => ButtonSource::Unknown(button),
}; };
button_handler( button_handler(
event::mouse_modifiers(&event), event::mouse_modifiers(&event),
device_id, device_id,
primary, primary,
event::mouse_position(&event).to_physical(super::scale_factor(&window)), event::pointer_position(&event).to_physical(super::scale_factor(&window)),
state, state,
button, button,
); );
@ -242,15 +254,8 @@ impl PointerHandler {
( (
event::mouse_modifiers(&event), event::mouse_modifiers(&event),
event.is_primary(), event.is_primary(),
event::mouse_position(&event).to_physical(scale), event::pointer_position(&event).to_physical(scale),
match kind { event::pointer_source(&event, kind),
PointerKind::Mouse => PointerSource::Mouse,
PointerKind::Touch(finger_id) => PointerSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
},
PointerKind::Unknown => PointerSource::Unknown,
},
) )
}), }),
); );

View file

@ -44,28 +44,29 @@ use windows_sys::Win32::UI::WindowsAndMessaging::{
GetMenu, LoadCursorW, MsgWaitForMultipleObjectsEx, PeekMessageW, PostMessageW, GetMenu, LoadCursorW, MsgWaitForMultipleObjectsEx, PeekMessageW, PostMessageW,
RegisterClassExW, RegisterWindowMessageA, SetCursor, SetWindowPos, SystemParametersInfoW, RegisterClassExW, RegisterWindowMessageA, SetCursor, SetWindowPos, SystemParametersInfoW,
TranslateMessage, CREATESTRUCTW, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, MINMAXINFO, TranslateMessage, CREATESTRUCTW, GWL_STYLE, GWL_USERDATA, HTCAPTION, HTCLIENT, MINMAXINFO,
MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, NCCALCSIZE_PARAMS, PM_REMOVE, PT_TOUCH, QS_ALLINPUT, MNC_CLOSE, MSG, MWMO_INPUTAVAILABLE, NCCALCSIZE_PARAMS, PEN_FLAG_BARREL, PEN_FLAG_ERASER,
RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE, SIZE_MAXIMIZED, PEN_MASK_PRESSURE, PEN_MASK_ROTATION, PEN_MASK_TILT_X, PEN_MASK_TILT_Y, PM_REMOVE, PT_PEN,
SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, PT_TOUCH, QS_ALLINPUT, RI_MOUSE_HWHEEL, RI_MOUSE_WHEEL, SC_MINIMIZE, SC_RESTORE,
SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WMSZ_BOTTOM, WMSZ_BOTTOMLEFT, WMSZ_BOTTOMRIGHT, SIZE_MAXIMIZED, SPI_GETWHEELSCROLLCHARS, SPI_GETWHEELSCROLLLINES, SWP_NOACTIVATE, SWP_NOMOVE,
WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT, WM_CAPTURECHANGED, WM_CLOSE, SWP_NOSIZE, SWP_NOZORDER, WHEEL_DELTA, WINDOWPOS, WMSZ_BOTTOM, WMSZ_BOTTOMLEFT,
WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WMSZ_BOTTOMRIGHT, WMSZ_LEFT, WMSZ_RIGHT, WMSZ_TOP, WMSZ_TOPLEFT, WMSZ_TOPRIGHT,
WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_CAPTURECHANGED, WM_CLOSE, WM_CREATE, WM_DESTROY, WM_DPICHANGED, WM_ENTERSIZEMOVE,
WM_INPUT, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_EXITSIZEMOVE, WM_GETMINMAXINFO, WM_IME_COMPOSITION, WM_IME_ENDCOMPOSITION,
WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_IME_SETCONTEXT, WM_IME_STARTCOMPOSITION, WM_INPUT, WM_KEYDOWN, WM_KEYUP, WM_KILLFOCUS,
WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN, WM_MBUTTONUP, WM_MENUCHAR, WM_MOUSEHWHEEL,
WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_NCACTIVATE, WM_NCCALCSIZE, WM_NCCREATE, WM_NCDESTROY,
WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_NCLBUTTONDOWN, WM_PAINT, WM_POINTERDOWN, WM_POINTERUP, WM_POINTERUPDATE, WM_RBUTTONDOWN,
WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WM_RBUTTONUP, WM_SETCURSOR, WM_SETFOCUS, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND,
WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WM_SYSKEYDOWN, WM_SYSKEYUP, WM_TOUCH, WM_WINDOWPOSCHANGED, WM_WINDOWPOSCHANGING,
WS_VISIBLE, WM_XBUTTONDOWN, WM_XBUTTONUP, WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW,
WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE,
}; };
use winit_core::application::ApplicationHandler; use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor, CustomCursorSource}; use winit_core::cursor::{CustomCursor, CustomCursorSource};
use winit_core::error::{EventLoopError, NotSupportedError, RequestError}; use winit_core::error::{EventLoopError, NotSupportedError, RequestError};
use winit_core::event::{ use winit_core::event::{
DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TouchPhase, DeviceEvent, DeviceId, FingerId, Force, Ime, RawKeyEvent, SurfaceSizeWriter, TabletToolButton,
WindowEvent, TabletToolData, TabletToolKind, TabletToolTilt, TouchPhase, WindowEvent,
}; };
use winit_core::event_loop::pump_events::PumpStatus; use winit_core::event_loop::pump_events::PumpStatus;
use winit_core::event_loop::{ use winit_core::event_loop::{
@ -106,6 +107,7 @@ pub(crate) struct WindowData {
pub key_event_builder: KeyEventBuilder, pub key_event_builder: KeyEventBuilder,
pub _file_drop_handler: Option<FileDropHandler>, pub _file_drop_handler: Option<FileDropHandler>,
pub userdata_removed: Cell<bool>, pub userdata_removed: Cell<bool>,
pub last_tablet_down_button_state: Cell<u32>,
pub recurse_depth: Cell<u32>, pub recurse_depth: Cell<u32>,
} }
@ -2085,22 +2087,6 @@ unsafe fn public_window_callback_inner(
continue; continue;
} }
let force = if let PT_TOUCH = pointer_info.pointerType {
let mut touch_info = mem::MaybeUninit::uninit();
util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| {
match unsafe {
GetPointerTouchInfo(pointer_info.pointerId, touch_info.as_mut_ptr())
} {
0 => None,
_ => normalize_pointer_pressure(unsafe {
touch_info.assume_init().pressure
}),
}
})
} else {
None
};
let x = location.x as f64 + x.fract(); let x = location.x as f64 + x.fract();
let y = location.y as f64 + y.fract(); let y = location.y as f64 + y.fract();
let position = PhysicalPosition::new(x, y); let position = PhysicalPosition::new(x, y);
@ -2108,60 +2094,83 @@ unsafe fn public_window_callback_inner(
let finger_id = FingerId::from_raw(pointer_info.pointerId as usize); let finger_id = FingerId::from_raw(pointer_info.pointerId as usize);
let primary = util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_PRIMARY); let primary = util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_PRIMARY);
if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_DOWN) { let is_down = util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_DOWN);
if is_down || util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UP) {
let (kind, button) = match pointer_info.pointerType {
PT_TOUCH => (PointerKind::Touch(finger_id), ButtonSource::Touch {
finger_id,
force: force_for_touch(pointer_info.pointerId),
}),
PT_PEN => {
let kind = PointerKind::TabletTool(TabletToolKind::Pen);
let (mut pen_flags, data) =
tablet_tool_info_for_pen(pointer_info.pointerId);
let old_pen_flags =
userdata.last_tablet_down_button_state.replace(pen_flags);
// For release, use a diff.
if !is_down {
pen_flags ^= old_pen_flags;
};
let button = ButtonSource::TabletTool {
kind: TabletToolKind::Pen,
button: pen_flags_to_button(pen_flags),
data,
};
(kind, button)
},
_ => (PointerKind::Unknown, ButtonSource::Unknown(0)),
};
if is_down {
userdata.send_window_event(window, WindowEvent::PointerEntered { userdata.send_window_event(window, WindowEvent::PointerEntered {
device_id: None, device_id: None,
primary, primary,
position, position,
kind: if let PT_TOUCH = pointer_info.pointerType { kind,
PointerKind::Touch(finger_id)
} else {
PointerKind::Unknown
},
}); });
userdata.send_window_event(window, WindowEvent::PointerButton { userdata.send_window_event(window, WindowEvent::PointerButton {
device_id: None, device_id: None,
primary, primary,
state: Pressed, state: Pressed,
position, position,
button: if let PT_TOUCH = pointer_info.pointerType { button,
ButtonSource::Touch { finger_id, force }
} else {
ButtonSource::Unknown(0)
},
}); });
} else if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UP) { } else {
userdata.send_window_event(window, WindowEvent::PointerButton { userdata.send_window_event(window, WindowEvent::PointerButton {
device_id: None, device_id: None,
primary, primary,
state: Released, state: Released,
position, position,
button: if let PT_TOUCH = pointer_info.pointerType { button,
ButtonSource::Touch { finger_id, force }
} else {
ButtonSource::Unknown(0)
},
}); });
userdata.send_window_event(window, WindowEvent::PointerLeft { userdata.send_window_event(window, WindowEvent::PointerLeft {
device_id: None, device_id: None,
primary, primary,
position: Some(position), position: Some(position),
kind: if let PT_TOUCH = pointer_info.pointerType { kind,
PointerKind::Touch(finger_id)
} else {
PointerKind::Unknown
},
}); });
}
} else if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UPDATE) { } else if util::has_flag(pointer_info.pointerFlags, POINTER_FLAG_UPDATE) {
let source = match pointer_info.pointerType {
PT_TOUCH => PointerSource::Touch {
finger_id,
force: force_for_touch(pointer_info.pointerId),
},
PT_PEN => PointerSource::TabletTool {
kind: TabletToolKind::Pen,
data: tablet_tool_info_for_pen(pointer_info.pointerId).1,
},
_ => PointerSource::Unknown,
};
userdata.send_window_event(window, WindowEvent::PointerMoved { userdata.send_window_event(window, WindowEvent::PointerMoved {
device_id: None, device_id: None,
primary, primary,
position, position,
source: if let PT_TOUCH = pointer_info.pointerType { source,
PointerSource::Touch { finger_id, force }
} else {
PointerSource::Unknown
},
}); });
} else { } else {
continue; continue;
@ -2691,3 +2700,57 @@ fn apply_win10_dpi_adjustment(
conservative_rect conservative_rect
} }
fn force_for_touch(pointer_id: u32) -> Option<Force> {
let mut touch_info = mem::MaybeUninit::uninit();
util::GET_POINTER_TOUCH_INFO.and_then(|GetPointerTouchInfo| {
match unsafe { GetPointerTouchInfo(pointer_id, touch_info.as_mut_ptr()) } {
0 => None,
_ => normalize_pointer_pressure(unsafe { touch_info.assume_init().pressure }),
}
})
}
// Information is stored on the same thing, so we don't save anything by
// splitting the functions.
fn tablet_tool_info_for_pen(pointer_id: u32) -> (u32, TabletToolData) {
let mut tool_data = TabletToolData::default();
let mut tool_button = 0;
let mut tablet_info = mem::MaybeUninit::uninit();
util::GET_POINTER_PEN_INFO.map(|GetPointerPenInfo| {
if unsafe { GetPointerPenInfo(pointer_id, tablet_info.as_mut_ptr()) } == 0 {
return;
}
let pen_info = unsafe { tablet_info.assume_init() };
if pen_info.penMask & PEN_MASK_PRESSURE != 0 {
tool_data.force = normalize_pointer_pressure(pen_info.pressure);
}
if pen_info.penMask & (PEN_MASK_TILT_X | PEN_MASK_TILT_Y) != 0 {
tool_data.tilt =
Some(TabletToolTilt { x: pen_info.tiltX as i8, y: pen_info.tiltY as i8 });
}
if pen_info.penMask & PEN_MASK_ROTATION != 0 {
tool_data.twist = Some(pen_info.rotation as u16);
}
tool_button = pen_info.penFlags;
});
(tool_button, tool_data)
}
// NOTE: According to firefox, the buttons while can be combined, in
// reality they are not.
fn pen_flags_to_button(flags: u32) -> TabletToolButton {
if flags & PEN_FLAG_BARREL != 0 {
TabletToolButton::Barrel
} else if flags & PEN_FLAG_ERASER != 0 {
// NOTE: this is likely not a correct thing to do, but route
// it that way for now.
TabletToolButton::Other(PEN_FLAG_ERASER as u16)
} else {
TabletToolButton::Contact
}
}

View file

@ -16,7 +16,7 @@ use windows_sys::Win32::UI::HiDpi::{
DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS, DPI_AWARENESS_CONTEXT, MONITOR_DPI_TYPE, PROCESS_DPI_AWARENESS,
}; };
use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow; use windows_sys::Win32::UI::Input::KeyboardAndMouse::GetActiveWindow;
use windows_sys::Win32::UI::Input::Pointer::{POINTER_INFO, POINTER_TOUCH_INFO}; use windows_sys::Win32::UI::Input::Pointer::{POINTER_INFO, POINTER_PEN_INFO, POINTER_TOUCH_INFO};
use windows_sys::Win32::UI::WindowsAndMessaging::{ use windows_sys::Win32::UI::WindowsAndMessaging::{
ClipCursor, GetClientRect, GetClipCursor, GetCursorPos, GetSystemMetrics, GetWindowPlacement, ClipCursor, GetClientRect, GetClipCursor, GetCursorPos, GetSystemMetrics, GetWindowPlacement,
GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP, GetWindowRect, IsIconic, ShowCursor, IDC_APPSTARTING, IDC_ARROW, IDC_CROSS, IDC_HAND, IDC_HELP,
@ -254,6 +254,8 @@ pub type GetPointerDeviceRects = unsafe extern "system" fn(
pub type GetPointerTouchInfo = pub type GetPointerTouchInfo =
unsafe extern "system" fn(pointer_id: u32, touch_info: *mut POINTER_TOUCH_INFO) -> BOOL; unsafe extern "system" fn(pointer_id: u32, touch_info: *mut POINTER_TOUCH_INFO) -> BOOL;
pub type GetPointerPenInfo =
unsafe extern "system" fn(pointer_id: u32, pen_info: *mut POINTER_PEN_INFO) -> BOOL;
pub(crate) static WIN10_BUILD_VERSION: LazyLock<Option<u32>> = LazyLock::new(|| { pub(crate) static WIN10_BUILD_VERSION: LazyLock<Option<u32>> = LazyLock::new(|| {
type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> NTSTATUS; type RtlGetVersion = unsafe extern "system" fn(*mut OSVERSIONINFOW) -> NTSTATUS;
@ -306,6 +308,8 @@ pub(crate) static GET_POINTER_DEVICE_RECTS: LazyLock<Option<GetPointerDeviceRect
LazyLock::new(|| get_function!("user32.dll", GetPointerDeviceRects)); LazyLock::new(|| get_function!("user32.dll", GetPointerDeviceRects));
pub(crate) static GET_POINTER_TOUCH_INFO: LazyLock<Option<GetPointerTouchInfo>> = pub(crate) static GET_POINTER_TOUCH_INFO: LazyLock<Option<GetPointerTouchInfo>> =
LazyLock::new(|| get_function!("user32.dll", GetPointerTouchInfo)); LazyLock::new(|| get_function!("user32.dll", GetPointerTouchInfo));
pub(crate) static GET_POINTER_PEN_INFO: LazyLock<Option<GetPointerPenInfo>> =
LazyLock::new(|| get_function!("user32.dll", GetPointerPenInfo));
pub(crate) fn wrap_device_id(id: u32) -> DeviceId { pub(crate) fn wrap_device_id(id: u32) -> DeviceId {
DeviceId::from_raw(id as i64) DeviceId::from_raw(id as i64)

View file

@ -1263,6 +1263,7 @@ impl InitData<'_> {
_file_drop_handler: file_drop_handler, _file_drop_handler: file_drop_handler,
userdata_removed: Cell::new(false), userdata_removed: Cell::new(false),
recurse_depth: Cell::new(0), recurse_depth: Cell::new(0),
last_tablet_down_button_state: Cell::new(0),
} }
} }

View file

@ -103,7 +103,14 @@ atom_manager! {
_NET_SUPPORTED, _NET_SUPPORTED,
_NET_SUPPORTING_WM_CHECK, _NET_SUPPORTING_WM_CHECK,
_XEMBED, _XEMBED,
_XSETTINGS_SETTINGS _XSETTINGS_SETTINGS,
// Stylus Atoms
ABS_X: b"Abs X",
ABS_Y: b"Abs Y",
ABS_PRESSURE: b"Abs Pressure",
ABS_TILT_X: b"Abs Tilt X",
ABS_TILT_Y: b"Abs Tilt Y"
} }
impl Index<AtomName> for Atoms { impl Index<AtomName> for Atoms {

View file

@ -1006,6 +1006,15 @@ pub struct Device {
// For master devices, this is the paired device (pointer <-> keyboard). // For master devices, this is the paired device (pointer <-> keyboard).
// For slave devices, this is the master. // For slave devices, this is the master.
pub(crate) attachment: c_int, pub(crate) attachment: c_int,
pub(crate) r#type: DeviceType,
}
#[derive(Clone, Copy, Debug)]
pub(crate) enum DeviceType {
Mouse,
Touch,
Pen,
Eraser,
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
@ -1022,9 +1031,10 @@ pub(crate) enum ScrollOrientation {
} }
impl Device { impl Device {
pub(crate) fn new(info: &ffi::XIDeviceInfo) -> Self { pub(crate) fn new(info: &ffi::XIDeviceInfo, atoms: &Atoms) -> Self {
let name = unsafe { CStr::from_ptr(info.name).to_string_lossy() }; let name = unsafe { CStr::from_ptr(info.name).to_string_lossy() };
let mut scroll_axes = Vec::new(); let mut scroll_axes = Vec::new();
let mut r#type = None;
if Device::physical_device(info) { if Device::physical_device(info) {
// Identify scroll axes // Identify scroll axes
@ -1041,12 +1051,34 @@ impl Device {
}, },
position: 0.0, position: 0.0,
})); }));
} else if ty == ffi::XITouchClass {
r#type = Some(DeviceType::Touch);
} else if r#type.is_none() && ty == ffi::XIValuatorClass {
let info = unsafe { &*(class_ptr as *const ffi::XIValuatorClassInfo) };
let atom = info.label as xproto::Atom;
if atom == atoms[ABS_X]
|| atom == atoms[ABS_Y]
|| atom == atoms[ABS_PRESSURE]
|| atom == atoms[ABS_TILT_X]
|| atom == atoms[ABS_TILT_Y]
{
if name.contains("eraser") {
r#type = Some(DeviceType::Eraser);
} else {
r#type = Some(DeviceType::Pen);
}
}
} }
} }
} }
let mut device = let mut device = Device {
Device { _name: name.into_owned(), scroll_axes, attachment: info.attachment }; _name: name.into_owned(),
scroll_axes,
attachment: info.attachment,
r#type: r#type.unwrap_or(DeviceType::Mouse),
};
device.reset_scroll_position(info); device.reset_scroll_position(info);
device device
} }

View file

@ -33,8 +33,8 @@ use xkbcommon_dl::xkb_mod_mask_t;
use crate::atoms::*; use crate::atoms::*;
use crate::dnd::{Dnd, DndState}; use crate::dnd::{Dnd, DndState};
use crate::event_loop::{ use crate::event_loop::{
mkdid, mkwid, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, ScrollOrientation, mkdid, mkwid, ActiveEventLoop, CookieResultExt, Device, DeviceInfo, DeviceType,
ALL_DEVICES, ScrollOrientation, ALL_DEVICES,
}; };
use crate::ime::{ImeEvent, ImeEventReceiver, ImeReceiver, ImeRequest}; use crate::ime::{ImeEvent, ImeEventReceiver, ImeReceiver, ImeRequest};
use crate::util; use crate::util;
@ -323,8 +323,10 @@ impl EventProcessor {
pub fn init_device(&self, device: xinput::DeviceId) { pub fn init_device(&self, device: xinput::DeviceId) {
let mut devices = self.devices.borrow_mut(); let mut devices = self.devices.borrow_mut();
if let Some(info) = DeviceInfo::get(&self.target.xconn, device as _) { if let Some(info) = DeviceInfo::get(&self.target.xconn, device as _) {
let atoms = self.target.x_connection().atoms();
for info in info.iter() { for info in info.iter() {
devices.insert(mkdid(info.deviceid as xinput::DeviceId), Device::new(info)); devices.insert(mkdid(info.deviceid as xinput::DeviceId), Device::new(info, atoms));
} }
} }
} }
@ -974,6 +976,15 @@ impl EventProcessor {
// Set the timestamp. // Set the timestamp.
self.target.xconn.set_timestamp(event.time as xproto::Timestamp); self.target.xconn.set_timestamp(event.time as xproto::Timestamp);
let Some(DeviceType::Mouse) = self
.devices
.borrow()
.get(&mkdid(event.sourceid as xinput::DeviceId))
.map(|device| device.r#type)
else {
return;
};
// Deliver multi-touch events instead of emulated mouse events. // Deliver multi-touch events instead of emulated mouse events.
if (event.flags & xinput2::XIPointerEmulated) != 0 { if (event.flags & xinput2::XIPointerEmulated) != 0 {
return; return;
@ -1051,6 +1062,15 @@ impl EventProcessor {
// Set the timestamp. // Set the timestamp.
self.target.xconn.set_timestamp(event.time as xproto::Timestamp); self.target.xconn.set_timestamp(event.time as xproto::Timestamp);
let Some(DeviceType::Mouse) = self
.devices
.borrow()
.get(&mkdid(event.sourceid as xinput::DeviceId))
.map(|device| device.r#type)
else {
return;
};
let device_id = Some(mkdid(event.deviceid as xinput::DeviceId)); let device_id = Some(mkdid(event.deviceid as xinput::DeviceId));
let window = event.event as xproto::Window; let window = event.event as xproto::Window;
let window_id = mkwid(window); let window_id = mkwid(window);
@ -1376,7 +1396,6 @@ impl EventProcessor {
self.target.xconn.set_timestamp(xev.time as xproto::Timestamp); self.target.xconn.set_timestamp(xev.time as xproto::Timestamp);
let did = Some(mkdid(xev.deviceid as xinput::DeviceId)); let did = Some(mkdid(xev.deviceid as xinput::DeviceId));
let mask = let mask =
unsafe { slice::from_raw_parts(xev.valuators.mask, xev.valuators.mask_len as usize) }; unsafe { slice::from_raw_parts(xev.valuators.mask, xev.valuators.mask_len as usize) };
let mut value = xev.raw_values; let mut value = xev.raw_values;
@ -1401,6 +1420,15 @@ impl EventProcessor {
value = unsafe { value.offset(1) }; value = unsafe { value.offset(1) };
} }
let Some(DeviceType::Mouse) = self
.devices
.borrow()
.get(&mkdid(xev.sourceid as xinput::DeviceId))
.map(|device| device.r#type)
else {
return;
};
if let Some(mouse_delta) = mouse_delta.consume() { if let Some(mouse_delta) = mouse_delta.consume() {
app.device_event(&self.target, did, DeviceEvent::PointerMotion { delta: mouse_delta }); app.device_event(&self.target, did, DeviceEvent::PointerMotion { delta: mouse_delta });
} }

View file

@ -8,3 +8,10 @@ These files are created by [Mads Marquart](https://github.com/madsmtm) using
[draw.io](https://draw.io/), and compressed using [svgomg.net](https://svgomg.net/). [draw.io](https://draw.io/), and compressed using [svgomg.net](https://svgomg.net/).
They are licensed under the [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/) license. They are licensed under the [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/) license.
## tool_*.webp
These files are converted versions of
[W3C Pointer Events spec images](https://github.com/w3c/pointerevents/tree/93938ae7fe0172e2ae7587ad7d7c4fc8562d7153/images)
by [patrickhlauke](https://github.com/patrickhlauke). It is licensed under the
[W3C Software and Document License](https://www.w3.org/copyright/software-license).

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

View file

@ -87,6 +87,7 @@ changelog entry.
- Add `Ime::DeleteSurrounding` to let the input method delete text. - Add `Ime::DeleteSurrounding` to let the input method delete text.
- Add more `ImePurpose` values. - Add more `ImePurpose` values.
- Add `ImeHints` to request particular IME behaviour. - Add `ImeHints` to request particular IME behaviour.
- Add Pen input support on Wayland, Windows, and Web via new Pointer event.
### Changed ### Changed
@ -210,6 +211,7 @@ changelog entry.
- Move `EventLoopExtRunOnDemand` from platform module to `winit::event_loop::run_on_demand`. - Move `EventLoopExtRunOnDemand` from platform module to `winit::event_loop::run_on_demand`.
- Use `NamedKey`, `Code` and `Location` from the `keyboard-types` v0.8 crate. - Use `NamedKey`, `Code` and `Location` from the `keyboard-types` v0.8 crate.
- Deprecate `Window::set_ime_allowed`, `Window::set_ime_cursor_area`, and `Window::set_ime_purpose`. - Deprecate `Window::set_ime_allowed`, `Window::set_ime_cursor_area`, and `Window::set_ime_purpose`.
- `Force::normalized()` now takes a `Option<ToolAngle>` to calculate the perpendicular force.
### Removed ### Removed
@ -249,6 +251,7 @@ changelog entry.
- Remove `NamedKey::Space`, match on `Key::Character(" ")` instead. - Remove `NamedKey::Space`, match on `Key::Character(" ")` instead.
- Remove `PartialEq` impl for `WindowAttributes`. - Remove `PartialEq` impl for `WindowAttributes`.
- `WindowAttributesExt*` platform extensions; use `WindowAttributes*` instead. - `WindowAttributesExt*` platform extensions; use `WindowAttributes*` instead.
- Remove `Force::Calibrated::altitude_angle` in favor of `ToolAngle::altitude`.
### Fixed ### Fixed
@ -264,3 +267,4 @@ changelog entry.
- On Windows, `Window::theme` will return the correct theme after setting it through `Window::set_theme`. - On Windows, `Window::theme` will return the correct theme after setting it through `Window::set_theme`.
- On Windows, `Window::set_theme` will change the title bar color immediately now. - On Windows, `Window::set_theme` will change the title bar color immediately now.
- On Windows 11, prevent incorrect shifting when dragging window onto a monitor with different DPI. - On Windows 11, prevent incorrect shifting when dragging window onto a monitor with different DPI.
- On Web, device events are emitted regardless of cursor type.