From 4c8334ec770af9c5e0f707874b8f71791a20e9cb Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Tue, 10 Nov 2026 04:27:38 +0900 Subject: [PATCH 1/3] Fix: Keep the top bar's controls where they take clicks when the appbar folds Folding the appbar moves the browser's panel, and the top bar with it, but the bar was painted again only on a later event, so its buttons showed at their old place while the clicks went to the new one. The bar now repaints right after the browser moves the panel. The window's hit test also gives the bar's controls to the bar itself, from the places it paints them, where the browser's own hit test called them title bar on Chrome, Brave and Samsung Internet. Co-Authored-By: Claude Opus 5.5 --- .../windows/src/features/address_bar.rs | 33 +++++++++++++++++-- rivet/runtime/windows/src/frame.rs | 20 +++++++++++ rivet/runtime/windows/src/lib.rs | 3 ++ scripts/tests/windows-features.ps1 | 8 +++-- 4 files changed, 59 insertions(+), 5 deletions(-) diff --git a/rivet/runtime/windows/src/features/address_bar.rs b/rivet/runtime/windows/src/features/address_bar.rs index 01eab90..e707a2c 100644 --- a/rivet/runtime/windows/src/features/address_bar.rs +++ b/rivet/runtime/windows/src/features/address_bar.rs @@ -692,14 +692,22 @@ pub(crate) unsafe fn with_field_pixels( }) } -unsafe fn part_at(window: HWND, point: POINT) -> Option { +/// The control of the top bar under a client pixel. The window's hit test asks it too, so the bar +/// takes the clicks of its controls wherever the browser would put its own title bar. +pub(crate) unsafe fn part_at(window: HWND, point: POINT) -> Option { let [x, y, width] = place(window)?; let f = factor(window); let (px, py) = (point.x as f32 / f - x, point.y as f32 / f - y); + part_in(width, in_pane(window), dev_reserve(window), px, py) +} + +/// The control of a bar `width` DIPs wide at `px`, `py` DIPs from its top left corner, from the +/// same places the bar is painted at. +pub(crate) fn part_in(width: f32, pane: bool, reserve: f32, px: f32, py: f32) -> Option { if py < 0.0 || py >= ROW_HEIGHT as f32 { return None; } - let parts = parts_reserving(width, in_pane(window), dev_reserve(window)); + let parts = parts_reserving(width, pane, reserve); // The chip lies inside the address, so it is looked for first. parts .iter() @@ -1709,6 +1717,27 @@ pub(crate) fn caret(window: HWND) -> Option { mod tests { use super::*; + #[test] + fn fr12_each_control_answers_where_it_is_painted_shown_and_folded() { + // A bar beside the shown appbar and the wider bar of a folded one, as the browser lays + // the panel out for each. + for width in [1156.0, 1422.0] { + for (part, at, w) in parts_reserving(width, false, 0.0) { + if part == Part::Chip { + continue; + } + let middle = at + w / 2.0; + assert_eq!( + part_in(width, false, 0.0, middle, ROW_HEIGHT as f32 / 2.0), + Some(part), + "{part:?} of a {width} DIP bar" + ); + } + assert_eq!(part_in(width, false, 0.0, 10.0, -1.0), None); + assert_eq!(part_in(width, false, 0.0, 10.0, ROW_HEIGHT as f32), None); + } + } + #[test] fn nfr2_one_key_asks_the_browser_once_and_rivets_own_completion_never() { // A key down and up leave the text alone, so only the character asks. diff --git a/rivet/runtime/windows/src/frame.rs b/rivet/runtime/windows/src/frame.rs index 0f6f243..e2861b8 100644 --- a/rivet/runtime/windows/src/frame.rs +++ b/rivet/runtime/windows/src/frame.rs @@ -3448,9 +3448,19 @@ fn panel_moved(window: HWND) { } } }); + // The top bar is the panel's first row, so it moves with the panel. It is painted after + // this layout, never inside it, and until then its old place would show where the bar no + // longer takes clicks, as after the appbar folds. + PostMessageW(window, bar_moved_message(), 0, 0); } } +const BAR_MOVED_MESSAGE: &str = "Rivet.BarMoved"; + +fn bar_moved_message() -> u32 { + unsafe { RegisterWindowMessageW(wide(BAR_MOVED_MESSAGE).as_ptr()) } +} + /// The thickness of the visible window border along the top edge. A maximized window has none. unsafe fn top_border(window: HWND) -> i32 { if IsZoomed(window) != 0 { @@ -3577,6 +3587,11 @@ unsafe fn hit_test(window: HWND, lparam: LPARAM) -> StripHit { && row_hit(window, point).is_none() && crate::dev_bar::hit(window, point).is_none() { + // The bar's controls are Rivet's, at the places it painted them, whatever the browser's + // own hit test would say of its title bar there. + if crate::features::address_bar::part_at(window, point).is_some() { + return StripHit::Client; + } let mut client: RECT = std::mem::zeroed(); GetClientRect(window, &mut client); // The panel starts one gap below the client area's top edge, so the row is measured from it. @@ -3755,6 +3770,10 @@ unsafe fn handle(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> toggle_appbar(window); return 0; } + if message == bar_moved_message() && attached(window) { + update_top_bar(window); + return 0; + } // Rivet's top bar takes the pointer over it and, while its address is edited, the keyboard and // the input method. if attached(window) && !menu_open(window) && !tab_dragging(window) && !dragging(window) { @@ -4133,6 +4152,7 @@ unsafe fn handle(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> StripHit::Top => HTTOP as LRESULT, StripHit::TopLeft => HTTOPLEFT as LRESULT, StripHit::Appbar | StripHit::Resizer => APPBAR_HIT as LRESULT, + StripHit::Client => 1, // HTCLIENT StripHit::Minimize => HTMINBUTTON as LRESULT, StripHit::Maximize => HTMAXBUTTON as LRESULT, StripHit::Close => HTCLOSE as LRESULT, diff --git a/rivet/runtime/windows/src/lib.rs b/rivet/runtime/windows/src/lib.rs index f379f6b..13bea41 100644 --- a/rivet/runtime/windows/src/lib.rs +++ b/rivet/runtime/windows/src/lib.rs @@ -189,6 +189,9 @@ enum StripHit { Resizer, /// Belongs to the appbar's own content. Appbar, + /// A control of Rivet's top bar, which takes its clicks in the client area. + #[cfg_attr(not(windows), allow(dead_code))] + Client, /// The caption buttons, which Windows needs by name for Snap Layouts. #[cfg_attr(not(windows), allow(dead_code))] Minimize, diff --git a/scripts/tests/windows-features.ps1 b/scripts/tests/windows-features.ps1 index 929fb4f..372101b 100644 --- a/scripts/tests/windows-features.ps1 +++ b/scripts/tests/windows-features.ps1 @@ -16,7 +16,7 @@ # rows run only when no other run is active (windows-test-safety.ps1). param( [string[]]$Browsers = @('chrome', 'edge', 'whale', 'brave', 'samsung'), - [string[]]$Features = @('split', 'sidebar', 'newtab', 'space', 'split-start', 'split-menu', 'split-menu-start', 'split-drag', 'split-drag-start', 'split-shortcut', 'split-one-tab', 'split-maximised', 'split-space', 'split-click', 'split-rows', 'split-drop-top', 'split-drop-bottom', 'split-grid-3', 'split-grid-4', 'bar-layout', 'mini-pop-out', 'mini-move', 'mini-return', 'mini-close', 'tab-sections', 'tab-close-select', 'tab-reopen', 'tab-archive', 'ctrl-t-setting', 'link-new-tab', 'command-bar', 'link-preview', 'rivet-settings', 'link-rules', 'address-suggest', 'appbar-width', 'appbar-peek', 'open-link-running', 'open-link-start', 'credentials-browser', 'credentials-system', 'view-click', 'view-click-other', 'view-keys', 'view-ime', 'tab-moves', 'first-frame', 'fullscreen-sidebar', 'taskbar-identity', 'taskbar-relaunch', 'sidebar-scale-monitor', 'split-panel-look', 'sidebar-floats', 'sidebar-open-button', 'dev-row', 'address-editing', 'security-chip', 'permission-bubble', 'password-bubble', 'typing-keeps-active', 'cli-control', 'cli-input', 'cli-hidden'), + [string[]]$Features = @('split', 'sidebar', 'newtab', 'space', 'split-start', 'split-menu', 'split-menu-start', 'split-drag', 'split-drag-start', 'split-shortcut', 'split-one-tab', 'split-maximised', 'split-space', 'split-click', 'split-rows', 'split-drop-top', 'split-drop-bottom', 'split-grid-3', 'split-grid-4', 'bar-layout', 'bar-folded', 'mini-pop-out', 'mini-move', 'mini-return', 'mini-close', 'tab-sections', 'tab-close-select', 'tab-reopen', 'tab-archive', 'ctrl-t-setting', 'link-new-tab', 'command-bar', 'link-preview', 'rivet-settings', 'link-rules', 'address-suggest', 'appbar-width', 'appbar-peek', 'open-link-running', 'open-link-start', 'credentials-browser', 'credentials-system', 'view-click', 'view-click-other', 'view-keys', 'view-ime', 'tab-moves', 'first-frame', 'fullscreen-sidebar', 'taskbar-identity', 'taskbar-relaunch', 'sidebar-scale-monitor', 'split-panel-look', 'sidebar-floats', 'sidebar-open-button', 'dev-row', 'address-editing', 'security-chip', 'permission-bubble', 'password-bubble', 'typing-keeps-active', 'cli-control', 'cli-input', 'cli-hidden'), [string]$OutDir, [switch]$NoBuild, # A browser build outside its usual folder, such as one a row job downloaded. Needs one -Browsers. @@ -3132,8 +3132,10 @@ function Invoke-BarFolded($Name, $shots) { $back = Get-Capture $ctx (Join-Path $shots 'bar-folded-3-shown.png') $again = @(Test-BarControls $ctx 'shown again' $back) $bad = @($open + $fold + $again) - if ($bad.Count) { return @('FAIL', $folded.Path, (($bad -join '; ') + '. ' + ($notes -join '; '))) } - @('PASS', $folded.Path, 'back, forward, reload, the address and the window buttons answer where they are drawn, shown, folded and shown again. ' + ($notes -join '; ')) + $painted = [string]::Join('; ', [string[]]$notes) + if ($bad.Count) { return @('FAIL', $folded.Path, ([string]::Join('; ', [string[]]$bad) + '. ' + $painted)) } + $reason = 'back, forward, reload, the address and the window buttons answer where they are drawn, shown, folded and shown again. ' + $painted + @('PASS', $folded.Path, $reason) } finally { Stop-Scene $ctx } } From 54f5cdc2120302e04758376e97aee297bd8a9ab1 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Tue, 10 Nov 2026 04:30:32 +0900 Subject: [PATCH 2/3] Test: Gate rows on bar-folded too Co-Authored-By: Claude Opus 5.5 --- rivet/platform/windows/src/row_watch.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/rivet/platform/windows/src/row_watch.rs b/rivet/platform/windows/src/row_watch.rs index e17a9b1..c960533 100644 --- a/rivet/platform/windows/src/row_watch.rs +++ b/rivet/platform/windows/src/row_watch.rs @@ -216,6 +216,7 @@ pub const FEATURES: &[&str] = &[ "split-grid-3", "split-grid-4", "bar-layout", + "bar-folded", "mini-pop-out", "mini-move", "mini-return", From a59aa92688c5fc5b6146ce4a02eb583bc7690025 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Tue, 10 Nov 2026 04:33:19 +0900 Subject: [PATCH 3/3] Test: Fold as a person does in bar-folded Three tabs with a pin and a folder, then three folds and three shows, by the appbar's own fold button where the runtime traced it and by the command, with every control checked and clicked after each change. Co-Authored-By: Claude Opus 5.5 --- rivet/runtime/windows/src/frame.rs | 6 +++ scripts/tests/windows-features.ps1 | 70 ++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 19 deletions(-) diff --git a/rivet/runtime/windows/src/frame.rs b/rivet/runtime/windows/src/frame.rs index e2861b8..1ab5c14 100644 --- a/rivet/runtime/windows/src/frame.rs +++ b/rivet/runtime/windows/src/frame.rs @@ -3325,6 +3325,12 @@ pub(crate) unsafe fn update_appbar(window: HWND) { let (x, y, w, _) = toggle_rect(page_x(window)); (scale(x, dpi), scale(y, dpi), scale(w, dpi)) }; + if crate::trace::tracing() { + crate::trace::trace_changed( + "toggle", + &format!("appbar: toggle at {x} {y} {size} px"), + ); + } if let Err(error) = appbar.update_toggle(visible, x, y, size, dpi) { log(&format!("rivet: could not draw the toggle: {error}")); } diff --git a/scripts/tests/windows-features.ps1 b/scripts/tests/windows-features.ps1 index 372101b..5bcaff6 100644 --- a/scripts/tests/windows-features.ps1 +++ b/scripts/tests/windows-features.ps1 @@ -3111,31 +3111,63 @@ function Test-BarControls($ctx, [string]$Stage, $Capture) { $bad } +# The appbar's own fold button, where the runtime painted it, clicked as a person clicks it: the +# window's hit test must give it to the appbar, and the press and release go there. +function Send-ToggleClick($ctx) { + $line = @(Read-Log | Where-Object { $_ -match '^appbar: toggle at ' }) | Select-Object -Last 1 + if (-not $line -or $line -notmatch 'toggle at (-?\d+) (-?\d+) (\d+) px') { return 'the runtime traced no fold button' } + $cx = [int]$Matches[1] + [int]([int]$Matches[3] / 2); $cy = [int]$Matches[2] + [int]([int]$Matches[3] / 2) + $r = New-Object FeatureWin+R; [FeatureWin]::GetWindowRect($ctx.Hwnd, [ref]$r) | Out-Null + $o = New-Object FeatureWin+P; $o.X = 0; $o.Y = 0 + [FeatureWin]::ClientToScreen($ctx.Hwnd, [ref]$o) | Out-Null + $hit = Get-HitTest $ctx ($cx + $o.X - $r.L) ($cy + $o.Y - $r.T) + if ($hit -ne 18) { return "the fold button at $cx,$cy hits $hit, not the appbar's 18" } + Send-AppbarNc $ctx 0xA1 $cx $cy + Start-Sleep -Milliseconds 120 + Send-AppbarNc $ctx 0xA2 $cx $cy + $null +} + +# As a person's window: three tabs, one pinned and two in a folder, folded and shown three times, +# with the fold button and with the command, and every control clicked after each change. A held +# build shows its warning over the page meanwhile. function Invoke-BarFolded($Name, $shots) { $ctx, $state = Start-Scene $Name 'bar-folded' $urlA try { - Open-Scene $ctx $state $urlA $null - if ($ctx.Error -and $ctx.Error -notin @('attach-failed', 'attach-incomplete')) { return @('NOT_DRIVEN', $null, $ctx.Error) } - $one = Get-Capture $ctx (Join-Path $shots 'bar-folded-1-open.png') + $why = Open-ThreeTabs $ctx $state $shots 'bar-folded' + if ($why) { return @('NOT_DRIVEN', $null, $why) } + foreach ($command in 'pin 2', 'folder 1 Launch Atlas', 'into 0') { + if (-not (Send-Flag $ctx 'rivet-tab.txt' $command)) { return @('NOT_DRIVEN', $null, "the runtime did not take '$command'") } + } + $one = Get-Capture $ctx (Join-Path $shots 'bar-folded-0-shown.png') if (-not $one) { return @('NOT_DRIVEN', $null, 'no capture') } - if (-not (Test-Attached $ctx)) { return @('FAIL', $one.Path, (Get-AttachFailure $ctx)) } - $notes = @() - $open = @(Test-BarControls $ctx 'shown' $one) + $notes = @(); $bad = @(@(Test-BarControls $ctx 'shown' $one)) $notes += (Get-PaintedBar).Line -replace '^address bar: ', 'shown: ' - Send-RivetMessage $ctx 'Rivet.ToggleAppbar' - Start-Sleep 4 - $folded = Get-Capture $ctx (Join-Path $shots 'bar-folded-2-folded.png') - $fold = @(Test-BarControls $ctx 'folded' $folded) - $notes += (Get-PaintedBar).Line -replace '^address bar: ', 'folded: ' - Send-RivetMessage $ctx 'Rivet.ToggleAppbar' - Start-Sleep 4 - $back = Get-Capture $ctx (Join-Path $shots 'bar-folded-3-shown.png') - $again = @(Test-BarControls $ctx 'shown again' $back) - $bad = @($open + $fold + $again) + $hidden = $false + foreach ($step in 1..6) { + $before = @(Read-Log).Count + # Odd steps fold, even steps show again; the fold button does it but for the third and fourth. + $byButton = $step -notin 3, 4 + if ($byButton) { + $why = Send-ToggleClick $ctx + if ($why) { $bad += "step $step`: $why"; break } + } else { + Send-RivetMessage $ctx 'Rivet.ToggleAppbar' + } + $want = if ($hidden) { 'false' } else { 'true' } + if (-not (Wait-Log $before "^appbar: hidden $want$" 5)) { $bad += "step $step`: the appbar did not $(if ($hidden) { 'show' } else { 'fold' })"; break } + $hidden = -not $hidden + Start-Sleep 3 + $stage = "step $step $(if ($hidden) { 'folded' } else { 'shown' })$(if ($byButton) { ' by the button' } else { ' by the command' })" + $shot = Get-Capture $ctx (Join-Path $shots "bar-folded-$step.png") + $bad += @(Test-BarControls $ctx $stage $shot) + $notes += (Get-PaintedBar).Line -replace '^address bar: ', "$stage`: " + } + $last = Join-Path $shots 'bar-folded-1.png' $painted = [string]::Join('; ', [string[]]$notes) - if ($bad.Count) { return @('FAIL', $folded.Path, ([string]::Join('; ', [string[]]$bad) + '. ' + $painted)) } - $reason = 'back, forward, reload, the address and the window buttons answer where they are drawn, shown, folded and shown again. ' + $painted - @('PASS', $folded.Path, $reason) + if ($bad.Count) { return @('FAIL', $last, ([string]::Join('; ', [string[]]$bad) + '. ' + $painted)) } + $reason = 'with three tabs, a pin and a folder, back, forward, reload, the address and the window buttons answer where they are drawn, shown and after three folds and three shows by the fold button and by the command. ' + $painted + @('PASS', $last, $reason) } finally { Stop-Scene $ctx } }