// Pure UIKit + CAEAGLLayer + CADisplayLink backend for iOS. // // Linking is per-target via the game's build.sx (`opts.add_framework("UIKit")` // + `opts.add_framework("OpenGLES")` + `opts.add_framework("QuartzCore")` on // `.ios`). The file compiles cleanly on every target — the UIKit-touching // bodies live behind `inline if OS == .ios` guards, so non-iOS builds never // reach the unresolved Obj-C symbols. #import "modules/std.sx"; #import "modules/std/objc.sx"; #import "modules/compiler.sx"; #import "modules/opengl.sx"; #import "modules/ui/types.sx"; #import "modules/ui/events.sx"; #import "modules/platform/types.sx"; #import "modules/platform/api.sx"; UIApplicationMain :: (argc: s32, argv: *void, principal_class: *void, delegate_class: *void) -> s32 #foreign; dlsym :: (handle: *void, name: [*]u8) -> *void #foreign; chdir :: (path: [*]u8) -> s32 #foreign; // kEAGLRenderingAPIOpenGLES3 = 3 EAGL_API_GLES3 :: 3; // CGFloat is a `double` on 64-bit Apple platforms; CGPoint = {x, y} fits in // 16 bytes and returns via the FP-register path on arm64. CGPoint :: struct { x: f64; y: f64; } // UIEdgeInsets = {top, left, bottom, right} CGFloats — 32 bytes; exceeds the // 16-byte registers cutoff so it returns via the x8 indirect-result-pointer // convention. sx generates the right call sequence when the function-pointer // type declares it as a by-value struct return. UIEdgeInsets :: struct { top: f64; left: f64; bottom: f64; right: f64; } // CGRect for unwrapping NSValue-wrapped rects (e.g. keyboard end frame). Same // 32-byte indirect-return shape as UIEdgeInsets. CGRect :: struct { x: f64; y: f64; width: f64; height: f64; } // GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's // loader path (they live on the framework's symbol table directly). GL_RENDERBUFFER :u32: 0x8D41; GL_FRAMEBUFFER :u32: 0x8D40; GL_COLOR_ATTACHMENT0 :u32: 0x8CE0; GL_FRAMEBUFFER_COMPLETE :u32: 0x8CD5; g_uikit_plat : *UIKitPlatform = null; UIKitPlatform :: struct { window: *void = null; // UIWindow* root_vc: *void = null; // UIViewController* gl_view: *void = null; // SxGLView* gl_layer: *void = null; // CAEAGLLayer* (= gl_view.layer) gl_ctx: *void = null; // EAGLContext* display_link: *void = null; color_renderbuffer: u32 = 0; framebuffer: u32 = 0; gl_initialized: bool = false; // Hidden UITextField; firstResponder ⇆ keyboard visibility. text_field: *void = null; viewport_w: f32 = 0.0; viewport_h: f32 = 0.0; pixel_w: s32 = 0; pixel_h: s32 = 0; dpi_scale: f32 = 1.0; delta_time: f32 = 0.016; frame_closure: Closure() = ---; has_frame_closure: bool = false; events: List(Event) = .{}; safe_top: f32 = 0.0; safe_left: f32 = 0.0; safe_bottom: f32 = 0.0; safe_right: f32 = 0.0; keyboard_visible: bool = false; keyboard_height: f32 = 0.0; // Keyboard height SNAPS to its target value when the observer fires. // It does NOT interpolate in lockstep with iOS's keyboard animation. // Reason: with OpenGL ES + CAEAGLLayer, our renderbuffer is baked at // `presentRenderbuffer` time, while UIKit's keyboard view is composited // by CoreAnimation at vsync. We can't make the compositor interpolate // the renderbuffer's contents in lockstep with the keyboard's frame. // True lockstep requires a Metal renderer (CAMetalLayer + // `present(at: targetTimestamp)` keeps the pipeline at 1 frame) plus // curve-accurate prediction. Tracked as the Metal port in // current/CHECKPOINT.md. saved_title: [*]u8 = null; } impl Platform for UIKitPlatform { init :: (self: *UIKitPlatform, title: [:0]u8, w: s32, h: s32) -> bool { self.dpi_scale = 1.0; self.delta_time = 0.016; self.has_frame_closure = false; self.gl_initialized = false; self.keyboard_visible = false; self.keyboard_height = 0.0; self.saved_title = title.ptr; g_uikit_plat = self; // iOS apps start with CWD=/. chdir to the bundle's resourcePath so the // game's relative `fopen("assets/...")` calls find their data — must // happen BEFORE any code that loads fonts/textures from disk. inline if OS == .ios { uikit_chdir_to_bundle(); uikit_register_classes(); uikit_create_gl_context(self); } true; } run_frame_loop :: (self: *UIKitPlatform, frame_fn: Closure()) { self.frame_closure = frame_fn; self.has_frame_closure = true; g_uikit_plat = self; inline if OS == .ios { UIApplicationMain(0, xx 0, xx 0, ns_string("SxAppDelegate".ptr)); } } poll_events :: (self: *UIKitPlatform) -> []Event { result : []Event = ---; result.ptr = self.events.items; result.len = self.events.len; self.events.len = 0; result; } begin_frame :: (self: *UIKitPlatform) -> FrameContext { inline if OS == .ios { uikit_refresh_safe_insets(self); } FrameContext.{ viewport_w = self.viewport_w, viewport_h = self.viewport_h, pixel_w = self.pixel_w, pixel_h = self.pixel_h, dpi_scale = self.dpi_scale, delta_time = self.delta_time, }; } end_frame :: (self: *UIKitPlatform) { inline if OS == .ios { uikit_present_renderbuffer(self); } } safe_insets :: (self: *UIKitPlatform) -> EdgeInsets { bottom := self.safe_bottom; if self.keyboard_visible { if self.keyboard_height > bottom { bottom = self.keyboard_height; } } EdgeInsets.{ top = self.safe_top, left = self.safe_left, bottom = bottom, right = self.safe_right, }; } keyboard :: (self: *UIKitPlatform) -> KeyboardState { KeyboardState.{ visible = self.keyboard_visible, height = self.keyboard_height, }; } show_keyboard :: (self: *UIKitPlatform) { inline if OS == .ios { if self.text_field == null { return; } sel_become := sel_registerName("becomeFirstResponder".ptr); msg_b : (*void, *void) -> u8 = xx objc_msgSend; msg_b(self.text_field, sel_become); } } hide_keyboard :: (self: *UIKitPlatform) { inline if OS == .ios { if self.text_field == null { return; } sel_resign := sel_registerName("resignFirstResponder".ptr); msg_b : (*void, *void) -> u8 = xx objc_msgSend; msg_b(self.text_field, sel_resign); } } stop :: (self: *UIKitPlatform) { } shutdown :: (self: *UIKitPlatform) { } } // dlsym(RTLD_DEFAULT, name) — Apple platforms. RTLD_DEFAULT is (void*)-2. ios_gl_proc :: (name: [*]u8) -> *void { rtld_default : *void = xx (0 - 2); dlsym(rtld_default, name); } // Read a `extern NSString * const k...` global from the loaded image. The // extern variable holds a pointer to the NSString instance — dlsym returns // the address of that variable, which we dereference. uikit_extern_nsstring :: (name: [*]u8) -> *void { rtld_default : *void = xx (0 - 2); p := dlsym(rtld_default, name); if p == null { return null; } pp : **void = xx p; pp.*; } // ─────────────────────────────────────────────────────────────────────────── // iOS-only helpers — only reachable from `inline if OS == .ios` call sites, // so non-iOS builds never reference the unresolved UIKit symbols below. // ─────────────────────────────────────────────────────────────────────────── uikit_refresh_safe_insets :: (plat: *UIKitPlatform) { inline if OS != .ios { return; } if plat.gl_view == null { return; } sel_safe := sel_registerName("safeAreaInsets".ptr); msg_insets : (*void, *void) -> UIEdgeInsets = xx objc_msgSend; i := msg_insets(plat.gl_view, sel_safe); plat.safe_top = xx i.top; plat.safe_left = xx i.left; plat.safe_bottom = xx i.bottom; plat.safe_right = xx i.right; } uikit_chdir_to_bundle :: () { inline if OS != .ios { return; } NSBundle := objc_getClass("NSBundle".ptr); sel_main_bundle := sel_registerName("mainBundle".ptr); sel_resource_path := sel_registerName("resourcePath".ptr); msg_o : (*void, *void) -> *void = xx objc_msgSend; bundle := msg_o(NSBundle, sel_main_bundle); rsrc := msg_o(bundle, sel_resource_path); if rsrc == null { return; } chdir(c_string(rsrc)); } uikit_register_classes :: () { inline if OS == .ios { UIResponder := objc_getClass("UIResponder".ptr); SxAppDelegate := objc_allocateClassPair(UIResponder, "SxAppDelegate".ptr, 0); class_addMethod(SxAppDelegate, sel_registerName("application:didFinishLaunchingWithOptions:".ptr), xx uikit_did_finish_launching, "c@:@@".ptr); class_addMethod(SxAppDelegate, sel_registerName("window".ptr), xx uikit_window_getter, "@@:".ptr); class_addMethod(SxAppDelegate, sel_registerName("setWindow:".ptr), xx uikit_window_setter, "v@:@".ptr); class_addMethod(SxAppDelegate, sel_registerName("sxKeyboardWillChangeFrame:".ptr), xx uikit_keyboard_will_change_frame, "v@:@".ptr); objc_registerClassPair(SxAppDelegate); uikit_register_gl_view_class(); } } // NSNotification callback. The notification's userInfo dict has the // keyboard's end-frame and animation curve/duration. // UIKeyboardFrameEndUserInfoKey → NSValue wrapping CGRect (screen coords) // UIKeyboardAnimationDurationUserInfoKey → NSNumber wrapping double // Keyboard height = how much of the screen the keyboard covers from the bottom. // If the keyboard's end Y >= screen.height, the keyboard is offscreen (hiding). uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *void) callconv(.c) { if g_uikit_plat == null { return; } plat := g_uikit_plat; sel_user_info := sel_registerName("userInfo".ptr); sel_obj_for_key := sel_registerName("objectForKey:".ptr); sel_cg_rect_value := sel_registerName("CGRectValue".ptr); sel_double_value := sel_registerName("doubleValue".ptr); sel_screen := sel_registerName("screen".ptr); sel_bounds := sel_registerName("bounds".ptr); msg_o : (*void, *void) -> *void = xx objc_msgSend; msg_oo : (*void, *void, *void) -> *void = xx objc_msgSend; msg_rect : (*void, *void) -> CGRect = xx objc_msgSend; msg_d : (*void, *void) -> f64 = xx objc_msgSend; user_info := msg_o(notification, sel_user_info); if user_info == null { return; } end_value := msg_oo(user_info, sel_obj_for_key, ns_string("UIKeyboardFrameEndUserInfoKey".ptr)); if end_value == null { return; } end_rect := msg_rect(end_value, sel_cg_rect_value); // UIKeyboardAnimationDurationUserInfoKey is also in userInfo; reading it // and running our inset update inside a `[UIView animateWithDuration:...]` // block would put us in the same CoreAnimation transaction as the keyboard // (zero-lag sync). Blocks aren't yet expressible from sx, so we update the // inset synchronously — content snaps while the keyboard slides. // Screen height in points. The window lives on the connected scene's screen. if plat.window == null { return; } win_screen := msg_o(plat.window, sel_screen); screen_bounds := msg_rect(win_screen, sel_bounds); // Keyboard height = how much of the screen the keyboard covers from the // bottom. When the keyboard is hiding, its end-frame.y == screen.height, // so the height comes out 0 and visible becomes false. kb_top : f64 = end_rect.y; sh : f64 = screen_bounds.height; h := sh - kb_top; if h < 0.0 { h = 0.0; } if h > sh { h = sh; } // SNAP to target. See comment on UIKitPlatform.keyboard_height for why // lockstep interpolation is deferred until the Metal renderer. plat.keyboard_height = xx h; plat.keyboard_visible = h > 0.5; } uikit_create_gl_context :: (plat: *UIKitPlatform) { inline if OS != .ios { return; } EAGLContext := objc_getClass("EAGLContext".ptr); UIScreen := objc_getClass("UIScreen".ptr); sel_alloc := sel_registerName("alloc".ptr); sel_init_with_api := sel_registerName("initWithAPI:".ptr); sel_set_current_ctx := sel_registerName("setCurrentContext:".ptr); sel_main_screen := sel_registerName("mainScreen".ptr); sel_native_scale := sel_registerName("nativeScale".ptr); msg_o : (*void, *void) -> *void = xx objc_msgSend; msg_oo : (*void, *void, *void) -> void = xx objc_msgSend; msg_oi32 : (*void, *void, s32) -> *void = xx objc_msgSend; msg_d : (*void, *void) -> f64 = xx objc_msgSend; // Read the screen scale up-front so callers can size font caches and // textures with the right DPI before the window even exists. screen := msg_o(UIScreen, sel_main_screen); scale_d : f64 = msg_d(screen, sel_native_scale); plat.dpi_scale = xx scale_d; ctx_raw := msg_o(EAGLContext, sel_alloc); plat.gl_ctx = msg_oi32(ctx_raw, sel_init_with_api, EAGL_API_GLES3); msg_oo(EAGLContext, sel_set_current_ctx, plat.gl_ctx); load_gl(@ios_gl_proc); } uikit_window_getter :: (self: *void, _cmd: *void) -> *void callconv(.c) { if g_uikit_plat == null { return xx 0; } g_uikit_plat.window; } uikit_window_setter :: (self: *void, _cmd: *void, w: *void) callconv(.c) { if g_uikit_plat == null { return; } g_uikit_plat.window = w; } uikit_did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) { result : u8 = 1; inline if OS == .ios { result = uikit_did_finish_launching_ios(self, app); } result; } uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 { if g_uikit_plat == null { NSLog(ns_string("[sx] no platform\n".ptr)); return 0; } plat := g_uikit_plat; UIWindow := objc_getClass("UIWindow".ptr); UIViewController := objc_getClass("UIViewController".ptr); SxGLView := objc_getClass("SxGLView".ptr); EAGLContext := objc_getClass("EAGLContext".ptr); CADisplayLink := objc_getClass("CADisplayLink".ptr); NSRunLoop := objc_getClass("NSRunLoop".ptr); sel_alloc := sel_registerName("alloc".ptr); sel_init := sel_registerName("init".ptr); sel_init_with_scene := sel_registerName("initWithWindowScene:".ptr); sel_init_with_frame := sel_registerName("initWithFrame:".ptr); sel_view := sel_registerName("view".ptr); sel_set_root_vc := sel_registerName("setRootViewController:".ptr); sel_make_key_visible := sel_registerName("makeKeyAndVisible".ptr); sel_connected_scenes := sel_registerName("connectedScenes".ptr); sel_any_object := sel_registerName("anyObject".ptr); sel_add_subview := sel_registerName("addSubview:".ptr); sel_set_frame := sel_registerName("setFrame:".ptr); sel_bounds := sel_registerName("bounds".ptr); sel_set_autoresizing := sel_registerName("setAutoresizingMask:".ptr); sel_init_with_api := sel_registerName("initWithAPI:".ptr); sel_set_current_ctx := sel_registerName("setCurrentContext:".ptr); sel_layer := sel_registerName("layer".ptr); sel_set_content_scale := sel_registerName("setContentScaleFactor:".ptr); sel_screen := sel_registerName("screen".ptr); sel_native_scale := sel_registerName("nativeScale".ptr); sel_link_with_target := sel_registerName("displayLinkWithTarget:selector:".ptr); sel_add_to_runloop := sel_registerName("addToRunLoop:forMode:".ptr); sel_current_runloop := sel_registerName("currentRunLoop".ptr); sel_tick := sel_registerName("sxTick:".ptr); sel_safe_insets := sel_registerName("safeAreaInsets".ptr); msg_o : (*void, *void) -> *void = xx objc_msgSend; msg_v : (*void, *void) -> void = xx objc_msgSend; msg_oo : (*void, *void, *void) -> void = xx objc_msgSend; msg_ooo : (*void, *void, *void) -> *void = xx objc_msgSend; msg_oso : (*void, *void, *void, *void) -> *void = xx objc_msgSend; msg_oi32 : (*void, *void, s32) -> *void = xx objc_msgSend; msg_oou64 : (*void, *void, u64) -> void = xx objc_msgSend; // CGFloat-returning msgSend. CGFloat is `double` on 64-bit Apple — reading // it as f32 reads the low 32 bits of `d0` which isn't a valid float // representation of the underlying double, so the value comes back as 0. msg_d : (*void, *void) -> f64 = xx objc_msgSend; msg_odbl : (*void, *void, f64) -> void = xx objc_msgSend; scenes := msg_o(app, sel_connected_scenes); scene := msg_o(scenes, sel_any_object); if scene == xx 0 { NSLog(ns_string("[sx] no scene\n".ptr)); return 0; } win_raw := msg_o(UIWindow, sel_alloc); plat.window = msg_ooo(win_raw, sel_init_with_scene, scene); vc_raw := msg_o(UIViewController, sel_alloc); plat.root_vc = msg_o(vc_raw, sel_init); // Allocate SxGLView and install it as the VC's view, so the standard // ViewController layout pipeline sizes the GL view to the window. Setting // it BEFORE setRootViewController avoids the VC lazy-loading a default // view first. glv_raw := msg_o(SxGLView, sel_alloc); plat.gl_view = msg_o(glv_raw, sel_init); sel_set_view := sel_registerName("setView:".ptr); msg_oo(plat.root_vc, sel_set_view, plat.gl_view); msg_oo(plat.window, sel_set_root_vc, plat.root_vc); plat.gl_layer = msg_o(plat.gl_view, sel_layer); // Mark the layer opaque (no compositor blend) + set the drawable properties // required by EAGLContext.renderbufferStorage:fromDrawable: (color format, // non-retained backing). Without this dict the renderbuffer allocation // silently fails and the framebuffer reports INCOMPLETE. sel_set_opaque := sel_registerName("setOpaque:".ptr); msg_obool : (*void, *void, u8) -> void = xx objc_msgSend; msg_obool(plat.gl_layer, sel_set_opaque, 1); NSMutableDictionary := objc_getClass("NSMutableDictionary".ptr); NSNumber := objc_getClass("NSNumber".ptr); sel_dictionary := sel_registerName("dictionary".ptr); sel_set_obj_for_key := sel_registerName("setObject:forKey:".ptr); sel_number_bool := sel_registerName("numberWithBool:".ptr); sel_set_drawable := sel_registerName("setDrawableProperties:".ptr); msg_oio : (*void, *void, u8) -> *void = xx objc_msgSend; ns_no := msg_oio(NSNumber, sel_number_bool, 0); // The EAGL dict keys/values must be the framework-provided NSString // constants (pointer identity is checked) — dlsym them from OpenGLES. retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr); colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr); rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr); dict := msg_o(NSMutableDictionary, sel_dictionary); msg_o3 : (*void, *void, *void, *void) -> void = xx objc_msgSend; msg_o3(dict, sel_set_obj_for_key, ns_no, retained_key); msg_o3(dict, sel_set_obj_for_key, rgba8_value, colorformat_key); msg_oo(plat.gl_layer, sel_set_drawable, dict); // EAGLContext + load_gl were already done in uikit_create_gl_context() // back when the game's main called plat.init() — so shaders/textures // built before the window exists already work. // Match the layer's drawable scale to the screen's native scale so we get // pixel-accurate rendering on retina displays. CGFloat is `double` on // 64-bit Apple platforms; reading as f32 would clobber the value. screen := msg_o(plat.window, sel_screen); scale := msg_d(screen, sel_native_scale); plat.dpi_scale = xx scale; msg_odbl(plat.gl_view, sel_set_content_scale, scale); // Renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once // the layer has its real on-screen bounds. makeKeyAndVisible triggers // a layout pass; layoutSubviews calls uikit_setup_renderbuffer. msg_v(plat.window, sel_make_key_visible); // Hidden UITextField as the firstResponder source for show_keyboard / // hide_keyboard. Lives as a subview of the GL view so it's in the // responder chain but is sized 0×0 so it can't be tapped. UITextField := objc_getClass("UITextField".ptr); sel_add_subview := sel_registerName("addSubview:".ptr); tf_raw := msg_o(UITextField, sel_alloc); plat.text_field = msg_o(tf_raw, sel_init); msg_oo(plat.gl_view, sel_add_subview, plat.text_field); // Subscribe SxAppDelegate to UIKeyboardWillChangeFrameNotification. We // use the AppDelegate as the observer since it already exists; the IMP // updates g_uikit_plat directly so we don't need ivar storage. NSNotificationCenter := objc_getClass("NSNotificationCenter".ptr); sel_default_center := sel_registerName("defaultCenter".ptr); sel_add_observer := sel_registerName("addObserver:selector:name:object:".ptr); msg_o5 : (*void, *void, *void, *void, *void, *void) -> void = xx objc_msgSend; center := msg_o(NSNotificationCenter, sel_default_center); msg_o5(center, sel_add_observer, delegate, sel_registerName("sxKeyboardWillChangeFrame:".ptr), ns_string("UIKeyboardWillChangeFrameNotification".ptr), xx 0); // CADisplayLink: vsync-driven tick into our SxGLView. plat.display_link = msg_oso(CADisplayLink, sel_link_with_target, plat.gl_view, sel_tick); runloop := msg_o(NSRunLoop, sel_current_runloop); mode := ns_string("kCFRunLoopDefaultMode".ptr); msg_oso(plat.display_link, sel_add_to_runloop, runloop, mode); NSLog(ns_string("[sx] UIKitPlatform booted\n".ptr)); 1; } // Allocate the color renderbuffer + framebuffer and bind them. The renderbuffer // gets its pixel storage from the CAEAGLLayer via // `[ctx renderbufferStorage:GL_RENDERBUFFER fromDrawable:layer]`, so the layer // must already be in the view hierarchy at the right size when this runs. uikit_setup_renderbuffer :: (plat: *UIKitPlatform) { inline if OS != .ios { return; } glGenFramebuffers(1, @plat.framebuffer); glGenRenderbuffers(1, @plat.color_renderbuffer); glBindRenderbuffer(GL_RENDERBUFFER, plat.color_renderbuffer); sel_renderbuffer_storage := sel_registerName("renderbufferStorage:fromDrawable:".ptr); msg_o_u32_o : (*void, *void, u32, *void) -> u8 = xx objc_msgSend; msg_o_u32_o(plat.gl_ctx, sel_renderbuffer_storage, GL_RENDERBUFFER, plat.gl_layer); glBindFramebuffer(GL_FRAMEBUFFER, plat.framebuffer); glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, plat.color_renderbuffer); // Query the actual pixel dimensions from the renderbuffer. pw : s32 = 0; ph : s32 = 0; GL_RENDERBUFFER_WIDTH :u32: 0x8D42; GL_RENDERBUFFER_HEIGHT :u32: 0x8D43; glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, @pw); glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, @ph); plat.pixel_w = pw; plat.pixel_h = ph; plat.viewport_w = xx pw; plat.viewport_h = xx ph; if plat.dpi_scale > 0.0 { plat.viewport_w = plat.viewport_w / plat.dpi_scale; plat.viewport_h = plat.viewport_h / plat.dpi_scale; } glViewport(0, 0, pw, ph); status := glCheckFramebufferStatus(GL_FRAMEBUFFER); if status != GL_FRAMEBUFFER_COMPLETE { NSLog(ns_string("[sx] framebuffer incomplete after renderbuffer setup\n".ptr)); } } uikit_present_renderbuffer :: (self: *UIKitPlatform) { inline if OS != .ios { return; } glBindRenderbuffer(GL_RENDERBUFFER, self.color_renderbuffer); sel_present := sel_registerName("presentRenderbuffer:".ptr); msg_ou : (*void, *void, u32) -> u8 = xx objc_msgSend; msg_ou(self.gl_ctx, sel_present, GL_RENDERBUFFER); } // ── SxGLView class ───────────────────────────────────────────────────────── // UIView subclass overriding `+layerClass` to return [CAEAGLLayer class]. // Instance method `sxTick:` is what CADisplayLink calls. uikit_gl_view_layer_class :: (cls: *void, _cmd: *void) -> *void callconv(.c) { objc_getClass("CAEAGLLayer".ptr); } uikit_gl_view_tick :: (self: *void, _cmd: *void, link: *void) callconv(.c) { if g_uikit_plat == null { return; } plat := g_uikit_plat; if !plat.has_frame_closure { return; } if !plat.gl_initialized { return; } sel_dur := sel_registerName("duration".ptr); msg_d : (*void, *void) -> f64 = xx objc_msgSend; dur_d : f64 = msg_d(link, sel_dur); plat.delta_time = xx dur_d; fn := plat.frame_closure; fn(); } uikit_gl_view_layout :: (self: *void, _cmd: *void) callconv(.c) { // Call super first via objc_msgSendSuper would be cleaner, but UIView's // default layoutSubviews is a no-op anyway. if g_uikit_plat == null { return; } plat := g_uikit_plat; if plat.gl_initialized { return; } uikit_setup_renderbuffer(plat); plat.gl_initialized = true; } // Touch IMPs — UIKit fires touchesBegan/Moved/Ended/Cancelled with an // NSSet + UIEvent. We take the first touch (single-touch model // matching the chess game's drag-and-tap UX) and push the resulting // Event into the platform's queue for the next poll_events drain. uikit_touch_location :: (touch: *void, view: *void) -> Point { sel_location := sel_registerName("locationInView:".ptr); msg_pt : (*void, *void, *void) -> CGPoint = xx objc_msgSend; p := msg_pt(touch, sel_location, view); Point.{ x = xx p.x, y = xx p.y }; } uikit_first_touch :: (touches: *void) -> *void { sel_any := sel_registerName("anyObject".ptr); msg_o : (*void, *void) -> *void = xx objc_msgSend; msg_o(touches, sel_any); } uikit_gl_view_touches_began :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) { if g_uikit_plat == null { return; } touch := uikit_first_touch(touches); if touch == null { return; } pos := uikit_touch_location(touch, self); g_uikit_plat.events.append(.mouse_down(.{ position = pos, button = .left })); } uikit_gl_view_touches_moved :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) { if g_uikit_plat == null { return; } touch := uikit_first_touch(touches); if touch == null { return; } pos := uikit_touch_location(touch, self); g_uikit_plat.events.append(.mouse_moved(.{ position = pos, delta = Point.zero() })); } uikit_gl_view_touches_ended :: (self: *void, _cmd: *void, touches: *void, event: *void) callconv(.c) { if g_uikit_plat == null { return; } touch := uikit_first_touch(touches); if touch == null { return; } pos := uikit_touch_location(touch, self); g_uikit_plat.events.append(.mouse_up(.{ position = pos, button = .left })); } uikit_register_gl_view_class :: () { inline if OS == .ios { UIView := objc_getClass("UIView".ptr); SxGLView := objc_allocateClassPair(UIView, "SxGLView".ptr, 0); // +layerClass is a CLASS method — registered on the metaclass. metaclass := object_getClass(SxGLView); class_addMethod(metaclass, sel_registerName("layerClass".ptr), xx uikit_gl_view_layer_class, "#@:".ptr); // -sxTick: is the CADisplayLink callback. -layoutSubviews allocates // the renderbuffer when the layer first gets non-zero bounds. class_addMethod(SxGLView, sel_registerName("sxTick:".ptr), xx uikit_gl_view_tick, "v@:@".ptr); class_addMethod(SxGLView, sel_registerName("layoutSubviews".ptr), xx uikit_gl_view_layout, "v@:".ptr); // Touch dispatch. class_addMethod(SxGLView, sel_registerName("touchesBegan:withEvent:".ptr), xx uikit_gl_view_touches_began, "v@:@@".ptr); class_addMethod(SxGLView, sel_registerName("touchesMoved:withEvent:".ptr), xx uikit_gl_view_touches_moved, "v@:@@".ptr); class_addMethod(SxGLView, sel_registerName("touchesEnded:withEvent:".ptr), xx uikit_gl_view_touches_ended, "v@:@@".ptr); class_addMethod(SxGLView, sel_registerName("touchesCancelled:withEvent:".ptr), xx uikit_gl_view_touches_ended, "v@:@@".ptr); objc_registerClassPair(SxGLView); } }