#![allow(deprecated)]
use super::keyboard::{make_modifiers, KeyboardState};
use super::window::WindowSharedState;
use crate::platform::macos::context::WindowContext;
use crate::wrappers::appkit::*;
use crate::MouseEvent::{ButtonPressed, ButtonReleased};
use crate::{
DropData, DropEffect, Event, EventStatus, MouseButton, MouseEvent, ScrollDelta, WindowEvent,
WindowHandler, WindowOpenOptions, WindowSize,
};
use dpi::{LogicalPosition, LogicalSize, Size};
use objc2::__framework_prelude::Retained;
use objc2::rc::Weak;
use objc2::runtime::{NSObjectProtocol, ProtocolObject};
use objc2::{msg_send, AllocAnyThread, MainThreadMarker};
use objc2_app_kit::{
NSApplication, NSDragOperation, NSDraggingInfo, NSEvent, NSFilenamesPboardType, NSTrackingArea,
NSTrackingAreaOptions, NSView, NSWindow,
};
use objc2_foundation::{NSArray, NSNotification, NSPoint, NSRect, NSSize, NSString};
use std::cell::{Cell, OnceCell};
use std::rc::Rc;
pub enum ViewParentingType {
Parented { parent_view: Weak<NSView> },
Windowed { owned_window: Weak<NSWindow>, running_app: Weak<NSApplication> },
}
pub(crate) struct BaseviewView {
pub(crate) state: Rc<WindowSharedState>,
pub(crate) mtm: MainThreadMarker,
window_handler: OnceCell<Box<dyn WindowHandler>>,
frame_timer: Cell<Option<TimerHandle>>,
notification_center_observer: Cell<Option<NotificationCenterObserver>>,
keyboard_state: KeyboardState,
parenting: ViewParentingType,
#[cfg(feature = "opengl")]
pub(crate) gl_context: OnceCell<super::gl::GlContext>,
}
impl BaseviewView {
pub fn new<H: WindowHandler + 'static>(
_options: WindowOpenOptions,
builder: impl FnOnce(crate::WindowContext) -> H + Send + 'static,
parenting: ViewParentingType, final_size: LogicalSize<f64>, mtm: MainThreadMarker,
) -> (Retained<View<Self>>, Rc<WindowSharedState>) {
let view_rect =
NSRect::new(NSPoint::ZERO, NSSize::new(final_size.width, final_size.height));
let state = Rc::new(WindowSharedState::new(final_size, 1.0));
let inner = BaseviewView {
mtm,
state: state.clone(),
keyboard_state: KeyboardState::new(),
frame_timer: None.into(),
window_handler: OnceCell::new(),
notification_center_observer: None.into(),
parenting,
#[cfg(feature = "opengl")]
gl_context: OnceCell::new(),
};
let view = View::new(view_rect, inner, |view| {
match &view.parenting {
ViewParentingType::Parented { parent_view } => {
let parent_view = parent_view.load().unwrap();
parent_view.addSubview(view.view);
}
ViewParentingType::Windowed { owned_window, .. } => {
let owned_window = owned_window.load().unwrap();
owned_window.setContentView(Some(view.view));
set_delegate(&owned_window, view.view);
}
}
view.state.scale_factor.set(view.view.backing_scale_factor());
view.state.size.set(view.view.size());
#[cfg(feature = "opengl")]
if let Some(gl_config) = _options.gl_config {
let gl_context = super::gl::GlContext::create(view.view, gl_config).unwrap();
let Ok(()) = view.gl_context.set(gl_context) else { unreachable!() };
}
let context = WindowContext::new(view);
let handler = Box::new(builder(crate::WindowContext::new(context)));
let Ok(()) = view.window_handler.set(handler) else { unreachable!() };
let ns_filenames_pboard_type = unsafe { NSFilenamesPboardType };
view.view.registerForDraggedTypes(&NSArray::from_slice(&[ns_filenames_pboard_type]));
let timer_view = Weak::new(view.view);
view.frame_timer.set(TimerHandle::new(0.015, move || {
if let Some(view) = timer_view.load() {
Self::trigger_frame(view.inner_ref());
}
}));
let notifier_view = Weak::new(view.view);
let observer = NotificationCenterObserver::register_window_key_change(move |n| {
if let Some(view) = notifier_view.load() {
BaseviewView::handle_notification(view.inner_ref(), n);
}
});
view.notification_center_observer.set(Some(observer));
});
(view, state)
}
pub fn close(this: ViewRef<Self>) {
this.state.closed.set(true);
this.view.removeFromSuperview();
if let ViewParentingType::Windowed { owned_window: parent_window, running_app } =
&this.parenting
{
if let Some(parent_window) = parent_window.load() {
parent_window.close();
}
if let Some(app) = running_app.load() {
app.stop(Some(&app));
}
}
}
pub fn resize(this: ViewRef<Self>, size: Size) {
let size = size.to_logical::<f64>(this.view.backing_scale_factor());
let size = NSSize::new(size.width.round(), size.height.round());
this.view.setFrameSize(size);
this.view.setNeedsDisplay(true);
#[cfg(feature = "opengl")]
if let Some(gl_context) = this.gl_context.get() {
gl_context.resize(size);
}
if let ViewParentingType::Windowed { owned_window, .. } = &this.parenting {
if let Some(owned_window) = owned_window.load() {
owned_window.setContentSize(size);
}
}
Self::view_did_change_backing_properties(this);
}
fn trigger_event(this: ViewRef<Self>, event: Event) -> EventStatus {
let Some(handler) = this.window_handler.get() else {
return EventStatus::Ignored;
};
handler.on_event(event)
}
fn trigger_frame(this: ViewRef<Self>) {
let Some(handler) = this.window_handler.get() else { return };
handler.on_frame();
}
}
impl Drop for BaseviewView {
fn drop(&mut self) {
self.state.closed.set(true);
}
}
impl ViewImpl for BaseviewView {
fn become_first_responder(this: ViewRef<Self>) -> bool {
let Some(window) = this.view.window() else {
return true;
};
if window.isKeyWindow() {
Self::trigger_event(this, Event::Window(WindowEvent::Focused));
}
true
}
fn resign_first_responder(this: ViewRef<Self>) -> bool {
Self::trigger_event(this, Event::Window(WindowEvent::Unfocused));
true
}
fn window_should_close(this: ViewRef<Self>) -> bool {
Self::trigger_event(this, Event::Window(WindowEvent::WillClose));
Self::close(this);
true
}
fn view_did_change_backing_properties(this: ViewRef<Self>) {
let current_size = this.view.size();
let current_scale_factor = this.view.backing_scale_factor();
if this.state.scale_factor.get() != current_scale_factor
|| this.state.size.get() != current_size
{
this.state.size.set(current_size);
this.state.scale_factor.set(current_scale_factor);
if let Some(handler) = this.window_handler.get() {
handler.resized(WindowSize::from_logical(current_size, current_scale_factor))
}
}
}
fn hit_test(this: ViewRef<'_, Self>, point: NSPoint) -> Option<&NSView> {
let superclass = this.view.class().superclass().unwrap();
let super_result: Option<&NSView> =
unsafe { msg_send![super(this.view, superclass), hitTest: point] };
let super_result = super_result?;
#[cfg(feature = "opengl")]
{
if let Some(gl_context) = this.gl_context.get() {
if *super_result == **gl_context.view {
return Some(this.view);
}
}
}
Some(super_result)
}
fn view_will_move_to_window(this: ViewRef<Self>, new_window: Option<&NSWindow>) {
let tracking_areas = this.view.trackingAreas();
match new_window {
None => {
if tracking_areas.count() > 0 {
let tracking_area = tracking_areas.objectAtIndex(0);
this.view.removeTrackingArea(&tracking_area);
}
}
Some(new_window) => {
if tracking_areas.is_empty() {
let tracking_area = new_tracking_area(this.view);
this.view.addTrackingArea(&tracking_area);
}
new_window.setAcceptsMouseMovedEvents(true);
new_window.makeFirstResponder(Some(this.view));
}
}
unsafe {
let superclass = msg_send![this.view, superclass];
let () = msg_send![super(this.view, superclass), viewWillMoveToWindow: new_window];
}
}
fn update_tracking_areas(this: ViewRef<Self>) {
let tracking_areas = this.view.trackingAreas();
if tracking_areas.count() > 0 {
let tracking_area = tracking_areas.objectAtIndex(0);
this.view.removeTrackingArea(&tracking_area);
}
let tracking_area = new_tracking_area(this.view);
this.view.addTrackingArea(&tracking_area);
}
fn mouse_moved(this: ViewRef<Self>, event: &NSEvent) {
let point = this.view.convertPoint_fromView(event.locationInWindow(), None);
let position = LogicalPosition { x: point.x, y: point.y };
Self::trigger_event(
this,
Event::Mouse(MouseEvent::CursorMoved {
position: position.to_physical(this.state.scale_factor.get()),
modifiers: make_modifiers(event.modifierFlags()),
}),
);
}
fn scroll_wheel(this: ViewRef<Self>, event: &NSEvent) {
let x = event.scrollingDeltaX() as f32;
let y = event.scrollingDeltaY() as f32;
let delta = if event.hasPreciseScrollingDeltas() {
ScrollDelta::Pixels { x, y }
} else {
ScrollDelta::Lines { x, y }
};
Self::trigger_event(
this,
Event::Mouse(MouseEvent::WheelScrolled {
delta,
modifiers: make_modifiers(event.modifierFlags()),
}),
);
}
fn dragging_entered(
this: ViewRef<Self>, sender: Option<&ProtocolObject<dyn NSDraggingInfo>>,
) -> NSDragOperation {
let modifiers = this.keyboard_state.last_mods();
let drop_data = get_drop_data(sender);
let event = MouseEvent::DragEntered {
position: get_drag_position(sender).to_physical(this.view.backing_scale_factor()),
modifiers: make_modifiers(modifiers),
data: drop_data,
};
on_event(this, event)
}
fn dragging_updated(
this: ViewRef<Self>, sender: Option<&ProtocolObject<dyn NSDraggingInfo>>,
) -> NSDragOperation {
let modifiers = this.keyboard_state.last_mods();
let drop_data = get_drop_data(sender);
let event = MouseEvent::DragMoved {
position: get_drag_position(sender).to_physical(this.view.backing_scale_factor()),
modifiers: make_modifiers(modifiers),
data: drop_data,
};
on_event(this, event)
}
fn prepare_for_drag_operation(
_this: ViewRef<Self>, _sender: Option<&ProtocolObject<dyn NSDraggingInfo>>,
) -> bool {
true
}
fn perform_drag_operation(
this: ViewRef<Self>, sender: Option<&ProtocolObject<dyn NSDraggingInfo>>,
) -> bool {
let modifiers = this.keyboard_state.last_mods();
let drop_data = get_drop_data(sender);
let event = MouseEvent::DragDropped {
position: get_drag_position(sender).to_physical(this.view.backing_scale_factor()),
modifiers: make_modifiers(modifiers),
data: drop_data,
};
let event_status = Self::trigger_event(this, Event::Mouse(event));
matches!(event_status, EventStatus::AcceptDrop(_))
}
fn dragging_exited(this: ViewRef<Self>, _sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) {
on_event(this, MouseEvent::DragLeft);
}
fn handle_notification(this: ViewRef<Self>, notification: &NSNotification) {
let Some(window) = this.view.window() else { return };
let Some(notification_object) = notification.object().and_then(|o| o.downcast().ok())
else {
return;
};
if window != notification_object {
return;
}
let Some(first_responder) = window.firstResponder() else { return };
if !this.view.isEqual(Some(&first_responder)) {
return;
}
Self::trigger_event(
this,
Event::Window(if window.isKeyWindow() {
WindowEvent::Focused
} else {
WindowEvent::Unfocused
}),
);
}
fn mouse_down(this: ViewRef<Self>, event: &NSEvent) {
Self::trigger_event(
this,
Event::Mouse(ButtonPressed {
button: MouseButton::Left,
modifiers: make_modifiers(event.modifierFlags()),
}),
);
}
fn mouse_up(this: ViewRef<Self>, event: &NSEvent) {
Self::trigger_event(
this,
Event::Mouse(ButtonReleased {
button: MouseButton::Left,
modifiers: make_modifiers(event.modifierFlags()),
}),
);
}
fn right_mouse_down(this: ViewRef<Self>, event: &NSEvent) {
Self::trigger_event(
this,
Event::Mouse(ButtonPressed {
button: MouseButton::Right,
modifiers: make_modifiers(event.modifierFlags()),
}),
);
}
fn right_mouse_up(this: ViewRef<Self>, event: &NSEvent) {
Self::trigger_event(
this,
Event::Mouse(ButtonReleased {
button: MouseButton::Right,
modifiers: make_modifiers(event.modifierFlags()),
}),
);
}
fn other_mouse_down(this: ViewRef<Self>, event: &NSEvent) {
Self::trigger_event(
this,
Event::Mouse(ButtonPressed {
button: MouseButton::Middle,
modifiers: make_modifiers(event.modifierFlags()),
}),
);
}
fn other_mouse_up(this: ViewRef<Self>, event: &NSEvent) {
Self::trigger_event(
this,
Event::Mouse(ButtonReleased {
button: MouseButton::Middle,
modifiers: make_modifiers(event.modifierFlags()),
}),
);
}
fn mouse_entered(this: ViewRef<Self>) {
Self::trigger_event(this, Event::Mouse(MouseEvent::CursorEntered));
}
fn mouse_exited(this: ViewRef<Self>) {
Self::trigger_event(this, Event::Mouse(MouseEvent::CursorLeft));
}
fn key_down(this: ViewRef<Self>, event: &NSEvent) {
if let Some(key_event) = this.keyboard_state.process_native_event(event) {
let status = Self::trigger_event(this, Event::Keyboard(key_event));
if let EventStatus::Ignored = status {
unsafe {
let superclass = msg_send![this.view, superclass];
let () = msg_send![super(this.view, superclass), keyDown:event];
}
}
}
}
fn key_up(this: ViewRef<Self>, event: &NSEvent) {
if let Some(key_event) = this.keyboard_state.process_native_event(event) {
let status = Self::trigger_event(this, Event::Keyboard(key_event));
if let EventStatus::Ignored = status {
unsafe {
let superclass = msg_send![this.view, superclass];
let () = msg_send![super(this.view, superclass), keyUp:event];
}
}
}
}
fn flags_changed(this: ViewRef<Self>, event: &NSEvent) {
if let Some(key_event) = this.keyboard_state.process_native_event(event) {
let status = Self::trigger_event(this, Event::Keyboard(key_event));
if let EventStatus::Ignored = status {
unsafe {
let superclass = msg_send![this.view, superclass];
let () = msg_send![super(this.view, superclass), flagsChanged:event];
}
}
}
}
}
fn new_tracking_area(this: &NSView) -> Retained<NSTrackingArea> {
let options = NSTrackingAreaOptions::MouseEnteredAndExited
| NSTrackingAreaOptions::MouseMoved
| NSTrackingAreaOptions::CursorUpdate
| NSTrackingAreaOptions::ActiveInActiveApp
| NSTrackingAreaOptions::InVisibleRect
| NSTrackingAreaOptions::EnabledDuringMouseDrag;
unsafe {
NSTrackingArea::initWithRect_options_owner_userInfo(
NSTrackingArea::alloc(),
this.bounds(),
options,
Some(this),
None,
)
}
}
fn get_drag_position(sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) -> LogicalPosition<f64> {
let point = match sender {
Some(sender) => sender.draggingLocation(),
None => NSPoint::ZERO,
};
LogicalPosition::new(point.x, point.y)
}
fn get_drop_data(sender: Option<&ProtocolObject<dyn NSDraggingInfo>>) -> DropData {
let Some(sender) = sender else {
return DropData::None;
};
let pasteboard = sender.draggingPasteboard();
let Some(file_list) = pasteboard.propertyListForType(unsafe { NSFilenamesPboardType }) else {
return DropData::None;
};
let Ok(file_list) = file_list.downcast::<NSArray>() else {
return DropData::None;
};
let files = file_list
.into_iter()
.filter_map(|s| s.downcast::<NSString>().ok())
.map(|s| s.to_string().into())
.collect();
DropData::Files(files)
}
fn on_event(this: ViewRef<BaseviewView>, event: MouseEvent) -> NSDragOperation {
let event_status = BaseviewView::trigger_event(this, Event::Mouse(event));
match event_status {
EventStatus::AcceptDrop(DropEffect::Copy) => NSDragOperation::Copy,
EventStatus::AcceptDrop(DropEffect::Move) => NSDragOperation::Move,
EventStatus::AcceptDrop(DropEffect::Link) => NSDragOperation::Link,
EventStatus::AcceptDrop(DropEffect::Scroll) => NSDragOperation::Generic,
_ => NSDragOperation::None,
}
}