ffi 3.2 C4: migrate uikit.sx UIKit chrome cluster to #objc_class

Fourth cluster — was blocked on issue-0043, now unblocked by the
preceding `Self`-substitution fix.

Classes declared:

- UIScreen         → mainScreen (class), nativeScale + bounds (instance)
- UIView           → safeAreaInsets, addSubview, layer (all instance)
- UIWindow         → alloc (class), initWithWindowScene, setRootViewController,
                     makeKeyAndVisible, screen (instance)
- UIViewController → alloc (class), init, setView (instance)
- UITextField      → alloc (class), init, becomeFirstResponder,
                     resignFirstResponder (instance)

Migration sites in uikit.sx:

- `show_keyboard` / `hide_keyboard` → `tf.becomeFirstResponder()` /
  `tf.resignFirstResponder()` on a `*UITextField` cast of `text_field`.
- `uikit_refresh_safe_insets` → `gl_view.safeAreaInsets()` on a
  `*UIView` cast of `plat.gl_view`.
- `uikit_read_screen_scale` and GL-context bring-up →
  `UIScreen.mainScreen().nativeScale()`.
- `uikit_keyboard_will_change_frame` → `win.screen().bounds()`.
- `uikit_scene_will_connect_ios` (the function that triggered 0043) →
  `UIWindow.alloc().initWithWindowScene(scene)`,
  `UIViewController.alloc().init()`, `vc.setView(...)`,
  `win.setRootViewController(...)`, `gl_view.layer()`,
  `UITextField.alloc().init()`, `gl_view.addSubview(...)`,
  `win.makeKeyAndVisible()`.

Three `objc_getClass(...)` lookups (UIWindow, UIViewController,
UITextField) are gone — the class slots come from the declarative
bindings via `__sx_objc_class_init`. UIScreen has the same shape.

167/167 example tests; chess clean on macOS / iOS sim / Android via
`tools/verify-step.sh`.
This commit is contained in:
agra
2026-05-25 17:53:11 +03:00
parent 2b717d9b38
commit 5b4969f9be
2 changed files with 140 additions and 55 deletions

View File

