ink-native 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -0,0 +1,443 @@
1
+ #ifndef FENSTER_H
2
+ #define FENSTER_H
3
+
4
+ #if defined(__APPLE__)
5
+ #include <CoreGraphics/CoreGraphics.h>
6
+ #include <objc/NSObjCRuntime.h>
7
+ #include <objc/objc-runtime.h>
8
+ #elif defined(_WIN32)
9
+ #include <windows.h>
10
+ #else
11
+ #define _DEFAULT_SOURCE 1
12
+ #include <X11/XKBlib.h>
13
+ #include <X11/Xlib.h>
14
+ #include <X11/keysym.h>
15
+ #include <time.h>
16
+ #endif
17
+
18
+ #include <stdint.h>
19
+ #include <stdlib.h>
20
+
21
+ struct fenster {
22
+ const char *title;
23
+ int width;
24
+ int height;
25
+ uint32_t *buf;
26
+ int keys[256]; /* keys are mostly ASCII, but arrows are 17..20 */
27
+ int mod; /* mod is 4 bits mask, ctrl=1, shift=2, alt=4, meta=8 */
28
+ int x;
29
+ int y;
30
+ int mouse;
31
+ float scale; /* backing scale factor (1.0 normal, 2.0 Retina) */
32
+ int phys_width; /* width * scale */
33
+ int phys_height; /* height * scale */
34
+ #if defined(__APPLE__)
35
+ id wnd;
36
+ #elif defined(_WIN32)
37
+ HWND hwnd;
38
+ #else
39
+ Display *dpy;
40
+ Window w;
41
+ GC gc;
42
+ XImage *img;
43
+ #endif
44
+ };
45
+
46
+ #ifndef FENSTER_API
47
+ #define FENSTER_API extern
48
+ #endif
49
+ FENSTER_API int fenster_open(struct fenster *f);
50
+ FENSTER_API int fenster_loop(struct fenster *f);
51
+ FENSTER_API void fenster_close(struct fenster *f);
52
+ FENSTER_API void fenster_sleep(int64_t ms);
53
+ FENSTER_API int64_t fenster_time(void);
54
+ #define fenster_pixel(f, x, y) ((f)->buf[((y) * (f)->width) + (x)])
55
+
56
+ #ifndef FENSTER_HEADER
57
+ #if defined(__APPLE__)
58
+ #define msg(r, o, s) ((r(*)(id, SEL))objc_msgSend)(o, sel_getUid(s))
59
+ #define msg1(r, o, s, A, a) \
60
+ ((r(*)(id, SEL, A))objc_msgSend)(o, sel_getUid(s), a)
61
+ #define msg2(r, o, s, A, a, B, b) \
62
+ ((r(*)(id, SEL, A, B))objc_msgSend)(o, sel_getUid(s), a, b)
63
+ #define msg3(r, o, s, A, a, B, b, C, c) \
64
+ ((r(*)(id, SEL, A, B, C))objc_msgSend)(o, sel_getUid(s), a, b, c)
65
+ #define msg4(r, o, s, A, a, B, b, C, c, D, d) \
66
+ ((r(*)(id, SEL, A, B, C, D))objc_msgSend)(o, sel_getUid(s), a, b, c, d)
67
+
68
+ #define cls(x) ((id)objc_getClass(x))
69
+
70
+ extern id const NSDefaultRunLoopMode;
71
+ extern id const NSApp;
72
+
73
+ static void fenster_draw_rect(id v, SEL s, CGRect r) {
74
+ (void)r, (void)s;
75
+ struct fenster *f = (struct fenster *)objc_getAssociatedObject(v, "fenster");
76
+ CGContextRef context =
77
+ msg(CGContextRef, msg(id, cls("NSGraphicsContext"), "currentContext"),
78
+ "graphicsPort");
79
+ CGColorSpaceRef space = CGColorSpaceCreateDeviceRGB();
80
+ CGDataProviderRef provider = CGDataProviderCreateWithData(
81
+ NULL, f->buf, (size_t)f->phys_width * (size_t)f->phys_height * 4, NULL);
82
+ CGImageRef img =
83
+ CGImageCreate(f->phys_width, f->phys_height, 8, 32, f->phys_width * 4,
84
+ space,
85
+ kCGImageAlphaNoneSkipFirst | kCGBitmapByteOrder32Little,
86
+ provider, NULL, false, kCGRenderingIntentDefault);
87
+ CGColorSpaceRelease(space);
88
+ CGDataProviderRelease(provider);
89
+ CGContextDrawImage(context, CGRectMake(0, 0, f->width, f->height), img);
90
+ CGImageRelease(img);
91
+ }
92
+
93
+ static BOOL fenster_should_close(id v, SEL s, id w) {
94
+ (void)v, (void)s, (void)w;
95
+ msg1(void, NSApp, "terminate:", id, NSApp);
96
+ return YES;
97
+ }
98
+
99
+ FENSTER_API int fenster_open(struct fenster *f) {
100
+ msg(id, cls("NSApplication"), "sharedApplication");
101
+ msg1(void, NSApp, "setActivationPolicy:", NSInteger, 0);
102
+ f->wnd = msg4(id, msg(id, cls("NSWindow"), "alloc"),
103
+ "initWithContentRect:styleMask:backing:defer:", CGRect,
104
+ CGRectMake(0, 0, f->width, f->height), NSUInteger, 11,
105
+ NSUInteger, 2, BOOL, NO);
106
+ Class windelegate =
107
+ objc_allocateClassPair((Class)cls("NSObject"), "FensterDelegate", 0);
108
+ class_addMethod(windelegate, sel_getUid("windowShouldClose:"),
109
+ (IMP)fenster_should_close, "c@:@");
110
+ objc_registerClassPair(windelegate);
111
+ msg1(void, f->wnd, "setDelegate:", id,
112
+ msg(id, msg(id, (id)windelegate, "alloc"), "init"));
113
+ Class c = objc_allocateClassPair((Class)cls("NSView"), "FensterView", 0);
114
+ class_addMethod(c, sel_getUid("drawRect:"), (IMP)fenster_draw_rect, "i@:@@");
115
+ objc_registerClassPair(c);
116
+
117
+ id v = msg(id, msg(id, (id)c, "alloc"), "init");
118
+ msg1(void, f->wnd, "setContentView:", id, v);
119
+ objc_setAssociatedObject(v, "fenster", (id)f, OBJC_ASSOCIATION_ASSIGN);
120
+
121
+ id title = msg1(id, cls("NSString"), "stringWithUTF8String:", const char *,
122
+ f->title);
123
+ msg1(void, f->wnd, "setTitle:", id, title);
124
+ msg1(void, f->wnd, "makeKeyAndOrderFront:", id, nil);
125
+ msg(void, f->wnd, "center");
126
+ msg1(void, NSApp, "activateIgnoringOtherApps:", BOOL, YES);
127
+
128
+ /* Detect Retina backing scale factor */
129
+ CGFloat s = ((CGFloat(*)(id, SEL))objc_msgSend)(f->wnd,
130
+ sel_getUid("backingScaleFactor"));
131
+ f->scale = (float)(s > 0 ? s : 1.0);
132
+ f->phys_width = (int)(f->width * f->scale);
133
+ f->phys_height = (int)(f->height * f->scale);
134
+
135
+ return 0;
136
+ }
137
+
138
+ FENSTER_API void fenster_close(struct fenster *f) {
139
+ msg(void, f->wnd, "close");
140
+ }
141
+
142
+ // clang-format off
143
+ static const uint8_t FENSTER_KEYCODES[128] = {65,83,68,70,72,71,90,88,67,86,0,66,81,87,69,82,89,84,49,50,51,52,54,53,61,57,55,45,56,48,93,79,85,91,73,80,10,76,74,39,75,59,92,44,47,78,77,46,9,32,96,8,0,27,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,26,2,3,127,0,5,0,4,0,20,19,18,17,0};
144
+ // clang-format on
145
+ FENSTER_API int fenster_loop(struct fenster *f) {
146
+ msg1(void, msg(id, f->wnd, "contentView"), "setNeedsDisplay:", BOOL, YES);
147
+ id ev = msg4(id, NSApp,
148
+ "nextEventMatchingMask:untilDate:inMode:dequeue:", NSUInteger,
149
+ NSUIntegerMax, id, NULL, id, NSDefaultRunLoopMode, BOOL, YES);
150
+ if (!ev)
151
+ return 0;
152
+ NSUInteger evtype = msg(NSUInteger, ev, "type");
153
+ switch (evtype) {
154
+ case 1: /* NSEventTypeMouseDown */
155
+ f->mouse |= 1;
156
+ break;
157
+ case 2: /* NSEventTypeMouseUp*/
158
+ f->mouse &= ~1;
159
+ break;
160
+ case 5:
161
+ case 6: { /* NSEventTypeMouseMoved */
162
+ CGPoint xy = msg(CGPoint, ev, "locationInWindow");
163
+ f->x = (int)xy.x;
164
+ f->y = (int)(f->height - xy.y);
165
+ return 0;
166
+ }
167
+ case 10: /*NSEventTypeKeyDown*/
168
+ case 11: /*NSEventTypeKeyUp:*/ {
169
+ NSUInteger k = msg(NSUInteger, ev, "keyCode");
170
+ f->keys[k < 127 ? FENSTER_KEYCODES[k] : 0] = evtype == 10;
171
+ NSUInteger mod = msg(NSUInteger, ev, "modifierFlags") >> 17;
172
+ f->mod = (mod & 0xc) | ((mod & 1) << 1) | ((mod >> 1) & 1);
173
+ return 0;
174
+ }
175
+ }
176
+ msg1(void, NSApp, "sendEvent:", id, ev);
177
+ /* Poll content view frame for resize */
178
+ CGRect frame = msg(CGRect, msg(id, f->wnd, "contentView"), "frame");
179
+ int newW = (int)frame.size.width;
180
+ int newH = (int)frame.size.height;
181
+ if (newW > 0 && newH > 0) {
182
+ f->width = newW;
183
+ f->height = newH;
184
+ }
185
+ /* Re-query scale (may change when window moves between displays) */
186
+ CGFloat s = ((CGFloat(*)(id, SEL))objc_msgSend)(f->wnd,
187
+ sel_getUid("backingScaleFactor"));
188
+ f->scale = (float)(s > 0 ? s : 1.0);
189
+ f->phys_width = (int)(f->width * f->scale);
190
+ f->phys_height = (int)(f->height * f->scale);
191
+ return 0;
192
+ }
193
+ #elif defined(_WIN32)
194
+ // clang-format off
195
+ static const uint8_t FENSTER_KEYCODES[] = {0,27,49,50,51,52,53,54,55,56,57,48,45,61,8,9,81,87,69,82,84,89,85,73,79,80,91,93,10,0,65,83,68,70,71,72,74,75,76,59,39,96,0,92,90,88,67,86,66,78,77,44,46,47,0,0,0,32,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,17,3,0,20,0,19,0,5,18,4,26,127};
196
+ // clang-format on
197
+ typedef struct BINFO{
198
+ BITMAPINFOHEADER bmiHeader;
199
+ RGBQUAD bmiColors[3];
200
+ }BINFO;
201
+ static LRESULT CALLBACK fenster_wndproc(HWND hwnd, UINT msg, WPARAM wParam,
202
+ LPARAM lParam) {
203
+ struct fenster *f = (struct fenster *)GetWindowLongPtr(hwnd, GWLP_USERDATA);
204
+ switch (msg) {
205
+ case WM_PAINT: {
206
+ PAINTSTRUCT ps;
207
+ HDC hdc = BeginPaint(hwnd, &ps);
208
+ HDC memdc = CreateCompatibleDC(hdc);
209
+ HBITMAP hbmp = CreateCompatibleBitmap(hdc, f->phys_width, f->phys_height);
210
+ HBITMAP oldbmp = SelectObject(memdc, hbmp);
211
+ BINFO bi = {{sizeof(bi), f->phys_width, -f->phys_height, 1, 32,
212
+ BI_BITFIELDS}};
213
+ bi.bmiColors[0].rgbRed = 0xff;
214
+ bi.bmiColors[1].rgbGreen = 0xff;
215
+ bi.bmiColors[2].rgbBlue = 0xff;
216
+ SetDIBitsToDevice(memdc, 0, 0, f->phys_width, f->phys_height, 0, 0, 0,
217
+ f->phys_height, f->buf, (BITMAPINFO *)&bi,
218
+ DIB_RGB_COLORS);
219
+ SetStretchBltMode(hdc, COLORONCOLOR);
220
+ StretchBlt(hdc, 0, 0, f->width, f->height, memdc, 0, 0, f->phys_width,
221
+ f->phys_height, SRCCOPY);
222
+ SelectObject(memdc, oldbmp);
223
+ DeleteObject(hbmp);
224
+ DeleteDC(memdc);
225
+ EndPaint(hwnd, &ps);
226
+ } break;
227
+ case WM_CLOSE:
228
+ DestroyWindow(hwnd);
229
+ break;
230
+ case WM_LBUTTONDOWN:
231
+ case WM_LBUTTONUP:
232
+ f->mouse = (msg == WM_LBUTTONDOWN);
233
+ break;
234
+ case WM_MOUSEMOVE:
235
+ f->y = HIWORD(lParam), f->x = LOWORD(lParam);
236
+ break;
237
+ case WM_KEYDOWN:
238
+ case WM_KEYUP: {
239
+ f->mod = ((GetKeyState(VK_CONTROL) & 0x8000) >> 15) |
240
+ ((GetKeyState(VK_SHIFT) & 0x8000) >> 14) |
241
+ ((GetKeyState(VK_MENU) & 0x8000) >> 13) |
242
+ (((GetKeyState(VK_LWIN) | GetKeyState(VK_RWIN)) & 0x8000) >> 12);
243
+ f->keys[FENSTER_KEYCODES[HIWORD(lParam) & 0x1ff]] = !((lParam >> 31) & 1);
244
+ } break;
245
+ case WM_SIZE:
246
+ f->width = LOWORD(lParam);
247
+ f->height = HIWORD(lParam);
248
+ f->phys_width = (int)(f->width * f->scale);
249
+ f->phys_height = (int)(f->height * f->scale);
250
+ break;
251
+ case WM_DPICHANGED: {
252
+ UINT dpi = HIWORD(wParam);
253
+ f->scale = (float)dpi / 96.0f;
254
+ f->phys_width = (int)(f->width * f->scale);
255
+ f->phys_height = (int)(f->height * f->scale);
256
+ RECT *rc = (RECT *)lParam;
257
+ SetWindowPos(hwnd, NULL, rc->left, rc->top, rc->right - rc->left,
258
+ rc->bottom - rc->top, SWP_NOZORDER | SWP_NOACTIVATE);
259
+ } break;
260
+ case WM_DESTROY:
261
+ PostQuitMessage(0);
262
+ break;
263
+ default:
264
+ return DefWindowProc(hwnd, msg, wParam, lParam);
265
+ }
266
+ return 0;
267
+ }
268
+
269
+ FENSTER_API int fenster_open(struct fenster *f) {
270
+ HINSTANCE hInstance = GetModuleHandle(NULL);
271
+ WNDCLASSEX wc = {0};
272
+ wc.cbSize = sizeof(WNDCLASSEX);
273
+ wc.style = CS_VREDRAW | CS_HREDRAW;
274
+ wc.lpfnWndProc = fenster_wndproc;
275
+ wc.hInstance = hInstance;
276
+ wc.lpszClassName = f->title;
277
+ RegisterClassEx(&wc);
278
+ f->hwnd = CreateWindowEx(WS_EX_CLIENTEDGE, f->title, f->title,
279
+ WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
280
+ f->width, f->height, NULL, NULL, hInstance, NULL);
281
+
282
+ if (f->hwnd == NULL)
283
+ return -1;
284
+ SetWindowLongPtr(f->hwnd, GWLP_USERDATA, (LONG_PTR)f);
285
+ ShowWindow(f->hwnd, SW_NORMAL);
286
+ UpdateWindow(f->hwnd);
287
+
288
+ /* Detect DPI scale via GetDpiForWindow (Win10 1607+, backward compat) */
289
+ f->scale = 1.0f;
290
+ {
291
+ HMODULE user32 = GetModuleHandleA("user32.dll");
292
+ if (user32) {
293
+ typedef UINT (WINAPI *PFN_GetDpiForWindow)(HWND);
294
+ PFN_GetDpiForWindow pGetDpiForWindow =
295
+ (PFN_GetDpiForWindow)GetProcAddress(user32, "GetDpiForWindow");
296
+ if (pGetDpiForWindow) {
297
+ UINT dpi = pGetDpiForWindow(f->hwnd);
298
+ if (dpi > 0) f->scale = (float)dpi / 96.0f;
299
+ }
300
+ }
301
+ }
302
+ f->phys_width = (int)(f->width * f->scale);
303
+ f->phys_height = (int)(f->height * f->scale);
304
+
305
+ return 0;
306
+ }
307
+
308
+ FENSTER_API void fenster_close(struct fenster *f) { (void)f; }
309
+
310
+ FENSTER_API int fenster_loop(struct fenster *f) {
311
+ MSG msg;
312
+ while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
313
+ if (msg.message == WM_QUIT)
314
+ return -1;
315
+ TranslateMessage(&msg);
316
+ DispatchMessage(&msg);
317
+ }
318
+ InvalidateRect(f->hwnd, NULL, TRUE);
319
+ return 0;
320
+ }
321
+ #else
322
+ // clang-format off
323
+ static int FENSTER_KEYCODES[124] = {XK_BackSpace,8,XK_Delete,127,XK_Down,18,XK_End,5,XK_Escape,27,XK_Home,2,XK_Insert,26,XK_Left,20,XK_Page_Down,4,XK_Page_Up,3,XK_Return,10,XK_Right,19,XK_Tab,9,XK_Up,17,XK_apostrophe,39,XK_backslash,92,XK_bracketleft,91,XK_bracketright,93,XK_comma,44,XK_equal,61,XK_grave,96,XK_minus,45,XK_period,46,XK_semicolon,59,XK_slash,47,XK_space,32,XK_a,65,XK_b,66,XK_c,67,XK_d,68,XK_e,69,XK_f,70,XK_g,71,XK_h,72,XK_i,73,XK_j,74,XK_k,75,XK_l,76,XK_m,77,XK_n,78,XK_o,79,XK_p,80,XK_q,81,XK_r,82,XK_s,83,XK_t,84,XK_u,85,XK_v,86,XK_w,87,XK_x,88,XK_y,89,XK_z,90,XK_0,48,XK_1,49,XK_2,50,XK_3,51,XK_4,52,XK_5,53,XK_6,54,XK_7,55,XK_8,56,XK_9,57};
324
+ // clang-format on
325
+ FENSTER_API int fenster_open(struct fenster *f) {
326
+ f->dpy = XOpenDisplay(NULL);
327
+ int screen = DefaultScreen(f->dpy);
328
+ f->w = XCreateSimpleWindow(f->dpy, RootWindow(f->dpy, screen), 0, 0, f->width,
329
+ f->height, 0, BlackPixel(f->dpy, screen),
330
+ WhitePixel(f->dpy, screen));
331
+ f->gc = XCreateGC(f->dpy, f->w, 0, 0);
332
+ XSelectInput(f->dpy, f->w,
333
+ ExposureMask | KeyPressMask | KeyReleaseMask | ButtonPressMask |
334
+ ButtonReleaseMask | PointerMotionMask |
335
+ StructureNotifyMask);
336
+ XStoreName(f->dpy, f->w, f->title);
337
+ XMapWindow(f->dpy, f->w);
338
+ XSync(f->dpy, f->w);
339
+ f->img = XCreateImage(f->dpy, DefaultVisual(f->dpy, 0), 24, ZPixmap, 0,
340
+ (char *)f->buf, f->width, f->height, 32, 0);
341
+ /* Linux: default scale 1.0 (no standard HiDPI detection) */
342
+ f->scale = 1.0f;
343
+ f->phys_width = f->width;
344
+ f->phys_height = f->height;
345
+ return 0;
346
+ }
347
+ FENSTER_API void fenster_close(struct fenster *f) { XCloseDisplay(f->dpy); }
348
+ FENSTER_API int fenster_loop(struct fenster *f) {
349
+ XEvent ev;
350
+ XPutImage(f->dpy, f->w, f->gc, f->img, 0, 0, 0, 0, f->width, f->height);
351
+ XFlush(f->dpy);
352
+ while (XPending(f->dpy)) {
353
+ XNextEvent(f->dpy, &ev);
354
+ switch (ev.type) {
355
+ case ButtonPress:
356
+ case ButtonRelease:
357
+ f->mouse = (ev.type == ButtonPress);
358
+ break;
359
+ case MotionNotify:
360
+ f->x = ev.xmotion.x, f->y = ev.xmotion.y;
361
+ break;
362
+ case KeyPress:
363
+ case KeyRelease: {
364
+ int m = ev.xkey.state;
365
+ int k = XkbKeycodeToKeysym(f->dpy, ev.xkey.keycode, 0, 0);
366
+ for (unsigned int i = 0; i < 124; i += 2) {
367
+ if (FENSTER_KEYCODES[i] == k) {
368
+ f->keys[FENSTER_KEYCODES[i + 1]] = (ev.type == KeyPress);
369
+ break;
370
+ }
371
+ }
372
+ f->mod = (!!(m & ControlMask)) | (!!(m & ShiftMask) << 1) |
373
+ (!!(m & Mod1Mask) << 2) | (!!(m & Mod4Mask) << 3);
374
+ } break;
375
+ case ConfigureNotify:
376
+ f->width = ev.xconfigure.width;
377
+ f->height = ev.xconfigure.height;
378
+ break;
379
+ }
380
+ }
381
+ return 0;
382
+ }
383
+ #endif
384
+
385
+ #ifdef _WIN32
386
+ FENSTER_API void fenster_sleep(int64_t ms) { Sleep(ms); }
387
+ FENSTER_API int64_t fenster_time() {
388
+ LARGE_INTEGER freq, count;
389
+ QueryPerformanceFrequency(&freq);
390
+ QueryPerformanceCounter(&count);
391
+ return (int64_t)(count.QuadPart * 1000.0 / freq.QuadPart);
392
+ }
393
+ #else
394
+ FENSTER_API void fenster_sleep(int64_t ms) {
395
+ struct timespec ts;
396
+ ts.tv_sec = ms / 1000;
397
+ ts.tv_nsec = (ms % 1000) * 1000000;
398
+ nanosleep(&ts, NULL);
399
+ }
400
+ FENSTER_API int64_t fenster_time(void) {
401
+ struct timespec time;
402
+ clock_gettime(CLOCK_REALTIME, &time);
403
+ return time.tv_sec * 1000 + (time.tv_nsec / 1000000);
404
+ }
405
+ #endif
406
+
407
+ #ifdef __cplusplus
408
+ class Fenster {
409
+ struct fenster f;
410
+ int64_t now;
411
+
412
+ public:
413
+ Fenster(int w, int h, const char *title)
414
+ : f{.title = title, .width = w, .height = h} {
415
+ this->f.buf = new uint32_t[w * h];
416
+ this->now = fenster_time();
417
+ fenster_open(&this->f);
418
+ }
419
+ ~Fenster() {
420
+ fenster_close(&this->f);
421
+ delete[] this->f.buf;
422
+ }
423
+ bool loop(const int fps) {
424
+ int64_t t = fenster_time();
425
+ if (t - this->now < 1000 / fps) {
426
+ fenster_sleep(t - now);
427
+ }
428
+ this->now = t;
429
+ return fenster_loop(&this->f) == 0;
430
+ }
431
+ inline uint32_t &px(const int x, const int y) {
432
+ return fenster_pixel(&this->f, x, y);
433
+ }
434
+ bool key(int c) { return c >= 0 && c < 128 ? this->f.keys[c] : false; }
435
+ int x() { return this->f.x; }
436
+ int y() { return this->f.y; }
437
+ int mouse() { return this->f.mouse; }
438
+ int mod() { return this->f.mod; }
439
+ };
440
+ #endif /* __cplusplus */
441
+
442
+ #endif /* !FENSTER_HEADER */
443
+ #endif /* FENSTER_H */
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Fenster Bridge
3
+ *
4
+ * Thin C wrapper around fenster.h for koffi FFI access.
5
+ * Fenster uses inline/static functions that koffi can't call directly,
6
+ * so this bridge exposes them as proper exported symbols.
7
+ */
8
+
9
+ #include <stdlib.h>
10
+ #include <string.h>
11
+ #include "fenster.h"
12
+
13
+ typedef struct {
14
+ struct fenster f;
15
+ uint32_t *buf;
16
+ char *title_copy;
17
+ int resized;
18
+ int prev_width;
19
+ int prev_height;
20
+ float prev_scale;
21
+ } fenster_bridge;
22
+
23
+ fenster_bridge *fenster_bridge_create(const char *title, int w, int h) {
24
+ fenster_bridge *fb = calloc(1, sizeof(fenster_bridge));
25
+ if (!fb)
26
+ return NULL;
27
+
28
+ fb->buf = calloc((size_t)w * (size_t)h, sizeof(uint32_t));
29
+ if (!fb->buf) {
30
+ free(fb);
31
+ return NULL;
32
+ }
33
+
34
+ /* Copy title string so caller doesn't need to keep it alive */
35
+ fb->title_copy = strdup(title);
36
+
37
+ /* Initialize fenster struct */
38
+ struct fenster init = {
39
+ .title = fb->title_copy, .width = w, .height = h, .buf = fb->buf};
40
+ memcpy(&fb->f, &init, sizeof(struct fenster));
41
+
42
+ fb->prev_width = w;
43
+ fb->prev_height = h;
44
+ fb->prev_scale = 1.0f;
45
+ fb->resized = 0;
46
+
47
+ return fb;
48
+ }
49
+
50
+ int fenster_bridge_open(fenster_bridge *fb) {
51
+ int result = fenster_open(&fb->f);
52
+ if (result != 0) return result;
53
+
54
+ /* Reallocate buffer at physical (scaled) dimensions */
55
+ if (fb->f.phys_width != fb->prev_width ||
56
+ fb->f.phys_height != fb->prev_height) {
57
+ uint32_t *new_buf = calloc((size_t)fb->f.phys_width *
58
+ (size_t)fb->f.phys_height, sizeof(uint32_t));
59
+ if (new_buf) {
60
+ free(fb->buf);
61
+ fb->buf = new_buf;
62
+ fb->f.buf = new_buf;
63
+ fb->prev_width = fb->f.phys_width;
64
+ fb->prev_height = fb->f.phys_height;
65
+ }
66
+ }
67
+ fb->prev_scale = fb->f.scale;
68
+
69
+ return 0;
70
+ }
71
+
72
+ int fenster_bridge_loop(fenster_bridge *fb) {
73
+ int result = fenster_loop(&fb->f);
74
+
75
+ /* Detect resize by physical dimensions (catches both logical resize and
76
+ * scale changes, e.g. window moved to a different-DPI display) */
77
+ if (fb->f.phys_width != fb->prev_width ||
78
+ fb->f.phys_height != fb->prev_height) {
79
+ int newW = fb->f.phys_width;
80
+ int newH = fb->f.phys_height;
81
+
82
+ /* Reallocate pixel buffer at physical dimensions */
83
+ uint32_t *new_buf = calloc((size_t)newW * (size_t)newH, sizeof(uint32_t));
84
+ if (new_buf) {
85
+ free(fb->buf);
86
+ fb->buf = new_buf;
87
+ fb->f.buf = new_buf;
88
+
89
+ #if !defined(__APPLE__) && !defined(_WIN32)
90
+ /* Linux: recreate XImage with new buffer */
91
+ if (fb->f.img) {
92
+ fb->f.img->data = NULL; /* prevent XDestroyImage from freeing our buf */
93
+ XDestroyImage(fb->f.img);
94
+ }
95
+ fb->f.img =
96
+ XCreateImage(fb->f.dpy, DefaultVisual(fb->f.dpy, 0), 24, ZPixmap, 0,
97
+ (char *)new_buf, newW, newH, 32, 0);
98
+ #endif
99
+
100
+ fb->resized = 1;
101
+ fb->prev_width = newW;
102
+ fb->prev_height = newH;
103
+ fb->prev_scale = fb->f.scale;
104
+ }
105
+ }
106
+
107
+ return result;
108
+ }
109
+
110
+ void fenster_bridge_close(fenster_bridge *fb) {
111
+ if (!fb)
112
+ return;
113
+ fenster_close(&fb->f);
114
+ free(fb->buf);
115
+ free(fb->title_copy);
116
+ free(fb);
117
+ }
118
+
119
+ uint32_t *fenster_bridge_get_buf(fenster_bridge *fb) { return fb->buf; }
120
+
121
+ void fenster_bridge_copy_buf(fenster_bridge *fb, const void *src,
122
+ int byte_length) {
123
+ memcpy(fb->buf, src, (size_t)byte_length);
124
+ }
125
+
126
+ int *fenster_bridge_get_keys(fenster_bridge *fb) { return fb->f.keys; }
127
+
128
+ int fenster_bridge_get_mod(fenster_bridge *fb) { return fb->f.mod; }
129
+
130
+ void fenster_bridge_get_size(fenster_bridge *fb, int *w, int *h) {
131
+ *w = fb->f.width;
132
+ *h = fb->f.height;
133
+ }
134
+
135
+ int fenster_bridge_get_resized(fenster_bridge *fb, int *w, int *h) {
136
+ int was_resized = fb->resized;
137
+ *w = fb->f.phys_width;
138
+ *h = fb->f.phys_height;
139
+ fb->resized = 0;
140
+ return was_resized;
141
+ }
142
+
143
+ float fenster_bridge_get_scale(fenster_bridge *fb) { return fb->f.scale; }
144
+
145
+ void fenster_bridge_set_scale(fenster_bridge *fb, float scale) {
146
+ fb->f.scale = scale;
147
+ fb->f.phys_width = (int)(fb->f.width * scale);
148
+ fb->f.phys_height = (int)(fb->f.height * scale);
149
+ }
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "ink-native",
3
+ "version": "0.1.0",
4
+ "description": "Render Ink terminal apps in a native window",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ }
13
+ },
14
+ "bin": {
15
+ "ink-native": "./dist/cli.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "fonts",
20
+ "native"
21
+ ],
22
+ "scripts": {
23
+ "dev": "tsx src/index.ts",
24
+ "demo": "tsx src/cli.tsx",
25
+ "build": "tsup src/index.ts src/cli.tsx --format esm --dts --clean",
26
+ "start": "node dist/index.js",
27
+ "lint": "biome lint src/",
28
+ "lint:fix": "biome lint --write src/",
29
+ "format": "biome format --write src/",
30
+ "test": "vitest",
31
+ "check": "biome check --write src/ && tsc --noEmit",
32
+ "build:fenster": "bash scripts/build-fenster.sh",
33
+ "generate-font": "tsx scripts/generate-font-data.ts",
34
+ "prepublishOnly": "pnpm run build"
35
+ },
36
+ "keywords": [
37
+ "terminal",
38
+ "tui",
39
+ "fenster",
40
+ "native",
41
+ "window",
42
+ "ink",
43
+ "react",
44
+ "cli"
45
+ ],
46
+ "author": "Derek Petersen",
47
+ "license": "MIT",
48
+ "packageManager": "pnpm@10.28.0",
49
+ "engines": {
50
+ "node": ">=24.0.0"
51
+ },
52
+ "dependencies": {
53
+ "koffi": "^2.9.3",
54
+ "remeda": "^2.33.4"
55
+ },
56
+ "peerDependencies": {
57
+ "ink": "^5.0.0 || ^6.0.0",
58
+ "react": "^18.0.0 || ^19.0.0"
59
+ },
60
+ "devDependencies": {
61
+ "@biomejs/biome": "^2.3.14",
62
+ "@types/node": "^22.13.1",
63
+ "@types/react": "^19.2.10",
64
+ "ink": "^6.6.0",
65
+ "react": "^19.2.4",
66
+ "tsup": "^8.5.1",
67
+ "tsx": "^4.19.2",
68
+ "typescript": "^5.7.3",
69
+ "vitest": "^3.0.4"
70
+ }
71
+ }