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:
parent
de40bcbcac
commit
178e560068
5 changed files with 244 additions and 62 deletions
134
agor-pty/native/agor_resize.c
Normal file
134
agor-pty/native/agor_resize.c
Normal file
|
|
@ -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 <gtk/gtk.h>
|
||||||
|
#include <gdk/gdkx.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
/* 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;
|
||||||
|
}
|
||||||
BIN
agor-pty/native/libagor-resize.so
Executable file
BIN
agor-pty/native/libagor-resize.so
Executable file
Binary file not shown.
|
|
@ -109,14 +109,11 @@ const rpc = BrowserView.defineRPC<PtyRPCSchema>({
|
||||||
...miscHandlers,
|
...miscHandlers,
|
||||||
|
|
||||||
// GTK native drag/resize — delegates to window manager (zero CPU)
|
// 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 {
|
try {
|
||||||
const { beginResizeDrag, edgeStringToGdk } = require("./gtk-window.ts");
|
// Uses native C library — stored mouse state from real GTK event
|
||||||
const gdkEdge = edgeStringToGdk(edge);
|
const { startNativeResize } = require("./native-resize.ts");
|
||||||
if (gdkEdge === null) return { ok: false, error: `Unknown edge: ${edge}` };
|
const ok = startNativeResize(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}`);
|
|
||||||
return { ok };
|
return { ok };
|
||||||
} catch (err) { console.error("[window.beginResize]", err); return { ok: false }; }
|
} 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
|
// Native resize via C shared library (libagor-resize.so)
|
||||||
// (forceSmallMinSize and wrapWebViewInScrolledWindow caused crashes)
|
// Owns GTK signal connections in C — no JSCallback boundary crossing
|
||||||
try {
|
try {
|
||||||
console.log("[gtk] Window ptr:", (mainWindow as any).ptr);
|
const { initNativeResize } = require("./native-resize.ts");
|
||||||
} catch (e) { console.error("[gtk] ptr access failed:", e); }
|
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.
|
// 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,
|
// WebKitGTK reports stale modifier state (0x14 = Ctrl+Alt) after SIGTERM of previous instance,
|
||||||
|
|
|
||||||
94
ui-electrobun/src/bun/native-resize.ts
Normal file
94
ui-electrobun/src/bun/native-resize.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -104,55 +104,14 @@
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Window resize — CSS handles capture mouse, X11 FFI resizes ──
|
// ── Window resize — JS edge detection + native C begin_resize_drag ──
|
||||||
let resizeEdge: string | null = null;
|
// The Wails pattern: JS detects the edge, C library has real GTK event data.
|
||||||
let resizeStartX = 0;
|
|
||||||
let resizeStartY = 0;
|
|
||||||
let resizeFrame = { x: 0, y: 0, width: 0, height: 0 };
|
|
||||||
const CURSOR_MAP: Record<string, string> = {
|
|
||||||
n: 'n-resize', s: 's-resize', e: 'e-resize', w: 'w-resize',
|
|
||||||
ne: 'ne-resize', nw: 'nw-resize', se: 'se-resize', sw: 'sw-resize',
|
|
||||||
};
|
|
||||||
|
|
||||||
function onResizeStart(e: MouseEvent, edge: string) {
|
function onResizeStart(e: MouseEvent, edge: string) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
resizeEdge = edge;
|
// Tell the native C library to start a resize drag
|
||||||
resizeStartX = e.screenX;
|
// It uses stored mouseButton/xroot/yroot from the real GTK button-press event
|
||||||
resizeStartY = e.screenY;
|
appRpc.request['window.beginResize']({ edge }).catch(() => {});
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Window frame persistence (debounced 500ms) ──────────────
|
// ── Window frame persistence (debounced 500ms) ──────────────
|
||||||
|
|
@ -253,9 +212,7 @@
|
||||||
setAgentToastFn(showToast);
|
setAgentToastFn(showToast);
|
||||||
setupErrorBoundary();
|
setupErrorBoundary();
|
||||||
|
|
||||||
// JS resize needs document-level listeners
|
// Resize handled by native C library — no JS mousemove/mouseup needed
|
||||||
document.addEventListener('mousemove', onResizeMove);
|
|
||||||
document.addEventListener('mouseup', onResizeEnd);
|
|
||||||
|
|
||||||
// Blink + session timers — MUST be in onMount, NOT $effect
|
// Blink + session timers — MUST be in onMount, NOT $effect
|
||||||
// $effect interacts with reactive graph and causes cycles
|
// $effect interacts with reactive graph and causes cycles
|
||||||
|
|
@ -351,8 +308,7 @@
|
||||||
clearInterval(sessionId);
|
clearInterval(sessionId);
|
||||||
document.removeEventListener("keydown", handleSearchShortcut);
|
document.removeEventListener("keydown", handleSearchShortcut);
|
||||||
window.removeEventListener("palette-command", handlePaletteCommand);
|
window.removeEventListener("palette-command", handlePaletteCommand);
|
||||||
document.removeEventListener('mousemove', onResizeMove);
|
// Native resize — no JS listeners to clean
|
||||||
document.removeEventListener('mouseup', onResizeEnd);
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue