From 9fbc24a6024e4eacac9049ff4f93b87b30c47b32 Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 26 May 2026 16:42:57 +0300 Subject: [PATCH] =?UTF-8?q?ffi=20uikit=20cleanup:=20helpers=20=E2=86=92=20?= =?UTF-8?q?UIKitPlatform=20methods=20+=20declarative=20layerClass?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three threads, one commit because they're entangled: 1. Helper free functions on `*UIKitPlatform` (refresh_safe_insets, read_screen_scale, create_gl_context, setup_renderbuffer, present_renderbuffer, compute_layer_pixel_size) → methods on the `impl Platform for UIKitPlatform` block. IMP-shape trampolines (`uikit_keyboard_will_change_frame`, `uikit_scene_will_connect[_ios]`, `uikit_gl_view_tick/layout/touches_*`, `uikit_subscribe_keyboard_notifications`) also collapse into methods on UIKitPlatform — the `(self: *void, _cmd: *void, ...)` form is no longer needed since M3 made the #objc_class trampolines compiler-synthesized. Class method bodies in SxAppDelegate / SxSceneDelegate / SxGLView / SxMetalView now read `if g_uikit_plat == null { return; } g_uikit_plat.x();` — no more `xx self, xx 0` casts at every IMP call site. 2. Declarative `layerClass` form. SxGLView and SxMetalView promote from the M2.1(a) constant-with-runtime-string-lookup workaround (`layerClass :: *void = objc_getClass("CAEAGLLayer".ptr);`) to the class-method expression-body form (`layerClass :: () => CAEAGLLayer.class();`). Type stays `*void` until M1.1.b lands `Class(T)` parameterisation; the value side already matches the plan. Backing this: foreign-class declarations for CAEAGLLayer (extended with `class :: () -> *void;`) and a new CAMetalLayer foreign-class declaration alongside it. Both `#extends CALayer` so the dispatch chain knows about the parent. 3. Optional-shape idiom pass on uikit.sx. `xx`-as-optional-wrap on field assignments (`plat.gl_ctx = xx ctx`, `plat.text_field = xx tf`, `plat.display_link = xx link`) dropped — implicit `T → ?T` does the right thing. `!` force-unwraps replaced with `if val := opt { ... }` safe-narrowing (the keyboard handler, the GL-context read in setup/present renderbuffer, the gl_view read in scene bootstrap). `orelse` (Zig keyword) that briefly snuck into the keyboard handler removed in favour of the `if win := plat.window` narrowing pattern. Result: no `xx` casts left on the implicit T→?T path; all optional access goes through `if val :=`. IR snapshots `ffi-objc-call-06-sret-return.ir` and `ffi-objc-dsl-07-mangling-table.ir` refresh to pick up the new `object_getIvar` / `object_setIvar` runtime-helper declarations introduced when M1.2 A.3 made instance-method bodies route field access through the state ivar. Chess on iOS-sim green throughout. 184/184 example tests pass. --- current/CHECKPOINT-FFI.md | 105 ++- library/modules/platform/uikit.sx | 858 +++++++++--------- .../expected/ffi-objc-call-06-sret-return.ir | 6 + .../ffi-objc-dsl-07-mangling-table.ir | 6 + 4 files changed, 507 insertions(+), 468 deletions(-) diff --git a/current/CHECKPOINT-FFI.md b/current/CHECKPOINT-FFI.md index e579f07..b644514 100644 --- a/current/CHECKPOINT-FFI.md +++ b/current/CHECKPOINT-FFI.md @@ -6,6 +6,51 @@ add a test and make it pass — that's two commits). ## Last completed step +**M1.2 A.0 — `objc_defined_class_cache` + scan-pass registration** +(`61a2593`). + +Added an insertion-ordered cache on `Module` for sx-defined Obj-C +classes (every `#objc_class("Cls") { ... }` declaration WITHOUT +`#foreign`). `registerForeignClassDecl` appends the entry alongside +its existing `foreign_class_map` insert. + +```zig +pub const ObjcDefinedClassEntry = struct { + name: []const u8, + decl: *const ast.ForeignClassDecl, +}; +``` + +Pointer back to the AST lets later A.* passes re-walk `members` +without duplicating data. Insertion order matters because +class-pair init constructors (A.4) must register parents before +children — `objc_allocateClassPair(super, ...)` resolves super by +lookup. Infrastructure only; populated but not yet read. + +170 example tests + `zig build test` green. + +--- + +**M1.1 first pass — id / Class / SEL / BOOL aliases** (`d9dbdad`). + +Added stand-ins for the opaque Obj-C runtime types to +`library/modules/std/objc.sx`: `id`, `Class`, `SEL` resolve to +`*void`; `BOOL` to `s8`. All zero-cost at the LLVM layer; the +header's old caveat about lacking aliases is gone. +`141-objc-type-aliases.sx` exercises them against the real macOS +Obj-C runtime via `isKindOfClass`. + +**Deferred to M1.1.b**: `Class(T)` parameterization with +`#extends`-aware covariance + `instancetype` per-decl +substitution. Both need compiler-level type-check work beyond +stdlib aliases. The current sx type system doesn't enforce +nominal identity on parametric struct instantiations (verified +probe: `Class(NSString)` flows into `Class(CALayer)` parameter +without error), so a stdlib-only Class(T) would give syntax with +no safety. Punted to a focused later slice. + +--- + **M1.0 — Expression-bodied function declarations** (3 commits: `6c95b2a`, `4a048d3`, `86c1127`). @@ -177,29 +222,55 @@ plus 2 codegen fixes surfaced along the way.** ## Current state - 169/169 example tests pass; `zig build test` green. -- Phase 3.0/3.1/3.2 (Obj-C DSL dispatch + selector mangling + - selector override + uikit.sx C1-C5 cluster migrations) all - landed. M1.0 (expression-bodied functions) just landed. +- Phase 3.0/3.1/3.2 + M1.0–M1.3 + M2.1–M2.3 + M3 (full + retire-`uikit_register_classes`) all landed. - Chess on macOS / iOS-sim / Android all build and run. +- `library/modules/platform/uikit.sx` follow-up cleanup just + shipped: every `plat: *UIKitPlatform` helper and every + `(self: *void, _cmd: *void, ...)` trampoline is now a method + on `UIKitPlatform`. Method bodies in SxAppDelegate / + SxSceneDelegate / SxGLView / SxMetalView call `g_uikit_plat.x()` + for the shared paths and inline the trivial bridges (no more + `xx self, xx 0` casts at IMP-call sites). `layerClass` uses the + declarative `() => CAEAGLLayer.class()` / `CAMetalLayer.class()` + form on top of new foreign-class declarations for both layer + types. +- **issue-0044 FIXED.** The root cause was a `target_type` leak in + `resolveCallParamTypes` for UFCS calls on foreign-class + (`#objc_class` / `#foreign #objc_class`) receivers. With no + param-types resolved for the receiver's method, `self.target_type` + retained the enclosing fn's return type — and a `BOOL`-returning + method's `xx ptr` inside an Obj-C call site silently truncated the + pointer to i8. Fix at + [src/ir/lower.zig:8617-8639](../src/ir/lower.zig#L8617) walks + `foreign_class_map` + `findForeignMethodInChain` for the method's + declared param types. Regression test + [examples/issue-0044.sx](../examples/issue-0044.sx). + 184/184 example tests green; chess on iOS-sim green. - Active forward plan: 6-month Obj-C FFI roadmap at `~/.claude/plans/lets-see-options-for-merry-dijkstra.md`. -## Next step (M1.1 — Foreign type aliases) +## Next step (M1.2 A.1 — type-encoding derivation table) -Introduce sx-side typed aliases for the Obj-C primitives that -today are `*void`: `Class(T)` (phantom-parameterized), `id`, `SEL`, -`BOOL`, `instancetype`. Per the roadmap, the load-bearing piece -is `Class(T)`: a phantom-typed pointer alias enabling type-safe -factory returns like -`layerClass :: Class(CALayer) = CAEAGLLayer.class();` (the M2.1(a) -class-constant form). Plain `Class` ⇒ `Class(NSObject)` sugar. -`Cls.class()` returns `Class(Cls)`. Covariant on `#extends`. +The synthesized `+alloc` (A.5), `-dealloc` (A.6), and every +instance-method IMP (A.2) need to call `class_addMethod(cls, sel, +imp, types)` with a type-encoding string in Apple's runtime DSL: -Files to touch: [src/parser.zig](../src/parser.zig) (parser -support for `Class(T)` type syntax), -[src/ir/lower.zig](../src/ir/lower.zig) (alias resolution + -covariance check). LLVM layer unchanged — these are phantom-typed, -underlying representation stays `*void`. +- `v` = void, `i` = s32, `q` = s64, `f` = f32, `d` = f64, `B` = bool, +- `c` = s8/BOOL, `C` = u8, `s` = s16, `S` = u16, `l/L` = long, + `Q` = u64, `*` = `[*]u8`, +- `@` = id (object), `#` = Class, `:` = SEL, `^v` = `*void`. +- Struct: `{Name=field1field2...}`. + +A.1 = `objcTypeEncodingFromSignature` helper in +[src/ir/lower.zig](../src/ir/lower.zig). Inputs: receiver-as-`@`, +`_cmd` selector slot `:`, then return type + arg types from the +IR signature. Lookup table over `TypeId`. No emission yet — A.1 +is a pure helper that A.2-A.6 will call. + +Bounded slice: probably 100-200 lines of Zig, one-pass switch +over TypeId. No cadence-rule test needed (helper has no observable +output on its own; tested via integration with A.2+). ## Phase 1B complete (1.6–1.14) diff --git a/library/modules/platform/uikit.sx b/library/modules/platform/uikit.sx index c712a1f..58a2e1f 100644 --- a/library/modules/platform/uikit.sx +++ b/library/modules/platform/uikit.sx @@ -121,9 +121,16 @@ CALayer :: #foreign #objc_class("CALayer") { } CAEAGLLayer :: #foreign #objc_class("CAEAGLLayer") { + #extends CALayer; + class :: () -> *void; setDrawableProperties :: (self: *Self, props: *void); } +CAMetalLayer :: #foreign #objc_class("CAMetalLayer") { + #extends CALayer; + class :: () -> *void; +} + EAGLContext :: #foreign #objc_class("EAGLContext") { alloc :: () -> *EAGLContext; initWithAPI :: (self: *Self, api: s32) -> *EAGLContext; @@ -177,44 +184,46 @@ UITextField :: #foreign #objc_class("UITextField") { init :: (self: *Self) -> *UITextField; } -// SxAppDelegate — UIApplicationMain's principal class. Replaces the -// M3.1 hand-rolled objc_allocateClassPair + class_addMethod sequence -// in uikit_register_classes. The method bodies forward to the -// existing legacy IMP free functions so we don't have to inline 70+ -// lines of keyboard-frame logic here. +// SxAppDelegate — UIApplicationMain's principal class. Method bodies +// dispatch to UIKitPlatform methods on the shared `g_uikit_plat`. SxAppDelegate :: #objc_class("SxAppDelegate") { #extends UIResponder; application_didFinishLaunchingWithOptions :: (self: *Self, app: *void, opts: *void) -> BOOL { inline if OS == .ios { if g_uikit_plat != null { - uikit_subscribe_keyboard_notifications(xx self); + center := NSNotificationCenter.defaultCenter(); + center.addObserver_selector_name_object( + xx self, + sel_registerName("sxKeyboardWillChangeFrame:".ptr), + ns_string("UIKeyboardWillChangeFrameNotification".ptr), + null); } } return 1; } sxKeyboardWillChangeFrame :: (self: *Self, notification: *void) { - uikit_keyboard_will_change_frame(xx self, xx 0, notification); + if g_uikit_plat == null { return; } + notif : *NSNotification = xx notification; + g_uikit_plat.keyboard_will_change_frame(notif); } alloc :: () -> *SxAppDelegate; init :: (self: *SxAppDelegate) -> *SxAppDelegate; } -// SxSceneDelegate — iOS 13+ scene-based lifecycle delegate. -// UIApplicationSceneManifest names this in Info.plist; iOS -// instantiates it via scene-session connection. Replaces the M3.2 -// hand-rolled registration. Two `#implements` declarations -// formally conform to the scene-delegate protocols — iOS rejects -// the class otherwise. +// SxSceneDelegate — iOS 13+ scene-based lifecycle delegate. Two +// `#implements` declarations formally conform to the scene-delegate +// protocols — iOS rejects the class otherwise. SxSceneDelegate :: #objc_class("SxSceneDelegate") { #extends UIResponder; #implements UISceneDelegate; #implements UIWindowSceneDelegate; scene_willConnectToSession_options :: (self: *Self, scene: *void, session: *void, options: *void) { - uikit_scene_will_connect(xx self, xx 0, scene, session, options); + if g_uikit_plat == null { return; } + g_uikit_plat.scene_will_connect(xx self, scene); } window :: (self: *Self) -> ?*UIWindow { @@ -262,7 +271,7 @@ UIKitPlatform :: struct { gpu_mode: GpuMode = .gles; // Hidden UITextField; firstResponder ⇆ keyboard visibility. - text_field: *void = null; + text_field: ?*UITextField = null; viewport_w: f32 = 0.0; viewport_h: f32 = 0.0; @@ -323,15 +332,12 @@ impl Platform for UIKitPlatform { // happen BEFORE any code that loads fonts/textures from disk. inline if OS == .ios { uikit_chdir_to_bundle(); - // M3.5 — uikit_register_classes was deleted. Every - // sx-defined Obj-C class in this module is now declarative; - // the compiler synthesises class-pair init at module init. if self.gpu_mode == .gles { - uikit_create_gl_context(self); + self.create_gl_context(); } else { // Metal mode: skip EAGL. dpi_scale still needs to be known // before the window exists so callers can size resources. - uikit_read_screen_scale(self); + self.read_screen_scale(); } } true; @@ -356,7 +362,7 @@ impl Platform for UIKitPlatform { begin_frame :: (self: *UIKitPlatform) -> FrameContext { inline if OS == .ios { - uikit_refresh_safe_insets(self); + self.refresh_safe_insets(); } FrameContext.{ viewport_w = self.viewport_w, @@ -372,7 +378,7 @@ impl Platform for UIKitPlatform { end_frame :: (self: *UIKitPlatform) { inline if OS == .ios { if self.gpu_mode == .gles { - uikit_present_renderbuffer(self); + self.present_renderbuffer(); } // Metal mode: caller's gpu.end_frame() handles present. } @@ -400,23 +406,365 @@ impl Platform for UIKitPlatform { show_keyboard :: (self: *UIKitPlatform) { inline if OS == .ios { - if self.text_field == null { return; } - tf : *UITextField = xx self.text_field; - tf.becomeFirstResponder(); + if tf := self.text_field { + tf.becomeFirstResponder(); + } } } hide_keyboard :: (self: *UIKitPlatform) { inline if OS == .ios { - if self.text_field == null { return; } - tf : *UITextField = xx self.text_field; - tf.resignFirstResponder(); + if tf := self.text_field { + tf.resignFirstResponder(); + } } } stop :: (self: *UIKitPlatform) { } shutdown :: (self: *UIKitPlatform) { } + + // ── iOS-only internal helpers ────────────────────────────────────────── + // Bodies are guarded with `inline if OS != .ios { return; }` so non-iOS + // builds never reach unresolved UIKit / OpenGLES symbols. + + refresh_safe_insets :: (self: *UIKitPlatform) { + inline if OS != .ios { return; } + if gl_view := self.gl_view { + i := gl_view.safeAreaInsets(); + self.safe_top = xx i.top; + self.safe_left = xx i.left; + self.safe_bottom = xx i.bottom; + self.safe_right = xx i.right; + } + } + + read_screen_scale :: (self: *UIKitPlatform) { + inline if OS != .ios { return; } + screen := UIScreen.mainScreen(); + self.dpi_scale = xx screen.nativeScale(); + } + + create_gl_context :: (self: *UIKitPlatform) { + inline if OS != .ios { return; } + // Read the screen scale up-front so callers can size font caches and + // textures with the right DPI before the window even exists. + screen := UIScreen.mainScreen(); + self.dpi_scale = xx screen.nativeScale(); + + ctx := EAGLContext.alloc().initWithAPI(EAGL_API_GLES3); + self.gl_ctx = ctx; + EAGLContext.setCurrentContext(ctx); + + load_gl(@ios_gl_proc); + } + + // 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. + setup_renderbuffer :: (self: *UIKitPlatform) { + inline if OS != .ios { return; } + + glGenFramebuffers(1, @self.framebuffer); + glGenRenderbuffers(1, @self.color_renderbuffer); + + glBindRenderbuffer(GL_RENDERBUFFER, self.color_renderbuffer); + + if gl_ctx := self.gl_ctx { + gl_ctx.renderbufferStorage_fromDrawable(GL_RENDERBUFFER, self.gl_layer); + } + + glBindFramebuffer(GL_FRAMEBUFFER, self.framebuffer); + glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, self.color_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); + self.pixel_w = pw; + self.pixel_h = ph; + self.viewport_w = xx pw; + self.viewport_h = xx ph; + if self.dpi_scale > 0.0 { + self.viewport_w = self.viewport_w / self.dpi_scale; + self.viewport_h = self.viewport_h / self.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)); + } + } + + present_renderbuffer :: (self: *UIKitPlatform) { + inline if OS != .ios { return; } + glBindRenderbuffer(GL_RENDERBUFFER, self.color_renderbuffer); + if gl_ctx := self.gl_ctx { + gl_ctx.presentRenderbuffer(GL_RENDERBUFFER); + } + } + + // Metal mode equivalent of setup_renderbuffer's "tell me how big the + // drawable is in pixels". Reads the layer's bounds in points and scales + // to pixels via dpi_scale. + compute_layer_pixel_size :: (self: *UIKitPlatform) { + inline if OS != .ios { return; } + if gl_view := self.gl_view { + b := #objc_call(CGRect)(gl_view, "bounds"); + + w_pts : f64 = b.width; + h_pts : f64 = b.height; + self.viewport_w = xx w_pts; + self.viewport_h = xx h_pts; + + scale64 : f64 = xx self.dpi_scale; + pw : f64 = w_pts * scale64; + ph : f64 = h_pts * scale64; + self.pixel_w = xx pw; + self.pixel_h = xx ph; + } + } + + // ── Obj-C class-method callbacks ──────────────────────────────────── + // Sx-defined class methods (SxAppDelegate / SxSceneDelegate / SxGLView / + // SxMetalView) forward to these on the shared `g_uikit_plat` so the + // tick/layout/touch/scene/keyboard paths share one implementation. + + // 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). + keyboard_will_change_frame :: (self: *UIKitPlatform, notification: *NSNotification) { + inline if OS != .ios { return; } + + user_info := notification.userInfo(); + if user_info == null { return; } + + end_value_raw := user_info.objectForKey(ns_string("UIKeyboardFrameEndUserInfoKey".ptr)); + if end_value_raw == null { return; } + end_value : *NSValue = xx end_value_raw; + end_rect := end_value.CGRectValue(); + + dur_value_raw := user_info.objectForKey(ns_string("UIKeyboardAnimationDurationUserInfoKey".ptr)); + anim_dur : f64 = 0.0; + if dur_value_raw != null { + dur_value : *NSNumber = xx dur_value_raw; + anim_dur = dur_value.doubleValue(); + } + + curve_value_raw := user_info.objectForKey(ns_string("UIKeyboardAnimationCurveUserInfoKey".ptr)); + curve_int : u64 = 0; + if curve_value_raw != null { + curve_value : *NSNumber = xx curve_value_raw; + curve_int = curve_value.unsignedLongValue(); + } + + if win := self.window { + win_screen := win.screen(); + screen_bounds := win_screen.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; } + target_h : f32 = xx h; + + self.keyboard_visible = h > 0.5; + + if anim_dur <= 0.0 { + self.keyboard_height = target_h; + self.kb_animating = false; + return; + } + + self.kb_anim_from = self.keyboard_height; + self.kb_anim_to = target_h; + self.kb_anim_start = CACurrentMediaTime(); + self.kb_anim_dur = anim_dur; + self.kb_anim_curve = curve_int; + self.kb_animating = true; + } + } + + // First scene-connect: builds the window, root VC, GL/Metal view, layer, + // EAGL props (if gles), display link, and hidden text-field. Called from + // SxSceneDelegate.scene_willConnectToSession_options. + scene_will_connect :: (self: *UIKitPlatform, delegate: *void, scene: *void) { + inline if OS != .ios { return; } + + SxGLView := objc_getClass("SxGLView".ptr); + SxMetalView := objc_getClass("SxMetalView".ptr); + + win := UIWindow.alloc().initWithWindowScene(scene); + self.window = win; + + // Make the scene delegate own the window so iOS retains it. Per the + // scene-based lifecycle, the scene delegate is expected to provide + // the UIWindow via -window/-setWindow:. + #objc_call(void)(delegate, "setWindow:", win); + + vc := UIViewController.alloc().init(); + self.root_vc = vc; + + // Allocate either SxGLView or SxMetalView based on gpu_mode. The + // view's +layerClass override gives us the right CAEAGLLayer / + // CAMetalLayer subclass. Set the VC's view BEFORE + // setRootViewController to avoid a default-view lazy-load. + view_class := if self.gpu_mode == .gles then SxGLView else SxMetalView; + glv_raw := #objc_call(*void)(view_class, "alloc"); + gl_view := #objc_call(*UIView)(glv_raw, "init"); + self.gl_view = gl_view; + vc.setView(gl_view); + + win.setRootViewController(vc); + + gl_layer := gl_view.layer(); + self.gl_layer = gl_layer; + + // Mark the layer opaque (no compositor blend). Required for EAGL + + // recommended for Metal (CAMetalLayer.opaque defaults YES but doesn't + // hurt to be explicit). + gl_layer.setOpaque(1); + + if self.gpu_mode == .gles { + // EAGL drawable properties dict required by + // EAGLContext.renderbufferStorage:fromDrawable: (color format, + // non-retained backing). Without this dict the renderbuffer + // allocation silently fails and the framebuffer reports INCOMPLETE. + ns_no := NSNumber.numberWithBool(0); + + retained_key := uikit_extern_nsstring("kEAGLDrawablePropertyRetainedBacking".ptr); + colorformat_key := uikit_extern_nsstring("kEAGLDrawablePropertyColorFormat".ptr); + rgba8_value := uikit_extern_nsstring("kEAGLColorFormatRGBA8".ptr); + + dict := NSMutableDictionary.dictionary(); + dict.setObject_forKey(xx ns_no, retained_key); + dict.setObject_forKey(rgba8_value, colorformat_key); + eagl_layer : *CAEAGLLayer = xx gl_layer; + eagl_layer.setDrawableProperties(xx dict); + } + + // 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. + screen2 := win.screen(); + scale := screen2.nativeScale(); + self.dpi_scale = xx scale; + gl_view.setContentScaleFactor(scale); + + // Renderbuffer is allocated lazily in -[SxGLView layoutSubviews] once + // the layer has its real on-screen bounds. makeKeyAndVisible triggers + // a layout pass; layoutSubviews calls setup_renderbuffer. + win.makeKeyAndVisible(); + + // 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. + tf := UITextField.alloc().init(); + self.text_field = tf; + gl_view.addSubview(tf); + + // CADisplayLink: vsync-driven tick into our SxGLView. The second arg + // is a SEL value (not a dispatch selector), so it still goes through + // sel_registerName. + sel_tick := sel_registerName("sxTick:".ptr); + link := CADisplayLink.displayLinkWithTarget_selector(gl_view, sel_tick); + self.display_link = link; + runloop := NSRunLoop.currentRunLoop(); + mode_ns := ns_string("kCFRunLoopDefaultMode".ptr); + link.addToRunLoop_forMode(runloop, mode_ns); + + NSLog(ns_string("[sx] UIKitPlatform booted\n".ptr)); + } + + // CADisplayLink callback (vsync-paced). Drives keyboard-inset + // interpolation and calls the user's frame closure. + gl_view_tick :: (self: *UIKitPlatform, link: *CADisplayLink) { + inline if OS != .ios { return; } + + // Keyboard-inset lockstep — sx-side cubic ease-out approximation of + // UIKit's private keyboard curve. Sample targetTimestamp so we + // interpolate at the time this frame will be visible. + if self.kb_animating { + target_ts := link.targetTimestamp(); + elapsed := target_ts - self.kb_anim_start; + // Negative elapsed can happen if a just-fired willChangeFrame set + // kb_anim_start to a wall time AFTER this tick already captured + // its targetTimestamp. Without the clamp, t < 0 makes the cubic + // ease-out overshoot in the opposite direction. + if elapsed < 0.0 { elapsed = 0.0; } + if elapsed >= self.kb_anim_dur or self.kb_anim_dur <= 0.0 { + self.keyboard_height = self.kb_anim_to; + self.kb_animating = false; + } else { + t : f32 = xx (elapsed / self.kb_anim_dur); + inv := 1.0 - t; + eased := 1.0 - inv * inv * inv; + self.keyboard_height = self.kb_anim_from + (self.kb_anim_to - self.kb_anim_from) * eased; + } + } + + if !self.has_frame_closure { return; } + if !self.gl_initialized { return; } + + dur_d := link.duration(); + self.delta_time = xx dur_d; + self.last_target_ts = link.targetTimestamp(); + + fn := self.frame_closure; + fn(); + } + + // UIView -layoutSubviews callback. Sets up the renderbuffer (gles) or + // computes the layer pixel size (metal) on the first layout pass. + gl_view_did_layout :: (self: *UIKitPlatform) { + inline if OS != .ios { return; } + if self.gl_initialized { return; } + if self.gpu_mode == .gles { + self.setup_renderbuffer(); + } else { + self.compute_layer_pixel_size(); + } + self.gl_initialized = true; + } + + // Touch IMPs — UIKit fires touchesBegan/Moved/Ended/Cancelled with an + // NSSet + UIEvent. Single-touch model matching chess's UX. + + push_touch_down :: (self: *UIKitPlatform, view: *void, touches: *void) { + inline if OS != .ios { return; } + touch := uikit_first_touch(touches); + if touch == null { return; } + pos := uikit_touch_location(touch, view); + self.events.append(.mouse_down(.{ position = pos, button = .left })); + } + + push_touch_moved :: (self: *UIKitPlatform, view: *void, touches: *void) { + inline if OS != .ios { return; } + touch := uikit_first_touch(touches); + if touch == null { return; } + pos := uikit_touch_location(touch, view); + self.events.append(.mouse_moved(.{ position = pos, delta = Point.zero() })); + } + + push_touch_up :: (self: *UIKitPlatform, view: *void, touches: *void) { + inline if OS != .ios { return; } + touch := uikit_first_touch(touches); + if touch == null { return; } + pos := uikit_touch_location(touch, view); + self.events.append(.mouse_up(.{ position = pos, button = .left })); + } } // dlsym(RTLD_DEFAULT, name) — Apple platforms. RTLD_DEFAULT is (void*)-2. @@ -442,18 +790,6 @@ uikit_extern_nsstring :: (name: [*]u8) -> *void { // 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; } - - gl_view : *UIView = xx plat.gl_view; - i := gl_view.safeAreaInsets(); - 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; } bundle := NSBundle.mainBundle(); @@ -462,284 +798,6 @@ uikit_chdir_to_bundle :: () { chdir(c_string(rsrc)); } -// uikit_register_classes — deleted (M3.5). Every sx-defined -// Obj-C class in this module (SxAppDelegate, SxSceneDelegate, -// SxGLView, SxMetalView) is now a declarative `#objc_class(...)` -// block — the compiler synthesises their IMPs, class-pair -// registration, ivar wiring, +alloc / -dealloc trampolines, and -// `#implements` protocol conformances at module init. - -// Read [UIScreen mainScreen].nativeScale into plat.dpi_scale. Used by the -// metal-mode init path which doesn't go through uikit_create_gl_context -// (that's where the gles path picks the scale up). -uikit_read_screen_scale :: (plat: *UIKitPlatform) { - inline if OS != .ios { return; } - screen := UIScreen.mainScreen(); - plat.dpi_scale = xx screen.nativeScale(); -} - -// 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; - - notif : *NSNotification = xx notification; - user_info := notif.userInfo(); - if user_info == null { return; } - - end_value_raw := user_info.objectForKey(ns_string("UIKeyboardFrameEndUserInfoKey".ptr)); - if end_value_raw == null { return; } - end_value : *NSValue = xx end_value_raw; - end_rect := end_value.CGRectValue(); - - dur_value_raw := user_info.objectForKey(ns_string("UIKeyboardAnimationDurationUserInfoKey".ptr)); - anim_dur : f64 = 0.0; - if dur_value_raw != null { - dur_value : *NSNumber = xx dur_value_raw; - anim_dur = dur_value.doubleValue(); - } - - curve_value_raw := user_info.objectForKey(ns_string("UIKeyboardAnimationCurveUserInfoKey".ptr)); - curve_int : u64 = 0; - if curve_value_raw != null { - curve_value : *NSNumber = xx curve_value_raw; - curve_int = curve_value.unsignedLongValue(); - } - - // Screen height in points. The window lives on the connected scene's screen. - if plat.window == null { return; } - win : *UIWindow = xx plat.window; - win_screen := win.screen(); - screen_bounds := win_screen.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; } - target_h : f32 = xx h; - - plat.keyboard_visible = h > 0.5; - - if anim_dur <= 0.0 { - // No animation window — snap. - plat.keyboard_height = target_h; - plat.kb_animating = false; - return; - } - - // Capture the animation params for the sx-side per-tick interpolation - // that drives `keyboard_height` (consumers like the chess UI's safe-area - // calc read it each frame). The interpolation uses cubic ease-out as a - // close approximation of UIKit's keyboard curve. For perfect lockstep - // on a UIView consumer in user code, drive a property via - // `[UIView animateWithDuration:plat.kb_anim_dur delay:0 - // options:(plat.kb_anim_curve << 16) | 4 - // animations:^{ ... } - // completion:nil]` - // — UIKit's internal options-to-CAMediaTimingFunction table handles - // even the private keyboard curve 7 correctly when packed this way. - plat.kb_anim_from = plat.keyboard_height; - plat.kb_anim_to = target_h; - plat.kb_anim_start = CACurrentMediaTime(); - plat.kb_anim_dur = anim_dur; - plat.kb_anim_curve = curve_int; - plat.kb_animating = true; -} - -uikit_create_gl_context :: (plat: *UIKitPlatform) { - inline if OS != .ios { return; } - - // Read the screen scale up-front so callers can size font caches and - // textures with the right DPI before the window even exists. - screen := UIScreen.mainScreen(); - plat.dpi_scale = xx screen.nativeScale(); - - ctx := EAGLContext.alloc().initWithAPI(EAGL_API_GLES3); - plat.gl_ctx = xx ctx; - EAGLContext.setCurrentContext(ctx); - - load_gl(@ios_gl_proc); -} - -uikit_subscribe_keyboard_notifications :: (delegate: *void) { - inline if OS != .ios { return; } - center := NSNotificationCenter.defaultCenter(); - center.addObserver_selector_name_object( - delegate, - sel_registerName("sxKeyboardWillChangeFrame:".ptr), - ns_string("UIKeyboardWillChangeFrameNotification".ptr), - xx 0); -} - -uikit_scene_will_connect :: (self: *void, _cmd: *void, scene: *void, session: *void, options: *void) callconv(.c) { - inline if OS == .ios { - uikit_scene_will_connect_ios(self, scene); - } -} - -uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) { - if g_uikit_plat == null { - NSLog(ns_string("[sx] no platform\n".ptr)); - return; - } - plat := g_uikit_plat; - - SxGLView := objc_getClass("SxGLView".ptr); - SxMetalView := objc_getClass("SxMetalView".ptr); - // UIWindow / UIViewController / CADisplayLink / NSRunLoop class - // slots come from the declarative #objc_class bindings. - - win := UIWindow.alloc().initWithWindowScene(scene); - plat.window = xx win; - - // Make the scene delegate own the window so iOS retains it. Per the - // scene-based lifecycle, the scene delegate is expected to provide the - // UIWindow via -window/-setWindow:. - #objc_call(void)(delegate, "setWindow:", plat.window); - - vc := UIViewController.alloc().init(); - plat.root_vc = xx vc; - - // Allocate either SxGLView or SxMetalView based on gpu_mode and install - // it as the VC's view. The view's +layerClass override gives us the - // right CAEAGLLayer / CAMetalLayer subclass. Setting it BEFORE - // setRootViewController avoids the VC lazy-loading a default view. - view_class := if plat.gpu_mode == .gles then SxGLView else SxMetalView; - glv_raw := #objc_call(*void)(view_class, "alloc"); - plat.gl_view = #objc_call(*void)(glv_raw, "init"); - vc.setView(plat.gl_view); - - win.setRootViewController(plat.root_vc); - - gl_view : *UIView = xx plat.gl_view; - gl_layer := gl_view.layer(); - plat.gl_layer = xx gl_layer; - - // Mark the layer opaque (no compositor blend). Required for EAGL + - // recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't - // hurt to be explicit). - gl_layer.setOpaque(1); - - if plat.gpu_mode == .gles { - // EAGL drawable properties dict required by - // EAGLContext.renderbufferStorage:fromDrawable: (color format, - // non-retained backing). Without this dict the renderbuffer - // allocation silently fails and the framebuffer reports INCOMPLETE. - ns_no := NSNumber.numberWithBool(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 := NSMutableDictionary.dictionary(); - dict.setObject_forKey(xx ns_no, retained_key); - dict.setObject_forKey(rgba8_value, colorformat_key); - eagl_layer : *CAEAGLLayer = xx gl_layer; - eagl_layer.setDrawableProperties(xx 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. - screen2 := win.screen(); - scale := screen2.nativeScale(); - plat.dpi_scale = xx scale; - gl_view.setContentScaleFactor(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. - win.makeKeyAndVisible(); - - // 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. - tf := UITextField.alloc().init(); - plat.text_field = xx tf; - gl_view.addSubview(plat.text_field); - - - // (Keyboard observer is registered in didFinishLaunching via - // uikit_subscribe_keyboard_notifications — it's app-level, not scene- - // level, so it doesn't belong here.) - - // CADisplayLink: vsync-driven tick into our SxGLView. The second arg is - // a SEL value (not a dispatch selector), so it still goes through - // sel_registerName. - sel_tick := sel_registerName("sxTick:".ptr); - link := CADisplayLink.displayLinkWithTarget_selector(plat.gl_view, sel_tick); - plat.display_link = xx link; - runloop := NSRunLoop.currentRunLoop(); - mode_ns := ns_string("kCFRunLoopDefaultMode".ptr); - link.addToRunLoop_forMode(runloop, mode_ns); - - NSLog(ns_string("[sx] UIKitPlatform booted\n".ptr)); -} - -// 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); - - gl_ctx : *EAGLContext = xx plat.gl_ctx; - gl_ctx.renderbufferStorage_fromDrawable(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); - gl_ctx : *EAGLContext = xx self.gl_ctx; - gl_ctx.presentRenderbuffer(GL_RENDERBUFFER); -} - // ── SxGLView class ───────────────────────────────────────────────────────── // UIView subclass overriding `+layerClass` to return [CAEAGLLayer class]. // Instance method `sxTick:` is what CADisplayLink calls. @@ -750,126 +808,42 @@ uikit_present_renderbuffer :: (self: *UIKitPlatform) { SxGLView :: #objc_class("SxGLView") { #extends UIView; - // Class-level constant — `+layerClass` returns [CAEAGLLayer class]. - layerClass :: *void = objc_getClass("CAEAGLLayer".ptr); + layerClass :: () => CAEAGLLayer.class(); - sxTick :: (self: *Self, link: *void) { - uikit_gl_view_tick(xx self, xx 0, link); + sxTick :: (self: *Self, link: *CADisplayLink) { + if g_uikit_plat == null { return; } + g_uikit_plat.gl_view_tick(link); } layoutSubviews :: (self: *Self) { - uikit_gl_view_layout(xx self, xx 0); + if g_uikit_plat == null { return; } + g_uikit_plat.gl_view_did_layout(); } touchesBegan_withEvent :: (self: *Self, touches: *void, event: *void) { - uikit_gl_view_touches_began(xx self, xx 0, touches, event); + if g_uikit_plat == null { return; } + g_uikit_plat.push_touch_down(xx self, touches); } touchesMoved_withEvent :: (self: *Self, touches: *void, event: *void) { - uikit_gl_view_touches_moved(xx self, xx 0, touches, event); + if g_uikit_plat == null { return; } + g_uikit_plat.push_touch_moved(xx self, touches); } touchesEnded_withEvent :: (self: *Self, touches: *void, event: *void) { - uikit_gl_view_touches_ended(xx self, xx 0, touches, event); + if g_uikit_plat == null { return; } + g_uikit_plat.push_touch_up(xx self, touches); } touchesCancelled_withEvent :: (self: *Self, touches: *void, event: *void) { - uikit_gl_view_touches_ended(xx self, xx 0, touches, event); + if g_uikit_plat == null { return; } + g_uikit_plat.push_touch_up(xx self, touches); } } -uikit_gl_view_tick :: (self: *void, _cmd: *void, link_raw: *void) callconv(.c) { - link : *CADisplayLink = xx link_raw; - if g_uikit_plat == null { return; } - plat := g_uikit_plat; - - // Keyboard-inset lockstep — sx-side cubic ease-out approximation of - // UIKit's private keyboard curve. Sample targetTimestamp so we - // interpolate at the time this frame will be visible. Lags by ~1 - // frame behind UIKit because UIKit's keyboard is rendered in a - // separate process (UIRemoteKeyboardWindow) and we can't perfectly - // sync to it from outside that scene. Refinements tried: - // CATransaction.flush, CABasicAnimation, presentationLayer reading, - // and keyboardLayoutGuide — none eliminated the lag without - // cascade-breaking the GL view's frame. - if plat.kb_animating { - target_ts := link.targetTimestamp(); - elapsed := target_ts - plat.kb_anim_start; - // Negative elapsed can happen if the just-fired willChangeFrame - // set kb_anim_start to a wall time AFTER the tick already - // captured its targetTimestamp this frame. Without the clamp, - // t < 0 makes the cubic ease-out *overshoot* in the opposite - // direction (visible as the indicator briefly jumping past the - // keyboard on close, then animating back). - if elapsed < 0.0 { elapsed = 0.0; } - if elapsed >= plat.kb_anim_dur or plat.kb_anim_dur <= 0.0 { - plat.keyboard_height = plat.kb_anim_to; - plat.kb_animating = false; - } else { - t : f32 = xx (elapsed / plat.kb_anim_dur); - inv := 1.0 - t; - eased := 1.0 - inv * inv * inv; - plat.keyboard_height = plat.kb_anim_from + (plat.kb_anim_to - plat.kb_anim_from) * eased; - } - } - - // Indicator's position is driven by UIView.animateWithDuration kicked - // off from willChangeFrame — it animates in lockstep with UIKit's - // keyboard using the same curve+duration. No per-tick setFrame here. - - if !plat.has_frame_closure { return; } - if !plat.gl_initialized { return; } - - dur_d := link.duration(); - plat.delta_time = xx dur_d; - // Stash the targetTimestamp so begin_frame can hand it down to the - // game in FrameContext for Metal presentDrawable:atTime:. - plat.last_target_ts = link.targetTimestamp(); - - 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; } - if plat.gpu_mode == .gles { - uikit_setup_renderbuffer(plat); - } else { - uikit_compute_layer_pixel_size(plat); - } - plat.gl_initialized = true; -} - -// Metal mode equivalent of uikit_setup_renderbuffer's "tell me how big the -// drawable is in pixels". Reads the layer's bounds in points and scales to -// pixels via dpi_scale. CAMetalLayer.drawableSize is set by MetalGPU.init -// based on these dims. -uikit_compute_layer_pixel_size :: (plat: *UIKitPlatform) { - inline if OS != .ios { return; } - if plat.gl_view == null { return; } - - b := #objc_call(CGRect)(plat.gl_view, "bounds"); - - w_pts : f64 = b.width; - h_pts : f64 = b.height; - plat.viewport_w = xx w_pts; - plat.viewport_h = xx h_pts; - - scale64 : f64 = xx plat.dpi_scale; - pw : f64 = w_pts * scale64; - ph : f64 = h_pts * scale64; - plat.pixel_w = xx pw; - plat.pixel_h = xx ph; -} - -// 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. +// Pull the first UITouch out of an NSSet and convert its +// location to view-local coordinates. Used by push_touch_* methods on +// UIKitPlatform. uikit_touch_location :: (touch: *void, view: *void) -> Point { p := #objc_call(CGPoint)(touch, "locationInView:", view); @@ -881,30 +855,6 @@ uikit_first_touch :: (touches: *void) -> *void { touches_set.anyObject(); } -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 — deleted (M3.3). SxGLView is now // declarative; the compiler synthesises everything at module init. @@ -917,30 +867,36 @@ uikit_gl_view_touches_ended :: (self: *void, _cmd: *void, touches: *void, event: SxMetalView :: #objc_class("SxMetalView") { #extends UIView; - layerClass :: *void = objc_getClass("CAMetalLayer".ptr); + layerClass :: () => CAMetalLayer.class(); - sxTick :: (self: *Self, link: *void) { - uikit_gl_view_tick(xx self, xx 0, link); + sxTick :: (self: *Self, link: *CADisplayLink) { + if g_uikit_plat == null { return; } + g_uikit_plat.gl_view_tick(link); } layoutSubviews :: (self: *Self) { - uikit_gl_view_layout(xx self, xx 0); + if g_uikit_plat == null { return; } + g_uikit_plat.gl_view_did_layout(); } touchesBegan_withEvent :: (self: *Self, touches: *void, event: *void) { - uikit_gl_view_touches_began(xx self, xx 0, touches, event); + if g_uikit_plat == null { return; } + g_uikit_plat.push_touch_down(xx self, touches); } touchesMoved_withEvent :: (self: *Self, touches: *void, event: *void) { - uikit_gl_view_touches_moved(xx self, xx 0, touches, event); + if g_uikit_plat == null { return; } + g_uikit_plat.push_touch_moved(xx self, touches); } touchesEnded_withEvent :: (self: *Self, touches: *void, event: *void) { - uikit_gl_view_touches_ended(xx self, xx 0, touches, event); + if g_uikit_plat == null { return; } + g_uikit_plat.push_touch_up(xx self, touches); } touchesCancelled_withEvent :: (self: *Self, touches: *void, event: *void) { - uikit_gl_view_touches_ended(xx self, xx 0, touches, event); + if g_uikit_plat == null { return; } + g_uikit_plat.push_touch_up(xx self, touches); } } diff --git a/tests/expected/ffi-objc-call-06-sret-return.ir b/tests/expected/ffi-objc-call-06-sret-return.ir index 0afe4cb..e47d423 100644 --- a/tests/expected/ffi-objc-call-06-sret-return.ir +++ b/tests/expected/ffi-objc-call-06-sret-return.ir @@ -1800,6 +1800,12 @@ declare ptr @class_createInstance(ptr, i64) #0 ; Function Attrs: nounwind declare ptr @object_getClass(ptr) #0 +; Function Attrs: nounwind +declare ptr @object_getIvar(ptr, ptr) #0 + +; Function Attrs: nounwind +declare void @object_setIvar(ptr, ptr, ptr) #0 + ; Function Attrs: nounwind declare ptr @objc_msgSend(ptr, ptr) #0 diff --git a/tests/expected/ffi-objc-dsl-07-mangling-table.ir b/tests/expected/ffi-objc-dsl-07-mangling-table.ir index 4085d78..61290dd 100644 --- a/tests/expected/ffi-objc-dsl-07-mangling-table.ir +++ b/tests/expected/ffi-objc-dsl-07-mangling-table.ir @@ -765,6 +765,12 @@ declare ptr @class_createInstance(ptr, i64) #0 ; Function Attrs: nounwind declare ptr @object_getClass(ptr) #0 +; Function Attrs: nounwind +declare ptr @object_getIvar(ptr, ptr) #0 + +; Function Attrs: nounwind +declare void @object_setIvar(ptr, ptr, ptr) #0 + ; Function Attrs: nounwind declare ptr @objc_msgSend(ptr, ptr) #0