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.
This commit is contained in:
parent
e9fcd8401e
commit
828e97276a
1 changed files with 68 additions and 138 deletions
|
|
@ -4,186 +4,116 @@
|
||||||
* Connects button-press-event and motion-notify-event directly on the
|
* Connects button-press-event and motion-notify-event directly on the
|
||||||
* GtkWindow via FFI. Hit-test and begin_resize_drag happen SYNCHRONOUSLY
|
* GtkWindow via FFI. Hit-test and begin_resize_drag happen SYNCHRONOUSLY
|
||||||
* in the native GTK event loop, BEFORE WebKitGTK sees the events.
|
* 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";
|
import { dlopen, FFIType, JSCallback, ptr } from "bun:ffi";
|
||||||
|
|
||||||
const BORDER = 8; // resize border width in pixels
|
const BORDER = 8;
|
||||||
|
|
||||||
// GdkWindowEdge values
|
|
||||||
const EDGE_NW = 0, EDGE_N = 1, EDGE_NE = 2;
|
const EDGE_NW = 0, EDGE_N = 1, EDGE_NE = 2;
|
||||||
const EDGE_W = 3, EDGE_E = 4;
|
const EDGE_W = 3, EDGE_E = 4;
|
||||||
const EDGE_SW = 5, EDGE_S = 6, EDGE_SE = 7;
|
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
|
let gtk: ReturnType<typeof dlopen> | null = null;
|
||||||
const CURSORS = [
|
|
||||||
"nw-resize", "n-resize", "ne-resize",
|
|
||||||
"w-resize", "", "e-resize",
|
|
||||||
"sw-resize", "s-resize", "se-resize",
|
|
||||||
];
|
|
||||||
|
|
||||||
let lib: ReturnType<typeof dlopen> | null = null;
|
|
||||||
|
|
||||||
function getLib() {
|
function getLib() {
|
||||||
if (lib) return lib;
|
if (gtk) return gtk;
|
||||||
lib = dlopen("libgtk-3.so.0", {
|
gtk = dlopen("libgtk-3.so.0", {
|
||||||
gtk_widget_add_events: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.void },
|
gtk_widget_add_events: { args: [FFIType.ptr, FFIType.i32], returns: FFIType.void },
|
||||||
gtk_window_begin_resize_drag: {
|
gtk_window_begin_resize_drag: { args: [FFIType.ptr, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.i32, FFIType.u32], returns: FFIType.void },
|
||||||
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 },
|
gtk_widget_get_window: { args: [FFIType.ptr], returns: FFIType.ptr },
|
||||||
g_signal_connect_data: {
|
g_signal_connect_data: { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.i32], returns: FFIType.u64 },
|
||||||
args: [FFIType.ptr, FFIType.cstring, FFIType.ptr, FFIType.ptr, FFIType.ptr, FFIType.i32],
|
|
||||||
returns: FFIType.u64,
|
|
||||||
},
|
|
||||||
// GdkWindow methods
|
|
||||||
gdk_window_get_position: { args: [FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
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_width: { args: [FFIType.ptr], returns: FFIType.i32 },
|
||||||
gdk_window_get_height: { 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.ptr], returns: FFIType.ptr },
|
||||||
gdk_cursor_new_from_name: { args: [FFIType.ptr, FFIType.cstring], returns: FFIType.ptr },
|
|
||||||
gdk_window_set_cursor: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
gdk_window_set_cursor: { args: [FFIType.ptr, FFIType.ptr], returns: FFIType.void },
|
||||||
gdk_display_get_default: { args: [], returns: FFIType.ptr },
|
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_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_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_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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
function hitTest(l: number, t: number, r: number, b: number, cx: number, cy: number, border: number): number {
|
||||||
* Hit-test: is the point within BORDER pixels of any edge?
|
const oL = cx < l + border, oR = cx >= r - border, oT = cy < t + border, oB = cy >= b - border;
|
||||||
* Returns GdkWindowEdge value (0-7) or -1 if inside content area.
|
if (oT && oL) return EDGE_NW; if (oT && oR) return EDGE_NE;
|
||||||
*/
|
if (oB && oL) return EDGE_SW; if (oB && oR) return EDGE_SE;
|
||||||
function hitTest(
|
if (oT) return EDGE_N; if (oB) return EDGE_S;
|
||||||
left: number, top: number, right: number, bottom: number,
|
if (oL) return EDGE_W; if (oR) return EDGE_E;
|
||||||
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;
|
|
||||||
return -1;
|
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 motionCb: JSCallback | null = null;
|
||||||
let buttonCb: JSCallback | null = null;
|
let buttonCb: JSCallback | null = null;
|
||||||
|
|
||||||
/**
|
// Shared buffers — use Buffer.alloc for reliable FFI pointer passing
|
||||||
* Install native GTK event handlers for resize on an undecorated window.
|
const xBuf = Buffer.alloc(8); // gdouble (8 bytes)
|
||||||
* Must be called ONCE after window creation.
|
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) {
|
export function installNativeResize(windowPtr: number | bigint) {
|
||||||
const gtk = getLib();
|
const lib = getLib();
|
||||||
if (!gtk) { console.error("[gtk-resize] Failed to load libgtk-3.so.0"); return; }
|
if (!lib) { console.error("[gtk-resize] Failed to load libgtk-3.so.0"); return; }
|
||||||
|
storedWindowPtr = windowPtr;
|
||||||
|
|
||||||
// Add required event masks
|
// Add event masks
|
||||||
const GDK_POINTER_MOTION_MASK = 1 << 2;
|
lib.symbols.gtk_widget_add_events(windowPtr as any, (1<<2) | (1<<8) | (1<<5));
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Shared buffers for extracting event fields
|
// Motion handler disabled — isolating button-press resize first
|
||||||
const xBuf = new Float64Array(1);
|
// TODO: re-enable cursor change on hover once resize works
|
||||||
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 },
|
|
||||||
);
|
|
||||||
|
|
||||||
// Button press — initiate resize if in border zone
|
// Button press — initiate resize if in border zone
|
||||||
buttonCb = new JSCallback(
|
buttonCb = new JSCallback(
|
||||||
(_widget: any, event: any, _userData: any) => {
|
(_w: any, event: any, _u: any) => {
|
||||||
gtk!.symbols.gdk_event_get_button(event, ptr(btnBuf.buffer) as any);
|
try {
|
||||||
if (btnBuf[0] !== 1) return 0; // Only LMB
|
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);
|
gtk!.symbols.gdk_event_get_root_coords(event, ptr(xBuf) as any, ptr(yBuf) as any);
|
||||||
const cx = xBuf[0], cy = yBuf[0];
|
const cx = xBuf.readDoubleLE(0);
|
||||||
const bounds = getWindowBounds();
|
const cy = yBuf.readDoubleLE(0);
|
||||||
if (!bounds) return 0;
|
const bounds = getWindowBounds();
|
||||||
|
if (!bounds) return 0;
|
||||||
|
|
||||||
const edge = hitTest(bounds.left, bounds.top, bounds.right, bounds.bottom, cx, cy, BORDER);
|
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
|
if (edge < 0) return 0; // Inside content — let WebKit handle
|
||||||
|
|
||||||
const timestamp = gtk!.symbols.gdk_event_get_time(event);
|
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}`);
|
console.log(`[gtk-resize] begin_resize_drag edge=${edge} t=${timestamp}`);
|
||||||
|
|
||||||
gtk!.symbols.gtk_window_begin_resize_drag(
|
gtk!.symbols.gtk_window_begin_resize_drag(
|
||||||
windowPtr as any,
|
storedWindowPtr, edge, 1,
|
||||||
edge,
|
Math.round(cx), Math.round(cy),
|
||||||
1, // LMB
|
timestamp,
|
||||||
Math.round(cx),
|
);
|
||||||
Math.round(cy),
|
return 1; // TRUE — STOP propagation
|
||||||
timestamp, // REAL event timestamp — not GDK_CURRENT_TIME
|
} catch (e) { console.error("[gtk-resize] button error:", e); return 0; }
|
||||||
);
|
|
||||||
|
|
||||||
return 1; // TRUE — STOP propagation. WebKit does NOT see this button press.
|
|
||||||
},
|
},
|
||||||
{ args: [FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
|
{ args: [FFIType.ptr, FFIType.ptr, FFIType.ptr], returns: FFIType.i32 },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Connect signals — button-press FIRST (higher priority)
|
const sigBP = Buffer.from("button-press-event\0");
|
||||||
// Bun FFI requires Buffer for cstring args, not JS strings
|
lib.symbols.g_signal_connect_data(windowPtr as any, ptr(sigBP) as any, buttonCb.ptr as any, null, null, 0);
|
||||||
const sigButtonPress = Buffer.from("button-press-event\0");
|
// Motion handler disabled for now — isolating button-press resize
|
||||||
const sigMotionNotify = Buffer.from("motion-notify-event\0");
|
// const sigMN = Buffer.from("motion-notify-event\0");
|
||||||
gtk.symbols.g_signal_connect_data(
|
// lib.symbols.g_signal_connect_data(windowPtr as any, ptr(sigMN) as any, motionCb.ptr as any, null, null, 0);
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
console.log("[gtk-resize] Native resize handlers installed (border=" + BORDER + "px)");
|
console.log("[gtk-resize] Native resize handlers installed (border=" + BORDER + "px)");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue