Winit: Change the way you get a window

Created on 3 Nov 2016  路  3Comments  路  Source: rust-windowing/winit

easy low help wanted needs discussion api

Most helpful comment

I did a small proof of concept of getting Winit to work as a UViewController that I later present from Swift in an iOS application and it takes touch events. Removed a lot of code from iOS/mod.rs for brevity. Also pretty sure the CFRunLoopRunInMode stuff is unnecessary for this to work.

PS, I haven no clue how the jump stuff works or if they are necessary.
This works for single touch events by implementing touchesBegan:withEvent on the custom controller Winit creates. I can add others for app life cycles (using NSNotification) or even let Swift dispatch them to the game directly from the controller.


pub struct Window {
    _events_queue: Arc<RefCell<VecDeque<Event>>>,
    delegate_state: Box<DelegateState>,
}

unsafe impl Send for Window {}
unsafe impl Sync for Window {}

#[derive(Debug)]
struct DelegateState {
    window: id,
    size: LogicalSize,
    scale: f64,
}

impl DelegateState {
    fn new(window: id, size: LogicalSize, scale: f64) -> DelegateState {
        DelegateState {
            window,
            size,
            scale,
        }
    }
}



pub struct EventsLoop {
    events_queue: Arc<RefCell<VecDeque<Event>>>,
}

#[derive(Clone)]
pub struct EventsLoopProxy;

impl EventsLoop {
    pub fn new() -> EventsLoop {
        // unsafe {
        //     if !msg_send![class!(NSThread), isMainThread] {
        //         panic!("`EventsLoop` can only be created on the main thread on iOS");
        //     }
        // }
        EventsLoop {
            events_queue: Default::default(),
        }
    }



    pub fn poll_events<F>(&mut self, mut callback: F)
    where
        F: FnMut(::Event),
    {
        if let Some(event) = self.events_queue.borrow_mut().pop_front() {
            callback(event);
            return;
        }

        unsafe {
            // jump hack, so we won't quit on willTerminate event before processing it
            // assert!(
            //     JMPBUF.is_some(),
            //     "`EventsLoop::poll_events` must be called after window creation on iOS"
            // );
//            if setjmp(mem::transmute_copy(&mut JMPBUF)) != 0 {
                if let Some(event) = self.events_queue.borrow_mut().pop_front() {
                    callback(event);
                    return;
//                }
            }
        }

        unsafe {
            // run runloop
            let seconds: CFTimeInterval = 0.000002;
            while CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, 1)
                == kCFRunLoopRunHandledSource
            {}
        }

        if let Some(event) = self.events_queue.borrow_mut().pop_front() {
            callback(event)
        }
    }


#[derive(Clone)]
pub struct PlatformSpecificWindowBuilderAttributes {
    pub root_view_class: &'static Class,
}

impl Default for PlatformSpecificWindowBuilderAttributes {
    fn default() -> Self {
        PlatformSpecificWindowBuilderAttributes {
            root_view_class: class!(UIView),
        }
    }
}

        extern fn handle_touches(this: &Object, _: Sel, touches: id, _:id) {
        unsafe {
            let events_queue: *mut c_void = *this.get_ivar("eventsQueue");
            let events_queue = &*(events_queue as *const RefCell<VecDeque<Event>>);

            let touches_enum: id = msg_send![touches, objectEnumerator];

            loop {
                let touch: id = msg_send![touches_enum, nextObject];
                if touch == nil {
                    break
                }
                let location: CGPoint = msg_send![touch, locationInView:nil];
                let touch_id = touch as u64;
                let phase: i32 = msg_send![touch, phase];

                events_queue.borrow_mut().push_back(Event::WindowEvent {
                    window_id: RootEventId(WindowId),
                    event: WindowEvent::Touch(Touch {
                        device_id: DEVICE_ID,
                        id: touch_id,
                        location: (location.x as f64, location.y as f64).into(),
                        phase: match phase {
                            0 => TouchPhase::Started,
                            1 => TouchPhase::Moved,
                            // 2 is UITouchPhaseStationary and is not expected here
                            3 => TouchPhase::Ended,
                            4 => TouchPhase::Cancelled,
                            _ => panic!("unexpected touch phase: {:?}", phase)
                        }
                    }),
                });
            }
        }
    }

impl Window {


