platform: iOS safe-area insets + keyboard observer
UIKitPlatform now reads `[UIView safeAreaInsets]` (UIEdgeInsets = 32-byte
struct: top, left, bottom, right CGFloats) in begin_frame, and subscribes
to UIKeyboardWillChangeFrameNotification on NSNotificationCenter. The
chess game's build_ui pads its root by `g_safe_insets`, so the Dynamic
Island no longer overlaps the board on iPhone 17 Pro — all 8 ranks and
files are visible.
Struct returns >16 bytes (UIEdgeInsets, CGRect) go through the arm64
x8 indirect-result-pointer convention; expressing the return type on a
typed `objc_msgSend` fn-pointer cast generates the right call sequence.
Same pattern used to unwrap the keyboard's CGRect from NSValue
(UIKeyboardFrameEndUserInfoKey).
show_keyboard / hide_keyboard now drive a hidden UITextField subview as
the firstResponder source. resignFirstResponder dismisses; observer
fires with height=0 → safe_insets bottom collapses.
Deferred (next iteration): wrap the inset update in
[UIView animateWithDuration: animations:^{ ... }] to land in the same
CoreAnimation transaction as the keyboard. sx doesn't have block
syntax yet — we'd need a C shim that takes an fn-ptr and builds the
block. Today the inset snaps while the keyboard slides; the lag is
visible but the rest of the wiring is in place.
examples/66-uikit-platform.sx updated: each tap toggles the keyboard
+ advances the clear color (red→green→blue), so the observer can be
observed firing via the visible keyboard slide.
This commit is contained in:
@@ -26,6 +26,26 @@ EAGL_API_GLES3 :: 3;
|
||||
// 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;
|
||||
@@ -46,6 +66,9 @@ UIKitPlatform :: struct {
|
||||
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;
|
||||
@@ -110,6 +133,9 @@ impl Platform for UIKitPlatform {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -146,8 +172,23 @@ impl Platform for UIKitPlatform {
|
||||
};
|
||||
}
|
||||
|
||||
show_keyboard :: (self: *UIKitPlatform) { }
|
||||
hide_keyboard :: (self: *UIKitPlatform) { }
|
||||
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) { }
|
||||
|
||||
@@ -176,6 +217,19 @@ 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; }
|
||||
|
||||
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);
|
||||
@@ -202,6 +256,9 @@ uikit_register_classes :: () {
|
||||
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);
|
||||
|
||||
@@ -209,6 +266,60 @@ uikit_register_classes :: () {
|
||||
}
|
||||
}
|
||||
|
||||
// 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; }
|
||||
|
||||
plat.keyboard_height = xx h;
|
||||
plat.keyboard_visible = h > 0.5;
|
||||
}
|
||||
|
||||
uikit_create_gl_context :: (plat: *UIKitPlatform) {
|
||||
inline if OS != .ios { return; }
|
||||
|
||||
@@ -251,12 +362,12 @@ uikit_window_setter :: (self: *void, _cmd: *void, w: *void) callconv(.c) {
|
||||
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(app);
|
||||
result = uikit_did_finish_launching_ios(self, app);
|
||||
}
|
||||
result;
|
||||
}
|
||||
|
||||
uikit_did_finish_launching_ios :: (app: *void) -> u8 {
|
||||
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;
|
||||
@@ -381,13 +492,25 @@ uikit_did_finish_launching_ios :: (app: *void) -> u8 {
|
||||
// a layout pass; layoutSubviews calls uikit_setup_renderbuffer.
|
||||
msg_v(plat.window, sel_make_key_visible);
|
||||
|
||||
// Safe insets stay zero for now — struct-return ABI for `safeAreaInsets`
|
||||
// (UIEdgeInsets = 4 CGFloats) isn't expressible without dedicated
|
||||
// msgSend_stret plumbing. Revisit when keyboard sync needs it.
|
||||
plat.safe_top = 0.0;
|
||||
plat.safe_left = 0.0;
|
||||
plat.safe_bottom = 0.0;
|
||||
plat.safe_right = 0.0;
|
||||
// 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);
|
||||
|
||||
Reference in New Issue
Block a user