bevy_uikit 0.0.0

Direct UIKit backend for Bevy Engine
#![expect(non_snake_case, reason = "UIKit does not use Rust naming conventions")]
use bevy_ecs::entity::Entity;
use bevy_window::{RequestRedraw, WindowFocused};
use objc2::{define_class, msg_send, rc::Retained, DefinedClass, MainThreadMarker, MainThreadOnly};
use objc2_core_foundation::{CGPoint, CGRect, CGSize};
use objc2_foundation::NSObjectProtocol;
use objc2_ui_kit::{
    UIFocusAnimationCoordinator, UIFocusEnvironment, UIFocusUpdateContext, UIResponder, UIView,
    UIViewController,
};
use tracing::trace;

use crate::app::send_window_event;

define_class!(
    #[unsafe(super(UIViewController))]
    #[name = "BevyViewController"]
    #[derive(Debug, PartialEq, Eq, Hash)]
    #[ivars = Entity]
    pub(crate) struct ViewController;

    unsafe impl NSObjectProtocol for ViewController {}

    /// Overridden UIResponder methods.
    impl ViewController {
        #[unsafe(method(canBecomeFirstResponder))]
        fn canBecomeFirstResponder(&self) -> bool {
            true
        }
    }

    /// Overridden UIViewController methods.
    impl ViewController {
        #[unsafe(method(loadView))]
        fn loadView(&self) {
            trace!("loadView");
            let view = View::new(self.mtm(), *self.ivars(), unsafe {
                self.preferredContentSize()
            });
            self.setView(Some(&view));

            // Docs say to _not_ call super
        }
    }

    unsafe impl UIFocusEnvironment for ViewController {
        #[unsafe(method(didUpdateFocusInContext:withAnimationCoordinator:))]
        fn didUpdateFocusInContext_withAnimationCoordinator(
            &self,
            context: &UIFocusUpdateContext,
            coordinator: &UIFocusAnimationCoordinator,
        ) {
            trace!(
                ?context,
                ?coordinator,
                "didUpdateFocusInContext:withAnimationCoordinator:"
            );
            unsafe {
                msg_send![super(self), didUpdateFocusInContext: context, withAnimationCoordinator: coordinator]
            }
        }
    }
);

impl ViewController {
    pub(crate) fn new(mtm: MainThreadMarker, window: Entity) -> Retained<Self> {
        let this = Self::alloc(mtm).set_ivars(window);
        unsafe { msg_send![super(this), init] }
    }
}

