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 }