/* * el_appkit.m — AppKit backend for the el native widget system. * * This file implements the macOS AppKit widget layer that el_seed.c calls * through to when EL_TARGET_MACOS is defined. * * Architecture: * el program (el code) * → __widget_* C builtins in el_seed.c * → el_appkit_* C-callable functions declared here * → NSView/NSWindow/NSStackView/NSButton/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 AppKit calls MUST run on the main thread. el_appkit_dispatch_main * bounces work to the main thread synchronously if called from a background thread. * el_appkit_run_loop MUST be called from the main thread; it never returns. * * Callback mechanism: when a widget fires an event (button click, 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. * * Compile (MRC — no ARC; manual retain/release needed for struct-embedded id): * clang -std=gnu11 -ObjC -fno-objc-arc -framework Cocoa -c el_appkit.m -o el_appkit.o * Then link el_appkit.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 Cocoa */ #import #include #include #include #include #include /* ── Widget table ─────────────────────────────────────────────────────────── */ #define EL_APPKIT_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; /* NSWindow* or NSView* subclass, retained */ char* cb_click; /* El function name for click/submit events */ char* cb_change; /* El function name for value-change events */ char* user_data; /* custom data string passed to click callbacks */ } ElWidget; static ElWidget _el_widgets[EL_APPKIT_MAX_WIDGETS]; static int _el_widget_count = 0; /* 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_APPKIT_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; _el_widgets[i].user_data = 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_APPKIT_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; free(w->user_data); w->user_data = NULL; } /* ── Dispatch helpers ─────────────────────────────────────────────────────── */ /* Run a block on the main thread, waiting for completion. Safe to call from * any thread including the main thread. */ static void el_appkit_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_appkit_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 delegate for button clicks ─────────────────────────────────────── */ @interface ElButtonTarget : NSObject @property int64_t widgetHandle; @end @implementation ElButtonTarget - (void)buttonClicked:(id)sender { ElWidget* w = el_widget_get(self.widgetHandle); if (w && w->cb_click) { el_appkit_invoke_cb(w->cb_click, self.widgetHandle, w->user_data ? w->user_data : ""); } } @end /* ── ObjC delegate for text field changes / enter key ───────────────────── */ @interface ElTextFieldDelegate : NSObject @property int64_t widgetHandle; @end @implementation ElTextFieldDelegate - (void)controlTextDidChange:(NSNotification*)notif { ElWidget* w = el_widget_get(self.widgetHandle); if (!w) return; NSTextField* tf = (NSTextField*)notif.object; const char* val = [[tf stringValue] UTF8String]; if (w->cb_change) { el_appkit_invoke_cb(w->cb_change, self.widgetHandle, val ? val : ""); } } - (void)controlTextDidEndEditing:(NSNotification*)notif { /* Enter key (or tab) submits. The userInfo value is an NSNumber, not NSEvent. */ NSNumber* movement = [[notif userInfo] objectForKey:@"NSTextMovement"]; if (movement && [movement intValue] == NSTextMovementReturn) { ElWidget* w = el_widget_get(self.widgetHandle); if (!w) return; NSTextField* tf = (NSTextField*)notif.object; const char* val = [[tf stringValue] UTF8String]; if (w->cb_click) { /* cb_click = on_submit for text fields */ el_appkit_invoke_cb(w->cb_click, self.widgetHandle, val ? val : ""); } } } @end /* We hold strong references to our ObjC delegate objects in a side table so * ARC/MRC don't collect them. Simple parallel array keyed by widget slot. */ static id _el_delegates[EL_APPKIT_MAX_WIDGETS]; /* ── Flipped wrapper for NSScrollView document views ───────────────────────── * NSScrollView's clip view uses the document view's coordinate system. When * the document view is non-flipped (origin at bottom-left), content stacks * from the bottom. This subclass overrides isFlipped so content starts at the * visual top-left, matching expected list/feed behaviour. */ @interface ElFlippedView : NSView @end @implementation ElFlippedView - (BOOL)isFlipped { return YES; } @end /* ── ElResizableContentView ─────────────────────────────────────────────────── * Used as NSWindow's content view instead of a plain NSView. * Overrides setFrameSize: to propagate the new size directly to the single * root child without going through the AL constraint graph. * * Why: with TAMIC:NO on the content view and 4-edge AL constraints to the * child, NSWindow calls [contentView fittingSize] to compute its minimum * resize floor. fittingSize walks the constraint graph and returns the * largest minimum it finds — locking horizontal resize to the initial width. * By removing the AL constraints and propagating via setFrameSize: instead, * NSWindow has no constraint-based minimum and respects setContentMinSize: only. */ @interface ElResizableContentView : NSView @end @implementation ElResizableContentView - (NSSize)fittingSize { return NSMakeSize(1.0, 1.0); } - (NSSize)intrinsicContentSize { return NSMakeSize(NSViewNoIntrinsicMetric, NSViewNoIntrinsicMetric); } @end /* ElWindow subclass — overrides minSize so the AL constraint graph cannot * inflate the resize floor beyond the manifest-specified minimum. * NSWindow normally sets minSize from the larger of the explicit setMinSize: * call and the AL-computed minimum content size; the override replaces that * with the explicit minimum only. */ @interface ElWindow : NSWindow @property (nonatomic) NSSize elMinFrameSize; @property (nonatomic) NSSize elInitialContentSize; /* designed width/height from manifest */ @end @implementation ElWindow - (NSSize)minSize { return _elMinFrameSize; } @end /* ── NSApplication setup ──────────────────────────────────────────────────── */ @interface ElAppDelegate : NSObject @end @implementation ElAppDelegate - (void)applicationDidFinishLaunching:(NSNotification*)notif { /* Nothing needed — windows are created programmatically before run loop. */ } - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication*)app { return YES; } @end static ElAppDelegate* _el_app_delegate = nil; /* ── Public C API (called from el_seed.c) ─────────────────────────────────── */ /* * el_appkit_init — initialize NSApplication. Must be called once on the main * thread before any other el_appkit_* function. Idempotent. */ void el_appkit_init(void) { static int done = 0; if (done) return; done = 1; [NSApplication sharedApplication]; [NSApp setActivationPolicy:NSApplicationActivationPolicyRegular]; _el_app_delegate = [[ElAppDelegate alloc] init]; [NSApp setDelegate:_el_app_delegate]; } /* * el_appkit_window_create — create an NSWindow and return its handle. * title, width, height, min_width, min_height come from manifest.el app{} block. */ int64_t el_appkit_window_create(const char* title, int width, int height, int min_width, int min_height) { __block int64_t handle = -1; el_appkit_sync_main(^{ NSRect frame = NSMakeRect(0, 0, (CGFloat)width, (CGFloat)height); NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskResizable; ElWindow* win = [[ElWindow alloc] initWithContentRect:frame styleMask:style backing:NSBackingStoreBuffered defer:NO]; [win setTitle:[NSString stringWithUTF8String:title ? title : ""]]; /* Store the manifest minimum as our explicit frame floor. */ NSRect minContentRect = NSMakeRect(0, 0, (CGFloat)min_width, (CGFloat)min_height); NSSize minFrame = [win frameRectForContentRect:minContentRect].size; win.elMinFrameSize = minFrame; [win setContentMinSize:NSMakeSize((CGFloat)min_width, (CGFloat)min_height)]; win.elInitialContentSize = NSMakeSize((CGFloat)width, (CGFloat)height); [win center]; ElResizableContentView* root = [[ElResizableContentView alloc] initWithFrame:frame]; [win setContentView:root]; handle = el_widget_alloc(EL_WIDGET_WINDOW, win); [win release]; [root release]; }); return handle; } /* * el_appkit_window_show — make the window visible and bring it to front. */ void el_appkit_window_show(int64_t handle) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w || w->kind != EL_WIDGET_WINDOW) return; NSWindow* win = (NSWindow*)w->obj; /* AL may have shrunk the window during the build phase. * Restore the designed content size before showing — the window * is not yet visible so the user never sees the shrunken state. * After this, AL runs within the correct frame and required * 4-edge constraints keep the child filling on every resize. */ if ([win isKindOfClass:[ElWindow class]]) { ElWindow* elWin = (ElWindow*)win; NSSize cs = elWin.elInitialContentSize; if (cs.width > 0) { [win setContentSize:cs]; [win center]; } } [win makeKeyAndOrderFront:nil]; [NSApp activateIgnoringOtherApps:YES]; }); } /* * el_appkit_window_set_title — update window title at runtime. */ void el_appkit_window_set_title(int64_t handle, const char* title) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w || w->kind != EL_WIDGET_WINDOW) return; NSWindow* win = (NSWindow*)w->obj; [win setTitle:[NSString stringWithUTF8String:title ? title : ""]]; }); } /* * el_appkit_run_loop — start the NSApplication run loop. Never returns. * Must be called from the main thread as the last step of main(). */ void el_appkit_run_loop(void) { [NSApp run]; } /* ── Layout container factories ───────────────────────────────────────────── */ static int64_t make_stack(NSUserInterfaceLayoutOrientation orient, CGFloat spacing) { __block int64_t handle = -1; el_appkit_sync_main(^{ NSStackView* sv = [[NSStackView alloc] initWithFrame:NSZeroRect]; [sv setOrientation:orient]; /* Alignment must match the stack's cross-axis to avoid NSStackView * silently flipping orientation. For a vertical stack, align children * to the leading edge (left); for a horizontal stack, align to top. * Using NSLayoutAttributeLeading on a horizontal stack flips it vertical. */ NSLayoutAttribute align = (orient == NSUserInterfaceLayoutOrientationVertical) ? NSLayoutAttributeLeading : NSLayoutAttributeTop; [sv setAlignment:align]; [sv setSpacing:spacing]; [sv setEdgeInsets:NSEdgeInsetsZero]; [sv setDistribution:NSStackViewDistributionFill]; [sv setTranslatesAutoresizingMaskIntoConstraints:NO]; ElWidgetKind kind = (orient == NSUserInterfaceLayoutOrientationVertical) ? EL_WIDGET_VSTACK : EL_WIDGET_HSTACK; handle = el_widget_alloc(kind, sv); [sv release]; }); return handle; } int64_t el_appkit_vstack_create(int spacing) { return make_stack(NSUserInterfaceLayoutOrientationVertical, (CGFloat)spacing); } int64_t el_appkit_hstack_create(int spacing) { return make_stack(NSUserInterfaceLayoutOrientationHorizontal, (CGFloat)spacing); } int64_t el_appkit_zstack_create(void) { /* ZStack = NSView with manual child positioning (no stack manager). */ __block int64_t handle = -1; el_appkit_sync_main(^{ NSView* v = [[NSView alloc] initWithFrame:NSZeroRect]; [v setTranslatesAutoresizingMaskIntoConstraints:NO]; handle = el_widget_alloc(EL_WIDGET_ZSTACK, v); [v release]; }); return handle; } int64_t el_appkit_scroll_create(void) { __block int64_t handle = -1; el_appkit_sync_main(^{ NSScrollView* sv = [[NSScrollView alloc] initWithFrame:NSZeroRect]; [sv setHasVerticalScroller:YES]; [sv setHasHorizontalScroller:NO]; [sv setAutohidesScrollers:YES]; [sv setTranslatesAutoresizingMaskIntoConstraints:NO]; handle = el_widget_alloc(EL_WIDGET_SCROLL, sv); [sv release]; }); return handle; } /* ── Widget factories ─────────────────────────────────────────────────────── */ int64_t el_appkit_label_create(const char* text) { __block int64_t handle = -1; el_appkit_sync_main(^{ NSTextField* lbl = [NSTextField labelWithString: [NSString stringWithUTF8String:text ? text : ""]]; [lbl setEditable:NO]; [lbl setBordered:NO]; [lbl setDrawsBackground:NO]; [lbl setTranslatesAutoresizingMaskIntoConstraints:NO]; handle = el_widget_alloc(EL_WIDGET_LABEL, lbl); }); return handle; } int64_t el_appkit_button_create(const char* label) { __block int64_t handle = -1; el_appkit_sync_main(^{ NSButton* btn = [[NSButton alloc] initWithFrame:NSZeroRect]; [btn setButtonType:NSButtonTypeMomentaryLight]; [btn setBezelStyle:NSBezelStyleShadowlessSquare]; [btn setTitle:[NSString stringWithUTF8String:label ? label : ""]]; [btn setAlignment:NSTextAlignmentLeft]; [btn setTranslatesAutoresizingMaskIntoConstraints:NO]; /* Create action target. */ ElButtonTarget* target = [[ElButtonTarget alloc] init]; /* handle assigned below after alloc — set after */ [btn setTarget:target]; [btn setAction:@selector(buttonClicked:)]; int64_t h = el_widget_alloc(EL_WIDGET_BUTTON, btn); target.widgetHandle = h; _el_delegates[h] = target; /* retain */ handle = h; [btn release]; }); return handle; } int64_t el_appkit_text_field_create(const char* placeholder) { __block int64_t handle = -1; el_appkit_sync_main(^{ NSTextField* tf = [[NSTextField alloc] initWithFrame:NSZeroRect]; [tf setTranslatesAutoresizingMaskIntoConstraints:NO]; if (placeholder && *placeholder) { [tf setPlaceholderString:[NSString stringWithUTF8String:placeholder]]; } ElTextFieldDelegate* del = [[ElTextFieldDelegate alloc] init]; [tf setDelegate:del]; int64_t h = el_widget_alloc(EL_WIDGET_TEXTFIELD, tf); del.widgetHandle = h; _el_delegates[h] = del; handle = h; [tf release]; }); return handle; } int64_t el_appkit_text_area_create(const char* placeholder) { __block int64_t handle = -1; el_appkit_sync_main(^{ NSScrollView* scroll = [[NSScrollView alloc] initWithFrame:NSZeroRect]; [scroll setHasVerticalScroller:YES]; [scroll setAutohidesScrollers:YES]; [scroll setTranslatesAutoresizingMaskIntoConstraints:NO]; NSTextView* tv = [[NSTextView alloc] initWithFrame:NSZeroRect]; [tv setMinSize:NSMakeSize(0, 60)]; [tv setMaxSize:NSMakeSize(FLT_MAX, FLT_MAX)]; [tv setVerticallyResizable:YES]; [tv setHorizontallyResizable:NO]; [tv setAutoresizingMask:NSViewWidthSizable]; if (placeholder && *placeholder) { /* NSTextView has no native placeholder; we set a hint via * a custom accessibility description as a minimal approach. */ [tv setAccessibilityPlaceholderValue: [NSString stringWithUTF8String:placeholder]]; } [[tv textContainer] setWidthTracksTextView:YES]; [scroll setDocumentView:tv]; int64_t h = el_widget_alloc(EL_WIDGET_TEXTAREA, scroll); handle = h; [scroll release]; [tv release]; }); return handle; } int64_t el_appkit_image_create(const char* path_or_name) { __block int64_t handle = -1; el_appkit_sync_main(^{ NSImage* img = nil; if (path_or_name && *path_or_name) { /* Try as filesystem path first, then as named system image. */ img = [[NSImage alloc] initWithContentsOfFile: [NSString stringWithUTF8String:path_or_name]]; if (!img) { img = [NSImage imageNamed:[NSString stringWithUTF8String:path_or_name]]; if (img) [img retain]; } } NSImageView* iv = [[NSImageView alloc] initWithFrame:NSZeroRect]; [iv setTranslatesAutoresizingMaskIntoConstraints:NO]; if (img) { [iv setImage:img]; [img release]; } handle = el_widget_alloc(EL_WIDGET_IMAGE, iv); [iv release]; }); return handle; } /* ── Widget property setters ─────────────────────────────────────────────── */ void el_appkit_widget_set_text(int64_t handle, const char* text) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSString* s = [NSString stringWithUTF8String:text ? text : ""]; switch (w->kind) { case EL_WIDGET_LABEL: case EL_WIDGET_TEXTFIELD: [(NSTextField*)w->obj setStringValue:s]; break; case EL_WIDGET_BUTTON: [(NSButton*)w->obj setTitle:s]; break; case EL_WIDGET_TEXTAREA: { NSScrollView* sv = (NSScrollView*)w->obj; NSTextView* tv = (NSTextView*)[sv documentView]; [tv setString:s]; break; } case EL_WIDGET_WINDOW: [(NSWindow*)w->obj setTitle:s]; break; default: break; } }); } const char* el_appkit_widget_get_text(int64_t handle) { __block const char* result = ""; el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSString* s = nil; switch (w->kind) { case EL_WIDGET_LABEL: case EL_WIDGET_TEXTFIELD: s = [(NSTextField*)w->obj stringValue]; break; case EL_WIDGET_BUTTON: s = [(NSButton*)w->obj title]; break; case EL_WIDGET_TEXTAREA: { NSScrollView* sv = (NSScrollView*)w->obj; NSTextView* tv = (NSTextView*)[sv documentView]; s = [tv string]; break; } default: break; } if (s) result = strdup([s UTF8String]); }); return result; } void el_appkit_widget_set_color(int64_t handle, float r, float g, float b, float a) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSColor* c = [NSColor colorWithSRGBRed:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a]; switch (w->kind) { case EL_WIDGET_LABEL: case EL_WIDGET_TEXTFIELD: [(NSTextField*)w->obj setTextColor:c]; break; case EL_WIDGET_BUTTON: { /* Update foreground color on attributed title, preserving font. */ NSButton* btn = (NSButton*)w->obj; NSAttributedString* existing = [btn attributedTitle]; NSMutableAttributedString* as; if (existing && [existing length] > 0) { as = [[NSMutableAttributedString alloc] initWithAttributedString:existing]; } else { NSString* t = [btn title]; if (!t) t = @""; as = [[NSMutableAttributedString alloc] initWithString:t]; } [as addAttribute:NSForegroundColorAttributeName value:c range:NSMakeRange(0, [as length])]; [btn setAttributedTitle:as]; [as release]; break; } case EL_WIDGET_VSTACK: case EL_WIDGET_HSTACK: case EL_WIDGET_ZSTACK: { /* Set background via wantsLayer + layer background. */ NSView* v = (NSView*)w->obj; [v setWantsLayer:YES]; [v layer].backgroundColor = [c CGColor]; break; } default: break; } }); } void el_appkit_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSColor* c = [NSColor colorWithSRGBRed:(CGFloat)r green:(CGFloat)g blue:(CGFloat)b alpha:(CGFloat)a]; if (w->kind == EL_WIDGET_WINDOW) { NSWindow* win = (NSWindow*)w->obj; [win setBackgroundColor:c]; /* Also paint the contentView layer so it shows through NSStackView. */ NSView* cv = [win contentView]; [cv setWantsLayer:YES]; [cv layer].backgroundColor = [c CGColor]; return; } if (w->kind == EL_WIDGET_BUTTON) { /* Disable native bezel so CALayer background is visible. */ NSButton* btn = (NSButton*)w->obj; [btn setBordered:NO]; [btn setWantsLayer:YES]; [btn layer].backgroundColor = [c CGColor]; return; } NSView* v = (NSView*)w->obj; if (w->kind == EL_WIDGET_SCROLL) { /* NSScrollView needs setBackgroundColor, not just layer bg. */ NSScrollView* sv = (NSScrollView*)v; [sv setBackgroundColor:c]; [sv setDrawsBackground:YES]; } [v setWantsLayer:YES]; [v layer].backgroundColor = [c CGColor]; }); } void el_appkit_widget_set_font(int64_t handle, const char* family, int size, int bold) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSFont* font; if (bold) { font = [NSFont boldSystemFontOfSize:(CGFloat)size]; } else if (family && *family && strcmp(family, "system") != 0) { font = [NSFont fontWithName:[NSString stringWithUTF8String:family] size:(CGFloat)size]; if (!font) font = [NSFont systemFontOfSize:(CGFloat)size]; } else { font = [NSFont systemFontOfSize:(CGFloat)size]; } switch (w->kind) { case EL_WIDGET_LABEL: case EL_WIDGET_TEXTFIELD: [(NSTextField*)w->obj setFont:font]; break; case EL_WIDGET_BUTTON: /* NSButton: set font via attributed title, preserving existing foreground color. */ { NSButton* fbtn = (NSButton*)w->obj; NSAttributedString* fexisting = [fbtn attributedTitle]; NSMutableAttributedString* as; if (fexisting && [fexisting length] > 0) { as = [[NSMutableAttributedString alloc] initWithAttributedString:fexisting]; } else { NSString* ft = [fbtn title]; if (!ft) ft = @""; as = [[NSMutableAttributedString alloc] initWithString:ft]; } [as addAttribute:NSFontAttributeName value:font range:NSMakeRange(0, [as length])]; [fbtn setAttributedTitle:as]; [as release]; } break; case EL_WIDGET_TEXTAREA: { NSScrollView* sv = (NSScrollView*)w->obj; NSTextView* tv = (NSTextView*)[sv documentView]; [tv setFont:font]; break; } default: break; } }); } void el_appkit_widget_set_padding(int64_t handle, int top, int right, int bottom, int left) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; /* Padding on stack views maps to edgeInsets. */ if (w->kind == EL_WIDGET_VSTACK || w->kind == EL_WIDGET_HSTACK) { NSStackView* sv = (NSStackView*)w->obj; [sv setEdgeInsets:NSEdgeInsetsMake((CGFloat)top, (CGFloat)left, (CGFloat)bottom, (CGFloat)right)]; } /* For individual widgets we set a custom layout margin via constraints. * For now, NSTextView respects textContainerInset. */ if (w->kind == EL_WIDGET_TEXTAREA) { NSScrollView* sv = (NSScrollView*)w->obj; NSTextView* tv = (NSTextView*)[sv documentView]; [tv setTextContainerInset:NSMakeSize((CGFloat)(left + right) / 2, (CGFloat)(top + bottom) / 2)]; } }); } void el_appkit_widget_set_width(int64_t handle, int width) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSView* v = (NSView*)w->obj; if ([v isKindOfClass:[NSWindow class]]) return; /* Add a fixed-width constraint. */ NSLayoutConstraint* c = [NSLayoutConstraint constraintWithItem:v attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:(CGFloat)width]; c.priority = NSLayoutPriorityDefaultHigh; [v addConstraint:c]; }); } void el_appkit_widget_set_height(int64_t handle, int height) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSView* v = (NSView*)w->obj; if ([v isKindOfClass:[NSWindow class]]) return; NSLayoutConstraint* c = [NSLayoutConstraint constraintWithItem:v attribute:NSLayoutAttributeHeight relatedBy:NSLayoutRelationEqual toItem:nil attribute:NSLayoutAttributeNotAnAttribute multiplier:1.0 constant:(CGFloat)height]; c.priority = NSLayoutPriorityDefaultHigh; [v addConstraint:c]; }); } void el_appkit_widget_set_flex(int64_t handle, int flex) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSView* v = (NSView*)w->obj; if ([v isKindOfClass:[NSWindow class]]) return; if (flex > 0) { /* flex:1 → low hugging (wants to expand) + low compression resistance * (willing to be compressed). This makes NSStackViewDistributionFill * choose this view to grow/shrink to fill available space. */ [v setContentHuggingPriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal]; [v setContentHuggingPriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationVertical]; [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationHorizontal]; [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultLow forOrientation:NSLayoutConstraintOrientationVertical]; } else { /* flex:0 → high hugging (stays at natural size) + high compression * resistance (resists shrinking). */ [v setContentHuggingPriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationHorizontal]; [v setContentHuggingPriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical]; [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationHorizontal]; [v setContentCompressionResistancePriority:NSLayoutPriorityDefaultHigh forOrientation:NSLayoutConstraintOrientationVertical]; } }); } void el_appkit_widget_set_corner_radius(int64_t handle, int radius) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSView* v = (NSView*)w->obj; if ([v isKindOfClass:[NSWindow class]]) return; [v setWantsLayer:YES]; [v layer].cornerRadius = (CGFloat)radius; [v layer].masksToBounds = YES; }); } void el_appkit_widget_set_disabled(int64_t handle, int disabled) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; if (w->kind == EL_WIDGET_BUTTON) { [(NSButton*)w->obj setEnabled:!disabled]; } else if (w->kind == EL_WIDGET_TEXTFIELD) { [(NSTextField*)w->obj setEnabled:!disabled]; } }); } void el_appkit_widget_set_hidden(int64_t handle, int hidden) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSView* v = (NSView*)w->obj; if (![v isKindOfClass:[NSWindow class]]) { [v setHidden:(BOOL)hidden]; } }); } /* ── Child management ─────────────────────────────────────────────────────── */ /* * el_appkit_widget_add_child — attach child widget to parent. * * - Window: child is added to contentView (NSStackView). * - VStack/HStack: child is added as an arranged subview. * - ZStack/generic view: child is added as a plain subview. * - ScrollView: child becomes the documentView (only first child honoured). */ void el_appkit_widget_add_child(int64_t parent, int64_t child) { el_appkit_sync_main(^{ ElWidget* pw = el_widget_get(parent); ElWidget* cw = el_widget_get(child); if (!pw || !cw) return; NSView* childView = (NSView*)cw->obj; if ([childView isKindOfClass:[NSWindow class]]) return; /* can't add window as child */ switch (pw->kind) { case EL_WIDGET_WINDOW: { NSWindow* win = (NSWindow*)pw->obj; NSView* cv = [win contentView]; /* Standard required 4-edge constraints so the child always * fills the content view during user-initiated resize. * AL may shrink the window during the build phase (before show), * but el_appkit_window_show calls setContentSize: to restore * the designed size before making the window visible. */ [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; [cv addSubview:childView]; [NSLayoutConstraint activateConstraints:@[ [childView.topAnchor constraintEqualToAnchor:cv.topAnchor], [childView.bottomAnchor constraintEqualToAnchor:cv.bottomAnchor], [childView.leadingAnchor constraintEqualToAnchor:cv.leadingAnchor], [childView.trailingAnchor constraintEqualToAnchor:cv.trailingAnchor], ]]; break; } case EL_WIDGET_VSTACK: { NSStackView* sv = (NSStackView*)pw->obj; [sv addArrangedSubview:childView]; /* Pin child leading+trailing to fill cross-axis (width) of the VStack. * Priority 999 avoids intrinsic-size feedback through NSWindow. */ [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; NSLayoutConstraint* vl = [NSLayoutConstraint constraintWithItem:childView attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:sv attribute:NSLayoutAttributeLeading multiplier:1.0 constant:0.0]; NSLayoutConstraint* vr = [NSLayoutConstraint constraintWithItem:childView attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:sv attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:0.0]; vl.priority = 999; vr.priority = 999; [sv addConstraints:@[vl, vr]]; break; } case EL_WIDGET_HSTACK: { NSStackView* sv = (NSStackView*)pw->obj; [sv addArrangedSubview:childView]; /* Pin child top+bottom to fill cross-axis (height) of the HStack. * Priority 499 (well below required 1000) lets the window frame * win over child intrinsic size, avoiding NSWindow auto-resize. */ [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; NSLayoutConstraint* ht = [NSLayoutConstraint constraintWithItem:childView attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:sv attribute:NSLayoutAttributeTop multiplier:1.0 constant:0.0]; NSLayoutConstraint* hb = [NSLayoutConstraint constraintWithItem:childView attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:sv attribute:NSLayoutAttributeBottom multiplier:1.0 constant:0.0]; ht.priority = 499; hb.priority = 499; [sv addConstraints:@[ht, hb]]; break; } case EL_WIDGET_SCROLL: { NSScrollView* sv = (NSScrollView*)pw->obj; /* Wrap the child in a flipped view so content starts at the * visual top-left. Without flipping, NSScrollView places the * document origin at the bottom-left and lists appear inverted. */ ElFlippedView* flipper = [[ElFlippedView alloc] initWithFrame:NSZeroRect]; [flipper setTranslatesAutoresizingMaskIntoConstraints:NO]; [childView setTranslatesAutoresizingMaskIntoConstraints:NO]; [flipper addSubview:childView]; /* Pin child to all 4 edges of the flipper. */ NSArray* hc = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|[c]|" options:0 metrics:nil views:@{@"c": childView}]; NSArray* vc = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|[c]|" options:0 metrics:nil views:@{@"c": childView}]; [flipper addConstraints:hc]; [flipper addConstraints:vc]; [sv setDocumentView:flipper]; /* Constrain flipper width to the scroll view so items fill the * full panel width (vertical-only scrolling). */ NSLayoutConstraint* wc = [NSLayoutConstraint constraintWithItem:flipper attribute:NSLayoutAttributeWidth relatedBy:NSLayoutRelationEqual toItem:sv attribute:NSLayoutAttributeWidth multiplier:1.0 constant:0.0]; [sv addConstraint:wc]; [flipper release]; break; } case EL_WIDGET_ZSTACK: default: { NSView* pv = (NSView*)pw->obj; [pv addSubview:childView]; break; } } }); } /* * el_appkit_widget_remove_child — remove a child widget from its parent. */ void el_appkit_widget_remove_child(int64_t parent, int64_t child) { el_appkit_sync_main(^{ ElWidget* cw = el_widget_get(child); if (!cw) return; NSView* v = (NSView*)cw->obj; if (![v isKindOfClass:[NSWindow class]]) { [v removeFromSuperview]; } }); } /* ── Event registration ───────────────────────────────────────────────────── */ void el_appkit_widget_set_data(int64_t handle, const char* data_str) { ElWidget* w = el_widget_get(handle); if (!w) return; free(w->user_data); w->user_data = data_str && *data_str ? strdup(data_str) : NULL; } void el_appkit_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_appkit_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_appkit_widget_on_submit(int64_t handle, const char* fn_name) { /* For text fields, submit = pressing Enter → stored in cb_click slot. */ el_appkit_widget_on_click(handle, fn_name); } /* ── Widget destroy ───────────────────────────────────────────────────────── */ void el_appkit_widget_destroy(int64_t handle) { el_appkit_sync_main(^{ ElWidget* w = el_widget_get(handle); if (!w) return; NSView* v = (NSView*)w->obj; if (![v isKindOfClass:[NSWindow class]]) { [v removeFromSuperview]; } else { [(NSWindow*)w->obj close]; } [_el_delegates[handle] release]; _el_delegates[handle] = nil; el_widget_free(handle); }); }