define_class!(
    #[unsafe(super(UIView, UIResponder))] // TODO: MTKView?
    #[name = "BevyView"]
    #[derive(Debug, PartialEq, Eq, Hash)]
    #[ivars = Entity]
    pub(crate) struct View;

    /// Overridden UIResponder methods.
    impl View {
        #[unsafe(method(canBecomeFirstResponder))]
        fn canBecomeFirstResponder(&self) -> bool {
            true
        }

        #[unsafe(method(becomeFirstResponder))]
        fn becomeFirstResponder(&self) -> bool {
            trace!("becomeFirstResponder");
            let success = unsafe { msg_send![super(self), becomeFirstResponder] };
            if success {
                send_window_event(
                    self.mtm(),
                    WindowFocused {
                        window: *self.ivars(),
                        focused: true,
                    },
                );
            }
            success
        }

        #[unsafe(method(canResignFirstResponder))]
        fn canResignFirstResponder(&self) -> bool {
            true
        }

        #[unsafe(method(resignFirstResponder))]
        fn resignFirstResponder(&self) -> bool {
            trace!("resignFirstResponder");
            let success = unsafe { msg_send![super(self), resignFirstResponder] };
            if success {
                send_window_event(
                    self.mtm(),
                    WindowFocused {
                        window: *self.ivars(),
                        focused: false,
                    },
                );
            }
            success
        }
    }

    /// Overridden UIView methods.
    impl View {
        #[unsafe(method(drawRect:))]
        fn draw_rect(&self, rect: CGRect) {
            trace!("drawRect:");
            // TODO: Add window entity here.
            unsafe { dbg!(self.isFirstResponder(), self.frame()) };
            send_window_event(self.mtm(), RequestRedraw);
            unsafe { msg_send![super(self), drawRect: rect] }
        }

        // #[unsafe(method(layoutSubviews))]
        // fn layout_subviews(&self) {
        //     let mtm = MainThreadMarker::new().unwrap();
        //     let _: () = unsafe { msg_send![super(self), layoutSubviews] };

        //     let frame = self.frame();
        //     let scale_factor = self.contentScaleFactor() as f64;
        //     let size = crate::dpi::LogicalSize {
        //         width: frame.size.width as f64,
        //         height: frame.size.height as f64,
        //     }
        //     .to_physical(scale_factor);

        //     let window = self.window().unwrap();
        //     app_state::handle_nonuser_event(
        //         mtm,
        //         EventWrapper::Window {
        //             window_id: window.id(),
        //             event: WindowEvent::SurfaceResized(size),
        //         },
        //     );
        // }

        // #[unsafe(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 frame = self.frame();
        //     let size = crate::dpi::LogicalSize {
        //         width: frame.size.width as f64,
        //         height: frame.size.height as f64,
        //     };
        //     let window_id = 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::Window {
        //             window_id,
        //             event: WindowEvent::SurfaceResized(size.to_physical(scale_factor)),
        //         })),
        //     );
        // }

        // #[unsafe(method(safeAreaInsetsDidChange))]
        // fn safe_area_changed(&self) {
        //     debug!("safeAreaInsetsDidChange was called, requesting redraw");
        //     // When the safe area changes we want to make sure to emit a redraw event
        //     self.setNeedsDisplay();
        // }

        // #[unsafe(method(touchesBegan:withEvent:))]
        // fn touches_began(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
        //     self.handle_touches(touches)
        // }

        // #[unsafe(method(touchesMoved:withEvent:))]
        // fn touches_moved(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
        //     self.handle_touches(touches)
        // }

        // #[unsafe(method(touchesEnded:withEvent:))]
        // fn touches_ended(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
        //     self.handle_touches(touches)
        // }

        // #[unsafe(method(touchesCancelled:withEvent:))]
        // fn touches_cancelled(&self, touches: &NSSet<UITouch>, _event: Option<&UIEvent>) {
        //     self.handle_touches(touches)
        // }

        // #[unsafe(method(pinchGesture:))]
        // fn pinch_gesture(&self, recognizer: &UIPinchGestureRecognizer) {
        //     let window = self.window().unwrap();

        //     let (phase, delta) = match recognizer.state() {
        //         UIGestureRecognizerState::Began => {
        //             self.ivars().pinch_last_delta.set(recognizer.scale());
        //             (TouchPhase::Started, 0.0)
        //         }
        //         UIGestureRecognizerState::Changed => {
        //             let last_scale: f64 = self.ivars().pinch_last_delta.replace(recognizer.scale());
        //             (TouchPhase::Moved, recognizer.scale() - last_scale)
        //         }
        //         UIGestureRecognizerState::Ended => {
        //             let last_scale: f64 = self.ivars().pinch_last_delta.replace(0.0);
        //             (TouchPhase::Moved, recognizer.scale() - last_scale)
        //         }
        //         UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
        //             self.ivars().rotation_last_delta.set(0.0);
        //             // Pass -delta so that action is reversed
        //             (TouchPhase::Cancelled, -recognizer.scale())
        //         }
        //         state => panic!("unexpected recognizer state: {state:?}"),
        //     };

        //     let gesture_event = EventWrapper::Window {
        //         window_id: window.id(),
        //         event: WindowEvent::PinchGesture {
        //             device_id: None,
        //             delta: delta as f64,
        //             phase,
        //         },
        //     };

        //     let mtm = MainThreadMarker::new().unwrap();
        //     app_state::handle_nonuser_event(mtm, gesture_event);
        // }

        // #[unsafe(method(doubleTapGesture:))]
        // fn double_tap_gesture(&self, recognizer: &UITapGestureRecognizer) {
        //     let window = self.window().unwrap();

        //     if recognizer.state() == UIGestureRecognizerState::Ended {
        //         let gesture_event = EventWrapper::Window {
        //             window_id: window.id(),
        //             event: WindowEvent::DoubleTapGesture { device_id: None },
        //         };

        //         let mtm = MainThreadMarker::new().unwrap();
        //         app_state::handle_nonuser_event(mtm, gesture_event);
        //     }
        // }

        // #[unsafe(method(rotationGesture:))]
        // fn rotation_gesture(&self, recognizer: &UIRotationGestureRecognizer) {
        //     let window = self.window().unwrap();

        //     let (phase, delta) = match recognizer.state() {
        //         UIGestureRecognizerState::Began => {
        //             self.ivars().rotation_last_delta.set(0.0);

        //             (TouchPhase::Started, 0.0)
        //         }
        //         UIGestureRecognizerState::Changed => {
        //             let last_rotation = self
        //                 .ivars()
        //                 .rotation_last_delta
        //                 .replace(recognizer.rotation());

        //             (TouchPhase::Moved, recognizer.rotation() - last_rotation)
        //         }
        //         UIGestureRecognizerState::Ended => {
        //             let last_rotation = self.ivars().rotation_last_delta.replace(0.0);

        //             (TouchPhase::Ended, recognizer.rotation() - last_rotation)
        //         }
        //         UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
        //             self.ivars().rotation_last_delta.set(0.0);

        //             // Pass -delta so that action is reversed
        //             (TouchPhase::Cancelled, -recognizer.rotation())
        //         }
        //         state => panic!("unexpected recognizer state: {state:?}"),
        //     };

        //     // Make delta negative to match macos, convert to degrees
        //     let gesture_event = EventWrapper::Window {
        //         window_id: window.id(),
        //         event: WindowEvent::RotationGesture {
        //             device_id: None,
        //             delta: -delta.to_degrees() as _,
        //             phase,
        //         },
        //     };

        //     let mtm = MainThreadMarker::new().unwrap();
        //     app_state::handle_nonuser_event(mtm, gesture_event);
        // }

        // #[unsafe(method(panGesture:))]
        // fn pan_gesture(&self, recognizer: &UIPanGestureRecognizer) {
        //     let window = self.window().unwrap();

        //     let translation = recognizer.translationInView(Some(self));

        //     let (phase, dx, dy) = match recognizer.state() {
        //         UIGestureRecognizerState::Began => {
        //             self.ivars().pan_last_delta.set(translation);

        //             (TouchPhase::Started, 0.0, 0.0)
        //         }
        //         UIGestureRecognizerState::Changed => {
        //             let last_pan: CGPoint = self.ivars().pan_last_delta.replace(translation);

        //             let dx = translation.x - last_pan.x;
        //             let dy = translation.y - last_pan.y;

        //             (TouchPhase::Moved, dx, dy)
        //         }
        //         UIGestureRecognizerState::Ended => {
        //             let last_pan: CGPoint = self
        //                 .ivars()
        //                 .pan_last_delta
        //                 .replace(CGPoint { x: 0.0, y: 0.0 });

        //             let dx = translation.x - last_pan.x;
        //             let dy = translation.y - last_pan.y;

        //             (TouchPhase::Ended, dx, dy)
        //         }
        //         UIGestureRecognizerState::Cancelled | UIGestureRecognizerState::Failed => {
        //             let last_pan: CGPoint = self
        //                 .ivars()
        //                 .pan_last_delta
        //                 .replace(CGPoint { x: 0.0, y: 0.0 });

        //             // Pass -delta so that action is reversed
        //             (TouchPhase::Cancelled, -last_pan.x, -last_pan.y)
        //         }
        //         state => panic!("unexpected recognizer state: {state:?}"),
        //     };

        //     let gesture_event = EventWrapper::Window {
        //         window_id: window.id(),
        //         event: WindowEvent::PanGesture {
        //             device_id: None,
        //             delta: PhysicalPosition::new(dx as _, dy as _),
        //             phase,
        //         },
        //     };

        //     let mtm = MainThreadMarker::new().unwrap();
        //     app_state::handle_nonuser_event(mtm, gesture_event);
        // }
    }
);

impl View {
    fn new(mtm: MainThreadMarker, window: Entity, size: CGSize) -> Retained<Self> {
        let this = Self::alloc(mtm).set_ivars(window);
        let frame = CGRect {
            origin: CGPoint::ZERO,
            size,
        };
        unsafe { msg_send![super(this), initWithFrame: frame] }
    }
}