fix(electrobun): wrap WebView in GtkScrolledWindow for resize-in

Codex review #2 found: set_size_request only controls minimum, not
natural/preferred size. WebView still reports content size as natural,
causing rubber-band effect during shrink.

Fix: reparent WebKitWebView into GtkScrolledWindow via FFI with
propagate-natural-width/height=FALSE and min-content=1x1. This
decouples WebView content size from window size negotiation.

Also reverted to native begin_resize_drag (WM handles everything).
This commit is contained in:
Hibryda 2026-03-25 13:38:29 +01:00
parent d1583f8ce4
commit 300bd30ca3
2 changed files with 132 additions and 2 deletions

View file

@ -66,6 +66,62 @@ function getGtk() {
args: [FFIType.ptr],
returns: FFIType.ptr, // GList*
},
// GtkScrolledWindow — wraps WebView to decouple natural size
gtk_scrolled_window_new: {
args: [FFIType.ptr, FFIType.ptr], // hadjustment, vadjustment (both null)
returns: FFIType.ptr,
},
gtk_scrolled_window_set_policy: {
args: [FFIType.ptr, FFIType.i32, FFIType.i32], // hscrollbar_policy, vscrollbar_policy
returns: FFIType.void,
},
gtk_scrolled_window_set_propagate_natural_width: {
args: [FFIType.ptr, FFIType.bool],
returns: FFIType.void,
},
gtk_scrolled_window_set_propagate_natural_height: {
args: [FFIType.ptr, FFIType.bool],
returns: FFIType.void,
},
gtk_scrolled_window_set_min_content_width: {
args: [FFIType.ptr, FFIType.i32],
returns: FFIType.void,
},
gtk_scrolled_window_set_min_content_height: {
args: [FFIType.ptr, FFIType.i32],
returns: FFIType.void,
},
// Reparenting
gtk_container_remove: {
args: [FFIType.ptr, FFIType.ptr], // container, widget
returns: FFIType.void,
},
gtk_container_add: {
args: [FFIType.ptr, FFIType.ptr], // container, widget
returns: FFIType.void,
},
gtk_widget_show_all: {
args: [FFIType.ptr],
returns: FFIType.void,
},
// Expand flags
gtk_widget_set_hexpand: {
args: [FFIType.ptr, FFIType.bool],
returns: FFIType.void,
},
gtk_widget_set_vexpand: {
args: [FFIType.ptr, FFIType.bool],
returns: FFIType.void,
},
// Reference counting (prevent widget destruction during reparent)
g_object_ref: {
args: [FFIType.ptr],
returns: FFIType.ptr,
},
g_object_unref: {
args: [FFIType.ptr],
returns: FFIType.void,
},
// GList traversal
g_list_length: {
args: [FFIType.ptr],
@ -233,6 +289,75 @@ export function gtkSetFrame(
}
}
/**
* Wrap the WebKitWebView inside a GtkScrolledWindow to decouple its
* natural/preferred size from the window's size negotiation.
* This prevents the "rubber band" effect during resize-in.
*
* Codex review: GtkScrolledWindow has propagate-natural-width/height = FALSE
* by default, which stops the child's natural size from reaching the window.
*/
export function wrapWebViewInScrolledWindow(windowPtr: number | bigint): boolean {
const lib = getGtk();
if (!lib) return false;
try {
// Get the window's child (container holding the WebView)
const container = lib.symbols.gtk_bin_get_child(windowPtr as any);
if (!container) { console.log("[gtk-window] No child container found"); return false; }
// Get the WebView from the container
const webview = lib.symbols.gtk_bin_get_child(container as any);
if (!webview) { console.log("[gtk-window] No WebView found in container"); return false; }
console.log("[gtk-window] Wrapping WebView in GtkScrolledWindow");
// Ref the WebView so it's not destroyed when removed from container
lib.symbols.g_object_ref(webview as any);
// Remove WebView from its current parent
lib.symbols.gtk_container_remove(container as any, webview as any);
// Create a GtkScrolledWindow
const scrolled = lib.symbols.gtk_scrolled_window_new(null, null);
if (!scrolled) {
// Put WebView back if scrolled window creation failed
lib.symbols.gtk_container_add(container as any, webview as any);
lib.symbols.g_object_unref(webview as any);
return false;
}
// Configure: no scrollbar policy (automatic), don't propagate natural size
const GTK_POLICY_AUTOMATIC = 1;
const GTK_POLICY_NEVER = 2;
lib.symbols.gtk_scrolled_window_set_policy(scrolled as any, GTK_POLICY_NEVER, GTK_POLICY_NEVER);
lib.symbols.gtk_scrolled_window_set_propagate_natural_width(scrolled as any, false);
lib.symbols.gtk_scrolled_window_set_propagate_natural_height(scrolled as any, false);
lib.symbols.gtk_scrolled_window_set_min_content_width(scrolled as any, 1);
lib.symbols.gtk_scrolled_window_set_min_content_height(scrolled as any, 1);
// Set expand flags
lib.symbols.gtk_widget_set_hexpand(scrolled as any, true);
lib.symbols.gtk_widget_set_vexpand(scrolled as any, true);
lib.symbols.gtk_widget_set_size_request(scrolled as any, 1, 1);
// Add WebView to ScrolledWindow
lib.symbols.gtk_container_add(scrolled as any, webview as any);
lib.symbols.g_object_unref(webview as any); // balance the ref
// Add ScrolledWindow to the original container
lib.symbols.gtk_container_add(container as any, scrolled as any);
// Show everything
lib.symbols.gtk_widget_show_all(scrolled as any);
console.log("[gtk-window] WebView successfully wrapped in GtkScrolledWindow");
return true;
} catch (err) {
console.error("[gtk-window] wrapWebViewInScrolledWindow failed:", err);
return false;
}
}
// Edge string → GDK_EDGE mapping
const EDGE_MAP: Record<string, GdkEdge> = {
n: GDK_EDGE.N, s: GDK_EDGE.S, e: GDK_EDGE.E, w: GDK_EDGE.W,

View file

@ -233,10 +233,15 @@ mainWindow = new BrowserWindow({
},
});
// Ensure GTK window is resizable (titleBarStyle:"hidden" may clear the flag)
// Ensure GTK window is resizable and wrap WebView for proper resize-in
{
const { ensureResizable } = require("./gtk-window.ts");
const { ensureResizable, wrapWebViewInScrolledWindow } = require("./gtk-window.ts");
ensureResizable((mainWindow as any).ptr);
// Wrap WebView in GtkScrolledWindow to decouple natural size from window size
// (prevents rubber-band effect when shrinking)
setTimeout(() => {
wrapWebViewInScrolledWindow((mainWindow as any).ptr);
}, 2000); // Delay to ensure WebView is fully initialized
}
// Prevent GTK's false Ctrl+click detection from closing the window on initial load.