Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(#3759): Implements Apple Pencil double tap functionality #3768

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
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
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -183,8 +183,10 @@ objc2-ui-kit = { version = "0.2.2", features = [
"UIEvent",
"UIGeometry",
"UIGestureRecognizer",
"UIInteraction",
"UIOrientation",
"UIPanGestureRecognizer",
"UIPencilInteraction",
"UIPinchGestureRecognizer",
"UIResponder",
"UIRotationGestureRecognizer",
Expand Down
1 change: 1 addition & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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** |
Expand Down
4 changes: 4 additions & 0 deletions examples/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,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
| WindowEvent::KeyboardInput { .. }
Expand All @@ -446,6 +449,7 @@ impl ApplicationHandler for Application {
| WindowEvent::HoveredFile(_)
| WindowEvent::Destroyed
| WindowEvent::Touch(_)
| WindowEvent::PenSpecialEvent(_)
| WindowEvent::Moved(_) => (),
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/changelog/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ changelog entry.

## Unreleased

### Added

- On iOS, add `PenSpecialEvent` and `PenPreferredAction` implementing Apple Pencil double-tap support (#3759, #99).

### Changed

- On Web, let events wake up event loop immediately when using `ControlFlow::Poll`.
Expand Down
108 changes: 108 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 [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 [PenPreferredAction]
// from the underlying Apple enum in case they change/add a field.
preferred_action: Option<PenPreferredAction>,
},
}

/// Represents the possible preferred actions for a [`PenSpecialEvent`].
///
/// ## Platform Specific
///
/// - **iOS** only
/// See <https://developer.apple.com/documentation/uikit/uipencilpreferredaction?language=objc>
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PenPreferredAction {
/// An action that does nothing.
///
/// ## Platform Specific
///
/// - **iOS** only
/// See <https://developer.apple.com/documentation/uikit/uipencilpreferredaction/uipencilpreferredactionignore?language=objc>
///
/// ### 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 <https://developer.apple.com/documentation/uikit/uipencilpreferredaction/uipencilpreferredactionswitcheraser?language=objc>
SwitchEraser,

/// An action that switches between the current tool and the last used tool.
///
/// ## Platform Specific
///
/// - **iOS** only
/// See <https://developer.apple.com/documentation/uikit/uipencilpreferredaction/uipencilpreferredactionswitcheraser?language=objc>
SwitchPrevious,

/// An action that toggles the display of the color palette.
///
/// ## Platform Specific
///
/// - **iOS** only
/// See <https://developer.apple.com/documentation/uikit/uipencilpreferredaction/uipencilpreferredactionshowcolorpalette?language=objc>
ShowColorPalette,

/// An action that toggles the display of the selected tool’s ink attributes.
///
/// ## Platform Specific
///
/// - **iOS** only
/// See <https://developer.apple.com/documentation/uikit/uipencilpreferredaction/uipencilpreferredactionshowinkattributes?language=objc>
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 <https://developer.apple.com/documentation/uikit/uipencilpreferredaction/uipencilpreferredactionshowcontextualpalette?language=objc>
ShowContextualPalette,

/// An action that runs a system shortcut.
///
/// ## Platform Specific
///
/// - **iOS** only
/// See <https://developer.apple.com/documentation/uikit/uipencilpreferredaction/uipencilpreferredactionrunsystemshortcut?language=objc>
RunSystemShortcut,
}

/// Identifier for a specific analog axis on some device.
pub type AxisId = u32;

Expand Down
81 changes: 78 additions & 3 deletions src/platform_impl/apple/uikit/view.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand All @@ -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, PenPreferredAction, PenSpecialEvent, Touch, TouchPhase, WindowEvent,
};
use crate::window::{WindowAttributes, WindowId as RootWindowId};

pub struct WinitViewState {
Expand Down Expand Up @@ -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<PenPreferredAction> {
Some(match action {
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::PenPreferredAction",
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 {
Expand All @@ -350,6 +392,27 @@ impl WinitView {
this.setContentScaleFactor(scale_factor as _);
}

// adds apple pencil support
let interaction: Retained<UIPencilInteraction> = {
let allocated: Allocated<UIPencilInteraction> = mtm.alloc();
// SAFETY: UIPencilInteraction can be safely initialized from empty memory
let empty_initialized: Retained<UIPencilInteraction> =
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<dyn UIInteraction> =
ProtocolObject::from_ref(interaction.deref());
// SAFETY: UIPencilInteraction is initialized (just above) and conforms to UIInteraction
unsafe {
this.addInteraction(type_erased_interaction);
}

this
}

Expand Down Expand Up @@ -512,4 +575,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),
}),
);
}
}