* Move application delegate to its own file * Move window subclass to window.rs * Split view controller to separate file
391 lines
16 KiB
Rust
391 lines
16 KiB
Rust
#![allow(clippy::unnecessary_cast)]
|
|
use std::cell::RefCell;
|
|
|
|
use icrate::Foundation::{CGFloat, CGRect, MainThreadMarker, NSObject, NSSet};
|
|
use objc2::rc::Id;
|
|
use objc2::runtime::AnyClass;
|
|
use objc2::{
|
|
declare_class, extern_methods, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass,
|
|
};
|
|
|
|
use super::app_state::{self, EventWrapper};
|
|
use super::uikit::{
|
|
UIEvent, UIForceTouchCapability, UIGestureRecognizerState, UIPinchGestureRecognizer,
|
|
UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, UITouch, UITouchPhase,
|
|
UITouchType, UITraitCollection, UIView,
|
|
};
|
|
use super::window::WinitUIWindow;
|
|
use crate::{
|
|
dpi::PhysicalPosition,
|
|
event::{Event, Force, Touch, TouchPhase, WindowEvent},
|
|
platform_impl::platform::DEVICE_ID,
|
|
window::{WindowAttributes, WindowId as RootWindowId},
|
|
};
|
|
|
|
pub struct WinitViewState {
|
|
pinch_gesture_recognizer: RefCell<Option<Id<UIPinchGestureRecognizer>>>,
|
|
doubletap_gesture_recognizer: RefCell<Option<Id<UITapGestureRecognizer>>>,
|
|
rotation_gesture_recognizer: RefCell<Option<Id<UIRotationGestureRecognizer>>>,
|
|
}
|
|
|
|
declare_class!(
|
|
pub(crate) struct WinitView;
|
|
|
|
unsafe impl ClassType for WinitView {
|
|
#[inherits(UIResponder, NSObject)]
|
|
type Super = UIView;
|
|
type Mutability = mutability::InteriorMutable;
|
|
const NAME: &'static str = "WinitUIView";
|
|
}
|
|
|
|
impl DeclaredClass for WinitView {
|
|
type Ivars = WinitViewState;
|
|
}
|
|
|
|
unsafe impl WinitView {
|
|
#[method(drawRect:)]
|
|
fn draw_rect(&self, rect: CGRect) {
|
|
let mtm = MainThreadMarker::new().unwrap();
|
|
let window = self.window().unwrap();
|
|
app_state::handle_nonuser_event(
|
|
mtm,
|
|
EventWrapper::StaticEvent(Event::WindowEvent {
|
|
window_id: RootWindowId(window.id()),
|
|
event: WindowEvent::RedrawRequested,
|
|
}),
|
|
);
|
|
let _: () = unsafe { msg_send![super(self), drawRect: rect] };
|
|
}
|
|
|
|
#[method(layoutSubviews)]
|
|
fn layout_subviews(&self) {
|
|
let mtm = MainThreadMarker::new().unwrap();
|
|
let _: () = unsafe { msg_send![super(self), layoutSubviews] };
|
|
|
|
let window = self.window().unwrap();
|
|
let window_bounds = window.bounds();
|
|
let screen = window.screen();
|
|
let screen_space = screen.coordinateSpace();
|
|
let screen_frame = self.convertRect_toCoordinateSpace(window_bounds, &screen_space);
|
|
let scale_factor = screen.scale();
|
|
let size = crate::dpi::LogicalSize {
|
|
width: screen_frame.size.width as f64,
|
|
height: screen_frame.size.height as f64,
|
|
}
|
|
.to_physical(scale_factor as f64);
|
|
|
|
// If the app is started in landscape, the view frame and window bounds can be mismatched.
|
|
// The view frame will be in portrait and the window bounds in landscape. So apply the
|
|
// window bounds to the view frame to make it consistent.
|
|
let view_frame = self.frame();
|
|
if view_frame != window_bounds {
|
|
self.setFrame(window_bounds);
|
|
}
|
|
|
|
app_state::handle_nonuser_event(
|
|
mtm,
|
|
EventWrapper::StaticEvent(Event::WindowEvent {
|
|
window_id: RootWindowId(window.id()),
|
|
event: WindowEvent::Resized(size),
|
|
}),
|
|
);
|
|
}
|
|
|
|
#[method(setContentScaleFactor:)]
|
|
fn set_content_scale_factor(&self, untrusted_scale_factor: CGFloat) {
|
|
let mtm = MainThreadMarker::new().unwrap();
|
|
let _: () =
|
|
unsafe { msg_send![super(self), setContentScaleFactor: untrusted_scale_factor] };
|
|
|
|
// `window` is null when `setContentScaleFactor` is invoked prior to `[UIWindow
|
|
// makeKeyAndVisible]` at window creation time (either manually or internally by
|
|
// UIKit when the `UIView` is first created), in which case we send no events here
|
|
let window = match self.window() {
|
|
Some(window) => window,
|
|
None => return,
|
|
};
|
|
// `setContentScaleFactor` may be called with a value of 0, which means "reset the
|
|
// content scale factor to a device-specific default value", so we can't use the
|
|
// parameter here. We can query the actual factor using the getter
|
|
let scale_factor = self.contentScaleFactor();
|
|
assert!(
|
|
!scale_factor.is_nan()
|
|
&& scale_factor.is_finite()
|
|
&& scale_factor.is_sign_positive()
|
|
&& scale_factor > 0.0,
|
|
"invalid scale_factor set on UIView",
|
|
);
|
|
let scale_factor = scale_factor as f64;
|
|
let bounds = self.bounds();
|
|
let screen = window.screen();
|
|
let screen_space = screen.coordinateSpace();
|
|
let screen_frame = self.convertRect_toCoordinateSpace(bounds, &screen_space);
|
|
let size = crate::dpi::LogicalSize {
|
|
width: screen_frame.size.width as f64,
|
|
height: screen_frame.size.height as f64,
|
|
};
|
|
let window_id = RootWindowId(window.id());
|
|
app_state::handle_nonuser_events(
|
|
mtm,
|
|
std::iter::once(EventWrapper::ScaleFactorChanged(
|
|
app_state::ScaleFactorChanged {
|
|
window,
|
|
scale_factor,
|
|
suggested_size: size.to_physical(scale_factor),
|
|
},
|
|
))
|
|
.chain(std::iter::once(EventWrapper::StaticEvent(
|
|
Event::WindowEvent {
|
|
window_id,
|
|
event: WindowEvent::Resized(size.to_physical(scale_factor)),
|
|
},
|
|
))),
|
|
);
|
|
}
|
|
|
|
#[method(touchesBegan:withEvent:)]
|
|
fn touches_began(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
|
|
self.handle_touches(touches)
|
|
}
|
|
|
|
#[method(touchesMoved:withEvent:)]
|
|
fn touches_moved(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
|
|
self.handle_touches(touches)
|
|
}
|
|
|
|
#[method(touchesEnded:withEvent:)]
|
|
fn touches_ended(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
|
|
self.handle_touches(touches)
|
|
}
|
|
|
|
#[method(touchesCancelled:withEvent:)]
|
|
fn touches_cancelled(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
|
|
self.handle_touches(touches)
|
|
}
|
|
|
|
#[method(pinchGesture:)]
|
|
fn pinch_gesture(&self, recognizer: &UIPinchGestureRecognizer) {
|
|
let window = self.window().unwrap();
|
|
|
|
let phase = match recognizer.state() {
|
|
UIGestureRecognizerState::Began => TouchPhase::Started,
|
|
UIGestureRecognizerState::Changed => TouchPhase::Moved,
|
|
UIGestureRecognizerState::Ended => TouchPhase::Ended,
|
|
UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
|
|
TouchPhase::Cancelled
|
|
}
|
|
state => panic!("unexpected recognizer state: {:?}", state),
|
|
};
|
|
|
|
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
|
|
window_id: RootWindowId(window.id()),
|
|
event: WindowEvent::PinchGesture {
|
|
device_id: DEVICE_ID,
|
|
delta: recognizer.velocity() as _,
|
|
phase,
|
|
},
|
|
});
|
|
|
|
let mtm = MainThreadMarker::new().unwrap();
|
|
app_state::handle_nonuser_event(mtm, gesture_event);
|
|
}
|
|
|
|
#[method(doubleTapGesture:)]
|
|
fn double_tap_gesture(&self, recognizer: &UITapGestureRecognizer) {
|
|
let window = self.window().unwrap();
|
|
|
|
if recognizer.state() == UIGestureRecognizerState::Ended {
|
|
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
|
|
window_id: RootWindowId(window.id()),
|
|
event: WindowEvent::DoubleTapGesture {
|
|
device_id: DEVICE_ID,
|
|
},
|
|
});
|
|
|
|
let mtm = MainThreadMarker::new().unwrap();
|
|
app_state::handle_nonuser_event(mtm, gesture_event);
|
|
}
|
|
}
|
|
|
|
#[method(rotationGesture:)]
|
|
fn rotation_gesture(&self, recognizer: &UIRotationGestureRecognizer) {
|
|
let window = self.window().unwrap();
|
|
|
|
let phase = match recognizer.state() {
|
|
UIGestureRecognizerState::Began => TouchPhase::Started,
|
|
UIGestureRecognizerState::Changed => TouchPhase::Moved,
|
|
UIGestureRecognizerState::Ended => TouchPhase::Ended,
|
|
UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
|
|
TouchPhase::Cancelled
|
|
}
|
|
state => panic!("unexpected recognizer state: {:?}", state),
|
|
};
|
|
|
|
// Flip the velocity to match macOS.
|
|
let delta = -recognizer.velocity() as _;
|
|
let gesture_event = EventWrapper::StaticEvent(Event::WindowEvent {
|
|
window_id: RootWindowId(window.id()),
|
|
event: WindowEvent::RotationGesture {
|
|
device_id: DEVICE_ID,
|
|
delta,
|
|
phase,
|
|
},
|
|
});
|
|
|
|
let mtm = MainThreadMarker::new().unwrap();
|
|
app_state::handle_nonuser_event(mtm, gesture_event);
|
|
}
|
|
}
|
|
);
|
|
|
|
extern_methods!(
|
|
#[allow(non_snake_case)]
|
|
unsafe impl WinitView {
|
|
fn window(&self) -> Option<Id<WinitUIWindow>> {
|
|
unsafe { msg_send_id![self, window] }
|
|
}
|
|
|
|
unsafe fn traitCollection(&self) -> Id<UITraitCollection> {
|
|
msg_send_id![self, traitCollection]
|
|
}
|
|
|
|
// TODO: Allow the user to customize this
|
|
#[method(layerClass)]
|
|
pub(crate) fn layerClass() -> &'static AnyClass;
|
|
}
|
|
);
|
|
|
|
impl WinitView {
|
|
pub(crate) fn new(
|
|
_mtm: MainThreadMarker,
|
|
window_attributes: &WindowAttributes,
|
|
frame: CGRect,
|
|
) -> Id<Self> {
|
|
let this = Self::alloc().set_ivars(WinitViewState {
|
|
pinch_gesture_recognizer: RefCell::new(None),
|
|
doubletap_gesture_recognizer: RefCell::new(None),
|
|
rotation_gesture_recognizer: RefCell::new(None),
|
|
});
|
|
let this: Id<Self> = unsafe { msg_send_id![super(this), initWithFrame: frame] };
|
|
|
|
this.setMultipleTouchEnabled(true);
|
|
|
|
if let Some(scale_factor) = window_attributes.platform_specific.scale_factor {
|
|
this.setContentScaleFactor(scale_factor as _);
|
|
}
|
|
|
|
this
|
|
}
|
|
|
|
pub(crate) fn recognize_pinch_gesture(&self, should_recognize: bool) {
|
|
if should_recognize {
|
|
if self.ivars().pinch_gesture_recognizer.borrow().is_none() {
|
|
let pinch: Id<UIPinchGestureRecognizer> = unsafe {
|
|
msg_send_id![UIPinchGestureRecognizer::alloc(), initWithTarget: self, action: sel!(pinchGesture:)]
|
|
};
|
|
self.addGestureRecognizer(&pinch);
|
|
self.ivars().pinch_gesture_recognizer.replace(Some(pinch));
|
|
}
|
|
} else if let Some(recognizer) = self.ivars().pinch_gesture_recognizer.take() {
|
|
self.removeGestureRecognizer(&recognizer);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn recognize_doubletap_gesture(&self, should_recognize: bool) {
|
|
if should_recognize {
|
|
if self.ivars().doubletap_gesture_recognizer.borrow().is_none() {
|
|
let tap: Id<UITapGestureRecognizer> = unsafe {
|
|
msg_send_id![UITapGestureRecognizer::alloc(), initWithTarget: self, action: sel!(doubleTapGesture:)]
|
|
};
|
|
tap.setNumberOfTapsRequired(2);
|
|
tap.setNumberOfTouchesRequired(1);
|
|
self.addGestureRecognizer(&tap);
|
|
self.ivars().doubletap_gesture_recognizer.replace(Some(tap));
|
|
}
|
|
} else if let Some(recognizer) = self.ivars().doubletap_gesture_recognizer.take() {
|
|
self.removeGestureRecognizer(&recognizer);
|
|
}
|
|
}
|
|
|
|
pub(crate) fn recognize_rotation_gesture(&self, should_recognize: bool) {
|
|
if should_recognize {
|
|
if self.ivars().rotation_gesture_recognizer.borrow().is_none() {
|
|
let rotation: Id<UIRotationGestureRecognizer> = unsafe {
|
|
msg_send_id![UIRotationGestureRecognizer::alloc(), initWithTarget: self, action: sel!(rotationGesture:)]
|
|
};
|
|
self.addGestureRecognizer(&rotation);
|
|
self.ivars()
|
|
.rotation_gesture_recognizer
|
|
.replace(Some(rotation));
|
|
}
|
|
} else if let Some(recognizer) = self.ivars().rotation_gesture_recognizer.take() {
|
|
self.removeGestureRecognizer(&recognizer);
|
|
}
|
|
}
|
|
|
|
fn handle_touches(&self, touches: &NSSet<UITouch>) {
|
|
let window = self.window().unwrap();
|
|
let mut touch_events = Vec::new();
|
|
let os_supports_force = app_state::os_capabilities().force_touch;
|
|
for touch in touches {
|
|
let logical_location = touch.locationInView(None);
|
|
let touch_type = touch.type_();
|
|
let force = if os_supports_force {
|
|
let trait_collection = unsafe { self.traitCollection() };
|
|
let touch_capability = trait_collection.forceTouchCapability();
|
|
// Both the OS _and_ the device need to be checked for force touch support.
|
|
if touch_capability == UIForceTouchCapability::Available
|
|
|| touch_type == UITouchType::Pencil
|
|
{
|
|
let force = touch.force();
|
|
let max_possible_force = touch.maximumPossibleForce();
|
|
let altitude_angle: Option<f64> = if touch_type == UITouchType::Pencil {
|
|
let angle = touch.altitudeAngle();
|
|
Some(angle as _)
|
|
} else {
|
|
None
|
|
};
|
|
Some(Force::Calibrated {
|
|
force: force as _,
|
|
max_possible_force: max_possible_force as _,
|
|
altitude_angle,
|
|
})
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
let touch_id = touch as *const UITouch as u64;
|
|
let phase = touch.phase();
|
|
let phase = match phase {
|
|
UITouchPhase::Began => TouchPhase::Started,
|
|
UITouchPhase::Moved => TouchPhase::Moved,
|
|
// 2 is UITouchPhase::Stationary and is not expected here
|
|
UITouchPhase::Ended => TouchPhase::Ended,
|
|
UITouchPhase::Cancelled => TouchPhase::Cancelled,
|
|
_ => panic!("unexpected touch phase: {:?}", phase as i32),
|
|
};
|
|
|
|
let physical_location = {
|
|
let scale_factor = self.contentScaleFactor();
|
|
PhysicalPosition::from_logical::<(f64, f64), f64>(
|
|
(logical_location.x as _, logical_location.y as _),
|
|
scale_factor as f64,
|
|
)
|
|
};
|
|
touch_events.push(EventWrapper::StaticEvent(Event::WindowEvent {
|
|
window_id: RootWindowId(window.id()),
|
|
event: WindowEvent::Touch(Touch {
|
|
device_id: DEVICE_ID,
|
|
id: touch_id,
|
|
location: physical_location,
|
|
force,
|
|
phase,
|
|
}),
|
|
}));
|
|
}
|
|
let mtm = MainThreadMarker::new().unwrap();
|
|
app_state::handle_nonuser_events(mtm, touch_events);
|
|
}
|
|
}
|