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", 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 1ae3539..5fbe88b 100644 --- a/rivet/runtime/windows/src/frame.rs +++ b/rivet/runtime/windows/src/frame.rs @@ -3460,9 +3460,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 { @@ -3589,6 +3599,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. @@ -3767,6 +3782,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) { @@ -4145,6 +4164,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 d16acae..5bcaff6 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.