@@ -113,6 +113,45 @@ CADisplayLink :: #foreign #objc_class("CADisplayLink") {
duration :: (self: *Self) -> f64;
}
// ── UIKit chrome (Phase 3.2 C4) ────────────────────────────────────────
UIScreen :: #foreign #objc_class("UIScreen") {
mainScreen :: () -> *UIScreen;
nativeScale :: (self: *Self) -> f64;
bounds :: (self: *Self) -> CGRect;
}
UIView :: #foreign #objc_class("UIView") {
safeAreaInsets :: (self: *Self) -> UIEdgeInsets;
addSubview :: (self: *Self, view: *void);
// `-layer` returns a CALayer*; declared as opaque pointer for now
// (CALayer's own declarative binding is in the C5 cluster).
layer :: (self: *Self) -> *void;
}
UIWindow :: #foreign #objc_class("UIWindow") {
alloc :: () -> *UIWindow;
initWithWindowScene :: (self: *Self, scene: *void) -> *UIWindow;
setRootViewController :: (self: *Self, vc: *void);
makeKeyAndVisible :: (self: *Self);
screen :: (self: *Self) -> *UIScreen;
}
UIViewController :: #foreign #objc_class("UIViewController") {
alloc :: () -> *UIViewController;
init :: (self: *Self) -> *UIViewController;
setView :: (self: *Self, view: *void);
}
UITextField :: #foreign #objc_class("UITextField") {
alloc :: () -> *UITextField;
init :: (self: *Self) -> *UITextField;
// Inherited from UIResponder via the runtime; declared here directly
// until `#extends UIResponder` lands (Phase 3.4).
becomeFirstResponder :: (self: *Self) -> s8;
resignFirstResponder :: (self: *Self) -> s8;
}
// 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;
@@ -281,14 +320,16 @@ impl Platform for UIKitPlatform {
show_keyboard :: (self: *UIKitPlatform) {
inline if OS == .ios {
if self.text_field == null { return; }
#objc_call(bool)(self.text_field, "becomeFirstResponder");
tf : *UITextField = xx self.text_field;
tf.becomeFirstResponder();
}
}
hide_keyboard :: (self: *UIKitPlatform) {
inline if OS == .ios {
if self.text_field == null { return; }
#objc_call(bool)(self.text_field, "resignFirstResponder");
tf : *UITextField = xx self.text_field;
tf.resignFirstResponder();
}
}
@@ -324,7 +365,8 @@ uikit_refresh_safe_insets :: (plat: *UIKitPlatform) {
inline if OS != .ios { return; }
if plat.gl_view == null { return; }
i := #objc_call(UIEdgeInsets)(plat.gl_view, "safeAreaInsets");
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;
@@ -400,10 +442,8 @@ uikit_register_classes :: () {
// (that's where the gles path picks the scale up).
uikit_read_screen_scale :: (plat: *UIKitPlatform) {
inline if OS != .ios { return; }
UIScreen := objc_getClass("UIScreen".ptr);
screen := #objc_call(*void)(UIScreen, "mainScreen");
scale_d := #objc_call(f64)(screen, "nativeScale");
plat.dpi_scale = xx scale_d;
screen := UIScreen.mainScreen();
plat.dpi_scale = xx screen.nativeScale();
}
// NSNotification callback. The notification's userInfo dict has the
@@ -441,8 +481,9 @@ uikit_keyboard_will_change_frame :: (self: *void, _cmd: *void, notification: *vo
// Screen height in points. The window lives on the connected scene's screen.
if plat.window == null { return; }
win_screen := #objc_call(*void)(plat.window, "screen");
screen_bounds := #objc_call(CGRect)(win_screen, "bounds");
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,
@@ -486,13 +527,12 @@ uikit_create_gl_context :: (plat: *UIKitPlatform) {
inline if OS != .ios { return; }
EAGLContext := objc_getClass("EAGLContext".ptr);
UIScreen := objc_getClass("UIScreen".ptr);
// UIScreen class slot comes from the declarative #objc_class binding.
// Read the screen scale up-front so callers can size font caches and
// textures with the right DPI before the window even exists.
screen := #objc_call(*void)(UIScreen, "mainScreen");
scale_d := #objc_call(f64)(screen, "nativeScale");
plat.dpi_scale = xx scale_d;
screen := UIScreen.mainScreen();
plat.dpi_scale = xx screen.nativeScale();
ctx_raw := #objc_call(*void)(EAGLContext, "alloc");
plat.gl_ctx = #objc_call(*void)(ctx_raw, "initWithAPI:", EAGL_API_GLES3);
@@ -543,24 +583,21 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
}
plat := g_uikit_plat;
UIWindow := objc_getClass("UIWindow".ptr);
UIViewController := objc_getClass("UIViewController".ptr);
SxGLView := objc_getClass("SxGLView".ptr);
SxMetalView := objc_getClass("SxMetalView".ptr);
// CADisplayLink and NSRunLoop class objects come from the
// declarative `#objc_class` slots populated by emit_llvm's
// __sx_objc_class_init constructor — no local objc_getClass needed.
// UIWindow / UIViewController / CADisplayLink / NSRunLoop class
// slots come from the declarative #objc_class bindings.
win_raw := #objc_call(*void)(UIWindow, "alloc");
plat.window = #objc_call(*void)(win_raw, "initWithWindowScene:", scene);
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_raw := #objc_call(*void)(UIViewController, "alloc");
plat.root_vc = #objc_call(*void)(vc_raw, "init");
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
@@ -569,11 +606,12 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
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");
#objc_call(void)(plat.root_vc, "setView:", plat.gl_view);
vc.setView(plat.gl_view);
#objc_call(void)(plat.window, "setRootViewController:", plat.root_vc);
win.setRootViewController(plat.root_vc);
plat.gl_layer = #objc_call(*void)(plat.gl_view, "layer");
gl_view : *UIView = xx plat.gl_view;
plat.gl_layer = gl_view.layer();
// Mark the layer opaque (no compositor blend). Required for EAGL +
// recommended for Metal (CAMetalLayer.opaque defaults to YES but doesn't
@@ -606,23 +644,22 @@ uikit_scene_will_connect_ios :: (delegate: *void, scene: *void) {
// 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 := #objc_call(*void)(plat.window, "screen");
scale := #objc_call(f64)(screen, "nativeScale");
screen2 := win.screen();
scale := screen2.nativeScale();
plat.dpi_scale = xx scale;
#objc_call(void)(plat.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.
#objc_call(void)(plat.window, "makeKeyAndVisible");
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.
UITextField := objc_getClass("UITextField".ptr);
tf_raw := #objc_call(*void)(UITextField, "alloc");
plat.text_field = #objc_call(*void)(tf_raw, "init");
#objc_call(void)(plat.gl_view, "addSubview:", plat.text_field);
tf := UITextField.alloc().init();
plat.text_field = xx tf;
gl_view.addSubview(plat.text_field);
// (Keyboard observer is registered in didFinishLaunching via