/* * 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 #import #include #include #include #include #include #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 @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 @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 @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 */