From 9fea1d16423603066a74d916ffdc9fa014779d09 Mon Sep 17 00:00:00 2001 From: ActuallyHappening <105958073+ActuallyHappening@users.noreply.github.com> Date: Sat, 29 Jun 2024 09:48:45 +1000 Subject: [PATCH 1/6] feat(#3759): Implements Apple Pencil double tap functionality Implements #3759 Also see #99 Based on the `master` branch --- Cargo.toml | 2 + FEATURES.md | 1 + examples/window.rs | 4 + src/changelog/unreleased.md | 4 + src/event.rs | 108 ++++++++++++++++++++++++++ src/platform_impl/apple/uikit/view.rs | 83 +++++++++++++++++++- 6 files changed, 199 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4302edf6e6..917c492af0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -183,8 +183,10 @@ objc2-ui-kit = { version = "0.2.2", features = [ "UIEvent", "UIGeometry", "UIGestureRecognizer", + "UIInteraction", "UIOrientation", "UIPanGestureRecognizer", + "UIPencilInteraction", "UIPinchGestureRecognizer", "UIResponder", "UIRotationGestureRecognizer", diff --git a/FEATURES.md b/FEATURES.md index 6f5aff3f4e..06678b1e0e 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -208,6 +208,7 @@ Legend: |Cursor hittest |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ | |Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A** | |Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |✔️ |**N/A** | +|Special pen events |❌ |**N/A** |❌ |❌ |❌ |✔️ |❌ |**N/A** | |Multitouch |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❌ |**N/A** | |Keyboard events |✔️ |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |✔️ | |Drag & Drop |▢[#720] |▢[#720] |▢[#720] |▢[#720] |**N/A**|**N/A**|❓ |**N/A** | diff --git a/examples/window.rs b/examples/window.rs index 3a2a1f72ab..a06ce411d5 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -436,6 +436,9 @@ impl ApplicationHandler for Application { }, WindowEvent::DoubleTapGesture { .. } => { info!("Smart zoom"); + }, + WindowEvent::PenSpecialEvent(winit::event::PenSpecialEvent::DoubleTap { .. }) => { + info!("Double tapped an Apple Pencil"); }, WindowEvent::TouchpadPressure { .. } | WindowEvent::HoveredFileCancelled @@ -446,6 +449,7 @@ impl ApplicationHandler for Application { | WindowEvent::HoveredFile(_) | WindowEvent::Destroyed | WindowEvent::Touch(_) + | WindowEvent::PenSpecialEvent(_) | WindowEvent::Moved(_) => (), } } diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 9e9a479cb4..0b3cbdc92c 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -40,6 +40,10 @@ changelog entry. ## Unreleased +## Added + +- On iOS, add `PenSpecialEvent` and `PenPreferredTapAction` implementing Apple Pencil double-tap support (#3759, #99). + ### Changed - On Web, let events wake up event loop immediately when using `ControlFlow::Poll`. diff --git a/src/event.rs b/src/event.rs index 22d31eca48..b1845d089d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -350,6 +350,13 @@ pub enum WindowEvent { /// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform Touch(Touch), + /// Special pen event has been received. + /// + /// ## Platform-specific + /// + /// - **iOS:** Only platform supported, see docs on [`PenSpecialEvent`] + PenSpecialEvent(PenSpecialEvent), + /// The window's scale factor has changed. /// /// The following user actions can cause DPI changes: @@ -899,6 +906,107 @@ impl Force { } } +/// Represents a pen event. +/// +/// Primarily wraps an [Apple Pencil](https://developer.apple.com/documentation/uikit/apple_pencil_interactions/handling_input_from_apple_pencil?language=objc) +// non_exhaustive so that other events can be added later, e.g. Squeeze +#[derive(Debug, Clone, Copy, PartialEq)] +#[non_exhaustive] +pub enum PenSpecialEvent { + /// Double tapping the end of the Apple Pencil. + /// + /// ## Platform Specific + /// + /// - **iOS** only + /// + /// From the Apple developer documentation [here](https://developer.apple.com/documentation/applepencil/handling-double-taps-from-apple-pencil#Overview): + /// ```text + /// You can use Apple Pencil interactions to allow people to access functionality in your app quickly. Double-tapping Apple Pencil lets a person perform actions such as switching between drawing tools without moving the pencil to another location on the screen. + /// ``` + // non_exhaustive so that other fields can be added later, e.g. azimuthal angle + #[non_exhaustive] + DoubleTap { + /// The preferred action for the pen event. + /// + /// See the docs on [PenPreferredTapAction] for more information. + // This is kept an [Option] to allow for other platforms to implement this if possible, + // and to allow for failures in 'deserializing' the variants of [PenPreferredAction] + // from the underlying Apple enum in case they change/add a field. + preferred_action: Option, + }, +} + +/// Represents the possible preferred actions for a [`PenEvent::DoubleTap`]. +/// +/// ## Platform Specific +/// +/// - **iOS** only +/// See +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum PenPreferredTapAction { + /// An action that does nothing. + /// + /// ## Platform Specific + /// + /// - **iOS** only + /// See + /// + /// ### Discussion + /// The system returns this action if any of the following conditions are true: + /// - The Apple Pencil doesn’t have a configured preferred action. + /// - The iPad’s accessibility settings disable Apple Pencil interactions. + Ignore, + + /// An action that switches between the current tool and the eraser. + /// + /// ## Platform Specific + /// + /// - **iOS** only + /// See + SwitchEraser, + + /// An action that switches between the current tool and the last used tool. + /// + /// ## Platform Specific + /// + /// - **iOS** only + /// See + SwitchPrevious, + + /// An action that toggles the display of the color palette. + /// + /// ## Platform Specific + /// + /// - **iOS** only + /// See + ShowColorPalette, + + /// An action that toggles the display of the selected tool’s ink attributes. + /// + /// ## Platform Specific + /// + /// - **iOS** only + /// See + ShowInkAttributes, + + /// An action that toggles shows a contextual palette of markup tools, or undo and redo options + /// if tools aren’t available. + /// + /// ## Platform Specific + /// + /// - **iOS** only + /// See + ShowContextualPalette, + + /// An action that runs a system shortcut. + /// + /// ## Platform Specific + /// + /// - **iOS** only + /// See + RunSystemShortcut, +} + /// Identifier for a specific analog axis on some device. pub type AxisId = u32; diff --git a/src/platform_impl/apple/uikit/view.rs b/src/platform_impl/apple/uikit/view.rs index 0c7fd39c0a..6dd5d0aee7 100644 --- a/src/platform_impl/apple/uikit/view.rs +++ b/src/platform_impl/apple/uikit/view.rs @@ -1,13 +1,15 @@ #![allow(clippy::unnecessary_cast)] use std::cell::{Cell, RefCell}; +use std::ops::Deref as _; -use objc2::rc::Retained; +use objc2::rc::{Allocated, Retained}; use objc2::runtime::{NSObjectProtocol, ProtocolObject}; use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass}; use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet}; use objc2_ui_kit::{ UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer, - UIGestureRecognizerDelegate, UIGestureRecognizerState, UIPanGestureRecognizer, + UIGestureRecognizerDelegate, UIGestureRecognizerState, UIInteraction, UIPanGestureRecognizer, + UIPencilInteraction, UIPencilInteractionDelegate, UIPencilInteractionTap, UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView, }; @@ -16,7 +18,9 @@ use super::app_state::{self, EventWrapper}; use super::window::WinitUIWindow; use super::DEVICE_ID; use crate::dpi::PhysicalPosition; -use crate::event::{Event, Force, Touch, TouchPhase, WindowEvent}; +use crate::event::{ + Event, Force, PenPreferredTapAction, PenSpecialEvent, Touch, TouchPhase, WindowEvent, +}; use crate::window::{WindowAttributes, WindowId as RootWindowId}; pub struct WinitViewState { @@ -324,6 +328,44 @@ declare_class!( true } } + + // Adapted from https://developer.apple.com/documentation/applepencil/handling-double-taps-from-apple-pencil + unsafe impl UIPencilInteractionDelegate for WinitView { + #[allow(non_snake_case)] + #[method(pencilInteraction:didReceiveTap:)] + unsafe fn pencilInteraction_didReceiveTap(&self, _interaction: &UIPencilInteraction, _tap: &UIPencilInteractionTap) { + let mtm = MainThreadMarker::new().unwrap(); + + fn convert_preferred_action(action: objc2_ui_kit::UIPencilPreferredAction) -> Option { + Some(match action { + objc2_ui_kit::UIPencilPreferredAction::Ignore => PenPreferredTapAction::Ignore, + objc2_ui_kit::UIPencilPreferredAction::SwitchEraser => PenPreferredTapAction::SwitchEraser, + objc2_ui_kit::UIPencilPreferredAction::SwitchPrevious => PenPreferredTapAction::SwitchPrevious, + objc2_ui_kit::UIPencilPreferredAction::ShowColorPalette => PenPreferredTapAction::ShowColorPalette, + objc2_ui_kit::UIPencilPreferredAction::ShowInkAttributes => PenPreferredTapAction::ShowInkAttributes, + objc2_ui_kit::UIPencilPreferredAction::ShowContextualPalette => PenPreferredTapAction::ShowContextualPalette, + objc2_ui_kit::UIPencilPreferredAction::RunSystemShortcut => PenPreferredTapAction::RunSystemShortcut, + _ => { + tracing::warn!( + message = "Unknown variant of UIPencilPreferredAction", + preferred_action = ?action, + note = "This is likely not a bug, but requires a new variant to be added to winit::event::PenPreferredTapAction", + note = "This will ignore the preferred action for this event" + ); + return None + } + }) + } + + // retrieving this every tap rather than storing it once is the correct approach since the user + // can change their preferences at runtime + let preferred_action = convert_preferred_action(unsafe { UIPencilInteraction::preferredTapAction(mtm) }); + let pen_event = PenSpecialEvent::DoubleTap { preferred_action }; + self.handle_pen_event(pen_event); + } + + // can add squeeze handler here in the future + } ); impl WinitView { @@ -350,6 +392,29 @@ impl WinitView { this.setContentScaleFactor(scale_factor as _); } + // adds apple pencil support + // let view: Retained = self.view().expect("View has already loaded"); + let interaction: Retained = { + let allocated: Allocated = + MainThreadMarker::new().unwrap().alloc(); + // SAFETY: UIPencilInteraction can be safely initialized from empty memory + let empty_initialized: Retained = + unsafe { UIPencilInteraction::init(allocated) }; + let type_erased_protocol_handler = ProtocolObject::from_ref(this.deref()); + // SAFETY: UIPencilInteraction is initialized (just above) + unsafe { + empty_initialized.setDelegate(Some(type_erased_protocol_handler)); + } + + empty_initialized + }; + let type_erased_interaction: &ProtocolObject = + ProtocolObject::from_ref(interaction.deref()); + // SAFETY: UIPencilInteraction is initialized (just above) and conforms to UIInteraction + unsafe { + this.addInteraction(type_erased_interaction); + } + this } @@ -512,4 +577,16 @@ impl WinitView { let mtm = MainThreadMarker::new().unwrap(); app_state::handle_nonuser_events(mtm, touch_events); } + + fn handle_pen_event(&self, pen_event: crate::event::PenSpecialEvent) { + let window = self.window().unwrap(); + let mtm = MainThreadMarker::new().unwrap(); + app_state::handle_nonuser_event( + mtm, + EventWrapper::StaticEvent(Event::WindowEvent { + window_id: RootWindowId(window.id()), + event: WindowEvent::PenSpecialEvent(pen_event), + }), + ); + } } From 6c225c64877a981d97d9a4d0176329beed1eab88 Mon Sep 17 00:00:00 2001 From: ActuallyHappening <105958073+ActuallyHappening@users.noreply.github.com> Date: Sat, 29 Jun 2024 10:12:08 +1000 Subject: [PATCH 2/6] fmt --- examples/window.rs | 4 ++-- src/event.rs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/window.rs b/examples/window.rs index a06ce411d5..806c3ba73d 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -437,7 +437,7 @@ impl ApplicationHandler for Application { WindowEvent::DoubleTapGesture { .. } => { info!("Smart zoom"); }, - WindowEvent::PenSpecialEvent(winit::event::PenSpecialEvent::DoubleTap { .. }) => { + WindowEvent::PenSpecialEvent(winit::event::PenSpecialEvent::DoubleTap { .. }) => { info!("Double tapped an Apple Pencil"); }, WindowEvent::TouchpadPressure { .. } @@ -449,7 +449,7 @@ impl ApplicationHandler for Application { | WindowEvent::HoveredFile(_) | WindowEvent::Destroyed | WindowEvent::Touch(_) - | WindowEvent::PenSpecialEvent(_) + | WindowEvent::PenSpecialEvent(_) | WindowEvent::Moved(_) => (), } } diff --git a/src/event.rs b/src/event.rs index b1845d089d..1d43dcee1b 100644 --- a/src/event.rs +++ b/src/event.rs @@ -907,7 +907,7 @@ impl Force { } /// Represents a pen event. -/// +/// /// Primarily wraps an [Apple Pencil](https://developer.apple.com/documentation/uikit/apple_pencil_interactions/handling_input_from_apple_pencil?language=objc) // non_exhaustive so that other events can be added later, e.g. Squeeze #[derive(Debug, Clone, Copy, PartialEq)] From e55446552fd7e9411939d2814b7b2e28a484ec8d Mon Sep 17 00:00:00 2001 From: ActuallyHappening <105958073+ActuallyHappening@users.noreply.github.com> Date: Sat, 29 Jun 2024 10:14:54 +1000 Subject: [PATCH 3/6] fix(doc): Broken link resolved --- src/event.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/event.rs b/src/event.rs index 1d43dcee1b..468b8f6e1e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -930,13 +930,13 @@ pub enum PenSpecialEvent { /// /// See the docs on [PenPreferredTapAction] for more information. // This is kept an [Option] to allow for other platforms to implement this if possible, - // and to allow for failures in 'deserializing' the variants of [PenPreferredAction] + // and to allow for failures in 'deserializing' the variants of [PenPreferredTapAction] // from the underlying Apple enum in case they change/add a field. preferred_action: Option, }, } -/// Represents the possible preferred actions for a [`PenEvent::DoubleTap`]. +/// Represents the possible preferred actions for a [`PenSpecialEvent::DoubleTap`]. /// /// ## Platform Specific /// From 38b556f1d664b75d12dd30d549f1a535cde4d599 Mon Sep 17 00:00:00 2001 From: ActuallyHappening <105958073+ActuallyHappening@users.noreply.github.com> Date: Sat, 29 Jun 2024 19:49:46 +1000 Subject: [PATCH 4/6] refactor: `PenPreferredTapAction` -> `PenPreferredAction` since this enum techniquely covers more than just taps when squeezes are added --- src/changelog/unreleased.md | 2 +- src/event.rs | 10 +++++----- src/platform_impl/apple/uikit/view.rs | 20 ++++++++++---------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 0b3cbdc92c..1efa985293 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -42,7 +42,7 @@ changelog entry. ## Added -- On iOS, add `PenSpecialEvent` and `PenPreferredTapAction` implementing Apple Pencil double-tap support (#3759, #99). +- On iOS, add `PenSpecialEvent` and `PenPreferredAction` implementing Apple Pencil double-tap support (#3759, #99). ### Changed diff --git a/src/event.rs b/src/event.rs index 468b8f6e1e..11dd8ff102 100644 --- a/src/event.rs +++ b/src/event.rs @@ -928,22 +928,22 @@ pub enum PenSpecialEvent { DoubleTap { /// The preferred action for the pen event. /// - /// See the docs on [PenPreferredTapAction] for more information. + /// See the docs on [PenPreferredAction] for more information. // This is kept an [Option] to allow for other platforms to implement this if possible, - // and to allow for failures in 'deserializing' the variants of [PenPreferredTapAction] + // and to allow for failures in 'deserializing' the variants of [PenPreferredAction] // from the underlying Apple enum in case they change/add a field. - preferred_action: Option, + preferred_action: Option, }, } -/// Represents the possible preferred actions for a [`PenSpecialEvent::DoubleTap`]. +/// Represents the possible preferred actions for a [`PenSpecialEvent`]. /// /// ## Platform Specific /// /// - **iOS** only /// See #[derive(Debug, Clone, Copy, PartialEq)] -pub enum PenPreferredTapAction { +pub enum PenPreferredAction { /// An action that does nothing. /// /// ## Platform Specific diff --git a/src/platform_impl/apple/uikit/view.rs b/src/platform_impl/apple/uikit/view.rs index 6dd5d0aee7..6b782b6635 100644 --- a/src/platform_impl/apple/uikit/view.rs +++ b/src/platform_impl/apple/uikit/view.rs @@ -19,7 +19,7 @@ use super::window::WinitUIWindow; use super::DEVICE_ID; use crate::dpi::PhysicalPosition; use crate::event::{ - Event, Force, PenPreferredTapAction, PenSpecialEvent, Touch, TouchPhase, WindowEvent, + Event, Force, PenPreferredAction, PenSpecialEvent, Touch, TouchPhase, WindowEvent, }; use crate::window::{WindowAttributes, WindowId as RootWindowId}; @@ -336,20 +336,20 @@ declare_class!( unsafe fn pencilInteraction_didReceiveTap(&self, _interaction: &UIPencilInteraction, _tap: &UIPencilInteractionTap) { let mtm = MainThreadMarker::new().unwrap(); - fn convert_preferred_action(action: objc2_ui_kit::UIPencilPreferredAction) -> Option { + fn convert_preferred_action(action: objc2_ui_kit::UIPencilPreferredAction) -> Option { Some(match action { - objc2_ui_kit::UIPencilPreferredAction::Ignore => PenPreferredTapAction::Ignore, - objc2_ui_kit::UIPencilPreferredAction::SwitchEraser => PenPreferredTapAction::SwitchEraser, - objc2_ui_kit::UIPencilPreferredAction::SwitchPrevious => PenPreferredTapAction::SwitchPrevious, - objc2_ui_kit::UIPencilPreferredAction::ShowColorPalette => PenPreferredTapAction::ShowColorPalette, - objc2_ui_kit::UIPencilPreferredAction::ShowInkAttributes => PenPreferredTapAction::ShowInkAttributes, - objc2_ui_kit::UIPencilPreferredAction::ShowContextualPalette => PenPreferredTapAction::ShowContextualPalette, - objc2_ui_kit::UIPencilPreferredAction::RunSystemShortcut => PenPreferredTapAction::RunSystemShortcut, + objc2_ui_kit::UIPencilPreferredAction::Ignore => PenPreferredAction::Ignore, + objc2_ui_kit::UIPencilPreferredAction::SwitchEraser => PenPreferredAction::SwitchEraser, + objc2_ui_kit::UIPencilPreferredAction::SwitchPrevious => PenPreferredAction::SwitchPrevious, + objc2_ui_kit::UIPencilPreferredAction::ShowColorPalette => PenPreferredAction::ShowColorPalette, + objc2_ui_kit::UIPencilPreferredAction::ShowInkAttributes => PenPreferredAction::ShowInkAttributes, + objc2_ui_kit::UIPencilPreferredAction::ShowContextualPalette => PenPreferredAction::ShowContextualPalette, + objc2_ui_kit::UIPencilPreferredAction::RunSystemShortcut => PenPreferredAction::RunSystemShortcut, _ => { tracing::warn!( message = "Unknown variant of UIPencilPreferredAction", preferred_action = ?action, - note = "This is likely not a bug, but requires a new variant to be added to winit::event::PenPreferredTapAction", + note = "This is likely not a bug, but requires a new variant to be added to winit::event::PenPreferredAction", note = "This will ignore the preferred action for this event" ); return None From b35b504c0557d2f64746b32a7e45cd34053da9e1 Mon Sep 17 00:00:00 2001 From: ActuallyHappening <105958073+ActuallyHappening@users.noreply.github.com> Date: Sat, 29 Jun 2024 19:50:35 +1000 Subject: [PATCH 5/6] clean: uses `mtm` as passed in from function parameters --- src/platform_impl/apple/uikit/view.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/platform_impl/apple/uikit/view.rs b/src/platform_impl/apple/uikit/view.rs index 6b782b6635..b7b515e4b1 100644 --- a/src/platform_impl/apple/uikit/view.rs +++ b/src/platform_impl/apple/uikit/view.rs @@ -393,10 +393,8 @@ impl WinitView { } // adds apple pencil support - // let view: Retained = self.view().expect("View has already loaded"); let interaction: Retained = { - let allocated: Allocated = - MainThreadMarker::new().unwrap().alloc(); + let allocated: Allocated = mtm.alloc(); // SAFETY: UIPencilInteraction can be safely initialized from empty memory let empty_initialized: Retained = unsafe { UIPencilInteraction::init(allocated) }; From 8c179903142203d5d1e7127fcfbe482d6c979f55 Mon Sep 17 00:00:00 2001 From: ActuallyHappening <105958073+ActuallyHappening@users.noreply.github.com> Date: Sun, 30 Jun 2024 07:25:52 +1000 Subject: [PATCH 6/6] fix: use three `#` for added section in changelog --- src/changelog/unreleased.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 1efa985293..86b0a393fe 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -40,7 +40,7 @@ changelog entry. ## Unreleased -## Added +### Added - On iOS, add `PenSpecialEvent` and `PenPreferredAction` implementing Apple Pencil double-tap support (#3759, #99).