From 852dbd65f0a0c954e9c9b075afb9a56c7651acde Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Tue, 10 Nov 2026 06:04:09 +0900 Subject: [PATCH] Fix: Resize a window once when a fold or show leaves its page panel where it was Brave and Whale keep the client area they laid out until the window's size changes, so after the appbar folded or showed the page stayed where it was. When the relayout did not move the page panel, a window of its own size is shrunk by one pixel without drawing and put back at its exact rect, which lays it out at once, and the trace says so. The browser view and non-client view layouts tried before did not help and are taken out. Co-Authored-By: Claude Opus 5.5 --- rivet/runtime/windows/src/chromium/view.rs | 14 --- .../windows/src/features/frame_layout.rs | 5 - rivet/runtime/windows/src/frame.rs | 108 +++++++++++++++++- rivet/runtime/windows/src/hooks/page_area.rs | 8 -- 4 files changed, 103 insertions(+), 32 deletions(-) diff --git a/rivet/runtime/windows/src/chromium/view.rs b/rivet/runtime/windows/src/chromium/view.rs index b8001c0..3e3faa7 100644 --- a/rivet/runtime/windows/src/chromium/view.rs +++ b/rivet/runtime/windows/src/chromium/view.rs @@ -39,20 +39,6 @@ impl View { } } -/// The parent of a live view of the loaded build, when its parent pointer holds a view of the -/// browser module: an object whose first word points into the module. `None` for a root, an -/// unreadable place or a build that records no bounds offset. -pub(crate) unsafe fn parent_of(view: usize) -> Option { - let offset = View::parent_offset(crate::builds::get().layout.view_bounds_offset)?; - if !crate::chromium::memory::readable(view.checked_add(offset)?, 8) { - return None; - } - let parent = View::at(view).parent_address(offset); - let image = crate::chromium::image::recorded()?; - let table = crate::chromium::memory::read_word(parent)?; - (table > image.base && table < image.base + image.size).then_some(parent) -} - #[cfg(test)] mod tests { use super::*; diff --git a/rivet/runtime/windows/src/features/frame_layout.rs b/rivet/runtime/windows/src/features/frame_layout.rs index 875faeb..6cd592e 100644 --- a/rivet/runtime/windows/src/features/frame_layout.rs +++ b/rivet/runtime/windows/src/features/frame_layout.rs @@ -1018,11 +1018,6 @@ pub unsafe fn relayout(window: HWND) -> bool { let layout_immediately: unsafe extern "system" fn(*mut c_void) = std::mem::transmute(base + bv().layout_immediately); layout_immediately(view as *mut c_void); - // The non-client view above the frame view gives the client view the bounds the frame - // view leaves it, so a new appbar width moves the page only when it lays out too. - if let Some(parent) = crate::chromium::view::parent_of(view) { - layout_immediately(parent as *mut c_void); - } true } None => false, diff --git a/rivet/runtime/windows/src/frame.rs b/rivet/runtime/windows/src/frame.rs index 722d5d7..def3888 100644 --- a/rivet/runtime/windows/src/frame.rs +++ b/rivet/runtime/windows/src/frame.rs @@ -2028,11 +2028,6 @@ unsafe fn sync_strip(window: HWND) { unsafe fn relayout(window: HWND) { // The frame view lays out again on request, which also works for a maximized window. crate::features::vertical_tabs::invalidate_strip(window); - // The frame view lays its client view out again only when the client view's bounds change, - // and a new appbar width can leave them as they were while the page area must move. The - // browser view lays its page area out again here, whether or not the frame view's own layout - // is recorded, as on Brave and Whale the page otherwise waits for the next input. - crate::hooks::page_area::relayout(window); if frame_layout::relayout(window) { if is_waiting(window) { crate::trace::trace("attach: asked the frame view to lay out again"); @@ -2146,16 +2141,82 @@ unsafe fn nudge(window: HWND, why: &str) -> bool { /// Hides the docked appbar so the page takes its width, or docks it again. unsafe fn toggle_appbar(window: HWND) { close_menu(window); + let before = frame_layout::panel_bounds(window).map(|panel| panel.x); edit_layout(window, |layout| layout.toggle()); crate::trace::trace(&format!("appbar: hidden {}", layout_of(window).collapsed)); sync_strip(window); SLIDE.with(|slides| slides.borrow_mut().remove(&(window as isize))); KillTimer(window, PEEK_TIMER); relayout(window); + let after = frame_layout::panel_bounds(window).map(|panel| panel.x); + if needs_size_nudge(before, after, sizing_state(window)) { + nudge_size(window, before, after); + } // The panel moves with the appbar, and the top bar with it. request_bar_paint(window); } +/// Whether a fold or show must be laid out by a size change: the browser laid the window out +/// again, yet its page panel did not move, as Brave and Whale keep the client area they laid out +/// until the window's size changes. Only a window of its own size is resized: never a minimized, +/// maximized, snapped or fullscreen one. +fn needs_size_nudge(before: Option, after: Option, state: NudgeState) -> bool { + before.is_some() + && before == after + && !state.minimized + && !state.maximized + && !state.snapped + && !state.fullscreen +} + +/// The window's size state, for a size change after a fold or show. +unsafe fn sizing_state(window: HWND) -> NudgeState { + use windows_sys::Win32::UI::WindowsAndMessaging::IsWindowArranged; + NudgeState { + minimized: IsIconic(window) != 0, + maximized: IsZoomed(window) != 0, + snapped: IsWindowArranged(window) != 0, + fullscreen: GetWindowLongPtrW(window, GWL_STYLE) as u32 & WS_THICKFRAME == 0, + ..NudgeState::default() + } +} + +/// Shrinks the window by one pixel without drawing and puts its exact rect back, which lays the +/// browser's root view out at once, and traces what happened. +unsafe fn nudge_size(window: HWND, before: Option, after: Option) { + use windows_sys::Win32::UI::WindowsAndMessaging::{GetWindowRect, SWP_NOREDRAW}; + let mut rect: RECT = std::mem::zeroed(); + GetWindowRect(window, &mut rect); + let (width, height) = (rect.right - rect.left, rect.bottom - rect.top); + SetWindowPos( + window, + std::ptr::null_mut(), + 0, + 0, + width, + height - 1, + SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_NOREDRAW, + ); + SetWindowPos( + window, + std::ptr::null_mut(), + rect.left, + rect.top, + width, + height, + SWP_NOZORDER | SWP_NOACTIVATE, + ); + let mut now: RECT = std::mem::zeroed(); + GetWindowRect(window, &mut now); + let restored = (now.left, now.top, now.right, now.bottom) + == (rect.left, rect.top, rect.right, rect.bottom); + crate::trace::trace(&format!( + "appbar: the panel stayed at {before:?} after {after:?}, so window {:#x} was resized by one pixel and back, rect restored {restored}, panel now at {:?}", + window as usize, + frame_layout::panel_bounds(window).map(|panel| panel.x) + )); +} + /// Shows or puts away the floating appbar of a hidden one, sliding it. unsafe fn set_peek(window: HWND, on: bool) { let changed = edit_layout(window, |layout| { @@ -4710,6 +4771,43 @@ unsafe fn handle(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> } } +#[cfg(test)] +mod nudge_tests { + use super::*; + + #[test] + fn fr2_a_fold_resizes_the_window_only_when_its_panel_did_not_move() { + let normal = NudgeState::default(); + // The relayout moved the panel: nothing more. + assert!(!needs_size_nudge(Some(268), Some(8), normal)); + // The panel stayed where it was: a size change lays it out. + assert!(needs_size_nudge(Some(268), Some(268), normal)); + // No panel known yet: the window's own first layout comes. + assert!(!needs_size_nudge(None, None, normal)); + // Never a window without a size of its own. + for state in [ + NudgeState { + minimized: true, + ..normal + }, + NudgeState { + maximized: true, + ..normal + }, + NudgeState { + snapped: true, + ..normal + }, + NudgeState { + fullscreen: true, + ..normal + }, + ] { + assert!(!needs_size_nudge(Some(268), Some(268), state)); + } + } +} + #[cfg(test)] mod laps_tests { use super::*; diff --git a/rivet/runtime/windows/src/hooks/page_area.rs b/rivet/runtime/windows/src/hooks/page_area.rs index f141114..aac6672 100644 --- a/rivet/runtime/windows/src/hooks/page_area.rs +++ b/rivet/runtime/windows/src/hooks/page_area.rs @@ -279,14 +279,6 @@ pub(crate) unsafe fn relayout(window: HWND) { }) { let layout: unsafe extern "system" fn(*mut c_void) = std::mem::transmute(base + row().layout_immediately); - // The browser view sits in the client view, below the non-client view that gives the - // client view its bounds. That view is laid out first, so a new appbar width reaches the - // page even where the frame view's own layout is not recorded, as on Whale. - if let Some(non_client) = crate::chromium::view::parent_of(host) - .and_then(|client| crate::chromium::view::parent_of(client)) - { - layout(non_client as *mut c_void); - } layout(host as *mut c_void); } }