diff --git a/agor-pty/native/agor_resize.c b/agor-pty/native/agor_resize.c new file mode 100644 index 0000000..c9b4a0e --- /dev/null +++ b/agor-pty/native/agor_resize.c @@ -0,0 +1,134 @@ +/** + * libagor-resize.so — Native GTK resize handler for Electrobun. + * + * Connects button-press-event on the GtkWindow in C (not JSCallback). + * Stores mouse state from the real GTK event. Exports start_resize(edge) + * for Bun FFI to call — uses the stored event data for begin_resize_drag. + * + * This is the Wails pattern adapted for Electrobun/Bun. + * + * Build: gcc -shared -fPIC -o libagor-resize.so agor_resize.c \ + * $(pkg-config --cflags --libs gtk+-3.0) + */ + +#include +#include +#include + +/* Stored mouse state from the last button-press event */ +static gdouble stored_xroot = 0.0; +static gdouble stored_yroot = 0.0; +static guint32 stored_time = 0; +static guint stored_button = 1; +static GtkWindow *stored_window = NULL; + +/* Border width for hit-test (pixels) */ +static int border_width = 8; + +/** + * GTK button-press-event handler — stores mouse state for later use. + * Connected directly in C — no JSCallback boundary crossing. + * Returns FALSE to let the event propagate to WebKitGTK normally. + */ +static gboolean +on_button_press(GtkWidget *widget, GdkEventButton *event, gpointer user_data) +{ + if (event == NULL) return FALSE; + if (event->button == 3) return FALSE; /* Skip right-click */ + + if (event->type == GDK_BUTTON_PRESS && event->button == 1) { + stored_xroot = event->x_root; + stored_yroot = event->y_root; + stored_time = event->time; + stored_button = event->button; + } + return FALSE; /* Propagate — WebKitGTK still processes the event */ +} + +/** + * Initialize: connect button-press-event on the GtkWindow. + * Call ONCE after window creation from Bun FFI. + * + * @param window_ptr GtkWindow* (from Electrobun's mainWindow.ptr) + * @param border Border width in pixels for edge detection + */ +void agor_resize_init(void *window_ptr, int border) +{ + if (!GTK_IS_WINDOW(window_ptr)) { + fprintf(stderr, "[agor-resize] ERROR: not a GtkWindow: %p\n", window_ptr); + return; + } + + stored_window = GTK_WINDOW(window_ptr); + border_width = border > 0 ? border : 8; + + /* Add event masks */ + gtk_widget_add_events(GTK_WIDGET(stored_window), + GDK_POINTER_MOTION_MASK | + GDK_BUTTON_PRESS_MASK | + GDK_BUTTON1_MOTION_MASK); + + /* Connect button-press on the window — fires even when WebView covers everything + because we also connect on the WebView child below */ + g_signal_connect(stored_window, "button-press-event", + G_CALLBACK(on_button_press), NULL); + + /* Walk down to find the WebView and connect there too */ + GtkWidget *child = gtk_bin_get_child(GTK_BIN(stored_window)); + while (child && GTK_IS_BIN(child)) { + g_signal_connect(child, "button-press-event", + G_CALLBACK(on_button_press), NULL); + child = gtk_bin_get_child(GTK_BIN(child)); + } + + fprintf(stderr, "[agor-resize] Initialized: window=%p border=%d\n", + window_ptr, border_width); +} + +/** + * Start a resize drag using the stored mouse state. + * Call from Bun FFI when JS detects a mousedown in the border zone. + * + * @param edge GdkWindowEdge value (0=NW, 1=N, 2=NE, 3=W, 4=E, 5=SW, 6=S, 7=SE) + * @return 1 on success, 0 on failure + */ +int agor_resize_start(int edge) +{ + if (!stored_window) { + fprintf(stderr, "[agor-resize] ERROR: not initialized\n"); + return 0; + } + if (edge < 0 || edge > 7) { + fprintf(stderr, "[agor-resize] ERROR: invalid edge %d\n", edge); + return 0; + } + if (stored_time == 0) { + fprintf(stderr, "[agor-resize] ERROR: no stored event (click first)\n"); + return 0; + } + + fprintf(stderr, "[agor-resize] begin_resize_drag edge=%d btn=%u xy=(%.0f,%.0f) t=%u\n", + edge, stored_button, stored_xroot, stored_yroot, stored_time); + + gtk_window_begin_resize_drag( + stored_window, + (GdkWindowEdge)edge, + stored_button, + (gint)stored_xroot, + (gint)stored_yroot, + stored_time + ); + + return 1; +} + +/** + * Get the stored mouse position (for debugging). + */ +void agor_resize_get_state(double *out_x, double *out_y, unsigned int *out_time, unsigned int *out_button) +{ + if (out_x) *out_x = stored_xroot; + if (out_y) *out_y = stored_yroot; + if (out_time) *out_time = stored_time; + if (out_button) *out_button = stored_button; +} diff --git a/agor-pty/native/libagor-resize.so b/agor-pty/native/libagor-resize.so new file mode 100755 index 0000000..cc3db4e Binary files /dev/null and b/agor-pty/native/libagor-resize.so differ diff --git a/ui-electrobun/src/bun/index.ts b/ui-electrobun/src/bun/index.ts index 53582a6..c32f752 100644 --- a/ui-electrobun/src/bun/index.ts +++ b/ui-electrobun/src/bun/index.ts @@ -109,14 +109,11 @@ const rpc = BrowserView.defineRPC({ ...miscHandlers, // GTK native drag/resize — delegates to window manager (zero CPU) - "window.beginResize": ({ edge, button, rootX, rootY }: { edge: string; button: number; rootX: number; rootY: number }) => { + "window.beginResize": ({ edge }: { edge: string }) => { try { - const { beginResizeDrag, edgeStringToGdk } = require("./gtk-window.ts"); - const gdkEdge = edgeStringToGdk(edge); - if (gdkEdge === null) return { ok: false, error: `Unknown edge: ${edge}` }; - console.log(`[resize] edge=${edge} gdkEdge=${gdkEdge} btn=${button} rootX=${rootX} rootY=${rootY} ptr=${(mainWindow as any).ptr}`); - const ok = beginResizeDrag((mainWindow as any).ptr, gdkEdge, button, rootX, rootY); - console.log(`[resize] result: ${ok}`); + // Uses native C library — stored mouse state from real GTK event + const { startNativeResize } = require("./native-resize.ts"); + const ok = startNativeResize(edge); return { ok }; } catch (err) { console.error("[window.beginResize]", err); return { ok: false }; } }, @@ -240,11 +237,12 @@ mainWindow = new BrowserWindow({ }, }); -// GTK window setup — only log resizable status, skip all FFI modifications -// (forceSmallMinSize and wrapWebViewInScrolledWindow caused crashes) +// Native resize via C shared library (libagor-resize.so) +// Owns GTK signal connections in C — no JSCallback boundary crossing try { - console.log("[gtk] Window ptr:", (mainWindow as any).ptr); -} catch (e) { console.error("[gtk] ptr access failed:", e); } + const { initNativeResize } = require("./native-resize.ts"); + initNativeResize((mainWindow as any).ptr, 8); +} catch (e) { console.error("[native-resize] init failed:", e); } // Prevent GTK's false Ctrl+click detection from closing the window on initial load. // WebKitGTK reports stale modifier state (0x14 = Ctrl+Alt) after SIGTERM of previous instance, diff --git a/ui-electrobun/src/bun/native-resize.ts b/ui-electrobun/src/bun/native-resize.ts new file mode 100644 index 0000000..40cb9ec --- /dev/null +++ b/ui-electrobun/src/bun/native-resize.ts @@ -0,0 +1,94 @@ +/** + * Native resize via libagor-resize.so — C shared library that owns + * GTK signal connections and calls begin_resize_drag with real event data. + * + * The C library: + * 1. Connects button-press-event on GtkWindow + WebView children (in C) + * 2. Stores mouseButton, xroot, yroot, dragTime from real GTK events + * 3. Exports agor_resize_start(edge) that Bun calls via FFI + * + * This avoids JSCallback boundary crossing (which crashes in Bun). + */ + +import { dlopen, FFIType } from "bun:ffi"; +import { join } from "path"; +import { existsSync } from "fs"; + +let lib: ReturnType | null = null; + +function loadLib(): ReturnType | null { + if (lib) return lib; + + // Search paths for the .so + const candidates = [ + join(import.meta.dir, "../../agor-pty/native/libagor-resize.so"), + join(import.meta.dir, "../../../agor-pty/native/libagor-resize.so"), + "/home/hibryda/code/ai/agent-orchestrator/agor-pty/native/libagor-resize.so", + ]; + + const soPath = candidates.find(p => existsSync(p)); + if (!soPath) { + console.error("[native-resize] libagor-resize.so not found. Searched:", candidates); + return null; + } + + try { + lib = dlopen(soPath, { + agor_resize_init: { + args: [FFIType.ptr, FFIType.i32], + returns: FFIType.void, + }, + agor_resize_start: { + args: [FFIType.i32], + returns: FFIType.i32, + }, + }); + console.log("[native-resize] Loaded:", soPath); + return lib; + } catch (err) { + console.error("[native-resize] Failed to load:", err); + return null; + } +} + +/** + * Initialize native resize handler. Call once after window creation. + */ +export function initNativeResize(windowPtr: number | bigint, borderPx: number = 8): boolean { + const l = loadLib(); + if (!l) return false; + try { + l.symbols.agor_resize_init(windowPtr as any, borderPx); + return true; + } catch (err) { + console.error("[native-resize] init failed:", err); + return false; + } +} + +// GdkWindowEdge mapping +const EDGE_MAP: Record = { + n: 1, s: 6, e: 4, w: 3, + ne: 2, nw: 0, se: 7, sw: 5, +}; + +/** + * Start a resize drag. Call from RPC when JS detects mousedown in border zone. + * The C library uses stored mouse state from the real GTK button-press event. + */ +export function startNativeResize(edge: string): boolean { + const l = loadLib(); + if (!l) return false; + const gdkEdge = EDGE_MAP[edge]; + if (gdkEdge === undefined) { + console.error("[native-resize] Unknown edge:", edge); + return false; + } + try { + const ok = l.symbols.agor_resize_start(gdkEdge); + return ok === 1; + } catch (err) { + console.error("[native-resize] start failed:", err); + return false; + } +} diff --git a/ui-electrobun/src/mainview/App.svelte b/ui-electrobun/src/mainview/App.svelte index 45924d2..98aa2a1 100644 --- a/ui-electrobun/src/mainview/App.svelte +++ b/ui-electrobun/src/mainview/App.svelte @@ -104,55 +104,14 @@ e.preventDefault(); } - // ── Window resize — CSS handles capture mouse, X11 FFI resizes ── - let resizeEdge: string | null = null; - let resizeStartX = 0; - let resizeStartY = 0; - let resizeFrame = { x: 0, y: 0, width: 0, height: 0 }; - const CURSOR_MAP: Record = { - n: 'n-resize', s: 's-resize', e: 'e-resize', w: 'w-resize', - ne: 'ne-resize', nw: 'nw-resize', se: 'se-resize', sw: 'sw-resize', - }; - + // ── Window resize — JS edge detection + native C begin_resize_drag ── + // The Wails pattern: JS detects the edge, C library has real GTK event data. function onResizeStart(e: MouseEvent, edge: string) { e.preventDefault(); e.stopPropagation(); - resizeEdge = edge; - resizeStartX = e.screenX; - resizeStartY = e.screenY; - // Capture frame synchronously - resizeFrame = { - x: window.screenX, y: window.screenY, - width: window.outerWidth, height: window.outerHeight, - }; - document.body.style.cursor = CURSOR_MAP[edge] || 'default'; - document.body.style.userSelect = 'none'; - } - - function onResizeMove(e: MouseEvent) { - if (!resizeEdge) return; - e.preventDefault(); - const dx = e.screenX - resizeStartX; - const dy = e.screenY - resizeStartY; - let { x, y, width, height } = resizeFrame; - const MIN_W = 400, MIN_H = 300; - if (resizeEdge.includes('e')) width = Math.max(MIN_W, width + dx); - if (resizeEdge.includes('w')) { const nw = Math.max(MIN_W, width - dx); x += width - nw; width = nw; } - if (resizeEdge.includes('s')) height = Math.max(MIN_H, height + dy); - if (resizeEdge.includes('n')) { const nh = Math.max(MIN_H, height - dy); y += height - nh; height = nh; } - // Use Electrobun's setPosition + setSize (simpler than X11 FFI) - appRpc.request['window.setFrame']({ - x: Math.round(x), y: Math.round(y), - width: Math.round(width), height: Math.round(height), - }).catch(() => {}); - } - - function onResizeEnd() { - if (!resizeEdge) return; - resizeEdge = null; - document.body.style.cursor = ''; - document.body.style.userSelect = ''; - saveWindowFrame(); + // Tell the native C library to start a resize drag + // It uses stored mouseButton/xroot/yroot from the real GTK button-press event + appRpc.request['window.beginResize']({ edge }).catch(() => {}); } // ── Window frame persistence (debounced 500ms) ────────────── @@ -253,9 +212,7 @@ setAgentToastFn(showToast); setupErrorBoundary(); - // JS resize needs document-level listeners - document.addEventListener('mousemove', onResizeMove); - document.addEventListener('mouseup', onResizeEnd); + // Resize handled by native C library — no JS mousemove/mouseup needed // Blink + session timers — MUST be in onMount, NOT $effect // $effect interacts with reactive graph and causes cycles @@ -351,8 +308,7 @@ clearInterval(sessionId); document.removeEventListener("keydown", handleSearchShortcut); window.removeEventListener("palette-command", handlePaletteCommand); - document.removeEventListener('mousemove', onResizeMove); - document.removeEventListener('mouseup', onResizeEnd); + // Native resize — no JS listeners to clean }; });