    pub fn new(
        ev: &EventsLoop,
        _attributes: WindowAttributes,
        pl_attributes: PlatformSpecificWindowBuilderAttributes,
    ) -> Result<Window, CreationError> {
        unsafe {
            // debug_assert!(mem::size_of_val(&JMPBUF) == mem::size_of::<Box<JmpBuf>>());
            // assert!(
            //     mem::replace(&mut JMPBUF, Some(Box::new([0; JBLEN]))).is_none(),
            //     "Only one `Window` is supported on iOS"
            // );

            let screen_class = class!(UIScreen);
            let window_class = class!(UIWindow);
            let controller_class = class!(UIViewController);
            let color_class = class!(UIColor);
            let green_color: id = msg_send![color_class, greenColor];
            let main_screen: id = msg_send![screen_class, mainScreen];
            let bounds: CGRect = msg_send![main_screen, bounds];
            let scale: CGFloat = msg_send![main_screen, nativeScale];


            let mut decl = ClassDecl::new("MyController", controller_class).expect("Failed to declare class `controller_class`");
            decl.add_method(sel!(touchesBegan:withEvent:),
                            handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id));
            decl.add_ivar::<*mut c_void>("eventsQueue");

            let decl = decl.register();

            let view_controller: id = msg_send![decl, alloc];
            let view_controller: id = msg_send![view_controller, init];
            let events_queue = &*ev.events_queue;

            (&mut *view_controller).set_ivar("eventsQueue", mem::transmute::<_, *mut c_void>(events_queue));

           let view: id =  msg_send![view_controller, view]; 

            let _: () = msg_send![view, setBackgroundColor: green_color];
            let size = (bounds.size.width as f64, bounds.size.height as f64).into();
            let delegate_state = Box::new(DelegateState::new(view_controller, size, scale as f64));


            return Ok(Window {
                _events_queue: ev.events_queue.clone(),
                delegate_state,
            });
        }
        //     }
        // }

        // // create_delegate_class();
        // // start_app();

        // panic!("Couldn't create `UIApplication`!")
    }

    #[inline]
    pub fn get_uiwindow(&self) -> id {
        self.delegate_state.window
    }

    #[inline]
    pub fn get_uiview(&self) -> id {
        self.delegate_state.window
    }

}

The client


pub struct GodObject {
    window: winit::Window,
    events_loop: winit::EventsLoop,

}

#[no_mangle]
pub extern fn run() -> *mut GodObject {
    let mut events_loop = winit::EventsLoop::new();
    let window = winit::Window::new(&events_loop).unwrap();    

    // events_loop.run_forever(|event| {
    //     ControlFlow::Continue
    // });
   let x = Box::new(GodObject { window, events_loop }) ;

   Box::into_raw(x)

}

#[no_mangle]
pub extern fn get_window(god_object: *const GodObject) -> *mut c_void {
    let god_object = unsafe { &*god_object };
    use winit::os::ios::WindowExt;  
    god_object.window.get_uiwindow()

}

#[no_mangle]
pub extern fn start_event_block(god_object: *mut GodObject) {
    let mut god_object = unsafe { &mut *god_object };
    use winit::os::ios::WindowExt;  
    let mut events_loop = &mut god_object.events_loop; 
    events_loop.poll_events(|event| {
        println!("{:?}", event);
        //ControlFlow::Continue
    });


}

And the Swift counter part

import UIKit
public class GameViewController: UIViewController {

    let game: OpaquePointer
    var displayLink: CADisplayLink? = nil

    public init() {
        self.game = run()!
        super.init(nibName: nil, bundle: nil)
    }

    @objc func tick(step: CADisplayLink) {
        start_event_block(self.game)
        print(step)
    }

    override public func viewDidLoad() {
        super.viewDidLoad()
        self.displayLink = CADisplayLink(target: self, selector: #selector(self.tick(step:)))
        let gameView = Unmanaged<UIViewController>.fromOpaque(get_window(self.game)!).takeRetainedValue()
        self.addChild(gameView)
        self.view.addSubview(gameView.view)
        gameView.didMove(toParent: self)

    }
    public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) }
    public override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.displayLink?.add(to: .current, forMode: .default)
    }
    public override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.displayLink?.invalidate()

    }
    public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated)}


    override public var shouldAutorotate: Bool { return true }
    override public var prefersStatusBarHidden: Bool { return true }
    override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone { return .all } else { return .all }
    }

}

All 3 comments

I agree with this, because this is a bit of an issue for iOS.
I got an issue where I need a game engine that uses Winit to work with the rest of the platform (macOS/iOS). The game engine in question uses Winit to set everything up (which btw is an amazing feature on its own). The problem is this won't work if you want it to coexist with existing applications.

An approach would be to let Winit remain is it is and set everything up for you - but also offer a way to give the lowest common denominator say a "window" for each platform.

For iOS, this could be a UIWindow, or a UIViewController or even just a UIView. Right now, it's a bit overbearing(?) for iOS where Winit sets up a whole application (UIApplicationMain & UIApplicatonDelegate) this work perfect if the goal is to be a single executable.

My issue here, if the game engine works that way, then it's going to be tricky to integrate with the rest of the platform. If you need to present other views or deal with In App Purchase transactions then the current Winit API doesn't work.

