Files
el/ui/examples/native-hello-ios/NativeHello/el_uikit.m

1119 lines
41 KiB
Objective-C

/*
* el_uikit.m — UIKit backend for the el native widget system.
*
* This file implements the iOS UIKit widget layer that el_seed.c calls
* through to when EL_TARGET_IOS is defined.
*
* Architecture:
* el program (el code)
* → __widget_* C builtins in el_seed.c
* → el_uikit_* C-callable functions declared here
* → UIView/UIWindow/UIStackView/UIButton/etc. via ObjC
*
* Widget handles: every widget (window, view, control) is assigned an int64_t
* slot index into _el_widgets[]. The el program holds these as opaque Int values.
* Slot 0 is never valid (reserved as null handle = -1 convention).
*
* Threading: All UIKit calls MUST run on the main thread. el_uikit_sync_main
* bounces work to the main thread synchronously if called from a background thread.
*
* Callback mechanism: when a widget fires an event (button tap, text change),
* the bridge looks up the registered callback function name, then calls
* dlsym(RTLD_DEFAULT, fn_name)(widget_handle, event_data_string)
* This resolves the El function symbol at runtime, matching the __thread_create
* pattern already established in el_seed.c.
*
* iOS lifecycle: UIApplicationMain never returns. The el program's "main" body
* (window creation, layout, show) must execute inside applicationDidFinishLaunchingWithOptions.
* The caller sets el_main_entry_fn before calling __native_run_loop so the
* delegate can invoke it at the right time.
*
* Compile (MRC — no ARC; manual retain/release needed for struct-embedded id):
* clang -std=gnu11 -ObjC -fno-objc-arc -DEL_TARGET_IOS -framework UIKit \
* -c el_uikit.m -o el_uikit.o
* Then link el_uikit.o alongside el_seed.c.
*
* Do NOT use -fobjc-arc: the widget table stores id in a plain C struct. ARC
* forbids explicit retain/release on struct-embedded object pointers and cannot
* insert automatic retain/release through C struct boundaries. MRC is the
* correct choice for this kind of C/ObjC bridge.
*
* Link flags required:
* -framework UIKit
*/
#ifdef EL_TARGET_IOS
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <dlfcn.h>
#include "el_runtime.h"
/* ── Widget table ─────────────────────────────────────────────────────────── */
#define EL_UIKIT_MAX_WIDGETS 4096
typedef enum {
EL_WIDGET_FREE = 0,
EL_WIDGET_WINDOW = 1,
EL_WIDGET_VSTACK = 2,
EL_WIDGET_HSTACK = 3,
EL_WIDGET_ZSTACK = 4,
EL_WIDGET_SCROLL = 5,
EL_WIDGET_LABEL = 6,
EL_WIDGET_BUTTON = 7,
EL_WIDGET_TEXTFIELD = 8,
EL_WIDGET_TEXTAREA = 9,
EL_WIDGET_IMAGE = 10,
EL_WIDGET_DIVIDER = 11,
EL_WIDGET_SPACER = 12,
} ElWidgetKind;
typedef struct {
ElWidgetKind kind;
id obj; /* UIWindow* or UIView* subclass, retained */
char* cb_click; /* El function name for click/submit events */
char* cb_change; /* El function name for value-change events */
} ElWidget;
static ElWidget _el_widgets[EL_UIKIT_MAX_WIDGETS];
static int _el_widget_count = 0;
/* Side table for ObjC delegate objects keyed by widget slot (MRC retained). */
static id _el_delegates[EL_UIKIT_MAX_WIDGETS];
/* Allocate a slot and return its index. Returns -1 if full. */
static int64_t el_widget_alloc(ElWidgetKind kind, id obj) {
for (int i = 1; i < EL_UIKIT_MAX_WIDGETS; i++) {
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
_el_widgets[i].kind = kind;
_el_widgets[i].obj = [obj retain];
_el_widgets[i].cb_click = NULL;
_el_widgets[i].cb_change = NULL;
if (i > _el_widget_count) _el_widget_count = i;
return (int64_t)i;
}
}
return -1;
}
static ElWidget* el_widget_get(int64_t handle) {
if (handle <= 0 || handle >= EL_UIKIT_MAX_WIDGETS) return NULL;
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
return &_el_widgets[handle];
}
static void el_widget_free(int64_t handle) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
[w->obj release];
w->obj = nil;
w->kind = EL_WIDGET_FREE;
free(w->cb_click); w->cb_click = NULL;
free(w->cb_change); w->cb_change = NULL;
}
/* ── Dispatch helpers ─────────────────────────────────────────────────────── */
/* Run a block on the main thread, waiting for completion. Safe to call from
* any thread including the main thread. Uses dispatch_sync so callers can
* rely on the block having run before the function returns. */
static void el_uikit_sync_main(void (^block)(void)) {
if ([NSThread isMainThread]) {
block();
} else {
dispatch_sync(dispatch_get_main_queue(), block);
}
}
/* ── El callback invocation ──────────────────────────────────────────────── */
/*
* Invoke an El callback by symbol name. The El function must have the
* signature: fn handler(handle: Int, data: String) -> Void
* which compiles to: void fn_name(el_val_t handle, el_val_t data)
*/
typedef void (*ElCb2)(int64_t handle, int64_t data);
static void el_uikit_invoke_cb(const char* fn_name, int64_t handle, const char* data) {
if (!fn_name || !*fn_name) return;
void* sym = dlsym(RTLD_DEFAULT, fn_name);
if (!sym) return;
ElCb2 fn = (ElCb2)sym;
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
}
/* ── ObjC helper: ElEventProxy — button tap and text event target ─────────── */
/*
* ElEventProxy is a lightweight ObjC action target / delegate aggregator.
* One instance is allocated per widget that has events. Stored in _el_delegates[].
*/
@interface ElEventProxy : NSObject <UITextFieldDelegate>
@property (nonatomic, assign) int64_t widgetHandle;
/* UIButton tap */
- (void)elButtonTapped:(id)sender;
/* UITextField UIControlEventEditingChanged */
- (void)elTextChanged:(id)sender;
@end
@implementation ElEventProxy
- (void)elButtonTapped:(id)sender {
(void)sender;
ElWidget* w = el_widget_get(self.widgetHandle);
if (w && w->cb_click) {
el_uikit_invoke_cb(w->cb_click, self.widgetHandle, "");
}
}
- (void)elTextChanged:(id)sender {
UITextField* tf = (UITextField*)sender;
ElWidget* w = el_widget_get(self.widgetHandle);
if (!w) return;
const char* val = [tf.text UTF8String];
if (w->cb_change) {
el_uikit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : "");
}
}
/* UITextFieldDelegate — return key fires on_submit (stored in cb_click). */
- (BOOL)textFieldShouldReturn:(UITextField*)textField {
ElWidget* w = el_widget_get(self.widgetHandle);
if (w && w->cb_click) {
const char* val = [textField.text UTF8String];
el_uikit_invoke_cb(w->cb_click, self.widgetHandle, val ? val : "");
}
[textField resignFirstResponder];
return YES;
}
@end
/* ── UITextView change delegate ──────────────────────────────────────────── */
@interface ElTextViewDelegate : NSObject <UITextViewDelegate>
@property (nonatomic, assign) int64_t widgetHandle;
@end
@implementation ElTextViewDelegate
- (void)textViewDidChange:(UITextView*)textView {
ElWidget* w = el_widget_get(self.widgetHandle);
if (!w || !w->cb_change) return;
const char* val = [textView.text UTF8String];
el_uikit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : "");
}
@end
/* ── iOS application lifecycle ───────────────────────────────────────────── */
/*
* el_main_entry_fn — set by el's main() (via __native_run_loop) before calling
* UIApplicationMain. The app delegate invokes it in didFinishLaunching so that
* the el program's window/widget setup runs at the correct lifecycle point.
*/
void (*el_main_entry_fn)(void) = NULL;
/* argc/argv forwarded from el_runtime_init_args (or main). */
static int _el_argc = 0;
static char** _el_argv = NULL;
/*
* el_uikit_set_args — call from el_runtime_init_args / main() before
* __native_run_loop to forward argc/argv to UIApplicationMain.
*/
void el_uikit_set_args(int argc, char** argv) {
_el_argc = argc;
_el_argv = argv;
}
@interface ElAppDelegate : UIResponder <UIApplicationDelegate>
@property (nonatomic, strong) UIWindow *keyWindow;
@end
@implementation ElAppDelegate
- (BOOL)application:(UIApplication*)app
didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
(void)app; (void)launchOptions;
/*
* The el program called __native_init and set up el_main_entry_fn before
* UIApplicationMain was entered. We call it here so window creation and
* widget layout happen on the main thread at the right lifecycle moment.
*/
if (el_main_entry_fn) {
el_main_entry_fn();
}
return YES;
}
- (void)applicationWillResignActive:(UIApplication*)app { (void)app; }
- (void)applicationDidEnterBackground:(UIApplication*)app { (void)app; }
- (void)applicationWillEnterForeground:(UIApplication*)app { (void)app; }
- (void)applicationDidBecomeActive:(UIApplication*)app { (void)app; }
@end
/* ── Public C API ─────────────────────────────────────────────────────────── */
/*
* el_uikit_init — one-time initialisation. Idempotent. Call before any other
* el_uikit_* function. On iOS there is no UIApplication equivalent of
* [NSApplication sharedApplication] to call here — the real initialisation
* happens inside UIApplicationMain which is entered from el_uikit_run_loop.
* This function records that init was requested.
*/
static int _el_uikit_init_done = 0;
void el_uikit_init(void) {
_el_uikit_init_done = 1;
}
/*
* el_uikit_run_loop — enters UIApplicationMain. Never returns.
* Must be called from the process main thread as the final step of main().
* Sets el_main_entry_fn to the provided function pointer before entering
* UIApplicationMain so the app delegate can call back into el code.
*/
void el_uikit_run_loop(void (*entry_fn)(void)) {
el_main_entry_fn = entry_fn;
UIApplicationMain(_el_argc, _el_argv, nil, @"ElAppDelegate");
}
/*
* el_uikit_window_create — create a UIWindow and configure a root
* UINavigationController + UIViewController. Returns window handle.
*
* On iOS there is exactly one full-screen window. width/height from the
* manifest are stored but the window always fills the screen. The title
* is applied to the root view controller's navigationItem.
*/
int64_t el_uikit_window_create(const char* title, int width, int height,
int min_width, int min_height) {
(void)width; (void)height; (void)min_width; (void)min_height;
__block int64_t handle = -1;
el_uikit_sync_main(^{
UIScreen* screen = [UIScreen mainScreen];
UIWindow* win = [[UIWindow alloc] initWithFrame:[screen bounds]];
win.backgroundColor = [UIColor systemBackgroundColor];
/* Root view controller with a vertical UIStackView as content. */
UIViewController* rootVC = [[UIViewController alloc] init];
NSString* titleStr = [NSString stringWithUTF8String:title ? title : ""];
rootVC.title = titleStr;
UIStackView* rootStack = [[UIStackView alloc] initWithFrame:CGRectZero];
rootStack.axis = UILayoutConstraintAxisVertical;
rootStack.alignment = UIStackViewAlignmentFill;
rootStack.distribution = UIStackViewDistributionFill;
rootStack.spacing = 0.0;
rootStack.translatesAutoresizingMaskIntoConstraints = NO;
[rootVC.view addSubview:rootStack];
/* Pin root stack to safe area. */
UILayoutGuide* safe = rootVC.view.safeAreaLayoutGuide;
[NSLayoutConstraint activateConstraints:@[
[rootStack.topAnchor constraintEqualToAnchor:safe.topAnchor],
[rootStack.leadingAnchor constraintEqualToAnchor:safe.leadingAnchor],
[rootStack.trailingAnchor constraintEqualToAnchor:safe.trailingAnchor],
[rootStack.bottomAnchor constraintEqualToAnchor:safe.bottomAnchor],
]];
UINavigationController* navCtrl =
[[UINavigationController alloc] initWithRootViewController:rootVC];
win.rootViewController = navCtrl;
/* Store the window. The root stack is accessible via
* ((UINavigationController*)win.rootViewController)
* .topViewController.view.subviews[0]
* but we reach it by casting obj to UIWindow* and digging at add_child time. */
handle = el_widget_alloc(EL_WIDGET_WINDOW, win);
[win release];
[rootVC release];
[rootStack release];
[navCtrl release];
});
return handle;
}
/*
* el_uikit_window_show — make the window key and visible.
*/
void el_uikit_window_show(int64_t handle) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w || w->kind != EL_WIDGET_WINDOW) return;
UIWindow* win = (UIWindow*)w->obj;
[win makeKeyAndVisible];
});
}
/*
* el_uikit_window_set_title — update nav bar title at runtime.
*/
void el_uikit_window_set_title(int64_t handle, const char* title) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w || w->kind != EL_WIDGET_WINDOW) return;
UIWindow* win = (UIWindow*)w->obj;
UINavigationController* nav = (UINavigationController*)win.rootViewController;
if ([nav isKindOfClass:[UINavigationController class]]) {
nav.topViewController.title =
[NSString stringWithUTF8String:title ? title : ""];
}
});
}
/* ── Layout container factories ───────────────────────────────────────────── */
static int64_t make_uistack(UILayoutConstraintAxis axis, CGFloat spacing) {
__block int64_t handle = -1;
el_uikit_sync_main(^{
UIStackView* sv = [[UIStackView alloc] initWithFrame:CGRectZero];
sv.axis = axis;
sv.alignment = UIStackViewAlignmentFill;
sv.distribution = UIStackViewDistributionFill;
sv.spacing = spacing;
sv.translatesAutoresizingMaskIntoConstraints = NO;
ElWidgetKind kind = (axis == UILayoutConstraintAxisVertical)
? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK;
handle = el_widget_alloc(kind, sv);
[sv release];
});
return handle;
}
int64_t el_uikit_vstack_create(int spacing) {
return make_uistack(UILayoutConstraintAxisVertical, (CGFloat)spacing);
}
int64_t el_uikit_hstack_create(int spacing) {
return make_uistack(UILayoutConstraintAxisHorizontal, (CGFloat)spacing);
}
int64_t el_uikit_zstack_create(void) {
/* ZStack = plain UIView; children are added with addSubview and use
* Auto Layout or absolute frames set by the caller. */
__block int64_t handle = -1;
el_uikit_sync_main(^{
UIView* v = [[UIView alloc] initWithFrame:CGRectZero];
v.translatesAutoresizingMaskIntoConstraints = NO;
handle = el_widget_alloc(EL_WIDGET_ZSTACK, v);
[v release];
});
return handle;
}
int64_t el_uikit_scroll_create(void) {
__block int64_t handle = -1;
el_uikit_sync_main(^{
UIScrollView* sv = [[UIScrollView alloc] initWithFrame:CGRectZero];
sv.translatesAutoresizingMaskIntoConstraints = NO;
sv.alwaysBounceVertical = YES;
sv.showsHorizontalScrollIndicator = NO;
handle = el_widget_alloc(EL_WIDGET_SCROLL, sv);
[sv release];
});
return handle;
}
/* ── Widget factories ─────────────────────────────────────────────────────── */
int64_t el_uikit_label_create(const char* text) {
__block int64_t handle = -1;
el_uikit_sync_main(^{
UILabel* lbl = [[UILabel alloc] initWithFrame:CGRectZero];
lbl.text = [NSString stringWithUTF8String:text ? text : ""];
lbl.numberOfLines = 0;
lbl.translatesAutoresizingMaskIntoConstraints = NO;
handle = el_widget_alloc(EL_WIDGET_LABEL, lbl);
[lbl release];
});
return handle;
}
int64_t el_uikit_button_create(const char* label) {
__block int64_t handle = -1;
el_uikit_sync_main(^{
UIButton* btn = [UIButton buttonWithType:UIButtonTypeSystem];
[btn setTitle:[NSString stringWithUTF8String:label ? label : ""]
forState:UIControlStateNormal];
btn.translatesAutoresizingMaskIntoConstraints = NO;
ElEventProxy* proxy = [[ElEventProxy alloc] init];
[btn addTarget:proxy
action:@selector(elButtonTapped:)
forControlEvents:UIControlEventTouchUpInside];
int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, btn);
proxy.widgetHandle = h;
_el_delegates[h] = proxy; /* retain in side table */
handle = h;
/* btn is autoreleased from buttonWithType: — no release needed */
});
return handle;
}
int64_t el_uikit_text_field_create(const char* placeholder) {
__block int64_t handle = -1;
el_uikit_sync_main(^{
UITextField* tf = [[UITextField alloc] initWithFrame:CGRectZero];
tf.translatesAutoresizingMaskIntoConstraints = NO;
if (placeholder && *placeholder) {
tf.placeholder = [NSString stringWithUTF8String:placeholder];
}
tf.borderStyle = UITextBorderStyleRoundedRect;
tf.autocorrectionType = UITextAutocorrectionTypeNo;
ElEventProxy* proxy = [[ElEventProxy alloc] init];
tf.delegate = proxy;
[tf addTarget:proxy
action:@selector(elTextChanged:)
forControlEvents:UIControlEventEditingChanged];
int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, tf);
proxy.widgetHandle = h;
_el_delegates[h] = proxy;
handle = h;
[tf release];
});
return handle;
}
int64_t el_uikit_text_area_create(const char* placeholder) {
__block int64_t handle = -1;
el_uikit_sync_main(^{
UITextView* tv = [[UITextView alloc] initWithFrame:CGRectZero];
tv.translatesAutoresizingMaskIntoConstraints = NO;
tv.font = [UIFont systemFontOfSize:14.0];
tv.layer.borderColor = [[UIColor systemGray4Color] CGColor];
tv.layer.borderWidth = 1.0;
tv.layer.cornerRadius = 6.0;
tv.clipsToBounds = YES;
/* UITextView has no native placeholder; set accessibility hint. */
if (placeholder && *placeholder) {
tv.accessibilityHint = [NSString stringWithUTF8String:placeholder];
}
ElTextViewDelegate* del = [[ElTextViewDelegate alloc] init];
tv.delegate = del;
int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, tv);
del.widgetHandle = h;
_el_delegates[h] = del;
handle = h;
[tv release];
});
return handle;
}
int64_t el_uikit_image_create(const char* path_or_name) {
__block int64_t handle = -1;
el_uikit_sync_main(^{
UIImage* img = nil;
if (path_or_name && *path_or_name) {
/* Try filesystem path first, then bundle asset name. */
img = [UIImage imageWithContentsOfFile:
[NSString stringWithUTF8String:path_or_name]];
if (!img) {
img = [UIImage imageNamed:
[NSString stringWithUTF8String:path_or_name]];
}
}
UIImageView* iv = [[UIImageView alloc] initWithFrame:CGRectZero];
iv.translatesAutoresizingMaskIntoConstraints = NO;
iv.contentMode = UIViewContentModeScaleAspectFit;
if (img) {
iv.image = img;
}
handle = el_widget_alloc(EL_WIDGET_IMAGE, iv);
[iv release];
});
return handle;
}
/* ── Widget property setters ─────────────────────────────────────────────── */
void el_uikit_widget_set_text(int64_t handle, const char* text) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w) return;
NSString* s = [NSString stringWithUTF8String:text ? text : ""];
switch (w->kind) {
case EL_WIDGET_LABEL:
((UILabel*)w->obj).text = s;
break;
case EL_WIDGET_TEXTFIELD:
((UITextField*)w->obj).text = s;
break;
case EL_WIDGET_BUTTON:
[(UIButton*)w->obj setTitle:s forState:UIControlStateNormal];
break;
case EL_WIDGET_TEXTAREA:
((UITextView*)w->obj).text = s;
break;
case EL_WIDGET_WINDOW:
el_uikit_window_set_title(handle, text);
break;
default: break;
}
});
}
const char* el_uikit_widget_get_text(int64_t handle) {
__block const char* result = "";
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w) return;
NSString* s = nil;
switch (w->kind) {
case EL_WIDGET_LABEL:
s = ((UILabel*)w->obj).text;
break;
case EL_WIDGET_TEXTFIELD:
s = ((UITextField*)w->obj).text;
break;
case EL_WIDGET_BUTTON:
s = [(UIButton*)w->obj titleForState:UIControlStateNormal];
break;
case EL_WIDGET_TEXTAREA:
s = ((UITextView*)w->obj).text;
break;
default: break;
}
if (s) result = strdup([s UTF8String]);
});
return result;
}
void el_uikit_widget_set_color(int64_t handle, float r, float g, float b, float a) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w) return;
UIColor* c = [UIColor colorWithRed:(CGFloat)(r / 255.0)
green:(CGFloat)(g / 255.0)
blue:(CGFloat)(b / 255.0)
alpha:(CGFloat)(a / 255.0)];
switch (w->kind) {
case EL_WIDGET_LABEL:
((UILabel*)w->obj).textColor = c;
break;
case EL_WIDGET_TEXTFIELD:
((UITextField*)w->obj).textColor = c;
break;
case EL_WIDGET_TEXTAREA:
((UITextView*)w->obj).textColor = c;
break;
case EL_WIDGET_BUTTON:
[(UIButton*)w->obj setTitleColor:c forState:UIControlStateNormal];
((UIButton*)w->obj).tintColor = c;
break;
case EL_WIDGET_VSTACK:
case EL_WIDGET_HSTACK:
case EL_WIDGET_ZSTACK:
((UIView*)w->obj).backgroundColor = c;
break;
default: break;
}
});
}
void el_uikit_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w) return;
if (w->kind == EL_WIDGET_WINDOW) {
UIWindow* win = (UIWindow*)w->obj;
win.backgroundColor = [UIColor colorWithRed:(CGFloat)(r / 255.0)
green:(CGFloat)(g / 255.0)
blue:(CGFloat)(b / 255.0)
alpha:(CGFloat)(a / 255.0)];
return;
}
UIView* v = (UIView*)w->obj;
v.backgroundColor = [UIColor colorWithRed:(CGFloat)(r / 255.0)
green:(CGFloat)(g / 255.0)
blue:(CGFloat)(b / 255.0)
alpha:(CGFloat)(a / 255.0)];
});
}
void el_uikit_widget_set_font(int64_t handle, const char* family, int size, int bold) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w) return;
UIFont* font;
if (bold) {
font = [UIFont boldSystemFontOfSize:(CGFloat)size];
} else if (family && *family && strcmp(family, "system") != 0) {
font = [UIFont fontWithName:[NSString stringWithUTF8String:family]
size:(CGFloat)size];
if (!font) font = [UIFont systemFontOfSize:(CGFloat)size];
} else {
font = [UIFont systemFontOfSize:(CGFloat)size];
}
switch (w->kind) {
case EL_WIDGET_LABEL:
((UILabel*)w->obj).font = font;
break;
case EL_WIDGET_TEXTFIELD:
((UITextField*)w->obj).font = font;
break;
case EL_WIDGET_TEXTAREA:
((UITextView*)w->obj).font = font;
break;
case EL_WIDGET_BUTTON:
((UIButton*)w->obj).titleLabel.font = font;
break;
default: break;
}
});
}
void el_uikit_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w) return;
UIEdgeInsets insets = UIEdgeInsetsMake(
(CGFloat)top, (CGFloat)left, (CGFloat)bottom, (CGFloat)right);
if (w->kind == EL_WIDGET_VSTACK || w->kind == EL_WIDGET_HSTACK) {
UIStackView* sv = (UIStackView*)w->obj;
sv.layoutMargins = insets;
sv.layoutMarginsRelativeArrangement = YES;
} else if (w->kind == EL_WIDGET_TEXTAREA) {
UITextView* tv = (UITextView*)w->obj;
tv.textContainerInset = insets;
} else {
UIView* v = (UIView*)w->obj;
v.layoutMargins = insets;
}
});
}
void el_uikit_widget_set_width(int64_t handle, int width) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w || w->kind == EL_WIDGET_WINDOW) return;
UIView* v = (UIView*)w->obj;
NSLayoutConstraint* c = [v.widthAnchor
constraintEqualToConstant:(CGFloat)width];
c.priority = UILayoutPriorityDefaultHigh;
c.active = YES;
});
}
void el_uikit_widget_set_height(int64_t handle, int height) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w || w->kind == EL_WIDGET_WINDOW) return;
UIView* v = (UIView*)w->obj;
NSLayoutConstraint* c = [v.heightAnchor
constraintEqualToConstant:(CGFloat)height];
c.priority = UILayoutPriorityDefaultHigh;
c.active = YES;
});
}
void el_uikit_widget_set_flex(int64_t handle, int flex) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w || w->kind == EL_WIDGET_WINDOW) return;
UIView* v = (UIView*)w->obj;
/* flex > 0 → low hugging priority so the view expands to fill space.
* flex == 0 → high hugging priority so the view stays at intrinsic size. */
UILayoutPriority p = (flex > 0)
? UILayoutPriorityDefaultLow
: UILayoutPriorityDefaultHigh;
[v setContentHuggingPriority:p
forAxis:UILayoutConstraintAxisHorizontal];
[v setContentHuggingPriority:p
forAxis:UILayoutConstraintAxisVertical];
});
}
void el_uikit_widget_set_corner_radius(int64_t handle, int radius) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w || w->kind == EL_WIDGET_WINDOW) return;
UIView* v = (UIView*)w->obj;
v.layer.cornerRadius = (CGFloat)radius;
v.clipsToBounds = YES;
});
}
void el_uikit_widget_set_disabled(int64_t handle, int disabled) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w) return;
switch (w->kind) {
case EL_WIDGET_BUTTON:
((UIButton*)w->obj).enabled = !disabled;
break;
case EL_WIDGET_TEXTFIELD:
((UITextField*)w->obj).enabled = !disabled;
break;
case EL_WIDGET_TEXTAREA:
((UITextView*)w->obj).editable = !disabled;
break;
default:
((UIView*)w->obj).userInteractionEnabled = !disabled;
break;
}
});
}
void el_uikit_widget_set_hidden(int64_t handle, int hidden) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w || w->kind == EL_WIDGET_WINDOW) return;
((UIView*)w->obj).hidden = (BOOL)hidden;
});
}
/* ── Child management ─────────────────────────────────────────────────────── */
/*
* el_uikit_widget_add_child — attach child widget to parent.
*
* - Window: child is added to the root UIStackView inside the root VC.
* - VStack/HStack: child is added as an arranged subview.
* - ZStack: child is added as a plain subview (caller manages layout).
* - ScrollView: child is added as a subview and pinned to content layout guide.
* - Generic view: child is added as a plain subview.
*/
void el_uikit_widget_add_child(int64_t parent, int64_t child) {
el_uikit_sync_main(^{
ElWidget* pw = el_widget_get(parent);
ElWidget* cw = el_widget_get(child);
if (!pw || !cw) return;
if (cw->kind == EL_WIDGET_WINDOW) return; /* can't add window as subview */
UIView* childView = (UIView*)cw->obj;
switch (pw->kind) {
case EL_WIDGET_WINDOW: {
/* Dig to the root stack view: window → navCtrl → topVC → view → subviews[0] */
UIWindow* win = (UIWindow*)pw->obj;
UINavigationController* nav =
(UINavigationController*)win.rootViewController;
UIView* rootVCView = nav.topViewController.view;
UIStackView* stack = nil;
for (UIView* sub in rootVCView.subviews) {
if ([sub isKindOfClass:[UIStackView class]]) {
stack = (UIStackView*)sub;
break;
}
}
if (stack) {
[stack addArrangedSubview:childView];
} else {
[rootVCView addSubview:childView];
}
break;
}
case EL_WIDGET_VSTACK:
case EL_WIDGET_HSTACK: {
UIStackView* sv = (UIStackView*)pw->obj;
[sv addArrangedSubview:childView];
break;
}
case EL_WIDGET_SCROLL: {
UIScrollView* sv = (UIScrollView*)pw->obj;
[sv addSubview:childView];
/* Pin child to content layout guide so scroll view knows content size. */
UILayoutGuide* cg = sv.contentLayoutGuide;
childView.translatesAutoresizingMaskIntoConstraints = NO;
[NSLayoutConstraint activateConstraints:@[
[childView.topAnchor constraintEqualToAnchor:cg.topAnchor],
[childView.leadingAnchor constraintEqualToAnchor:cg.leadingAnchor],
[childView.trailingAnchor constraintEqualToAnchor:cg.trailingAnchor],
[childView.bottomAnchor constraintEqualToAnchor:cg.bottomAnchor],
/* Width matches scroll view's frame (vertical scroll only). */
[childView.widthAnchor constraintEqualToAnchor:sv.frameLayoutGuide.widthAnchor],
]];
break;
}
case EL_WIDGET_ZSTACK:
default: {
UIView* pv = (UIView*)pw->obj;
[pv addSubview:childView];
break;
}
}
});
}
/*
* el_uikit_widget_remove_child — detach a child widget from its parent.
*/
void el_uikit_widget_remove_child(int64_t parent, int64_t child) {
el_uikit_sync_main(^{
ElWidget* pw = el_widget_get(parent);
ElWidget* cw = el_widget_get(child);
if (!cw || cw->kind == EL_WIDGET_WINDOW) return;
UIView* childView = (UIView*)cw->obj;
/* If parent is a stack view, remove from arranged subviews too. */
if (pw && (pw->kind == EL_WIDGET_VSTACK || pw->kind == EL_WIDGET_HSTACK)) {
UIStackView* sv = (UIStackView*)pw->obj;
[sv removeArrangedSubview:childView];
} else if (pw && pw->kind == EL_WIDGET_WINDOW) {
/* Find and remove from root stack. */
UIWindow* win = (UIWindow*)pw->obj;
UINavigationController* nav =
(UINavigationController*)win.rootViewController;
for (UIView* sub in nav.topViewController.view.subviews) {
if ([sub isKindOfClass:[UIStackView class]]) {
[(UIStackView*)sub removeArrangedSubview:childView];
break;
}
}
}
[childView removeFromSuperview];
});
}
/* ── Event registration ───────────────────────────────────────────────────── */
void el_uikit_widget_on_click(int64_t handle, const char* fn_name) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
free(w->cb_click);
w->cb_click = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
}
void el_uikit_widget_on_change(int64_t handle, const char* fn_name) {
ElWidget* w = el_widget_get(handle);
if (!w) return;
free(w->cb_change);
w->cb_change = (fn_name && *fn_name) ? strdup(fn_name) : NULL;
}
void el_uikit_widget_on_submit(int64_t handle, const char* fn_name) {
/* For text fields, submit = return key → stored in cb_click slot. */
el_uikit_widget_on_click(handle, fn_name);
}
/* ── Widget destroy ───────────────────────────────────────────────────────── */
void el_uikit_widget_destroy(int64_t handle) {
el_uikit_sync_main(^{
ElWidget* w = el_widget_get(handle);
if (!w) return;
if (w->kind != EL_WIDGET_WINDOW) {
UIView* v = (UIView*)w->obj;
/* If it's in a stack view, also remove from arranged subviews. */
if ([v.superview isKindOfClass:[UIStackView class]]) {
[(UIStackView*)v.superview removeArrangedSubview:v];
}
[v removeFromSuperview];
}
[_el_delegates[handle] release];
_el_delegates[handle] = nil;
el_widget_free(handle);
});
}
/* ── Manifest reader ──────────────────────────────────────────────────────── */
/*
* el_uikit_manifest_read — read an app manifest file. Tries the given path
* first; if not found, looks in the main bundle. Returns a C string with the
* file contents (heap-allocated; caller must not free — matches AppKit bridge
* convention of returning static/permanent lifetime strings from manifest).
* Returns NULL on failure.
*/
const char* el_uikit_manifest_read(const char* path) {
if (!path || !*path) return NULL;
NSString* pathStr = [NSString stringWithUTF8String:path];
NSString* content = [NSString stringWithContentsOfFile:pathStr
encoding:NSUTF8StringEncoding
error:nil];
if (!content) {
/* Try bundle resource lookup — strip to last component without extension. */
NSString* baseName = [[pathStr lastPathComponent]
stringByDeletingPathExtension];
NSString* ext = [pathStr pathExtension];
NSString* bundlePath = [[NSBundle mainBundle]
pathForResource:baseName ofType:(ext.length ? ext : nil)];
if (bundlePath) {
content = [NSString stringWithContentsOfFile:bundlePath
encoding:NSUTF8StringEncoding
error:nil];
}
}
if (!content) return NULL;
return strdup([content UTF8String]);
}
/* ── __widget_* C API (called from el_seed.c) ─────────────────────────────── */
/*
* These are the symbols that el compiled programs actually call. They forward
* directly to the el_uikit_* implementations above. el_val_t is int64_t (the
* same type used by the AppKit bridge).
*/
void __native_init(void) {
el_uikit_init();
}
void __native_run_loop(void) {
/* el_main_entry_fn must be set by the caller before this. On iOS the
* entry function pointer pattern is inverted vs macOS: the el program
* packages up its "build UI" work into a lambda / function and registers
* it via el_main_entry_fn, then calls __native_run_loop.
* el_uikit_run_loop takes that fn pointer and passes it to UIApplicationMain. */
el_uikit_run_loop(el_main_entry_fn);
}
el_val_t __window_create(el_val_t title, el_val_t width, el_val_t height,
el_val_t min_width, el_val_t min_height) {
return (el_val_t)el_uikit_window_create(
(const char*)(uintptr_t)title,
(int)width, (int)height,
(int)min_width, (int)min_height);
}
void __window_show(el_val_t handle) {
el_uikit_window_show((int64_t)handle);
}
void __window_set_title(el_val_t handle, el_val_t title) {
el_uikit_window_set_title((int64_t)handle,
(const char*)(uintptr_t)title);
}
el_val_t __vstack_create(el_val_t spacing) {
return (el_val_t)el_uikit_vstack_create((int)spacing);
}
el_val_t __hstack_create(el_val_t spacing) {
return (el_val_t)el_uikit_hstack_create((int)spacing);
}
el_val_t __zstack_create(void) {
return (el_val_t)el_uikit_zstack_create();
}
el_val_t __scroll_create(void) {
return (el_val_t)el_uikit_scroll_create();
}
el_val_t __label_create(el_val_t text) {
return (el_val_t)el_uikit_label_create((const char*)(uintptr_t)text);
}
el_val_t __button_create(el_val_t label) {
return (el_val_t)el_uikit_button_create((const char*)(uintptr_t)label);
}
el_val_t __text_field_create(el_val_t placeholder) {
return (el_val_t)el_uikit_text_field_create(
(const char*)(uintptr_t)placeholder);
}
el_val_t __text_area_create(el_val_t placeholder) {
return (el_val_t)el_uikit_text_area_create(
(const char*)(uintptr_t)placeholder);
}
el_val_t __image_create(el_val_t path_or_name) {
return (el_val_t)el_uikit_image_create(
(const char*)(uintptr_t)path_or_name);
}
void __widget_set_text(el_val_t handle, el_val_t text) {
el_uikit_widget_set_text((int64_t)handle, (const char*)(uintptr_t)text);
}
el_val_t __widget_get_text(el_val_t handle) {
return (el_val_t)(uintptr_t)el_uikit_widget_get_text((int64_t)handle);
}
void __widget_set_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a) {
el_uikit_widget_set_color((int64_t)handle,
(float)r, (float)g, (float)b, (float)a);
}
void __widget_set_bg_color(el_val_t handle, el_val_t r, el_val_t g,
el_val_t b, el_val_t a) {
el_uikit_widget_set_bg_color((int64_t)handle,
(float)r, (float)g, (float)b, (float)a);
}
void __widget_set_font(el_val_t handle, el_val_t family,
el_val_t size, el_val_t bold) {
el_uikit_widget_set_font((int64_t)handle,
(const char*)(uintptr_t)family, (int)size, (int)bold);
}
void __widget_set_padding(el_val_t handle, el_val_t top, el_val_t right,
el_val_t bottom, el_val_t left) {
el_uikit_widget_set_padding((int64_t)handle,
(int)top, (int)right, (int)bottom, (int)left);
}
void __widget_set_width(el_val_t handle, el_val_t width) {
el_uikit_widget_set_width((int64_t)handle, (int)width);
}
void __widget_set_height(el_val_t handle, el_val_t height) {
el_uikit_widget_set_height((int64_t)handle, (int)height);
}
void __widget_set_flex(el_val_t handle, el_val_t flex) {
el_uikit_widget_set_flex((int64_t)handle, (int)flex);
}
void __widget_set_corner_radius(el_val_t handle, el_val_t radius) {
el_uikit_widget_set_corner_radius((int64_t)handle, (int)radius);
}
void __widget_set_disabled(el_val_t handle, el_val_t disabled) {
el_uikit_widget_set_disabled((int64_t)handle, (int)disabled);
}
void __widget_set_hidden(el_val_t handle, el_val_t hidden) {
el_uikit_widget_set_hidden((int64_t)handle, (int)hidden);
}
void __widget_add_child(el_val_t parent, el_val_t child) {
el_uikit_widget_add_child((int64_t)parent, (int64_t)child);
}
void __widget_remove_child(el_val_t parent, el_val_t child) {
el_uikit_widget_remove_child((int64_t)parent, (int64_t)child);
}
void __widget_destroy(el_val_t handle) {
el_uikit_widget_destroy((int64_t)handle);
}
void __widget_on_click(el_val_t handle, el_val_t fn_name) {
el_uikit_widget_on_click((int64_t)handle,
(const char*)(uintptr_t)fn_name);
}
void __widget_on_change(el_val_t handle, el_val_t fn_name) {
el_uikit_widget_on_change((int64_t)handle,
(const char*)(uintptr_t)fn_name);
}
void __widget_on_submit(el_val_t handle, el_val_t fn_name) {
el_uikit_widget_on_submit((int64_t)handle,
(const char*)(uintptr_t)fn_name);
}
el_val_t __manifest_read(el_val_t path) {
return (el_val_t)(uintptr_t)el_uikit_manifest_read(
(const char*)(uintptr_t)path);
}
#endif /* EL_TARGET_IOS */