Files
el/lang/el-compiler/runtime/el_appkit.m

1019 lines
42 KiB
Objective-C

/*
* 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 <Cocoa/Cocoa.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <dlfcn.h>
/* ── 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 <NSTextFieldDelegate>
@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 <NSApplicationDelegate>
@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);
});
}