I am not asking to change the current approach, because there's a lot of value in that.
What I am suggesting is an approach where Winit (and Glutin) can work with creating components (say a UIView) for iOS. You could still offer the "all in one" way of setting up an application for platform, but could reuse code for setting up the lowest denominator component (calling it a Window). Because the existing iOS implementation for Winit still does all of that, but it's wrapped in also creating a whole application.

I did a small proof of concept of getting Winit to work as a UViewController that I later present from Swift in an iOS application and it takes touch events. Removed a lot of code from iOS/mod.rs for brevity. Also pretty sure the CFRunLoopRunInMode stuff is unnecessary for this to work.

PS, I haven no clue how the jump stuff works or if they are necessary.
This works for single touch events by implementing touchesBegan:withEvent on the custom controller Winit creates. I can add others for app life cycles (using NSNotification) or even let Swift dispatch them to the game directly from the controller.


pub struct Window {
    _events_queue: Arc<RefCell<VecDeque<Event>>>,
    delegate_state: Box<DelegateState>,
}

unsafe impl Send for Window {}
unsafe impl Sync for Window {}

#[derive(Debug)]
struct DelegateState {
    window: id,
    size: LogicalSize,
    scale: f64,
}

impl DelegateState {
    fn new(window: id, size: LogicalSize, scale: f64) -> DelegateState {
        DelegateState {
            window,
            size,
            scale,
        }
    }
}



pub struct EventsLoop {
    events_queue: Arc<RefCell<VecDeque<Event>>>,
}

#[derive(Clone)]
pub struct EventsLoopProxy;

impl EventsLoop {
    pub fn new() -> EventsLoop {
        // unsafe {
        //     if !msg_send![class!(NSThread), isMainThread] {
        //         panic!("`EventsLoop` can only be created on the main thread on iOS");
        //     }
        // }
        EventsLoop {
            events_queue: Default::default(),
        }
    }



    pub fn poll_events<F>(&mut self, mut callback: F)
    where
        F: FnMut(::Event),
    {
        if let Some(event) = self.events_queue.borrow_mut().pop_front() {
            callback(event);
            return;
        }

        unsafe {
            // jump hack, so we won't quit on willTerminate event before processing it
            // assert!(
            //     JMPBUF.is_some(),
            //     "`EventsLoop::poll_events` must be called after window creation on iOS"
            // );
//            if setjmp(mem::transmute_copy(&mut JMPBUF)) != 0 {
                if let Some(event) = self.events_queue.borrow_mut().pop_front() {
                    callback(event);
                    return;
//                }
            }
        }

        unsafe {
            // run runloop
            let seconds: CFTimeInterval = 0.000002;
            while CFRunLoopRunInMode(kCFRunLoopDefaultMode, seconds, 1)
                == kCFRunLoopRunHandledSource
            {}
        }

        if let Some(event) = self.events_queue.borrow_mut().pop_front() {
            callback(event)
        }
    }


#[derive(Clone)]
pub struct PlatformSpecificWindowBuilderAttributes {
    pub root_view_class: &'static Class,
}

impl Default for PlatformSpecificWindowBuilderAttributes {
    fn default() -> Self {
        PlatformSpecificWindowBuilderAttributes {
            root_view_class: class!(UIView),
        }
    }
}

        extern fn handle_touches(this: &Object, _: Sel, touches: id, _:id) {
        unsafe {
            let events_queue: *mut c_void = *this.get_ivar("eventsQueue");
            let events_queue = &*(events_queue as *const RefCell<VecDeque<Event>>);

            let touches_enum: id = msg_send![touches, objectEnumerator];

            loop {
                let touch: id = msg_send![touches_enum, nextObject];
                if touch == nil {
                    break
                }
                let location: CGPoint = msg_send![touch, locationInView:nil];
                let touch_id = touch as u64;
                let phase: i32 = msg_send![touch, phase];

                events_queue.borrow_mut().push_back(Event::WindowEvent {
                    window_id: RootEventId(WindowId),
                    event: WindowEvent::Touch(Touch {
                        device_id: DEVICE_ID,
                        id: touch_id,
                        location: (location.x as f64, location.y as f64).into(),
                        phase: match phase {
                            0 => TouchPhase::Started,
                            1 => TouchPhase::Moved,
                            // 2 is UITouchPhaseStationary and is not expected here
                            3 => TouchPhase::Ended,
                            4 => TouchPhase::Cancelled,
                            _ => panic!("unexpected touch phase: {:?}", phase)
                        }
                    }),
                });
            }
        }
    }

impl Window {


