From 828e97276ae614ed6d7a27ee505b27e31c04b824 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Wed, 25 Mar 2026 15:59:51 +0100 Subject: [PATCH] fix(electrobun): fix gtk-resize.ts syntax error + Buffer-based FFI pointers Fixed malformed block comment that prevented bun bundler from compiling. Switched from TypedArray.buffer to Buffer.alloc for FFI output pointers. Disabled motion handler to isolate button-press resize. NOTE: Electrobun's dev command rebuilds the bundle on every run, overwriting manual builds. To test changes, run: bun build src/bun/index.ts --outfile build/.../app/bun/index.js --target=bun --external=electrobun AFTER electrobun dev finishes building, then restart the app. --- ui-electrobun/src/bun/gtk-resize.ts | 206 +++++++++------------------- 1 file changed, 68 insertions(+), 138 deletions(-) diff --git a/ui-electrobun/src/bun/gtk-resize.ts b/ui-electrobun/src/bun/gtk-resize.ts index 267d8f3..42e5e33 100644 --- a/ui-electrobun/src/bun/gtk-resize.ts +++ b/ui-electrobun/src/bun/gtk-resize.ts @@ -4,186 +4,116 @@ * Connects button-press-event and motion-notify-event directly on the * GtkWindow via FFI. Hit-test and begin_resize_drag happen SYNCHRONOUSLY * in the native GTK event loop, BEFORE WebKitGTK sees the events. - * - * This is the ONLY approach that works for undecorated WebKitGTK windows. - * JS-side approaches (RPC, window.resizeTo, XMoveResizeWindow) all fail - * because WebKitGTK either steals the grab or blocks the resize. */ import { dlopen, FFIType, JSCallback, ptr } from "bun:ffi"; -const BORDER = 8; // resize border width in pixels - -// GdkWindowEdge values +const BORDER = 8; const EDGE_NW = 0, EDGE_N = 1, EDGE_NE = 2; const EDGE_W = 3, EDGE_E = 4; const EDGE_SW = 5, EDGE_S = 6, EDGE_SE = 7; +const CURSORS = ["nw-resize","n-resize","ne-resize","w-resize","","e-resize","sw-resize","s-resize","se-resize"]; -// Cursor names indexed by edge -const CURSORS = [ - "nw-resize", "n-resize", "ne-resize", - "w-resize", "", "e-resize", - "sw-resize", "s-resize", "se-resize", -]; - -let lib: ReturnType | null = null; - +let gtk: ReturnType | null = null; function getLib() { - if (lib) return lib; - lib = dlopen("libgtk-3.so.0", { + if (gtk) return gtk; + gtk = dlopen("libgtk-3.so.0", { gtk_widget_add_events: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.void }, - gtk_window_begin_resize_drag: { - args: [FFIType.ptr, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32], - returns: FFIType.void, - }, + gtk_window_begin_resize_drag: { args: [FFIType.ptr, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32], returns: FFIType.void }, gtk_widget_get_window: { args: [FFIType.ptr], returns: FFIType.ptr }, - g_signal_connect_data: { - args: [FFIType.ptr, FFIType.cstring, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.i32], - returns: FFIType.u64, - }, - // GdkWindow methods + g_signal_connect_data: { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.i32], returns: FFIType.u64 }, gdk_window_get_position: { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.void }, gdk_window_get_width: { args: [FFIType.ptr], returns: FFIType.i32 }, gdk_window_get_height: { args: [FFIType.ptr], returns: FFIType.i32 }, - // GdkCursor - gdk_cursor_new_from_name: { args: [FFIType.ptr, FFIType.cstring], returns: FFIType.ptr }, + gdk_cursor_new_from_name: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.ptr }, gdk_window_set_cursor: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void }, gdk_display_get_default: { args: [], returns: FFIType.ptr }, - // GdkEvent field extraction gdk_event_get_button: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.bool }, gdk_event_get_root_coords: { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.bool }, gdk_event_get_time: { args: [FFIType.ptr], returns: FFIType.u32 }, - gdk_event_get_coords: { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.bool }, }); - return lib; + return gtk; } -/** - * Hit-test: is the point within BORDER pixels of any edge? - * Returns GdkWindowEdge value (0-7) or -1 if inside content area. - */ -function hitTest( - left: number, top: number, right: number, bottom: number, - cx: number, cy: number, border: number, -): number { - const onLeft = cx < left + border; - const onRight = cx >= right - border; - const onTop = cy < top + border; - const onBottom = cy >= bottom - border; - - if (onTop && onLeft) return EDGE_NW; - if (onTop && onRight) return EDGE_NE; - if (onBottom && onLeft) return EDGE_SW; - if (onBottom && onRight) return EDGE_SE; - if (onTop) return EDGE_N; - if (onBottom) return EDGE_S; - if (onLeft) return EDGE_W; - if (onRight) return EDGE_E; +function hitTest(l: number, t: number, r: number, b: number, cx: number, cy: number, border: number): number { + const oL = cx < l + border, oR = cx >= r - border, oT = cy < t + border, oB = cy >= b - border; + if (oT && oL) return EDGE_NW; if (oT && oR) return EDGE_NE; + if (oB && oL) return EDGE_SW; if (oB && oR) return EDGE_SE; + if (oT) return EDGE_N; if (oB) return EDGE_S; + if (oL) return EDGE_W; if (oR) return EDGE_E; return -1; } -// Keep JSCallback references alive (prevent GC) +// Module-level state — set by installNativeResize, read by callbacks +let storedWindowPtr: any = null; let motionCb: JSCallback | null = null; let buttonCb: JSCallback | null = null; -/** - * Install native GTK event handlers for resize on an undecorated window. - * Must be called ONCE after window creation. - */ +// Shared buffers — use Buffer.alloc for reliable FFI pointer passing +const xBuf = Buffer.alloc(8); // gdouble (8 bytes) +const yBuf = Buffer.alloc(8); +const btnBuf = Buffer.alloc(4); // guint (4 bytes) +const posXBuf = Buffer.alloc(4); // gint (4 bytes) +const posYBuf = Buffer.alloc(4); + +function getWindowBounds() { + if (!gtk || !storedWindowPtr) return null; + const gdkWin = gtk.symbols.gtk_widget_get_window(storedWindowPtr); + if (!gdkWin) return null; + gtk.symbols.gdk_window_get_position(gdkWin, ptr(posXBuf) as any, ptr(posYBuf) as any); + const w = gtk.symbols.gdk_window_get_width(gdkWin); + const h = gtk.symbols.gdk_window_get_height(gdkWin); + const left = posXBuf.readInt32LE(0); + const top = posYBuf.readInt32LE(0); + return { left, top, right: left + w, bottom: top + h }; +} + export function installNativeResize(windowPtr: number | bigint) { - const gtk = getLib(); - if (!gtk) { console.error("[gtk-resize] Failed to load libgtk-3.so.0"); return; } + const lib = getLib(); + if (!lib) { console.error("[gtk-resize] Failed to load libgtk-3.so.0"); return; } + storedWindowPtr = windowPtr; - // Add required event masks - const GDK_POINTER_MOTION_MASK = 1 << 2; - const GDK_BUTTON_PRESS_MASK = 1 << 8; - const GDK_BUTTON1_MOTION_MASK = 1 << 5; - gtk.symbols.gtk_widget_add_events( - windowPtr as any, - GDK_POINTER_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON1_MOTION_MASK, - ); + // Add event masks + lib.symbols.gtk_widget_add_events(windowPtr as any, (1<<2) | (1<<8) | (1<<5)); - // Shared buffers for extracting event fields - const xBuf = new Float64Array(1); - const yBuf = new Float64Array(1); - const btnBuf = new Uint32Array(1); - - function getWindowBounds(): { left: number; top: number; right: number; bottom: number } | null { - const gdkWin = gtk!.symbols.gtk_widget_get_window(windowPtr as any); - if (!gdkWin) return null; - const xArr = new Int32Array(1); - const yArr = new Int32Array(1); - gtk!.symbols.gdk_window_get_position(gdkWin, ptr(xArr.buffer) as any, ptr(yArr.buffer) as any); - const w = gtk!.symbols.gdk_window_get_width(gdkWin); - const h = gtk!.symbols.gdk_window_get_height(gdkWin); - return { left: xArr[0], top: yArr[0], right: xArr[0] + w, bottom: yArr[0] + h }; - } - - // Motion notify — update cursor when hovering over edges - motionCb = new JSCallback( - (_widget: any, event: any, _userData: any) => { - const bounds = getWindowBounds(); - if (!bounds) return 0; - gtk!.symbols.gdk_event_get_root_coords(event, ptr(xBuf.buffer) as any, ptr(yBuf.buffer) as any); - const cx = xBuf[0], cy = yBuf[0]; - const edge = hitTest(bounds.left, bounds.top, bounds.right, bounds.bottom, cx, cy, BORDER); - - const gdkWin = gtk!.symbols.gtk_widget_get_window(windowPtr as any); - if (gdkWin) { - const display = gtk!.symbols.gdk_display_get_default(); - if (display) { - const cursorName = edge >= 0 ? CURSORS[edge] : "default"; - const cursorBuf = Buffer.from(cursorName + "\0"); - const cursor = gtk!.symbols.gdk_cursor_new_from_name(display, ptr(cursorBuf) as any); - gtk!.symbols.gdk_window_set_cursor(gdkWin, cursor); - } - } - return 0; // Proceed — let motion events reach WebKit for hover etc. - }, - { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 }, - ); + // Motion handler disabled — isolating button-press resize first + // TODO: re-enable cursor change on hover once resize works // Button press — initiate resize if in border zone buttonCb = new JSCallback( - (_widget: any, event: any, _userData: any) => { - gtk!.symbols.gdk_event_get_button(event, ptr(btnBuf.buffer) as any); - if (btnBuf[0] !== 1) return 0; // Only LMB + (_w: any, event: any, _u: any) => { + try { + gtk!.symbols.gdk_event_get_button(event, ptr(btnBuf) as any); + if (btnBuf.readUInt32LE(0) !== 1) return 0; // LMB only - gtk!.symbols.gdk_event_get_root_coords(event, ptr(xBuf.buffer) as any, ptr(yBuf.buffer) as any); - const cx = xBuf[0], cy = yBuf[0]; - const bounds = getWindowBounds(); - if (!bounds) return 0; + gtk!.symbols.gdk_event_get_root_coords(event, ptr(xBuf) as any, ptr(yBuf) as any); + const cx = xBuf.readDoubleLE(0); + const cy = yBuf.readDoubleLE(0); + const bounds = getWindowBounds(); + if (!bounds) return 0; - const edge = hitTest(bounds.left, bounds.top, bounds.right, bounds.bottom, cx, cy, BORDER); - if (edge < 0) return 0; // Inside content — let WebKit handle it + const edge = hitTest(bounds.left, bounds.top, bounds.right, bounds.bottom, cx, cy, BORDER); + if (edge < 0) return 0; // Inside content — let WebKit handle - const timestamp = gtk!.symbols.gdk_event_get_time(event); - console.log(`[gtk-resize] begin_resize_drag edge=${edge} @${Math.round(cx)},${Math.round(cy)} t=${timestamp}`); + const timestamp = gtk!.symbols.gdk_event_get_time(event); + console.log(`[gtk-resize] begin_resize_drag edge=${edge} t=${timestamp}`); - gtk!.symbols.gtk_window_begin_resize_drag( - windowPtr as any, - edge, - 1, // LMB - Math.round(cx), - Math.round(cy), - timestamp, // REAL event timestamp — not GDK_CURRENT_TIME - ); - - return 1; // TRUE — STOP propagation. WebKit does NOT see this button press. + gtk!.symbols.gtk_window_begin_resize_drag( + storedWindowPtr, edge, 1, + Math.round(cx), Math.round(cy), + timestamp, + ); + return 1; // TRUE — STOP propagation + } catch (e) { console.error("[gtk-resize] button error:", e); return 0; } }, { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 }, ); - // Connect signals — button-press FIRST (higher priority) - // Bun FFI requires Buffer for cstring args, not JS strings - const sigButtonPress = Buffer.from("button-press-event\0"); - const sigMotionNotify = Buffer.from("motion-notify-event\0"); - gtk.symbols.g_signal_connect_data( - windowPtr as any, ptr(sigButtonPress) as any, buttonCb.ptr as any, null, null, 0, - ); - gtk.symbols.g_signal_connect_data( - windowPtr as any, ptr(sigMotionNotify) as any, motionCb.ptr as any, null, null, 0, - ); + const sigBP = Buffer.from("button-press-event\0"); + lib.symbols.g_signal_connect_data(windowPtr as any, ptr(sigBP) as any, buttonCb.ptr as any, null, null, 0); + // Motion handler disabled for now — isolating button-press resize + // const sigMN = Buffer.from("motion-notify-event\0"); + // lib.symbols.g_signal_connect_data(windowPtr as any, ptr(sigMN) as any, motionCb.ptr as any, null, null, 0); console.log("[gtk-resize] Native resize handlers installed (border=" + BORDER + "px)"); }