Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 122 additions & 8 deletions winit-appkit/src/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use objc2::rc::Retained;
use objc2::runtime::{AnyObject, Sel};
use objc2::{AnyThread, DefinedClass, MainThreadMarker, define_class, msg_send};
use objc2_app_kit::{
NSApplication, NSCursor, NSEvent, NSEventPhase, NSResponder, NSTextInputClient, NSTrackingArea,
NSTrackingAreaOptions, NSView, NSWindow,
NSApplication, NSCursor, NSEvent, NSEventPhase, NSEventSubtype, NSPointingDeviceType,
NSResponder, NSTextInputClient, NSTrackingArea, NSTrackingAreaOptions, NSView, NSWindow,
};
use objc2_core_foundation::CGRect;
use objc2_foundation::{
Expand All @@ -18,8 +18,9 @@ use objc2_foundation::{
};
use tracing::{debug_span, trace_span};
use winit_core::event::{
DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta,
PointerKind, PointerSource, TouchPhase, WindowEvent,
ButtonSource, DeviceEvent, ElementState, Force, Ime, KeyEvent, Modifiers, MouseButton,
MouseScrollDelta, PointerKind, PointerSource, TabletToolButton, TabletToolData, TabletToolKind,
TabletToolTilt, TouchPhase, WindowEvent,
};
use winit_core::keyboard::{Key, KeyCode, KeyLocation, ModifiersState, NamedKey};
use winit_core::window::ImeCapabilities;
Expand Down Expand Up @@ -135,6 +136,13 @@ pub struct ViewState {

/// The state of the `Option` as `Alt`.
option_as_alt: Cell<OptionAsAlt>,

/// The tablet tool currently in proximity, if any.
///
/// AppKit only reports the tool type on proximity events; tablet-point events always
/// report `Unknown`, so the tool announced at proximity time is cached here for use
/// by subsequent point and enter/exit events.
tablet_tool: Cell<Option<TabletToolKind>>,
}

define_class!(
Expand Down Expand Up @@ -633,7 +641,7 @@ define_class!(
device_id: None,
primary: true,
position,
kind: PointerKind::Mouse,
kind: self.current_pointer_kind(),
});
}

Expand All @@ -647,10 +655,17 @@ define_class!(
device_id: None,
primary: true,
position: Some(position),
kind: PointerKind::Mouse,
kind: self.current_pointer_kind(),
});
}

#[unsafe(method(tabletProximity:))]
fn tablet_proximity(&self, event: &NSEvent) {
let _entered = debug_span!("tabletProximity:").entered();

self.update_tablet_tool(event);
}

#[unsafe(method(scrollWheel:))]
fn scroll_wheel(&self, event: &NSEvent) {
let _entered = debug_span!("scrollWheel:").entered();
Expand Down Expand Up @@ -792,6 +807,7 @@ impl WinitView {
marked_text: Default::default(),
accepts_first_mouse,
option_as_alt: Cell::new(option_as_alt),
tablet_tool: Cell::new(None),
});
let this: Retained<Self> = unsafe { msg_send![super(this), init] };
*this.ivars().input_source.borrow_mut() = this.current_input_source();
Expand Down Expand Up @@ -1074,6 +1090,7 @@ impl WinitView {
}

fn mouse_click(&self, event: &NSEvent, button_state: ElementState) {
let tablet = self.tablet_event_data(event);
let position = self.mouse_view_point(event).to_physical(self.scale_factor());
let button = mouse_button(event);

Expand All @@ -1084,11 +1101,27 @@ impl WinitView {
primary: true,
state: button_state,
position,
button: button.into(),
button: match tablet {
Some((kind, data)) => ButtonSource::TabletTool {
kind,
button: match button {
// Mirror winit-core's `TabletToolButton` to `MouseButton` conversion table.
MouseButton::Left => TabletToolButton::Contact,
MouseButton::Right => TabletToolButton::Barrel,
MouseButton::Middle => TabletToolButton::Other(1),
MouseButton::Back => TabletToolButton::Other(3),
MouseButton::Forward => TabletToolButton::Other(4),
other => TabletToolButton::Other(other as u16),
},
data,
},
None => button.into(),
},
});
}

fn mouse_motion(&self, event: &NSEvent) {
let tablet = self.tablet_event_data(event);
let view_point = self.mouse_view_point(event);
let frame = self.frame();

Expand All @@ -1110,10 +1143,53 @@ impl WinitView {
device_id: None,
primary: true,
position: view_point.to_physical(self.scale_factor()),
source: PointerSource::Mouse,
source: match tablet {
Some((kind, data)) => PointerSource::TabletTool { kind, data },
None => PointerSource::Mouse,
},
});
}