    pub fn new(
        ev: &EventsLoop,
        _attributes: WindowAttributes,
        pl_attributes: PlatformSpecificWindowBuilderAttributes,
    ) -> Result<Window, CreationError> {
        unsafe {
            // debug_assert!(mem::size_of_val(&JMPBUF) == mem::size_of::<Box<JmpBuf>>());
            // assert!(
            //     mem::replace(&mut JMPBUF, Some(Box::new([0; JBLEN]))).is_none(),
            //     "Only one `Window` is supported on iOS"
            // );

            let screen_class = class!(UIScreen);
            let window_class = class!(UIWindow);
            let controller_class = class!(UIViewController);
            let color_class = class!(UIColor);
            let green_color: id = msg_send![color_class, greenColor];
            let main_screen: id = msg_send![screen_class, mainScreen];
            let bounds: CGRect = msg_send![main_screen, bounds];
            let scale: CGFloat = msg_send![main_screen, nativeScale];


            let mut decl = ClassDecl::new("MyController", controller_class).expect("Failed to declare class `controller_class`");
            decl.add_method(sel!(touchesBegan:withEvent:),
                            handle_touches as extern fn(this: &Object, _: Sel, _: id, _:id));
            decl.add_ivar::<*mut c_void>("eventsQueue");

            let decl = decl.register();

            let view_controller: id = msg_send![decl, alloc];
            let view_controller: id = msg_send![view_controller, init];
            let events_queue = &*ev.events_queue;

            (&mut *view_controller).set_ivar("eventsQueue", mem::transmute::<_, *mut c_void>(events_queue));

           let view: id =  msg_send![view_controller, view]; 

            let _: () = msg_send![view, setBackgroundColor: green_color];
            let size = (bounds.size.width as f64, bounds.size.height as f64).into();
            let delegate_state = Box::new(DelegateState::new(view_controller, size, scale as f64));


            return Ok(Window {
                _events_queue: ev.events_queue.clone(),
                delegate_state,
            });
        }
        //     }
        // }

        // // create_delegate_class();
        // // start_app();

        // panic!("Couldn't create `UIApplication`!")
    }

    #[inline]
    pub fn get_uiwindow(&self) -> id {
        self.delegate_state.window
    }

    #[inline]
    pub fn get_uiview(&self) -> id {
        self.delegate_state.window
    }

}

The client


pub struct GodObject {
    window: winit::Window,
    events_loop: winit::EventsLoop,

}

#[no_mangle]
pub extern fn run() -> *mut GodObject {
    let mut events_loop = winit::EventsLoop::new();
    let window = winit::Window::new(&events_loop).unwrap();    

    // events_loop.run_forever(|event| {
    //     ControlFlow::Continue
    // });
   let x = Box::new(GodObject { window, events_loop }) ;

   Box::into_raw(x)

}

#[no_mangle]
pub extern fn get_window(god_object: *const GodObject) -> *mut c_void {
    let god_object = unsafe { &*god_object };
    use winit::os::ios::WindowExt;  
    god_object.window.get_uiwindow()

}

#[no_mangle]
pub extern fn start_event_block(god_object: *mut GodObject) {
    let mut god_object = unsafe { &mut *god_object };
    use winit::os::ios::WindowExt;  
    let mut events_loop = &mut god_object.events_loop; 
    events_loop.poll_events(|event| {
        println!("{:?}", event);
        //ControlFlow::Continue
    });


}

And the Swift counter part

import UIKit
public class GameViewController: UIViewController {

    let game: OpaquePointer
    var displayLink: CADisplayLink? = nil

    public init() {
        self.game = run()!
        super.init(nibName: nil, bundle: nil)
    }

    @objc func tick(step: CADisplayLink) {
        start_event_block(self.game)
        print(step)
    }

    override public func viewDidLoad() {
        super.viewDidLoad()
        self.displayLink = CADisplayLink(target: self, selector: #selector(self.tick(step:)))
        let gameView = Unmanaged<UIViewController>.fromOpaque(get_window(self.game)!).takeRetainedValue()
        self.addChild(gameView)
        self.view.addSubview(gameView.view)
        gameView.didMove(toParent: self)

    }
    public override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) }
    public override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.displayLink?.add(to: .current, forMode: .default)
    }
    public override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        self.displayLink?.invalidate()

    }
    public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated)}


    override public var shouldAutorotate: Bool { return true }
    override public var prefersStatusBarHidden: Bool { return true }
    override public var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        if UIDevice.current.userInterfaceIdiom == .phone { return .all } else { return .all }
    }

}

This is exactly my use case. I want Winit to live in a UIView at my existing iOS application. At this current form, Winit handles so much job.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

e00E picture e00E  路  5Comments

felixrabe picture felixrabe  路  3Comments

Osspial picture Osspial  路  5Comments

chrisduerr picture chrisduerr  路  3Comments

chemicstry picture chemicstry  路  3Comments