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:
@@ -1,7 +1,9 @@
|
|||||||
// UIKitPlatform end-to-end smoke: boots the AppDelegate, installs an
|
// UIKitPlatform end-to-end smoke: boots the AppDelegate, installs an
|
||||||
// SxGLView with a CAEAGLLayer + GLES3 context + CADisplayLink, polls
|
// SxGLView with a CAEAGLLayer + GLES3 context + CADisplayLink, polls
|
||||||
// UITouch events into ui.Event, and on every vsync clears the screen
|
// UITouch events into ui.Event, and on every vsync clears the screen
|
||||||
// to a color that advances on each tap.
|
// to a color that advances on each tap. Each tap also toggles the
|
||||||
|
// on-screen keyboard so safe_insets.bottom can be observed growing /
|
||||||
|
// shrinking under it.
|
||||||
//
|
//
|
||||||
// Build + run:
|
// Build + run:
|
||||||
// sx build --target ios-sim examples/66-uikit-platform.sx \
|
// sx build --target ios-sim examples/66-uikit-platform.sx \
|
||||||
@@ -21,6 +23,7 @@
|
|||||||
#import "modules/platform/uikit.sx";
|
#import "modules/platform/uikit.sx";
|
||||||
|
|
||||||
g_color_index : s64 = 0;
|
g_color_index : s64 = 0;
|
||||||
|
g_keyboard_up : bool = false;
|
||||||
|
|
||||||
tap_frame :: () {
|
tap_frame :: () {
|
||||||
fc := g_uikit_plat.begin_frame();
|
fc := g_uikit_plat.begin_frame();
|
||||||
@@ -30,7 +33,16 @@ tap_frame :: () {
|
|||||||
while i < events.len {
|
while i < events.len {
|
||||||
ev := events.ptr[i];
|
ev := events.ptr[i];
|
||||||
if ev == {
|
if ev == {
|
||||||
case .mouse_down: { g_color_index += 1; }
|
case .mouse_down: {
|
||||||
|
g_color_index += 1;
|
||||||
|
if g_keyboard_up {
|
||||||
|
g_uikit_plat.hide_keyboard();
|
||||||
|
g_keyboard_up = false;
|
||||||
|
} else {
|
||||||
|
g_uikit_plat.show_keyboard();
|
||||||
|
g_keyboard_up = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
i += 1;
|
i += 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,26 @@ EAGL_API_GLES3 :: 3;
|
|||||||
// 16 bytes and returns via the FP-register path on arm64.
|
// 16 bytes and returns via the FP-register path on arm64.
|
||||||
CGPoint :: struct { x: f64; y: f64; }
|
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
|
// GLenum constants for renderbuffer/framebuffer setup that aren't in opengl.sx's
|
||||||
// loader path (they live on the framework's symbol table directly).
|
// loader path (they live on the framework's symbol table directly).
|
||||||
GL_RENDERBUFFER :u32: 0x8D41;
|
GL_RENDERBUFFER :u32: 0x8D41;
|
||||||
@@ -46,6 +66,9 @@ UIKitPlatform :: struct {
|
|||||||
framebuffer: u32 = 0;
|
framebuffer: u32 = 0;
|
||||||
gl_initialized: bool = false;
|
gl_initialized: bool = false;
|
||||||
|
|
||||||
|
// Hidden UITextField; firstResponder ⇆ keyboard visibility.
|
||||||
|
text_field: *void = null;
|
||||||
|
|
||||||
viewport_w: f32 = 0.0;
|
viewport_w: f32 = 0.0;
|
||||||
viewport_h: f32 = 0.0;
|
viewport_h: f32 = 0.0;
|
||||||
pixel_w: s32 = 0;
|
pixel_w: s32 = 0;
|
||||||
@@ -110,6 +133,9 @@ impl Platform for UIKitPlatform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
begin_frame :: (self: *UIKitPlatform) -> FrameContext {
|
begin_frame :: (self: *UIKitPlatform) -> FrameContext {
|
||||||
|
inline if OS == .ios {
|
||||||
|
uikit_refresh_safe_insets(self);
|
||||||
|
}
|
||||||
FrameContext.{
|
FrameContext.{
|
||||||
viewport_w = self.viewport_w,
|
viewport_w = self.viewport_w,
|
||||||
viewport_h = self.viewport_h,
|
viewport_h = self.viewport_h,
|
||||||
@@ -146,8 +172,23 @@ impl Platform for UIKitPlatform {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
show_keyboard :: (self: *UIKitPlatform) { }
|
show_keyboard :: (self: *UIKitPlatform) {
|
||||||
hide_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) { }
|
stop :: (self: *UIKitPlatform) { }
|
||||||
|
|
||||||
@@ -176,6 +217,19 @@ uikit_extern_nsstring :: (name: [*]u8) -> *void {
|
|||||||
// so non-iOS builds never reference the unresolved UIKit symbols below.
|
// 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 :: () {
|
uikit_chdir_to_bundle :: () {
|
||||||
inline if OS != .ios { return; }
|
inline if OS != .ios { return; }
|
||||||
NSBundle := objc_getClass("NSBundle".ptr);
|
NSBundle := objc_getClass("NSBundle".ptr);
|
||||||
@@ -202,6 +256,9 @@ uikit_register_classes :: () {
|
|||||||
class_addMethod(SxAppDelegate,
|
class_addMethod(SxAppDelegate,
|
||||||
sel_registerName("setWindow:".ptr),
|
sel_registerName("setWindow:".ptr),
|
||||||
xx uikit_window_setter, "v@:@".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);
|
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) {
|
uikit_create_gl_context :: (plat: *UIKitPlatform) {
|
||||||
inline if OS != .ios { return; }
|
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) {
|
uikit_did_finish_launching :: (self: *void, _cmd: *void, app: *void, opts: *void) -> u8 callconv(.c) {
|
||||||
result : u8 = 1;
|
result : u8 = 1;
|
||||||
inline if OS == .ios {
|
inline if OS == .ios {
|
||||||
result = uikit_did_finish_launching_ios(app);
|
result = uikit_did_finish_launching_ios(self, app);
|
||||||
}
|
}
|
||||||
result;
|
result;
|
||||||
}
|
}
|
||||||
|
|
||||||
uikit_did_finish_launching_ios :: (app: *void) -> u8 {
|
uikit_did_finish_launching_ios :: (delegate: *void, app: *void) -> u8 {
|
||||||
if g_uikit_plat == null {
|
if g_uikit_plat == null {
|
||||||
NSLog(ns_string("[sx] no platform\n".ptr));
|
NSLog(ns_string("[sx] no platform\n".ptr));
|
||||||
return 0;
|
return 0;
|
||||||
@@ -381,13 +492,25 @@ uikit_did_finish_launching_ios :: (app: *void) -> u8 {
|
|||||||
// a layout pass; layoutSubviews calls uikit_setup_renderbuffer.
|
// a layout pass; layoutSubviews calls uikit_setup_renderbuffer.
|
||||||
msg_v(plat.window, sel_make_key_visible);
|
msg_v(plat.window, sel_make_key_visible);
|
||||||
|
|
||||||
// Safe insets stay zero for now — struct-return ABI for `safeAreaInsets`
|
// Hidden UITextField as the firstResponder source for show_keyboard /
|
||||||
// (UIEdgeInsets = 4 CGFloats) isn't expressible without dedicated
|
// hide_keyboard. Lives as a subview of the GL view so it's in the
|
||||||
// msgSend_stret plumbing. Revisit when keyboard sync needs it.
|
// responder chain but is sized 0×0 so it can't be tapped.
|
||||||
plat.safe_top = 0.0;
|
UITextField := objc_getClass("UITextField".ptr);
|
||||||
plat.safe_left = 0.0;
|
sel_add_subview := sel_registerName("addSubview:".ptr);
|
||||||
plat.safe_bottom = 0.0;
|
tf_raw := msg_o(UITextField, sel_alloc);
|
||||||
plat.safe_right = 0.0;
|
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.
|
// CADisplayLink: vsync-driven tick into our SxGLView.
|
||||||
plat.display_link = msg_oso(CADisplayLink, sel_link_with_target, plat.gl_view, sel_tick);
|
plat.display_link = msg_oso(CADisplayLink, sel_link_with_target, plat.gl_view, sel_tick);
|
||||||
|
|||||||
Reference in New Issue
Block a user