1364 lines
49 KiB
C
1364 lines
49 KiB
C
/*
|
|
* el_win32.c — Win32 backend for the el native widget system.
|
|
*
|
|
* This file implements the Windows Win32 widget layer that el_seed.c calls
|
|
* through to when EL_TARGET_WIN32 is defined.
|
|
*
|
|
* Architecture:
|
|
* el program (el code)
|
|
* → __widget_* C builtins in el_seed.c
|
|
* → el_win32_* C functions declared here
|
|
* → HWND/Win32 API / GDI
|
|
*
|
|
* 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; handle -1 = invalid).
|
|
*
|
|
* Threading: Win32 UI must run on the main thread. el is single-threaded for
|
|
* native UI, so all calls are direct. The message loop runs on the main thread
|
|
* and 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
|
|
* GetProcAddress(GetModuleHandle(NULL), fn_name)(widget_handle, event_data)
|
|
* This resolves the El function symbol at runtime, matching el_seed.c's
|
|
* dlsym(RTLD_DEFAULT, fn_name) pattern on POSIX.
|
|
*
|
|
* Layout: VStack and HStack containers maintain a child HWND list and reflow
|
|
* children on WM_SIZE using SetWindowPos. ZStack uses absolute positioning.
|
|
*
|
|
* Subclassing: SetWindowSubclass (comctl32) is used to intercept WM_COMMAND
|
|
* and WM_NOTIFY on container windows so button clicks and edit changes route
|
|
* back to el callbacks.
|
|
*
|
|
* String encoding: el strings are UTF-8 (const char*). All Win32 text APIs
|
|
* use wide strings (WCHAR*). Conversion via MultiByteToWideChar /
|
|
* WideCharToMultiByte throughout.
|
|
*
|
|
* Compile:
|
|
* cl /DEL_TARGET_WIN32 el_win32.c /c /Fo:el_win32.obj
|
|
* (link with: comctl32.lib user32.lib gdi32.lib)
|
|
* or with clang-cl:
|
|
* clang-cl /DEL_TARGET_WIN32 -c el_win32.c -o el_win32.obj
|
|
*/
|
|
|
|
#ifdef EL_TARGET_WIN32
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#ifndef UNICODE
|
|
#define UNICODE
|
|
#endif
|
|
#ifndef _UNICODE
|
|
#define _UNICODE
|
|
#endif
|
|
|
|
#include <windows.h>
|
|
#include <commctrl.h>
|
|
#include <stdint.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include "el_runtime.h"
|
|
|
|
/* ── Compile-time linkage ─────────────────────────────────────────────────── */
|
|
#pragma comment(lib, "comctl32.lib")
|
|
#pragma comment(lib, "user32.lib")
|
|
#pragma comment(lib, "gdi32.lib")
|
|
|
|
/* ── Custom window messages ───────────────────────────────────────────────── */
|
|
/* WM_USER+1: posted to a container to trigger a layout reflow pass. */
|
|
#define WM_EL_REFLOW (WM_USER + 1)
|
|
|
|
/* ── Widget type enum ─────────────────────────────────────────────────────── */
|
|
|
|
#define EL_WIN32_MAX_WIDGETS 4096
|
|
#define EL_WIN32_MAX_CHILDREN 256
|
|
|
|
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,
|
|
} ElWidgetKind;
|
|
|
|
/* Per-slot data. */
|
|
typedef struct {
|
|
ElWidgetKind kind;
|
|
HWND hwnd; /* primary window handle; NULL if free */
|
|
char* cb_click; /* El function name for click / submit events */
|
|
char* cb_change; /* El function name for value-change events */
|
|
|
|
/* Layout data (used by VSTACK / HSTACK / ZSTACK / WINDOW containers) */
|
|
int spacing; /* pixels between children */
|
|
int pad_top;
|
|
int pad_right;
|
|
int pad_bottom;
|
|
int pad_left;
|
|
int flex; /* flex weight; 0 = fixed size */
|
|
|
|
/* Fixed size overrides (0 = not set) */
|
|
int fixed_w;
|
|
int fixed_h;
|
|
|
|
/* Child list for layout containers */
|
|
int64_t children[EL_WIN32_MAX_CHILDREN];
|
|
int child_count;
|
|
|
|
/* Color state (stored for WM_CTLCOLOR* replies) */
|
|
COLORREF fg_color;
|
|
COLORREF bg_color;
|
|
HBRUSH bg_brush;
|
|
int has_fg;
|
|
int has_bg;
|
|
|
|
/* Font */
|
|
HFONT font; /* NULL = default system font */
|
|
|
|
/* Image: HBITMAP loaded for SS_BITMAP static */
|
|
HBITMAP hbmp;
|
|
} ElWidget;
|
|
|
|
static ElWidget _el_widgets[EL_WIN32_MAX_WIDGETS];
|
|
static int _el_widget_count = 0;
|
|
|
|
/* ── Window class names ───────────────────────────────────────────────────── */
|
|
static const WCHAR* EL_WNDCLASS_VSTACK = L"ElVStack";
|
|
static const WCHAR* EL_WNDCLASS_HSTACK = L"ElHStack";
|
|
static const WCHAR* EL_WNDCLASS_ZSTACK = L"ElZStack";
|
|
static const WCHAR* EL_WNDCLASS_SCROLL = L"ElScroll";
|
|
static const WCHAR* EL_WNDCLASS_WINDOW = L"ElWindow";
|
|
|
|
/* ── Forward declarations ─────────────────────────────────────────────────── */
|
|
static void el_win32_reflow(int64_t container_handle);
|
|
static LRESULT CALLBACK el_container_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
|
|
static LRESULT CALLBACK el_toplevel_wndproc(HWND hwnd, UINT msg, WPARAM wp, LPARAM lp);
|
|
|
|
/* ── String helpers ───────────────────────────────────────────────────────── */
|
|
|
|
/* UTF-8 → WCHAR*. Caller must free(). Returns NULL on failure. */
|
|
static WCHAR* el_utf8_to_wide(const char* s) {
|
|
if (!s) s = "";
|
|
int n = MultiByteToWideChar(CP_UTF8, 0, s, -1, NULL, 0);
|
|
if (n <= 0) return _wcsdup(L"");
|
|
WCHAR* buf = (WCHAR*)malloc((size_t)n * sizeof(WCHAR));
|
|
if (!buf) return _wcsdup(L"");
|
|
MultiByteToWideChar(CP_UTF8, 0, s, -1, buf, n);
|
|
return buf;
|
|
}
|
|
|
|
/* WCHAR* → UTF-8 char*. Caller must free(). Returns NULL on failure. */
|
|
static char* el_wide_to_utf8(const WCHAR* ws) {
|
|
if (!ws) return strdup("");
|
|
int n = WideCharToMultiByte(CP_UTF8, 0, ws, -1, NULL, 0, NULL, NULL);
|
|
if (n <= 0) return strdup("");
|
|
char* buf = (char*)malloc((size_t)n);
|
|
if (!buf) return strdup("");
|
|
WideCharToMultiByte(CP_UTF8, 0, ws, -1, buf, n, NULL, NULL);
|
|
return buf;
|
|
}
|
|
|
|
/* ── Slot management ──────────────────────────────────────────────────────── */
|
|
|
|
static int64_t el_widget_alloc(ElWidgetKind kind, HWND hwnd) {
|
|
for (int i = 1; i < EL_WIN32_MAX_WIDGETS; i++) {
|
|
if (_el_widgets[i].kind == EL_WIDGET_FREE) {
|
|
ElWidget* w = &_el_widgets[i];
|
|
memset(w, 0, sizeof(ElWidget));
|
|
w->kind = kind;
|
|
w->hwnd = hwnd;
|
|
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_WIN32_MAX_WIDGETS) return NULL;
|
|
if (_el_widgets[handle].kind == EL_WIDGET_FREE) return NULL;
|
|
return &_el_widgets[handle];
|
|
}
|
|
|
|
static void el_widget_free_slot(int64_t handle) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w) return;
|
|
free(w->cb_click); w->cb_click = NULL;
|
|
free(w->cb_change); w->cb_change = NULL;
|
|
if (w->bg_brush) { DeleteObject(w->bg_brush); w->bg_brush = NULL; }
|
|
if (w->font) { DeleteObject(w->font); w->font = NULL; }
|
|
if (w->hbmp) { DeleteObject(w->hbmp); w->hbmp = NULL; }
|
|
w->hwnd = NULL;
|
|
w->kind = EL_WIDGET_FREE;
|
|
}
|
|
|
|
/* Map HWND → widget slot (O(n) linear scan; table is small). */
|
|
static int64_t el_hwnd_to_slot(HWND hwnd) {
|
|
for (int i = 1; i <= _el_widget_count; i++) {
|
|
if (_el_widgets[i].kind != EL_WIDGET_FREE && _el_widgets[i].hwnd == hwnd)
|
|
return (int64_t)i;
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/* ── El callback invocation ──────────────────────────────────────────────── */
|
|
/*
|
|
* Invoke an El callback by symbol name. El functions have the signature:
|
|
* fn handler(handle: Int, data: String) -> Void
|
|
* compiled to: void fn_name(el_val_t handle, el_val_t data)
|
|
*/
|
|
typedef void (*ElCb2)(int64_t handle, int64_t data);
|
|
|
|
static void el_win32_invoke_cb(const char* fn_name, int64_t handle, const char* data) {
|
|
if (!fn_name || !*fn_name) return;
|
|
FARPROC proc = GetProcAddress(GetModuleHandleW(NULL), fn_name);
|
|
if (!proc) return;
|
|
ElCb2 fn = (ElCb2)(void*)proc;
|
|
fn(handle, (int64_t)(uintptr_t)(data ? data : ""));
|
|
}
|
|
|
|
/* ── Layout reflow ────────────────────────────────────────────────────────── */
|
|
/*
|
|
* Reflow the direct children of a container widget.
|
|
*
|
|
* For VSTACK: stack children top-to-bottom, respecting padding and spacing.
|
|
* Children with flex > 0 divide the leftover height proportionally.
|
|
* Children with fixed_h > 0 use that height; otherwise QueryWidget.
|
|
*
|
|
* For HSTACK: same logic, left-to-right.
|
|
*
|
|
* For WINDOW: its content area is the client rect; children stacked vertically
|
|
* (WINDOW acts like an outer VSTACK for direct child attachment purposes).
|
|
*
|
|
* For ZSTACK: children fill the entire client rect (absolute; no reflow).
|
|
*
|
|
* For SCROLL: the single child fills the client rect width; height is its
|
|
* natural height (scroll bar handles overflow).
|
|
*/
|
|
static void el_win32_reflow(int64_t handle) {
|
|
ElWidget* pw = el_widget_get(handle);
|
|
if (!pw || !pw->hwnd) return;
|
|
|
|
RECT cr;
|
|
GetClientRect(pw->hwnd, &cr);
|
|
int total_w = cr.right - cr.left;
|
|
int total_h = cr.bottom - cr.top;
|
|
|
|
int pt = pw->pad_top;
|
|
int pr = pw->pad_right;
|
|
int pb = pw->pad_bottom;
|
|
int pl = pw->pad_left;
|
|
int sp = pw->spacing;
|
|
|
|
int n = pw->child_count;
|
|
if (n == 0) return;
|
|
|
|
if (pw->kind == EL_WIDGET_ZSTACK) {
|
|
/* All children fill the client rect. */
|
|
int cw = total_w - pl - pr;
|
|
int ch = total_h - pt - pb;
|
|
if (cw < 0) cw = 0;
|
|
if (ch < 0) ch = 0;
|
|
for (int i = 0; i < n; i++) {
|
|
ElWidget* cw_ = el_widget_get(pw->children[i]);
|
|
if (!cw_ || !cw_->hwnd) continue;
|
|
SetWindowPos(cw_->hwnd, NULL, pl, pt, cw, ch,
|
|
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (pw->kind == EL_WIDGET_SCROLL) {
|
|
/* Single child fills width; natural height. */
|
|
if (n < 1) return;
|
|
ElWidget* cw_ = el_widget_get(pw->children[0]);
|
|
if (!cw_ || !cw_->hwnd) return;
|
|
int cw = total_w - pl - pr;
|
|
if (cw < 0) cw = 0;
|
|
/* Measure natural height. */
|
|
RECT cr2;
|
|
GetWindowRect(cw_->hwnd, &cr2);
|
|
int ch = cr2.bottom - cr2.top;
|
|
if (cw_->fixed_h > 0) ch = cw_->fixed_h;
|
|
SetWindowPos(cw_->hwnd, NULL, pl, pt, cw, ch,
|
|
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
/* Update scroll range. */
|
|
SCROLLINFO si;
|
|
si.cbSize = sizeof(si);
|
|
si.fMask = SIF_RANGE | SIF_PAGE;
|
|
si.nMin = 0;
|
|
si.nMax = ch + pt + pb;
|
|
si.nPage = (UINT)total_h;
|
|
SetScrollInfo(pw->hwnd, SB_VERT, &si, TRUE);
|
|
return;
|
|
}
|
|
|
|
/* VSTACK, HSTACK, WINDOW */
|
|
int is_vert = (pw->kind == EL_WIDGET_VSTACK ||
|
|
pw->kind == EL_WIDGET_WINDOW);
|
|
|
|
/* First pass: determine fixed sizes and total flex weight. */
|
|
int total_flex = 0;
|
|
int fixed_used = 0; /* pixels consumed by fixed children + spacing */
|
|
|
|
/* Per-child resolved size in the main axis. */
|
|
int* sizes = (int*)malloc((size_t)n * sizeof(int));
|
|
if (!sizes) return;
|
|
|
|
for (int i = 0; i < n; i++) {
|
|
ElWidget* cw_ = el_widget_get(pw->children[i]);
|
|
sizes[i] = 0;
|
|
if (!cw_ || !cw_->hwnd) continue;
|
|
int fixed = is_vert ? cw_->fixed_h : cw_->fixed_w;
|
|
if (fixed > 0) {
|
|
sizes[i] = fixed;
|
|
fixed_used += fixed;
|
|
} else if (cw_->flex > 0) {
|
|
total_flex += cw_->flex;
|
|
} else {
|
|
/* No flex, no fixed: measure current window size as default. */
|
|
RECT wr;
|
|
GetWindowRect(cw_->hwnd, &wr);
|
|
int nat = is_vert ? (wr.bottom - wr.top) : (wr.right - wr.left);
|
|
if (nat < 1) nat = is_vert ? 24 : 80; /* reasonable fallback */
|
|
sizes[i] = nat;
|
|
fixed_used += nat;
|
|
}
|
|
if (i < n - 1) fixed_used += sp; /* spacing between children */
|
|
}
|
|
|
|
/* Available space for flex children. */
|
|
int avail = (is_vert ? (total_h - pt - pb) : (total_w - pl - pr)) - fixed_used;
|
|
if (avail < 0) avail = 0;
|
|
|
|
/* Second pass: assign flex sizes. */
|
|
if (total_flex > 0) {
|
|
for (int i = 0; i < n; i++) {
|
|
ElWidget* cw_ = el_widget_get(pw->children[i]);
|
|
if (!cw_ || !cw_->hwnd) continue;
|
|
if (cw_->flex > 0 && sizes[i] == 0) {
|
|
sizes[i] = (avail * cw_->flex) / total_flex;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Third pass: position children. */
|
|
int cursor = is_vert ? pt : pl;
|
|
for (int i = 0; i < n; i++) {
|
|
ElWidget* cw_ = el_widget_get(pw->children[i]);
|
|
if (!cw_ || !cw_->hwnd) continue;
|
|
|
|
int x, y, cw, ch;
|
|
if (is_vert) {
|
|
x = pl;
|
|
y = cursor;
|
|
cw = (cw_->fixed_w > 0) ? cw_->fixed_w : (total_w - pl - pr);
|
|
ch = sizes[i];
|
|
if (cw < 0) cw = 0;
|
|
if (ch < 0) ch = 0;
|
|
cursor += ch + sp;
|
|
} else {
|
|
x = cursor;
|
|
y = pt;
|
|
cw = sizes[i];
|
|
ch = (cw_->fixed_h > 0) ? cw_->fixed_h : (total_h - pt - pb);
|
|
if (cw < 0) cw = 0;
|
|
if (ch < 0) ch = 0;
|
|
cursor += cw + sp;
|
|
}
|
|
|
|
SetWindowPos(cw_->hwnd, NULL, x, y, cw, ch,
|
|
SWP_NOZORDER | SWP_NOACTIVATE);
|
|
|
|
/* Recursively reflow child containers. */
|
|
if (cw_->kind == EL_WIDGET_VSTACK ||
|
|
cw_->kind == EL_WIDGET_HSTACK ||
|
|
cw_->kind == EL_WIDGET_ZSTACK ||
|
|
cw_->kind == EL_WIDGET_SCROLL ||
|
|
cw_->kind == EL_WIDGET_WINDOW) {
|
|
el_win32_reflow(pw->children[i]);
|
|
}
|
|
}
|
|
|
|
free(sizes);
|
|
}
|
|
|
|
/* ── Container WndProc ────────────────────────────────────────────────────── */
|
|
/*
|
|
* Used for VSTACK, HSTACK, ZSTACK, SCROLL containers.
|
|
* Handles WM_SIZE (reflow), WM_CTLCOLOR* (color theming), WM_COMMAND (events),
|
|
* WM_VSCROLL (scroll container).
|
|
*/
|
|
static LRESULT CALLBACK el_container_wndproc(HWND hwnd, UINT msg,
|
|
WPARAM wp, LPARAM lp) {
|
|
int64_t slot = el_hwnd_to_slot(hwnd);
|
|
ElWidget* w = (slot >= 0) ? el_widget_get(slot) : NULL;
|
|
|
|
switch (msg) {
|
|
case WM_SIZE:
|
|
if (w) el_win32_reflow(slot);
|
|
return 0;
|
|
|
|
case WM_EL_REFLOW:
|
|
if (w) el_win32_reflow(slot);
|
|
return 0;
|
|
|
|
case WM_VSCROLL: {
|
|
if (!w || w->kind != EL_WIDGET_SCROLL) break;
|
|
SCROLLINFO si;
|
|
si.cbSize = sizeof(si);
|
|
si.fMask = SIF_ALL;
|
|
GetScrollInfo(hwnd, SB_VERT, &si);
|
|
int pos = si.nPos;
|
|
switch (LOWORD(wp)) {
|
|
case SB_LINEUP: pos -= 20; break;
|
|
case SB_LINEDOWN: pos += 20; break;
|
|
case SB_PAGEUP: pos -= si.nPage; break;
|
|
case SB_PAGEDOWN: pos += si.nPage; break;
|
|
case SB_THUMBTRACK: pos = si.nTrackPos; break;
|
|
case SB_TOP: pos = si.nMin; break;
|
|
case SB_BOTTOM: pos = si.nMax; break;
|
|
}
|
|
if (pos < si.nMin) pos = si.nMin;
|
|
if (pos > si.nMax - (int)si.nPage) pos = si.nMax - (int)si.nPage;
|
|
si.fMask = SIF_POS;
|
|
si.nPos = pos;
|
|
SetScrollInfo(hwnd, SB_VERT, &si, TRUE);
|
|
/* Scroll child. */
|
|
if (w->child_count > 0) {
|
|
ElWidget* cw_ = el_widget_get(w->children[0]);
|
|
if (cw_ && cw_->hwnd) {
|
|
RECT cr; GetClientRect(hwnd, &cr);
|
|
SetWindowPos(cw_->hwnd, NULL, 0, -pos,
|
|
cr.right - cr.left, 0,
|
|
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
case WM_COMMAND: {
|
|
/* Route click / change notifications to el callbacks. */
|
|
HWND ctrl = (HWND)lp;
|
|
if (!ctrl) break;
|
|
int64_t cs = el_hwnd_to_slot(ctrl);
|
|
ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL;
|
|
if (!cw) break;
|
|
|
|
WORD notif = HIWORD(wp);
|
|
if (cw->kind == EL_WIDGET_BUTTON && notif == BN_CLICKED) {
|
|
el_win32_invoke_cb(cw->cb_click, cs, "");
|
|
} else if (cw->kind == EL_WIDGET_TEXTFIELD) {
|
|
if (notif == EN_CHANGE) {
|
|
el_win32_invoke_cb(cw->cb_change, cs, "");
|
|
}
|
|
} else if (cw->kind == EL_WIDGET_TEXTAREA) {
|
|
if (notif == EN_CHANGE) {
|
|
el_win32_invoke_cb(cw->cb_change, cs, "");
|
|
}
|
|
}
|
|
/* Propagate to parent so top-level window also sees it. */
|
|
HWND parent = GetParent(hwnd);
|
|
if (parent) SendMessageW(parent, msg, wp, lp);
|
|
return 0;
|
|
}
|
|
|
|
case WM_CTLCOLORSTATIC:
|
|
case WM_CTLCOLOREDIT:
|
|
case WM_CTLCOLORBTN: {
|
|
HWND ctrl = (HWND)lp;
|
|
int64_t cs = el_hwnd_to_slot(ctrl);
|
|
ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL;
|
|
if (cw) {
|
|
HDC hdc = (HDC)wp;
|
|
if (cw->has_fg) SetTextColor(hdc, cw->fg_color);
|
|
if (cw->has_bg) {
|
|
SetBkColor(hdc, cw->bg_color);
|
|
if (cw->bg_brush) return (LRESULT)cw->bg_brush;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_ERASEBKGND: {
|
|
if (w && w->has_bg && w->bg_brush) {
|
|
HDC hdc = (HDC)wp;
|
|
RECT rc;
|
|
GetClientRect(hwnd, &rc);
|
|
FillRect(hdc, &rc, w->bg_brush);
|
|
return 1;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_DESTROY:
|
|
return 0;
|
|
}
|
|
|
|
return DefWindowProcW(hwnd, msg, wp, lp);
|
|
}
|
|
|
|
/* ── Top-level window WndProc ────────────────────────────────────────────── */
|
|
static LRESULT CALLBACK el_toplevel_wndproc(HWND hwnd, UINT msg,
|
|
WPARAM wp, LPARAM lp) {
|
|
int64_t slot = el_hwnd_to_slot(hwnd);
|
|
ElWidget* w = (slot >= 0) ? el_widget_get(slot) : NULL;
|
|
|
|
switch (msg) {
|
|
case WM_SIZE:
|
|
if (w) el_win32_reflow(slot);
|
|
return 0;
|
|
|
|
case WM_EL_REFLOW:
|
|
if (w) el_win32_reflow(slot);
|
|
return 0;
|
|
|
|
case WM_COMMAND: {
|
|
/* Bubble COMMAND messages from child controls. */
|
|
HWND ctrl = (HWND)lp;
|
|
if (!ctrl) break;
|
|
int64_t cs = el_hwnd_to_slot(ctrl);
|
|
ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL;
|
|
if (!cw) {
|
|
/* Not a direct slot child — pass to DefWindowProc. */
|
|
break;
|
|
}
|
|
WORD notif = HIWORD(wp);
|
|
if (cw->kind == EL_WIDGET_BUTTON && notif == BN_CLICKED) {
|
|
el_win32_invoke_cb(cw->cb_click, cs, "");
|
|
} else if ((cw->kind == EL_WIDGET_TEXTFIELD ||
|
|
cw->kind == EL_WIDGET_TEXTAREA) && notif == EN_CHANGE) {
|
|
el_win32_invoke_cb(cw->cb_change, cs, "");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
case WM_CTLCOLORSTATIC:
|
|
case WM_CTLCOLOREDIT:
|
|
case WM_CTLCOLORBTN: {
|
|
HWND ctrl = (HWND)lp;
|
|
int64_t cs = el_hwnd_to_slot(ctrl);
|
|
ElWidget* cw = (cs >= 0) ? el_widget_get(cs) : NULL;
|
|
if (cw) {
|
|
HDC hdc = (HDC)wp;
|
|
if (cw->has_fg) SetTextColor(hdc, cw->fg_color);
|
|
if (cw->has_bg) {
|
|
SetBkColor(hdc, cw->bg_color);
|
|
if (cw->bg_brush) return (LRESULT)cw->bg_brush;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_ERASEBKGND: {
|
|
if (w && w->has_bg && w->bg_brush) {
|
|
HDC hdc = (HDC)wp;
|
|
RECT rc;
|
|
GetClientRect(hwnd, &rc);
|
|
FillRect(hdc, &rc, w->bg_brush);
|
|
return 1;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case WM_GETMINMAXINFO: {
|
|
/* Stored in the window's GWLP_USERDATA as packed two ints: min_w | min_h. */
|
|
LONG_PTR ud = GetWindowLongPtrW(hwnd, GWLP_USERDATA);
|
|
if (ud) {
|
|
int min_w = (int)(ud & 0xFFFF);
|
|
int min_h = (int)((ud >> 16) & 0xFFFF);
|
|
MINMAXINFO* mm = (MINMAXINFO*)lp;
|
|
if (min_w > 0) mm->ptMinTrackSize.x = min_w;
|
|
if (min_h > 0) mm->ptMinTrackSize.y = min_h;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
case WM_CLOSE:
|
|
DestroyWindow(hwnd);
|
|
return 0;
|
|
|
|
case WM_DESTROY:
|
|
PostQuitMessage(0);
|
|
return 0;
|
|
}
|
|
|
|
return DefWindowProcW(hwnd, msg, wp, lp);
|
|
}
|
|
|
|
/* ── Subclass proc for EDIT controls (Enter key → on_submit) ────────────── */
|
|
static LRESULT CALLBACK el_edit_subclass_proc(HWND hwnd, UINT msg, WPARAM wp,
|
|
LPARAM lp, UINT_PTR uid,
|
|
DWORD_PTR ref_data) {
|
|
(void)uid; (void)ref_data;
|
|
if (msg == WM_KEYDOWN && wp == VK_RETURN) {
|
|
int64_t slot = el_hwnd_to_slot(hwnd);
|
|
ElWidget* w = (slot >= 0) ? el_widget_get(slot) : NULL;
|
|
if (w && w->cb_click) {
|
|
/* Gather current text as data. */
|
|
int len = GetWindowTextLengthW(hwnd);
|
|
WCHAR* wbuf = (WCHAR*)malloc((size_t)(len + 2) * sizeof(WCHAR));
|
|
if (wbuf) {
|
|
GetWindowTextW(hwnd, wbuf, len + 1);
|
|
char* utf8 = el_wide_to_utf8(wbuf);
|
|
free(wbuf);
|
|
el_win32_invoke_cb(w->cb_click, slot, utf8);
|
|
free(utf8);
|
|
}
|
|
return 0; /* suppress default beep */
|
|
}
|
|
}
|
|
return DefSubclassProc(hwnd, msg, wp, lp);
|
|
}
|
|
|
|
/* ── Window-class registration ────────────────────────────────────────────── */
|
|
static int _el_classes_registered = 0;
|
|
|
|
static void el_win32_register_classes(void) {
|
|
if (_el_classes_registered) return;
|
|
_el_classes_registered = 1;
|
|
|
|
/* Initialize common controls (needed for SetWindowSubclass). */
|
|
INITCOMMONCONTROLSEX icc;
|
|
icc.dwSize = sizeof(icc);
|
|
icc.dwICC = ICC_WIN95_CLASSES | ICC_STANDARD_CLASSES;
|
|
InitCommonControlsEx(&icc);
|
|
|
|
HINSTANCE hinst = GetModuleHandleW(NULL);
|
|
|
|
/* Container class (VStack, HStack, ZStack, Scroll) */
|
|
WNDCLASSW wc;
|
|
memset(&wc, 0, sizeof(wc));
|
|
wc.lpfnWndProc = el_container_wndproc;
|
|
wc.hInstance = hinst;
|
|
wc.hCursor = LoadCursorW(NULL, IDC_ARROW);
|
|
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
|
wc.lpszClassName = EL_WNDCLASS_VSTACK;
|
|
RegisterClassW(&wc);
|
|
|
|
wc.lpszClassName = EL_WNDCLASS_HSTACK;
|
|
RegisterClassW(&wc);
|
|
|
|
wc.lpszClassName = EL_WNDCLASS_ZSTACK;
|
|
RegisterClassW(&wc);
|
|
|
|
wc.style = CS_HREDRAW | CS_VREDRAW;
|
|
wc.lpszClassName = EL_WNDCLASS_SCROLL;
|
|
RegisterClassW(&wc);
|
|
|
|
/* Top-level window class */
|
|
memset(&wc, 0, sizeof(wc));
|
|
wc.style = CS_HREDRAW | CS_VREDRAW;
|
|
wc.lpfnWndProc = el_toplevel_wndproc;
|
|
wc.hInstance = hinst;
|
|
wc.hCursor = LoadCursorW(NULL, IDC_ARROW);
|
|
wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
|
|
wc.lpszClassName = EL_WNDCLASS_WINDOW;
|
|
RegisterClassW(&wc);
|
|
}
|
|
|
|
/* ── Public C API ─────────────────────────────────────────────────────────── */
|
|
|
|
/*
|
|
* el_win32_init — one-time initialisation. Must be called before any other
|
|
* el_win32_* function.
|
|
*/
|
|
void el_win32_init(void) {
|
|
static int done = 0;
|
|
if (done) return;
|
|
done = 1;
|
|
el_win32_register_classes();
|
|
}
|
|
|
|
/*
|
|
* el_win32_run_loop — enter the Win32 message loop. Never returns.
|
|
* Must be called from the main thread after all windows are shown.
|
|
*/
|
|
void el_win32_run_loop(void) {
|
|
MSG msg;
|
|
while (GetMessageW(&msg, NULL, 0, 0) > 0) {
|
|
TranslateMessage(&msg);
|
|
DispatchMessageW(&msg);
|
|
}
|
|
}
|
|
|
|
/* ── Window ───────────────────────────────────────────────────────────────── */
|
|
|
|
int64_t el_win32_window_create(const char* title, int width, int height,
|
|
int min_width, int min_height) {
|
|
el_win32_register_classes();
|
|
HINSTANCE hinst = GetModuleHandleW(NULL);
|
|
WCHAR* wtitle = el_utf8_to_wide(title ? title : "");
|
|
|
|
DWORD style = WS_OVERLAPPEDWINDOW;
|
|
HWND hwnd = CreateWindowW(
|
|
EL_WNDCLASS_WINDOW,
|
|
wtitle,
|
|
style,
|
|
CW_USEDEFAULT, CW_USEDEFAULT,
|
|
width, height,
|
|
NULL, NULL, hinst, NULL);
|
|
|
|
free(wtitle);
|
|
if (!hwnd) return -1;
|
|
|
|
/* Pack min_width / min_height into GWLP_USERDATA (low 16 = min_w, next 16 = min_h). */
|
|
LONG_PTR ud = ((LONG_PTR)(min_height & 0xFFFF) << 16) | (LONG_PTR)(min_width & 0xFFFF);
|
|
SetWindowLongPtrW(hwnd, GWLP_USERDATA, ud);
|
|
|
|
int64_t handle = el_widget_alloc(EL_WIDGET_WINDOW, hwnd);
|
|
if (handle < 0) { DestroyWindow(hwnd); return -1; }
|
|
|
|
ElWidget* w = el_widget_get(handle);
|
|
w->spacing = 0;
|
|
|
|
return handle;
|
|
}
|
|
|
|
void el_win32_window_show(int64_t handle) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || w->kind != EL_WIDGET_WINDOW || !w->hwnd) return;
|
|
ShowWindow(w->hwnd, SW_SHOW);
|
|
UpdateWindow(w->hwnd);
|
|
}
|
|
|
|
void el_win32_window_set_title(int64_t handle, const char* title) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || w->kind != EL_WIDGET_WINDOW || !w->hwnd) return;
|
|
WCHAR* wt = el_utf8_to_wide(title ? title : "");
|
|
SetWindowTextW(w->hwnd, wt);
|
|
free(wt);
|
|
}
|
|
|
|
/* ── Layout container factories ───────────────────────────────────────────── */
|
|
|
|
static int64_t el_win32_make_container(const WCHAR* class_name, ElWidgetKind kind,
|
|
int spacing) {
|
|
el_win32_register_classes();
|
|
HWND hwnd = CreateWindowW(
|
|
class_name,
|
|
L"",
|
|
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN,
|
|
0, 0, 0, 0,
|
|
HWND_MESSAGE, /* temporary parent; reparented on add_child */
|
|
NULL,
|
|
GetModuleHandleW(NULL),
|
|
NULL);
|
|
if (!hwnd) return -1;
|
|
|
|
int64_t handle = el_widget_alloc(kind, hwnd);
|
|
if (handle < 0) { DestroyWindow(hwnd); return -1; }
|
|
|
|
ElWidget* w = el_widget_get(handle);
|
|
w->spacing = spacing;
|
|
return handle;
|
|
}
|
|
|
|
int64_t el_win32_vstack_create(int spacing) {
|
|
return el_win32_make_container(EL_WNDCLASS_VSTACK, EL_WIDGET_VSTACK, spacing);
|
|
}
|
|
|
|
int64_t el_win32_hstack_create(int spacing) {
|
|
return el_win32_make_container(EL_WNDCLASS_HSTACK, EL_WIDGET_HSTACK, spacing);
|
|
}
|
|
|
|
int64_t el_win32_zstack_create(void) {
|
|
return el_win32_make_container(EL_WNDCLASS_ZSTACK, EL_WIDGET_ZSTACK, 0);
|
|
}
|
|
|
|
int64_t el_win32_scroll_create(void) {
|
|
HWND hwnd = CreateWindowW(
|
|
EL_WNDCLASS_SCROLL,
|
|
L"",
|
|
WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_VSCROLL,
|
|
0, 0, 0, 0,
|
|
HWND_MESSAGE,
|
|
NULL,
|
|
GetModuleHandleW(NULL),
|
|
NULL);
|
|
if (!hwnd) return -1;
|
|
|
|
int64_t handle = el_widget_alloc(EL_WIDGET_SCROLL, hwnd);
|
|
if (handle < 0) { DestroyWindow(hwnd); return -1; }
|
|
return handle;
|
|
}
|
|
|
|
/* ── Widget factories ─────────────────────────────────────────────────────── */
|
|
|
|
int64_t el_win32_label_create(const char* text) {
|
|
el_win32_register_classes();
|
|
WCHAR* wt = el_utf8_to_wide(text ? text : "");
|
|
HWND hwnd = CreateWindowW(
|
|
L"STATIC",
|
|
wt,
|
|
WS_CHILD | WS_VISIBLE | SS_LEFT,
|
|
0, 0, 0, 24,
|
|
HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL);
|
|
free(wt);
|
|
if (!hwnd) return -1;
|
|
return el_widget_alloc(EL_WIDGET_LABEL, hwnd);
|
|
}
|
|
|
|
int64_t el_win32_button_create(const char* label) {
|
|
el_win32_register_classes();
|
|
WCHAR* wl = el_utf8_to_wide(label ? label : "");
|
|
HWND hwnd = CreateWindowW(
|
|
L"BUTTON",
|
|
wl,
|
|
WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
|
|
0, 0, 0, 32,
|
|
HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL);
|
|
free(wl);
|
|
if (!hwnd) return -1;
|
|
return el_widget_alloc(EL_WIDGET_BUTTON, hwnd);
|
|
}
|
|
|
|
int64_t el_win32_text_field_create(const char* placeholder) {
|
|
el_win32_register_classes();
|
|
HWND hwnd = CreateWindowW(
|
|
L"EDIT",
|
|
L"",
|
|
WS_CHILD | WS_VISIBLE | WS_BORDER |
|
|
ES_AUTOHSCROLL | ES_LEFT,
|
|
0, 0, 0, 26,
|
|
HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL);
|
|
if (!hwnd) return -1;
|
|
|
|
/* Set placeholder / cue banner (Windows Vista+). */
|
|
if (placeholder && *placeholder) {
|
|
WCHAR* wp = el_utf8_to_wide(placeholder);
|
|
SendMessageW(hwnd, EM_SETCUEBANNER, TRUE, (LPARAM)wp);
|
|
free(wp);
|
|
}
|
|
|
|
int64_t handle = el_widget_alloc(EL_WIDGET_TEXTFIELD, hwnd);
|
|
if (handle < 0) { DestroyWindow(hwnd); return -1; }
|
|
|
|
/* Subclass to intercept VK_RETURN for on_submit. */
|
|
SetWindowSubclass(hwnd, el_edit_subclass_proc, (UINT_PTR)handle, 0);
|
|
return handle;
|
|
}
|
|
|
|
int64_t el_win32_text_area_create(const char* placeholder) {
|
|
el_win32_register_classes();
|
|
HWND hwnd = CreateWindowW(
|
|
L"EDIT",
|
|
L"",
|
|
WS_CHILD | WS_VISIBLE | WS_BORDER |
|
|
ES_MULTILINE | ES_AUTOVSCROLL | ES_WANTRETURN | WS_VSCROLL,
|
|
0, 0, 0, 80,
|
|
HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL);
|
|
if (!hwnd) return -1;
|
|
|
|
/* EDIT multiline does not support EM_SETCUEBANNER, so no placeholder. */
|
|
(void)placeholder;
|
|
|
|
int64_t handle = el_widget_alloc(EL_WIDGET_TEXTAREA, hwnd);
|
|
if (handle < 0) { DestroyWindow(hwnd); return -1; }
|
|
return handle;
|
|
}
|
|
|
|
int64_t el_win32_image_create(const char* path) {
|
|
el_win32_register_classes();
|
|
HWND hwnd = CreateWindowW(
|
|
L"STATIC",
|
|
L"",
|
|
WS_CHILD | WS_VISIBLE | SS_BITMAP | SS_REALSIZECONTROL,
|
|
0, 0, 0, 0,
|
|
HWND_MESSAGE, NULL, GetModuleHandleW(NULL), NULL);
|
|
if (!hwnd) return -1;
|
|
|
|
int64_t handle = el_widget_alloc(EL_WIDGET_IMAGE, hwnd);
|
|
if (handle < 0) { DestroyWindow(hwnd); return -1; }
|
|
|
|
ElWidget* w = el_widget_get(handle);
|
|
|
|
if (path && *path) {
|
|
WCHAR* wpath = el_utf8_to_wide(path);
|
|
HBITMAP hbmp = (HBITMAP)LoadImageW(NULL, wpath, IMAGE_BITMAP,
|
|
0, 0,
|
|
LR_LOADFROMFILE | LR_DEFAULTSIZE);
|
|
free(wpath);
|
|
if (hbmp) {
|
|
w->hbmp = hbmp;
|
|
SendMessageW(hwnd, STM_SETIMAGE, IMAGE_BITMAP, (LPARAM)hbmp);
|
|
}
|
|
}
|
|
|
|
return handle;
|
|
}
|
|
|
|
/* ── Widget property setters ─────────────────────────────────────────────── */
|
|
|
|
void el_win32_widget_set_text(int64_t handle, const char* text) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || !w->hwnd) return;
|
|
WCHAR* wt = el_utf8_to_wide(text ? text : "");
|
|
SetWindowTextW(w->hwnd, wt);
|
|
free(wt);
|
|
}
|
|
|
|
const char* el_win32_widget_get_text(int64_t handle) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || !w->hwnd) return strdup("");
|
|
int len = GetWindowTextLengthW(w->hwnd);
|
|
if (len <= 0) return strdup("");
|
|
WCHAR* wbuf = (WCHAR*)malloc((size_t)(len + 2) * sizeof(WCHAR));
|
|
if (!wbuf) return strdup("");
|
|
GetWindowTextW(w->hwnd, wbuf, len + 1);
|
|
char* result = el_wide_to_utf8(wbuf);
|
|
free(wbuf);
|
|
return result; /* caller must free() */
|
|
}
|
|
|
|
void el_win32_widget_set_color(int64_t handle, float r, float g, float b, float a) {
|
|
(void)a; /* Win32 classic does not support per-control alpha */
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || !w->hwnd) return;
|
|
w->fg_color = RGB((int)(r * 255.0f), (int)(g * 255.0f), (int)(b * 255.0f));
|
|
w->has_fg = 1;
|
|
InvalidateRect(w->hwnd, NULL, TRUE);
|
|
}
|
|
|
|
void el_win32_widget_set_bg_color(int64_t handle, float r, float g, float b, float a) {
|
|
(void)a;
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || !w->hwnd) return;
|
|
w->bg_color = RGB((int)(r * 255.0f), (int)(g * 255.0f), (int)(b * 255.0f));
|
|
w->has_bg = 1;
|
|
if (w->bg_brush) DeleteObject(w->bg_brush);
|
|
w->bg_brush = CreateSolidBrush(w->bg_color);
|
|
InvalidateRect(w->hwnd, NULL, TRUE);
|
|
}
|
|
|
|
void el_win32_widget_set_font(int64_t handle, const char* family, int size, int bold) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || !w->hwnd) return;
|
|
|
|
LOGFONTW lf;
|
|
memset(&lf, 0, sizeof(lf));
|
|
lf.lfHeight = -MulDiv(size > 0 ? size : 12, GetDeviceCaps(GetDC(NULL), LOGPIXELSY), 72);
|
|
lf.lfWeight = bold ? FW_BOLD : FW_NORMAL;
|
|
lf.lfCharSet = DEFAULT_CHARSET;
|
|
|
|
if (family && *family && strcmp(family, "system") != 0) {
|
|
WCHAR* wf = el_utf8_to_wide(family);
|
|
wcsncpy(lf.lfFaceName, wf, LF_FACESIZE - 1);
|
|
free(wf);
|
|
}
|
|
/* family == NULL or "system" → lfFaceName stays zeroed → system font */
|
|
|
|
HFONT hfont = CreateFontIndirectW(&lf);
|
|
if (!hfont) return;
|
|
|
|
if (w->font) DeleteObject(w->font);
|
|
w->font = hfont;
|
|
SendMessageW(w->hwnd, WM_SETFONT, (WPARAM)hfont, MAKELPARAM(TRUE, 0));
|
|
}
|
|
|
|
void el_win32_widget_set_padding(int64_t handle, int top, int right,
|
|
int bottom, int left) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w) return;
|
|
w->pad_top = top;
|
|
w->pad_right = right;
|
|
w->pad_bottom = bottom;
|
|
w->pad_left = left;
|
|
/* Trigger reflow if already parented. */
|
|
if (w->hwnd) PostMessageW(w->hwnd, WM_EL_REFLOW, 0, 0);
|
|
}
|
|
|
|
void el_win32_widget_set_width(int64_t handle, int width) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w) return;
|
|
w->fixed_w = width;
|
|
if (w->hwnd) {
|
|
RECT r; GetWindowRect(w->hwnd, &r);
|
|
int h = r.bottom - r.top;
|
|
SetWindowPos(w->hwnd, NULL, 0, 0, width, h,
|
|
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
}
|
|
|
|
void el_win32_widget_set_height(int64_t handle, int height) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w) return;
|
|
w->fixed_h = height;
|
|
if (w->hwnd) {
|
|
RECT r; GetWindowRect(w->hwnd, &r);
|
|
int cw = r.right - r.left;
|
|
SetWindowPos(w->hwnd, NULL, 0, 0, cw, height,
|
|
SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE);
|
|
}
|
|
}
|
|
|
|
void el_win32_widget_set_flex(int64_t handle, int flex) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w) return;
|
|
w->flex = flex;
|
|
/* Let parent reflow absorb the change. */
|
|
}
|
|
|
|
void el_win32_widget_set_corner_radius(int64_t handle, int radius) {
|
|
/*
|
|
* Classic Win32 does not have native per-control corner radii.
|
|
* If radius > 0 we apply a rounded rectangular window region so the
|
|
* control at least clips to a rounded rect. This is approximate — it
|
|
* does not anti-alias and may look boxy on high-DPI displays.
|
|
*/
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || !w->hwnd || radius <= 0) return;
|
|
RECT r; GetWindowRect(w->hwnd, &r);
|
|
int ww = r.right - r.left;
|
|
int wh = r.bottom - r.top;
|
|
if (ww <= 0 || wh <= 0) return;
|
|
HRGN rgn = CreateRoundRectRgn(0, 0, ww, wh, radius * 2, radius * 2);
|
|
SetWindowRgn(w->hwnd, rgn, TRUE);
|
|
/* Note: SetWindowRgn takes ownership of rgn; do NOT DeleteObject it. */
|
|
}
|
|
|
|
void el_win32_widget_set_disabled(int64_t handle, int disabled) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || !w->hwnd) return;
|
|
EnableWindow(w->hwnd, disabled ? FALSE : TRUE);
|
|
}
|
|
|
|
void el_win32_widget_set_hidden(int64_t handle, int hidden) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || !w->hwnd) return;
|
|
ShowWindow(w->hwnd, hidden ? SW_HIDE : SW_SHOW);
|
|
}
|
|
|
|
/* ── Child management ─────────────────────────────────────────────────────── */
|
|
|
|
/*
|
|
* el_win32_widget_add_child — attach child widget to parent.
|
|
*
|
|
* - WINDOW / VSTACK / HSTACK / ZSTACK: SetParent child into container.
|
|
* - SCROLL: first child becomes the scrolled content.
|
|
*
|
|
* After reparenting, triggers a layout reflow of the parent.
|
|
*/
|
|
void el_win32_widget_add_child(int64_t parent, int64_t child) {
|
|
ElWidget* pw = el_widget_get(parent);
|
|
ElWidget* cw = el_widget_get(child);
|
|
if (!pw || !cw || !pw->hwnd || !cw->hwnd) return;
|
|
if (cw->kind == EL_WIDGET_WINDOW) return; /* cannot add window as child */
|
|
if (pw->child_count >= EL_WIN32_MAX_CHILDREN) return;
|
|
|
|
/* Reparent the child HWND into the container. */
|
|
SetParent(cw->hwnd, pw->hwnd);
|
|
|
|
/* Make sure the WS_CHILD style is set and WS_POPUP is cleared. */
|
|
LONG_PTR style = GetWindowLongPtrW(cw->hwnd, GWL_STYLE);
|
|
style |= WS_CHILD;
|
|
style &= ~WS_POPUP;
|
|
SetWindowLongPtrW(cw->hwnd, GWL_STYLE, style);
|
|
ShowWindow(cw->hwnd, SW_SHOW);
|
|
|
|
pw->children[pw->child_count++] = child;
|
|
|
|
/* Reflow parent so the new child gets positioned. */
|
|
el_win32_reflow(parent);
|
|
}
|
|
|
|
void el_win32_widget_remove_child(int64_t parent, int64_t child) {
|
|
ElWidget* pw = el_widget_get(parent);
|
|
ElWidget* cw = el_widget_get(child);
|
|
if (!cw || !cw->hwnd) return;
|
|
|
|
/* Detach HWND from parent. */
|
|
SetParent(cw->hwnd, NULL);
|
|
|
|
/* Remove from parent's child list. */
|
|
if (pw) {
|
|
for (int i = 0; i < pw->child_count; i++) {
|
|
if (pw->children[i] == child) {
|
|
memmove(&pw->children[i], &pw->children[i + 1],
|
|
(size_t)(pw->child_count - i - 1) * sizeof(int64_t));
|
|
pw->child_count--;
|
|
break;
|
|
}
|
|
}
|
|
el_win32_reflow(parent);
|
|
}
|
|
}
|
|
|
|
/* ── Event registration ───────────────────────────────────────────────────── */
|
|
|
|
void el_win32_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_win32_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_win32_widget_on_submit(int64_t handle, const char* fn_name) {
|
|
/* For text fields, submit (Enter key) is stored in cb_click, same as AppKit. */
|
|
el_win32_widget_on_click(handle, fn_name);
|
|
}
|
|
|
|
/* ── Widget destroy ───────────────────────────────────────────────────────── */
|
|
|
|
void el_win32_widget_destroy(int64_t handle) {
|
|
ElWidget* w = el_widget_get(handle);
|
|
if (!w || !w->hwnd) return;
|
|
DestroyWindow(w->hwnd);
|
|
el_widget_free_slot(handle);
|
|
}
|
|
|
|
/* ── Manifest reader ──────────────────────────────────────────────────────── */
|
|
/*
|
|
* Same parser as the macOS implementation in el_seed.c.
|
|
* Reads the app{} block from a manifest.el file:
|
|
*
|
|
* app {
|
|
* window_title "My App"
|
|
* window_width 1200
|
|
* window_height 800
|
|
* min_width 600
|
|
* min_height 400
|
|
* }
|
|
*
|
|
* Returns JSON: {"title":"...","width":N,"height":N,"min_width":N,"min_height":N}
|
|
* Returns "{}" on failure.
|
|
*
|
|
* NOTE: el_seed.c already defines __manifest_read for both EL_TARGET_MACOS and
|
|
* EL_TARGET_WIN32 using the same parser body. We re-implement it here because
|
|
* el_seed.c's implementation is inside #ifdef EL_TARGET_MACOS; the Win32
|
|
* version lives here, symmetric with the macOS one.
|
|
*/
|
|
|
|
static void w32_manifest_skip_ws(const char** p) {
|
|
while (**p && (unsigned char)**p <= ' ') (*p)++;
|
|
}
|
|
|
|
static char* w32_manifest_read_string(const char** p) {
|
|
w32_manifest_skip_ws(p);
|
|
if (**p != '"') return NULL;
|
|
(*p)++;
|
|
const char* start = *p;
|
|
while (**p && **p != '"') {
|
|
if (**p == '\\') (*p)++;
|
|
(*p)++;
|
|
}
|
|
size_t len = (size_t)(*p - start);
|
|
if (**p == '"') (*p)++;
|
|
char* out = (char*)malloc(len + 1);
|
|
if (!out) return NULL;
|
|
memcpy(out, start, len);
|
|
out[len] = '\0';
|
|
return out;
|
|
}
|
|
|
|
static int64_t w32_manifest_read_int(const char** p) {
|
|
w32_manifest_skip_ws(p);
|
|
int64_t result = 0;
|
|
int neg = 0;
|
|
if (**p == '-') { neg = 1; (*p)++; }
|
|
while (**p >= '0' && **p <= '9') {
|
|
result = result * 10 + (**p - '0');
|
|
(*p)++;
|
|
}
|
|
return neg ? -result : result;
|
|
}
|
|
|
|
static const char* w32_manifest_find_app_block(const char* src) {
|
|
while (*src) {
|
|
if (src[0] == '/' && src[1] == '/') {
|
|
while (*src && *src != '\n') src++;
|
|
continue;
|
|
}
|
|
if (strncmp(src, "app", 3) == 0 &&
|
|
(src[3] == ' ' || src[3] == '\t' || src[3] == '\n' || src[3] == '{')) {
|
|
src += 3;
|
|
while (*src && *src != '{') src++;
|
|
if (*src == '{') return src + 1;
|
|
return NULL;
|
|
}
|
|
src++;
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* el_val_t helpers — mirror of what el_seed.c provides. */
|
|
#ifndef EL_CSTR
|
|
#define EL_CSTR(v) ((const char*)(uintptr_t)(v))
|
|
#endif
|
|
#ifndef EL_STR
|
|
#define EL_STR(p) ((el_val_t)(uintptr_t)(p))
|
|
#endif
|
|
|
|
el_val_t el_win32_manifest_read(const char* path) {
|
|
if (!path || !*path) return EL_STR(strdup("{}"));
|
|
|
|
FILE* f = fopen(path, "r");
|
|
if (!f) return EL_STR(strdup("{}"));
|
|
fseek(f, 0, SEEK_END);
|
|
long sz = ftell(f);
|
|
rewind(f);
|
|
if (sz <= 0) { fclose(f); return EL_STR(strdup("{}")); }
|
|
char* src = (char*)malloc((size_t)sz + 1);
|
|
if (!src) { fclose(f); return EL_STR(strdup("{}")); }
|
|
size_t got = fread(src, 1, (size_t)sz, f);
|
|
src[got] = '\0';
|
|
fclose(f);
|
|
|
|
const char* app_start = w32_manifest_find_app_block(src);
|
|
if (!app_start) { free(src); return EL_STR(strdup("{}")); }
|
|
|
|
char* title = NULL;
|
|
int64_t width = 1200, height = 800;
|
|
int64_t min_w = 600, min_h = 400;
|
|
|
|
const char* cur = app_start;
|
|
while (*cur && *cur != '}') {
|
|
while (*cur && (unsigned char)*cur <= ' ') cur++;
|
|
if (*cur == '}') break;
|
|
if (cur[0] == '/' && cur[1] == '/') {
|
|
while (*cur && *cur != '\n') cur++;
|
|
continue;
|
|
}
|
|
const char* key_start = cur;
|
|
while (*cur && (unsigned char)*cur > ' ' && *cur != '{' && *cur != '}') cur++;
|
|
size_t key_len = (size_t)(cur - key_start);
|
|
if (key_len == 0) { cur++; continue; }
|
|
while (*cur == ' ' || *cur == '\t') cur++;
|
|
|
|
if (key_len == 12 && strncmp(key_start, "window_title", 12) == 0) { free(title); title = w32_manifest_read_string(&cur); }
|
|
else if (key_len == 12 && strncmp(key_start, "window_width", 12) == 0) { width = w32_manifest_read_int(&cur); }
|
|
else if (key_len == 13 && strncmp(key_start, "window_height", 13) == 0) { height = w32_manifest_read_int(&cur); }
|
|
else if (key_len == 9 && strncmp(key_start, "min_width", 9) == 0) { min_w = w32_manifest_read_int(&cur); }
|
|
else if (key_len == 10 && strncmp(key_start, "min_height", 10) == 0) { min_h = w32_manifest_read_int(&cur); }
|
|
else { while (*cur && *cur != '\n' && *cur != '}') cur++; }
|
|
}
|
|
|
|
free(src);
|
|
|
|
const char* t = title ? title : "App";
|
|
size_t buf_sz = strlen(t) * 6 + 128;
|
|
char* buf = (char*)malloc(buf_sz);
|
|
if (!buf) { free(title); return EL_STR(strdup("{}")); }
|
|
snprintf(buf, buf_sz,
|
|
"{\"title\":\"%s\",\"width\":%lld,\"height\":%lld,\"min_width\":%lld,\"min_height\":%lld}",
|
|
t, (long long)width, (long long)height, (long long)min_w, (long long)min_h);
|
|
free(title);
|
|
return EL_STR(buf);
|
|
}
|
|
|
|
/* ── el_seed.c __* builtins for EL_TARGET_WIN32 ──────────────────────────── */
|
|
/*
|
|
* These are the thin el_val_t wrappers that el_seed.c (with EL_TARGET_WIN32)
|
|
* will call. They mirror the EL_TARGET_MACOS section in el_seed.c exactly.
|
|
*
|
|
* el_seed.c includes this file's declarations via el_native_target.h and
|
|
* links el_win32.o. The __* symbol definitions below are compiled into
|
|
* el_win32.c itself (not el_seed.c) when EL_TARGET_WIN32 is defined, keeping
|
|
* el_seed.c platform-agnostic.
|
|
*/
|
|
|
|
/* Helper: extract el string pointer. */
|
|
static inline const char* _w32_cstr(el_val_t v) {
|
|
return (const char*)(uintptr_t)v;
|
|
}
|
|
static inline el_val_t _w32_str(const char* p) {
|
|
return (el_val_t)(uintptr_t)p;
|
|
}
|
|
|
|
/* Bit-cast helper for float args (el_val_t carries IEEE 754 doubles). */
|
|
static inline double _w32_to_float(el_val_t v) {
|
|
union { double d; int64_t i; } u; u.i = (int64_t)v; return u.d;
|
|
}
|
|
|
|
void __native_init(void) { el_win32_init(); }
|
|
void __native_run_loop(void) { el_win32_run_loop(); }
|
|
|
|
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_win32_window_create(
|
|
_w32_cstr(title),
|
|
(int)(int64_t)width, (int)(int64_t)height,
|
|
(int)(int64_t)min_width, (int)(int64_t)min_height);
|
|
}
|
|
|
|
void __window_show(el_val_t h) { el_win32_window_show((int64_t)h); }
|
|
void __window_set_title(el_val_t h, el_val_t t) { el_win32_window_set_title((int64_t)h, _w32_cstr(t)); }
|
|
|
|
el_val_t __vstack_create(el_val_t spacing) { return (el_val_t)el_win32_vstack_create((int)(int64_t)spacing); }
|
|
el_val_t __hstack_create(el_val_t spacing) { return (el_val_t)el_win32_hstack_create((int)(int64_t)spacing); }
|
|
el_val_t __zstack_create(void) { return (el_val_t)el_win32_zstack_create(); }
|
|
el_val_t __scroll_create(void) { return (el_val_t)el_win32_scroll_create(); }
|
|
|
|
el_val_t __label_create(el_val_t text) { return (el_val_t)el_win32_label_create(_w32_cstr(text)); }
|
|
el_val_t __button_create(el_val_t label) { return (el_val_t)el_win32_button_create(_w32_cstr(label)); }
|
|
el_val_t __text_field_create(el_val_t placeholder) { return (el_val_t)el_win32_text_field_create(_w32_cstr(placeholder)); }
|
|
el_val_t __text_area_create(el_val_t placeholder) { return (el_val_t)el_win32_text_area_create(_w32_cstr(placeholder)); }
|
|
el_val_t __image_create(el_val_t path) { return (el_val_t)el_win32_image_create(_w32_cstr(path)); }
|
|
|
|
void __widget_set_text(el_val_t h, el_val_t t) { el_win32_widget_set_text((int64_t)h, _w32_cstr(t)); }
|
|
|
|
el_val_t __widget_get_text(el_val_t h) {
|
|
const char* s = el_win32_widget_get_text((int64_t)h);
|
|
/* el_win32_widget_get_text returns a malloc'd string; caller treats as
|
|
* short-lived (same contract as el_appkit version). */
|
|
return s ? _w32_str(s) : _w32_str(strdup(""));
|
|
}
|
|
|
|
void __widget_set_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) {
|
|
el_win32_widget_set_color((int64_t)h,
|
|
(float)_w32_to_float(r), (float)_w32_to_float(g),
|
|
(float)_w32_to_float(b), (float)_w32_to_float(a));
|
|
}
|
|
|
|
void __widget_set_bg_color(el_val_t h, el_val_t r, el_val_t g, el_val_t b, el_val_t a) {
|
|
el_win32_widget_set_bg_color((int64_t)h,
|
|
(float)_w32_to_float(r), (float)_w32_to_float(g),
|
|
(float)_w32_to_float(b), (float)_w32_to_float(a));
|
|
}
|
|
|
|
void __widget_set_font(el_val_t h, el_val_t family, el_val_t size, el_val_t bold) {
|
|
el_win32_widget_set_font((int64_t)h, _w32_cstr(family),
|
|
(int)(int64_t)size, (int)(int64_t)bold);
|
|
}
|
|
|
|
void __widget_set_padding(el_val_t h, el_val_t top, el_val_t right,
|
|
el_val_t bottom, el_val_t left) {
|
|
el_win32_widget_set_padding((int64_t)h,
|
|
(int)(int64_t)top, (int)(int64_t)right,
|
|
(int)(int64_t)bottom, (int)(int64_t)left);
|
|
}
|
|
|
|
void __widget_set_width(el_val_t h, el_val_t w) { el_win32_widget_set_width((int64_t)h, (int)(int64_t)w); }
|
|
void __widget_set_height(el_val_t h, el_val_t ht) { el_win32_widget_set_height((int64_t)h, (int)(int64_t)ht); }
|
|
void __widget_set_flex(el_val_t h, el_val_t f) { el_win32_widget_set_flex((int64_t)h, (int)(int64_t)f); }
|
|
void __widget_set_corner_radius(el_val_t h, el_val_t r) { el_win32_widget_set_corner_radius((int64_t)h, (int)(int64_t)r); }
|
|
void __widget_set_disabled(el_val_t h, el_val_t d) { el_win32_widget_set_disabled((int64_t)h, (int)(int64_t)d); }
|
|
void __widget_set_hidden(el_val_t h, el_val_t hid) { el_win32_widget_set_hidden((int64_t)h, (int)(int64_t)hid); }
|
|
|
|
void __widget_add_child(el_val_t p, el_val_t c) { el_win32_widget_add_child((int64_t)p, (int64_t)c); }
|
|
void __widget_remove_child(el_val_t p, el_val_t c) { el_win32_widget_remove_child((int64_t)p, (int64_t)c); }
|
|
void __widget_destroy(el_val_t h) { el_win32_widget_destroy((int64_t)h); }
|
|
|
|
void __widget_on_click(el_val_t h, el_val_t fn_name) { el_win32_widget_on_click((int64_t)h, _w32_cstr(fn_name)); }
|
|
void __widget_on_change(el_val_t h, el_val_t fn_name) { el_win32_widget_on_change((int64_t)h, _w32_cstr(fn_name)); }
|
|
void __widget_on_submit(el_val_t h, el_val_t fn_name) { el_win32_widget_on_submit((int64_t)h, _w32_cstr(fn_name)); }
|
|
|
|
el_val_t __manifest_read(el_val_t path) {
|
|
return el_win32_manifest_read(_w32_cstr(path));
|
|
}
|
|
|
|
#endif /* EL_TARGET_WIN32 */
|