/// The kind of pointer currently driving the cursor.
///
/// Enter/exit events carry no tablet information (`NSEvent::subtype` raises on them),
/// so the kind is derived from the cached proximity state instead.
fn current_pointer_kind(&self) -> PointerKind {
match self.ivars().tablet_tool.get() {
Some(kind) => PointerKind::TabletTool(kind),
None => PointerKind::Mouse,
}
}

/// Update the cached tablet tool from a proximity event.
///
/// The caller must ensure `event` is a `TabletProximity` event or a mouse event with
/// the `TabletProximity` subtype: the proximity accessors (`pointingDeviceType`,
/// `isEnteringProximity`) raise on other events.
fn update_tablet_tool(&self, event: &NSEvent) {
let tool = event.isEnteringProximity().then(|| tablet_tool_kind(event));
self.ivars().tablet_tool.set(tool);
}

/// Handle the tablet parts of a mouse event: update the cached tool on proximity
/// transitions and return the tool and its data for tablet-point events.
fn tablet_event_data(&self, event: &NSEvent) -> Option<(TabletToolKind, TabletToolData)> {
match event.subtype() {
NSEventSubtype::TabletPoint => {
// The tool kind is only announced at proximity time; if no proximity event
// was seen (e.g. the tool was already on the tablet at startup), assume the
// most common tool, a pen.
let kind = self.ivars().tablet_tool.get().unwrap_or(TabletToolKind::Pen);
Some((kind, tablet_tool_data(event)))
},
NSEventSubtype::TabletProximity => {
self.update_tablet_tool(event);
None
},
_ => None,
}
}

fn mouse_view_point(&self, event: &NSEvent) -> LogicalPosition<f64> {
let window_point = event.locationInWindow();
let view_point = self.convertPoint_fromView(window_point, None);
Expand All @@ -1136,6 +1212,44 @@ fn mouse_button(event: &NSEvent) -> MouseButton {
.expect("expected MacOS button number in the range 0..=31")
}

/// Map a tablet proximity `NSEvent`'s pointing device type to a [`TabletToolKind`].
///
/// The caller must ensure `event` is a proximity event (type `TabletProximity` or a mouse
/// event with that subtype): the device type is only reported at proximity time —
/// tablet-point events always report `Unknown`.
fn tablet_tool_kind(event: &NSEvent) -> TabletToolKind {
let device = event.pointingDeviceType();
if device == NSPointingDeviceType::Eraser {
TabletToolKind::Eraser
} else if device == NSPointingDeviceType::Cursor {
// AppKit's `Cursor` is the tablet puck; winit's closest tool kind is `Mouse`.
TabletToolKind::Mouse
} else {
// `Pen` and `Unknown` both map to a pen; it is the most common tool and a
// reasonable default for hardware that does not report a specific type.
TabletToolKind::Pen
}
}

/// Collect tablet tool data (pressure, barrel pressure, twist, tilt) from a tablet `NSEvent`.
///
/// The caller must ensure `event` originates from a tablet (subtype `TabletPoint`): the
/// tablet-specific accessors (`tilt`, `rotation`, `tangentialPressure`) raise on
/// non-tablet events.
fn tablet_tool_data(event: &NSEvent) -> TabletToolData {
// `NSEvent::tilt` reports each axis in [-1, 1]; winit expects degrees in [-90, 90].
// AppKit's y-up orientation means positive y tilts away from the user, while winit
// documents positive y as towards the user, so negate y.
let tilt = event.tilt();
TabletToolData {
force: Some(Force::Normalized(event.pressure() as f64)),
tangential_force: Some(event.tangentialPressure()),
twist: Some(event.rotation().rem_euclid(360.0) as u16),
tilt: Some(TabletToolTilt { x: (tilt.x * 90.0) as i8, y: (-tilt.y * 90.0) as i8 }),
angle: None,
}
}

// NOTE: to get option as alt working we need to rewrite events
// we're getting from the operating system, which makes it
// impossible to provide such events as extra in `KeyEvent`.
Expand Down
7 changes: 7 additions & 0 deletions winit-core/src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,13 @@ pub enum TabletToolKind {
Lens,
}

/// Data describing how a tablet tool is held and used.
///
/// ## Platform-specific
///
/// **macOS:** `angle` is always [`None`]; orientation is reported through `tilt` instead.
/// Sensor support is not detected, so all other fields are always [`Some`], and tools
/// lacking a given sensor report zero.
#[derive(Default, Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))]
pub struct TabletToolData {
Expand Down
1 change: 1 addition & 0 deletions winit/src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ changelog entry.

- Add `keyboard` support for OpenHarmony.
- On iOS, add Apple Pencil support with force, altitude, and azimuth data.
- On macOS, add tablet tool support with tool type (pen/eraser), pressure, tilt, twist, and barrel pressure data.
- On Redox, add support for missing keyboard scancodes.
- Implement `Send` and `Sync` for `OwnedDisplayHandle`.
- Use new macOS 15 cursors for resize icons.
Expand Down
Loading