From 300bd30ca388c23ea46c326aa8a7231cb72c104d Mon Sep 17 00:00:00 2001 From: Hibryda Date: Wed, 25 Mar 2026 13:38:29 +0100 Subject: [PATCH] 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). --- ui-electrobun/src/bun/gtk-window.ts | 125 ++++++++++++++++++++++++++++ ui-electrobun/src/bun/index.ts | 9 +- 2 files changed, 132 insertions(+), 2 deletions(-) diff --git a/ui-electrobun/src/bun/gtk-window.ts b/ui-electrobun/src/bun/gtk-window.ts index 1b0ecc1..af68508 100644 --- a/ui-electrobun/src/bun/gtk-window.ts +++ b/ui-electrobun/src/bun/gtk-window.ts @@ -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 = { n: GDK_EDGE.N, s: GDK_EDGE.S, e: GDK_EDGE.E, w: GDK_EDGE.W, diff --git a/ui-electrobun/src/bun/index.ts b/ui-electrobun/src/bun/index.ts index c8ff6bd..a6cd5a6 100644 --- a/ui-electrobun/src/bun/index.ts +++ b/ui-electrobun/src/bun/index.ts @@ -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.