From a2a10933e36417046e2158c27fafdf13b9ffdb27 Mon Sep 17 00:00:00 2001 From: metif12 Date: Wed, 21 Oct 2026 04:32:29 +0200 Subject: [PATCH] Add on_window_resize hook for embedders, wire WM_SIZE and windowDidResize MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebased onto master, dropping the stacked native_window_handle and on_window_ready layers that have since merged there in refined form. The port follows master's window_ready_fn field naming and the defer-safe test style from the on_window_ready merge. Review feedback folded in: the hook only fires on the native Windows and macOS backends — the immediate renderer (Linux, Android, and the custom renderer everywhere) stores the handler and never calls it, so the shared doc says so instead of promising a dead callback. Units are platform native: physical pixels on Windows, AppKit points on macOS, which differ on Retina displays. The Windows hook also fires with 0x0 while the window is minimized, and never for the initial window size, so embedders size their child in the on_window_ready callback. --- appkit/ui_macos_darwin.v | 25 +++++++++++++ embed_test.v | 80 ++++++++++++++++++++++++++++++++++++++++ ui/ui.v | 14 +++++++ ui/ui_immediate.c.v | 9 +++++ windows/ui_windows.v | 27 +++++++++++++- 5 files changed, 153 insertions(+), 2 deletions(-) diff --git a/appkit/ui_macos_darwin.v b/appkit/ui_macos_darwin.v index 499b00b7..4693e063 100644 --- a/appkit/ui_macos_darwin.v +++ b/appkit/ui_macos_darwin.v @@ -103,6 +103,7 @@ mut: scroll_handler ScrollFn = ScrollFn(unsafe { nil }) drop_handler DropFn = DropFn(unsafe { nil }) window_ready_fn WindowReadyFn = WindowReadyFn(unsafe { nil }) + window_resize_fn WindowResizeFn = WindowResizeFn(unsafe { nil }) window NativeView root_view NativeView button_handler NativeView @@ -389,6 +390,29 @@ fn fire_window_ready() { st.window_ready_fn(st.window) } +// on_window_resize registers a handler called with the content size in +// points (AppKit logical units, which differ from pixels on Retina displays) +// whenever the window is resized, so an embedder can size its child view to +// match. +pub fn on_window_resize(handler WindowResizeFn) { + mut st := state() + st.window_resize_fn = handler +} + +// fire_window_resize notifies the embedder registered through +// on_window_resize, if any. +fn fire_window_resize() { + st := state() + if voidptr(st.window_resize_fn) == unsafe { nil } { + return + } + if native_is_nil(st.root_view) { + return + } + b := native_bounds(st.root_view) + st.window_resize_fn(int(b.width), int(b.height)) +} + // set_window_title updates the current AppKit window title. pub fn set_window_title(title string) { st := state() @@ -3924,6 +3948,7 @@ fn scroll_view_move(scroll NativeView, horizontal bool, distance f64) { @[export: 'ui2_window_did_resize'] fn ui2_window_did_resize(_self voidptr, _cmd voidptr, _notification voidptr) { refresh() + fire_window_resize() } @[export: 'ui2_window_key_down'] diff --git a/embed_test.v b/embed_test.v index a1f893e4..86fe1aeb 100644 --- a/embed_test.v +++ b/embed_test.v @@ -70,6 +70,69 @@ $if windows && !ui2_custom_rendering ? { windows_fire_window_ready() assert native_window_handle() == previous_window } + + fn window_resize_test_handler(width int, height int) { + // Report through the state so the test needs no globals. + mut st := windows_state() + st.scroll_positions['resize_test_w'] = width + st.scroll_positions['resize_test_h'] = height + } + + // The resize hook reports the size it is handed, and is a silent no-op when + // no handler is registered. + fn test_windows_window_resize_hook_reports_client_size() { + mut st := windows_state() + previous_handler := st.window_resize_fn + defer { + st.window_resize_fn = previous_handler + st.scroll_positions.delete('resize_test_w') + st.scroll_positions.delete('resize_test_h') + } + st.scroll_positions.delete('resize_test_w') + st.scroll_positions.delete('resize_test_h') + on_window_resize(window_resize_test_handler) + windows_fire_window_resize(800, 600) + assert st.scroll_positions['resize_test_w'] or { -1 } == 800 + assert st.scroll_positions['resize_test_h'] or { -1 } == 600 + on_window_resize(WindowResizeFn(unsafe { nil })) + windows_fire_window_resize(1, 2) + assert st.scroll_positions['resize_test_w'] or { -1 } == 800 + assert st.scroll_positions['resize_test_h'] or { -1 } == 600 + } + + // End-to-end wiring: a real WM_SIZE delivered to the window proc reaches + // the registered handler with the window's actual client size. Calling + // windows_fire_window_resize directly would not prove the WM_SIZE branch is + // wired up. + fn test_windows_real_wm_size_reaches_the_resize_hook() { + mut st := windows_state() + previous_root := st.root + previous_handler := st.window_resize_fn + defer { + st.root = previous_root + st.window_resize_fn = previous_handler + st.scroll_positions.delete('resize_test_w') + st.scroll_positions.delete('resize_test_h') + } + assert C.ui2_win_register_classes() != 0 + title := 'resize test'.to_wide() + root := C.ui2_win_create_main_window(title, 320, 200) + unsafe { + free(title) + } + assert root != unsafe { nil } + defer { + C.ui2_win_destroy(root) + } + st.root = root + on_window_resize(window_resize_test_handler) + st.scroll_positions.delete('resize_test_w') + st.scroll_positions.delete('resize_test_h') + ui2_windows_window_proc(root, win_wm_size, 0, 0) + assert st.scroll_positions['resize_test_w'] or { -1 } == C.ui2_win_client_width(root) + assert st.scroll_positions['resize_test_h'] or { -1 } == C.ui2_win_client_height(root) + assert (st.scroll_positions['resize_test_w'] or { -1 }) > 0 + } } $if ui2_custom_rendering ? { @@ -100,3 +163,20 @@ $if ui2_custom_rendering ? { assert native_window_handle() == previous_window } } + +$if ui2_custom_rendering ? { + fn window_resize_custom_test_handler(_width int, _height int) {} + + // The gg backends have no native child to size, so the hook is a + // documented no-op that still accepts a registration. + fn test_custom_window_resize_hook_is_a_documented_noop() { + previous_handler := g_window_resize_handler + defer { + g_window_resize_handler = previous_handler + } + on_window_resize(window_resize_custom_test_handler) + assert voidptr(g_window_resize_handler) != unsafe { nil } + on_window_resize(WindowResizeFn(unsafe { nil })) + assert voidptr(g_window_resize_handler) == unsafe { nil } + } +} diff --git a/ui/ui.v b/ui/ui.v index cca162a7..bf65ea44 100644 --- a/ui/ui.v +++ b/ui/ui.v @@ -217,6 +217,20 @@ pub type WheelFn = fn (dx f64, dy f64) // Linux. pub type WindowReadyFn = fn (handle voidptr) +// WindowResizeFn receives the main window's client size whenever it changes, +// so an embedder can resize its child (e.g. a webview) to match. ui2 never +// manages the child itself. The hook does not fire for the initial window +// size: create and size the child in the on_window_ready callback instead. +// +// - Windows: client size in physical pixels. The hook also fires with 0x0 +// while the window is minimized, then with the real size again on +// restore. +// - macOS: content size in AppKit points, not pixels; the two differ on +// Retina displays. +// - Immediate renderer (Linux, Android, and the custom renderer on every +// platform): never fires, because there is no native child to size. +pub type WindowResizeFn = fn (width int, height int) + pub struct DropEvent { pub: paths []string diff --git a/ui/ui_immediate.c.v b/ui/ui_immediate.c.v index f482715d..d920fc3f 100644 --- a/ui/ui_immediate.c.v +++ b/ui/ui_immediate.c.v @@ -171,6 +171,7 @@ $if (android || linux || ((macos || windows) && ui2_custom_rendering ?)) && !ui2 __global g_scroll_handler = ScrollFn(unsafe { nil }) __global g_drop_handler = DropFn(unsafe { nil }) __global g_window_ready_handler = WindowReadyFn(unsafe { nil }) + __global g_window_resize_handler = WindowResizeFn(unsafe { nil }) __global g_key_consumed = false __global g_gg_app = &GgApp{} __global g_refresh_mode = RefreshMode.on_demand @@ -570,6 +571,14 @@ $if (android || linux || ((macos || windows) && ui2_custom_rendering ?)) && !ui2 g_window_ready_handler(unsafe { nil }) } + // on_window_resize only stores the handler: a documented no-op. The + // immediate renderer backends (Linux, Android, and the custom renderer on + // every platform) have no native child to resize — but accepting the + // registration keeps embedder code uniform across backends. + pub fn on_window_resize(handler WindowResizeFn) { + g_window_resize_handler = handler + } + pub fn text(id string) string { // State maps store owned strings so that replacing their values can // release the previous allocation. Callers, in particular QML bindings, diff --git a/windows/ui_windows.v b/windows/ui_windows.v index 49aa463e..b393be14 100644 --- a/windows/ui_windows.v +++ b/windows/ui_windows.v @@ -408,6 +408,7 @@ mut: scroll_handler ScrollFn = ScrollFn(unsafe { nil }) drop_handler DropFn = DropFn(unsafe { nil }) window_ready_fn WindowReadyFn = WindowReadyFn(unsafe { nil }) + window_resize_fn WindowResizeFn = WindowResizeFn(unsafe { nil }) root voidptr nodes map[string]voidptr node_kinds map[string]Kind @@ -846,6 +847,25 @@ fn windows_fire_window_ready() { st.window_ready_fn(st.root) } +// on_window_resize registers a handler called with the client size in +// physical pixels whenever the main window is resized, so an embedder can +// size its child window (e.g. a webview) to match. A minimized window +// reports 0x0; the real size follows on restore. +pub fn on_window_resize(handler WindowResizeFn) { + mut st := windows_state() + st.window_resize_fn = handler +} + +// windows_fire_window_resize notifies the embedder registered through +// on_window_resize, if any. +fn windows_fire_window_resize(width int, height int) { + st := windows_state() + if voidptr(st.window_resize_fn) == unsafe { nil } { + return + } + st.window_resize_fn(width, height) +} + // set_window_title updates the current Win32 window title. pub fn set_window_title(title string) { st := windows_state() @@ -2309,8 +2329,11 @@ fn ui2_windows_window_proc(hwnd voidptr, message u32, wparam usize, lparam isize return 0 } win_wm_size { - if hwnd == st.root && !st.rendering { - refresh() + if hwnd == st.root { + if !st.rendering { + refresh() + } + windows_fire_window_resize(C.ui2_win_client_width(hwnd), C.ui2_win_client_height(hwnd)) } return 0 }