fix(electrobun): set_size_request(1,1) not (-1,-1) + revert to begin_resize_drag

Codex review found: set_size_request(-1,-1) means "use preferred size"
which RE-ENABLES WebView content-based minimum. Using (1,1) FORCES a
1x1 minimum, actually overriding the preferred size.

Reverted to native begin_resize_drag (WM handles resize smoothly).
Fixed onResizeStart sync: e.stopPropagation() now runs BEFORE any async
work, preventing sidebar drag handler from intercepting.

Removed JS mousemove resize loop — native GTK resize is correct approach.
This commit is contained in:
Hibryda 2026-03-25 13:24:41 +01:00
parent fd2f626c20
commit d1583f8ce4
2 changed files with 27 additions and 116 deletions

View file

@ -102,60 +102,9 @@ export function ensureResizable(windowPtr: number | bigint): boolean {
const nowResizable = lib.symbols.gtk_window_get_resizable(windowPtr as any); const nowResizable = lib.symbols.gtk_window_get_resizable(windowPtr as any);
console.log(`[gtk-window] After set_resizable(true): ${nowResizable}`); console.log(`[gtk-window] After set_resizable(true): ${nowResizable}`);
} }
// Override minimum size on the entire widget tree. // Force small min-size on entire widget tree (1×1 per widget, 400×300 via geometry hints)
// WebKitWebView requests min_size = content_size, which GTK propagates console.log("[gtk-window] Forcing small min-size on widget tree");
// to WM_NORMAL_HINTS, blocking resize. Fix: set -1 (no minimum) recursively. forceSmallMinSize(lib, windowPtr as any);
console.log("[gtk-window] Resetting minimum size constraints on widget tree");
function clearMinSize(widget: any, depth: number) {
if (!widget || depth > 10) return;
lib!.symbols.gtk_widget_set_size_request(widget, -1, -1);
// Try as GtkBin (single child)
try {
const child = lib!.symbols.gtk_bin_get_child(widget);
if (child) {
console.log(`[gtk-window] ${" ".repeat(depth)}child (bin)`);
clearMinSize(child, depth + 1);
}
} catch { /* not a GtkBin */ }
// Try as GtkContainer (multiple children)
try {
const list = lib!.symbols.gtk_container_get_children(widget);
if (list) {
const len = lib!.symbols.g_list_length(list);
console.log(`[gtk-window] ${" ".repeat(depth)}container (${len} children)`);
for (let i = 0; i < len && i < 20; i++) {
const child = lib!.symbols.g_list_nth_data(list, i);
if (child) clearMinSize(child, depth + 1);
}
lib!.symbols.g_list_free(list);
}
} catch { /* not a GtkContainer */ }
}
clearMinSize(windowPtr as any, 0);
// Set geometry hints with small minimum via GdkGeometry struct
// GdkGeometry: min_width(i32), min_height(i32), ... (rest are padding)
// GdkWindowHints: GDK_HINT_MIN_SIZE = 1<<1 = 2
try {
// Allocate GdkGeometry struct (18 ints = 72 bytes)
const buf = new ArrayBuffer(72);
const view = new Int32Array(buf);
view[0] = 400; // min_width
view[1] = 300; // min_height
view[2] = 32767; // max_width
view[3] = 32767; // max_height
const GDK_HINT_MIN_SIZE = 2;
const GDK_HINT_MAX_SIZE = 4;
lib.symbols.gtk_window_set_geometry_hints(
windowPtr as any,
null, // widget = null → applies to window itself
ptr(buf) as any, // GdkGeometry*
GDK_HINT_MIN_SIZE | GDK_HINT_MAX_SIZE,
);
console.log("[gtk-window] Set geometry hints: min=400×300 max=32767×32767");
} catch (err) {
console.error("[gtk-window] geometry hints failed:", err);
}
return true; return true;
} catch (err) { } catch (err) {
console.error("[gtk-window] ensureResizable failed:", err); console.error("[gtk-window] ensureResizable failed:", err);
@ -168,10 +117,17 @@ export function ensureResizable(windowPtr: number | bigint): boolean {
* WebKitWebView re-propagates content size as minimum on every layout cycle, * WebKitWebView re-propagates content size as minimum on every layout cycle,
* so we must clear it each time not just at init. * so we must clear it each time not just at init.
*/ */
function clearMinSizeTree(lib: NonNullable<typeof gtk3>, windowPtr: any) { /**
* Force small minimum size on the entire widget tree.
* Key insight (from Codex review): set_size_request(-1, -1) means "use preferred size"
* which RE-ENABLES the WebView's content-based minimum. Using set_size_request(1, 1)
* FORCES a 1×1 minimum, overriding the preferred size.
*/
function forceSmallMinSize(lib: NonNullable<typeof gtk3>, windowPtr: any) {
function walk(widget: any, depth: number) { function walk(widget: any, depth: number) {
if (!widget || depth > 10) return; if (!widget || depth > 10) return;
lib.symbols.gtk_widget_set_size_request(widget, -1, -1); // Force 1x1 minimum — NOT -1,-1 which means "use preferred size"
lib.symbols.gtk_widget_set_size_request(widget, 1, 1);
try { try {
const child = lib.symbols.gtk_bin_get_child(widget); const child = lib.symbols.gtk_bin_get_child(widget);
if (child) walk(child, depth + 1); if (child) walk(child, depth + 1);
@ -189,7 +145,7 @@ function clearMinSizeTree(lib: NonNullable<typeof gtk3>, windowPtr: any) {
} catch { /* not a GtkContainer */ } } catch { /* not a GtkContainer */ }
} }
walk(windowPtr, 0); walk(windowPtr, 0);
// Re-apply geometry hints with small minimum // Set geometry hints: min=400×300 (our real minimum), max=32767×32767
try { try {
const buf = new ArrayBuffer(72); const buf = new ArrayBuffer(72);
const view = new Int32Array(buf); const view = new Int32Array(buf);
@ -213,7 +169,7 @@ export function beginResizeDrag(
if (!lib) return false; if (!lib) return false;
try { try {
// Clear min-size RIGHT BEFORE resize so shrinking is allowed // Clear min-size RIGHT BEFORE resize so shrinking is allowed
clearMinSizeTree(lib, windowPtr as any); forceSmallMinSize(lib, windowPtr as any);
lib.symbols.gtk_window_begin_resize_drag( lib.symbols.gtk_window_begin_resize_drag(
windowPtr as any, windowPtr as any,
edge, edge,
@ -267,7 +223,7 @@ export function gtkSetFrame(
if (!lib) return false; if (!lib) return false;
try { try {
// Clear min-size on every frame update during resize // Clear min-size on every frame update during resize
clearMinSizeTree(lib, windowPtr as any); forceSmallMinSize(lib, windowPtr as any);
lib.symbols.gtk_window_resize(windowPtr as any, Math.max(400, Math.round(width)), Math.max(300, Math.round(height))); lib.symbols.gtk_window_resize(windowPtr as any, Math.max(400, Math.round(width)), Math.max(300, Math.round(height)));
lib.symbols.gtk_window_move(windowPtr as any, Math.round(x), Math.round(y)); lib.symbols.gtk_window_move(windowPtr as any, Math.round(x), Math.round(y));
return true; return true;

View file

@ -104,60 +104,18 @@
e.preventDefault(); e.preventDefault();
} }
// ── Window resize — JS-based with GTK min-size clear ──────── // ── Window resize — native GTK begin_resize_drag ────────
let resizeEdge: string | null = null; function onResizeStart(e: MouseEvent, edge: string) {
let resizeStartX = 0; // MUST stop propagation SYNCHRONOUSLY to prevent sidebar drag handler
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',
};
async function onResizeStart(e: MouseEvent, edge: string) {
// Clear min-size constraints first (enables shrink)
appRpc.request['window.clearMinSize']({}).catch(() => {});
// Capture current frame BEFORE starting resize
try {
const frame = await appRpc.request['window.getFrame']({});
resizeFrame = { x: frame.x, y: frame.y, width: frame.width, height: frame.height };
} catch {
// fallback — use defaults
resizeFrame = { x: 100, y: 100, width: 1400, height: 900 };
}
resizeEdge = edge;
resizeStartX = e.screenX;
resizeStartY = e.screenY;
// Lock cursor during resize
document.body.style.cursor = CURSOR_MAP[edge] || 'default';
document.body.style.userSelect = 'none';
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} // Delegate to GTK window manager — handles cursor, animation, constraints
appRpc.request['window.beginResize']({
function onResizeMove(e: MouseEvent) { edge,
if (!resizeEdge) return; button: e.button + 1, // DOM: 0=left, GTK: 1=left
const dx = e.screenX - resizeStartX; rootX: e.screenX,
const dy = e.screenY - resizeStartY; rootY: e.screenY,
let { x, y, width, height } = resizeFrame; }).catch(() => {});
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 GTK FFI directly — bypasses Electrobun's setSize which respects WebView min-size
appRpc.request['window.gtkSetFrame']({ 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) ──────────────
@ -258,9 +216,7 @@
setAgentToastFn(showToast); setAgentToastFn(showToast);
setupErrorBoundary(); setupErrorBoundary();
// JS-based resize needs global mouse listeners // Native GTK begin_resize_drag handles resize — no JS mousemove 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
@ -356,8 +312,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); // no resize listeners to clean up — native GTK handles it
document.removeEventListener('mouseup', onResizeEnd);
}; };
}); });
</script> </script>