Skip to content

Commit

Permalink
feat(rust-windowing#3759): Implements Apple Pencil double tap functio…
Browse files Browse the repository at this point in the history
…nality

Implements rust-windowing#3759
Also see rust-windowing#99
  • Loading branch information
ActuallyHappening committed Jun 28, 2024
1 parent d2d4d20 commit 633564e
Show file tree
Hide file tree
Showing 4 changed files with 196 additions and 3 deletions.
2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -189,8 +189,10 @@ features = [
"UIEvent",
"UIGeometry",
"UIGestureRecognizer",
"UIInteraction",
"UIOrientation",
"UIPanGestureRecognizer",
"UIPencilInteraction",
"UIPinchGestureRecognizer",
"UIResponder",
"UIRotationGestureRecognizer",
Expand Down
4 changes: 4 additions & 0 deletions examples/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,9 @@ impl ApplicationHandler<UserEvent> for Application {
WindowEvent::DoubleTapGesture { .. } => {
info!("Smart zoom");
},
WindowEvent::PenEvent(winit::event::PenEvent::DoubleTap { .. }) => {
info!("Double tapped an Apple Pencil");
},
WindowEvent::TouchpadPressure { .. }
| WindowEvent::HoveredFileCancelled
| WindowEvent::KeyboardInput { .. }
Expand All @@ -452,6 +455,7 @@ impl ApplicationHandler<UserEvent> for Application {
| WindowEvent::HoveredFile(_)
| WindowEvent::Destroyed
| WindowEvent::Touch(_)
| WindowEvent::PenEvent(_)
| WindowEvent::Moved(_) => (),
}
}
Expand Down
108 changes: 108 additions & 0 deletions src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,13 @@ pub enum WindowEvent {
/// [`transform`]: https://developer.mozilla.org/en-US/docs/Web/CSS/transform
Touch(Touch),

/// Pen event has been received.
///
/// ## Platform-specific
///
/// - **iOS:** Only platform supported, see [apple docs](https://developer.apple.com/documentation/applepencil#)
PenEvent(PenEvent),

/// The window's scale factor has changed.
///
/// The following user actions can cause DPI changes:
Expand Down Expand Up @@ -913,6 +920,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)
///
/// [`PenEvent::DoubleTap`]: iOS only
// non_exhaustive so that other events can be added later, e.g. Squeeze
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum PenEvent {
/// 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 [`PenEvent::DoubleTap`].
///
/// ## 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
85 changes: 82 additions & 3 deletions src/platform_impl/ios/view.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
#![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,
};

use super::app_state::{self, EventWrapper};
use super::window::WinitUIWindow;
use crate::dpi::PhysicalPosition;
use crate::event::{Event, Force, Touch, TouchPhase, WindowEvent};
use crate::event::{Event, Force, PenEvent, PenPreferredAction, Touch, TouchPhase, WindowEvent};
use crate::platform_impl::platform::DEVICE_ID;
use crate::window::{WindowAttributes, WindowId as RootWindowId};

Expand All @@ -35,6 +37,8 @@ declare_class!(
pub(crate) struct WinitView;

unsafe impl ClassType for WinitView {
// [UIView](https://developer.apple.com/documentation/uikit/uiview)
// [UIResponder](https://developer.apple.com/documentation/uikit/uiresponder)
#[inherits(UIResponder, NSObject)]
type Super = UIView;
type Mutability = mutability::MainThreadOnly;
Expand Down Expand Up @@ -324,6 +328,46 @@ 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 PenPreferredAction"
);
return None
}
})
}

// retrieving this every tap rather than storing it once is the correct approach since the user
// can actually change their preferences at runtime
let preferred_action = convert_preferred_action(unsafe { UIPencilInteraction::preferredTapAction(mtm) });
tracing::error!(remove_me = true, message = "PENCIL TAPPED", ?preferred_action);

let pen_event = PenEvent::DoubleTap { preferred_action };

self.handle_pen_events(pen_event);
}

// can add squeeze handler here in the future
}
);

impl WinitView {
Expand All @@ -350,6 +394,29 @@ impl WinitView {
this.setContentScaleFactor(scale_factor as _);
}

// adds apple pencil support
// let view: Retained<UIView> = self.view().expect("View has already loaded");
let interaction: Retained<UIPencilInteraction> = {
let allocated: Allocated<UIPencilInteraction> =
MainThreadMarker::new().unwrap().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 @@ -447,6 +514,18 @@ impl WinitView {
}
}

fn handle_pen_events(&self, pen_event: crate::event::PenEvent) {
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::PenEvent(pen_event),
}),
);
}

fn handle_touches(&self, touches: &NSSet<UITouch>) {
let window = self.window().unwrap();
let mut touch_events = Vec::new();
Expand Down

0 comments on commit 633564e

Please sign in to comment.