feat(electrobun): native C resize library (Wails pattern)

New: agor-pty/native/agor_resize.c — C shared library that:
- Connects button-press-event on GtkWindow + WebView children in C
- Stores mouseButton, xroot, yroot, dragTime from real GTK events
- Exports agor_resize_start(edge) for Bun FFI

Build: gcc -shared -fPIC -o libagor-resize.so agor_resize.c $(pkg-config --cflags --libs gtk+-3.0)

New: ui-electrobun/src/bun/native-resize.ts — Bun FFI wrapper
App.svelte: simplified to just edge detection + RPC call
This commit is contained in:
Hibryda 2026-03-25 16:44:51 +01:00
parent de40bcbcac
commit 178e560068
5 changed files with 244 additions and 62 deletions

View file

@ -109,14 +109,11 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
...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,

View file

@ -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<typeof dlopen> | null = null;
function loadLib(): ReturnType<typeof dlopen> | 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<string, number> = {
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;
}
}