From 0771c1ba1d7b595ca2a79080e7a7196ecbc63d32 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:29:30 +0900 Subject: [PATCH 1/2] Feat: Carry ConvertPointToScreen in a row and trace Chromium's own bubble anchor The rows generator finds views::View::ConvertPointToScreen by its public name in the new build and records its address with its first whole instructions as screen_origin, unless the prior row already carried it. A build without a single place for the name goes without the part and is not stopped. The runtime checks those bytes at attach and, in the GetAnchorRect hook, traces where Chromium puts the address row and chip beside the monitor based place, with a same flag. Nothing changes where a bubble opens yet. Co-Authored-By: Claude Opus 5.5 --- .../platform/windows/src/bin/chrome-builds.rs | 8 +- rivet/platform/windows/src/row_port.rs | 95 +++++++++++++++++++ rivet/runtime/windows/src/builds/brave.rs | 3 + rivet/runtime/windows/src/builds/chrome.rs | 5 + rivet/runtime/windows/src/builds/edge.rs | 1 + rivet/runtime/windows/src/builds/mod.rs | 4 + rivet/runtime/windows/src/builds/samsung.rs | 1 + rivet/runtime/windows/src/builds/tests.rs | 12 +++ rivet/runtime/windows/src/builds/whale.rs | 1 + rivet/runtime/windows/src/chromium/mod.rs | 1 + rivet/runtime/windows/src/chromium/screen.rs | 69 ++++++++++++++ .../windows/src/features/bubble_anchor.rs | 71 ++++++++++++-- 12 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 rivet/runtime/windows/src/chromium/screen.rs diff --git a/rivet/platform/windows/src/bin/chrome-builds.rs b/rivet/platform/windows/src/bin/chrome-builds.rs index d1ca5ac..c278196 100644 --- a/rivet/platform/windows/src/bin/chrome-builds.rs +++ b/rivet/platform/windows/src/bin/chrome-builds.rs @@ -160,7 +160,12 @@ fn port( let wanted = row_port::wanted_names(&named); drop(old); let mut new = std::collections::HashMap::>::new(); + // `views::View::ConvertPointToScreen` is found by its own name, for a prior row without it. + let mut screen_origin = Vec::new(); symbols::publics(new_pdb, &mut |address, name| { + if row_port::is_screen_origin_name(name) { + screen_origin.push(address); + } let name = row_port::stable_name(name); if wanted.contains(name) { new.entry(name.to_owned()).or_default().push(address); @@ -178,7 +183,7 @@ fn port( }, ) .collect(); - let (row, mut report) = row_port::port_row( + let (mut row, mut report) = row_port::port_row( &prior, &named, &NewBuild { @@ -188,6 +193,7 @@ fn port( private: &private, }, ); + row_port::seed_screen_origin(&mut row, &screen_origin, &pe, &mut report); report.review.extend(row_port::hand_only_review(&prior)); let text = |value: &dyn erased::Json| value.pretty(); std::fs::write(row_path, text(&row)).map_err(|error| error.to_string())?; diff --git a/rivet/platform/windows/src/row_port.rs b/rivet/platform/windows/src/row_port.rs index 383e477..33b0dbd 100644 --- a/rivet/platform/windows/src/row_port.rs +++ b/rivet/platform/windows/src/row_port.rs @@ -62,6 +62,8 @@ pub struct PortReport { pub derived: Vec, /// Values that are not addresses and have no recipe, so they were carried over. pub carried: Vec, + /// Optional parts the new build could not supply. The runtime does without them. + pub left_out: Vec, } fn is_address(value: u64) -> bool { @@ -1348,6 +1350,61 @@ fn check_frame_slot(frame: &mut Value, pe: &PeFile, report: &mut PortReport) { } } +/// The start of the public name of `views::View::ConvertPointToScreen(const View*, gfx::Point*)`: +/// a static function returning nothing. The other overload returns the point and is named `SA?AV`. +pub const SCREEN_ORIGIN_NAME: &str = "?ConvertPointToScreen@View@views@@SAX"; + +/// How many bytes of that function's start the runtime compares before it ever calls it. +const SCREEN_ORIGIN_BYTES: usize = 12; + +/// Whether a public name is the one `screen_origin` is found by. +pub fn is_screen_origin_name(name: &str) -> bool { + stable_name(name).starts_with(SCREEN_ORIGIN_NAME) +} + +/// The first whole instructions of a function, at least `SCREEN_ORIGIN_BYTES` long. +fn function_start(pe: &PeFile, address: u32) -> Option> { + if !pe.is_code(address) { + return None; + } + let window = pe.read(address, 32)?; + let (ends, _) = x64::boundaries(window, SCREEN_ORIGIN_BYTES); + let end = *ends.iter().find(|end| **end >= SCREEN_ORIGIN_BYTES)?; + Some(window[..end].to_vec()) +} + +/// Adds `screen_origin`, `[address, first bytes]` of `views::View::ConvertPointToScreen`, to a row +/// whose prior row had none, by the function's public name in the new build. A prior row that had +/// it was ported with every other address. A build with no single place for the name goes without +/// it, and the runtime keeps placing bubbles from the window's monitor, so the row is not stopped. +pub fn seed_screen_origin(row: &mut Value, places: &[u32], pe: &PeFile, report: &mut PortReport) { + if row["build"]["screen_origin"].is_array() { + return; + } + let mut places = places.to_vec(); + places.sort_unstable(); + places.dedup(); + let found = match places.as_slice() { + [only] => function_start(pe, *only).map(|bytes| (*only, bytes)), + _ => None, + }; + match found { + Some((address, bytes)) => { + row["build"]["screen_origin"] = + json!([write_address(u64::from(address)), hex_text(&bytes)]); + report.derived.push(format!( + "build.screen_origin: {SCREEN_ORIGIN_NAME} at {address:#x}" + )); + } + None => { + row["build"]["screen_origin"] = Value::Null; + report.left_out.push(format!( + "build.screen_origin: no single readable function named {SCREEN_ORIGIN_NAME}: {places:x?}" + )); + } + } +} + /// The review items for the parts of the prior row that only a person writes today. A row made /// without them opens Rivet's frame with those features off, so it goes to a person instead of /// the table. The runtime names them in the prior row's `hand_only` list. @@ -1489,6 +1546,44 @@ mod tests { .collect() } + #[test] + fn nfr3_screen_origin_is_found_by_its_public_name_in_the_new_build() { + // push rsi; push rdi; sub rsp, 0x28; mov rsi, rdx; mov rdi, rcx; call. + let start = hex("56574883ec284889d64889cfe800000000").unwrap(); + let bytes = module(&[(0x101400, start.as_slice())]); + let pe = PeFile::parse(&bytes).unwrap(); + let mut row = prior(); + let mut report = PortReport::default(); + seed_screen_origin(&mut row, &[0x101400, 0x101400], &pe, &mut report); + assert_eq!( + row["build"]["screen_origin"], + json!(["0x101400", "56574883ec284889d64889cf"]) + ); + assert!(report.review.is_empty() && report.left_out.is_empty()); + // A row that already has it keeps the place the port gave it. + seed_screen_origin(&mut row, &[0x101500], &pe, &mut report); + assert_eq!(row["build"]["screen_origin"][0], "0x101400"); + // No name, or two places for it, leaves the part out without stopping the row. + for places in [&[][..], &[0x101400, 0x101500][..]] { + let mut row = prior(); + let mut report = PortReport::default(); + seed_screen_origin(&mut row, places, &pe, &mut report); + assert!(row["build"]["screen_origin"].is_null()); + assert_eq!(report.left_out.len(), 1); + assert!(report.review.is_empty()); + } + // Only the in-place overload, which returns nothing, is the one. + assert!(is_screen_origin_name( + "?ConvertPointToScreen@View@views@@SAXPEBV12@PEAVPoint@gfx@@@Z" + )); + assert!(!is_screen_origin_name( + "?ConvertPointToScreen@View@views@@SA?AVPoint@gfx@@PEBV12@AEBV34@@Z" + )); + assert!(!is_screen_origin_name( + "?ConvertRectToScreen@View@views@@SAXPEBV12@PEAVRect@gfx@@@Z" + )); + } + #[test] fn nfr3_a_new_row_takes_addresses_by_name_and_bytes_from_the_new_module() { let code = new_code(); diff --git a/rivet/runtime/windows/src/builds/brave.rs b/rivet/runtime/windows/src/builds/brave.rs index 3d92c89..eeb8057 100644 --- a/rivet/runtime/windows/src/builds/brave.rs +++ b/rivet/runtime/windows/src/builds/brave.rs @@ -43,6 +43,7 @@ pub static BRAVE: Build = Build { omnibox: None, page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: None, browser: "Brave", module: "chrome.dll", @@ -309,6 +310,7 @@ pub static BRAVE_63: Build = Build { omnibox: Some(super::omnibox_rows::OMNIBOX_BRAVE_63), page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: Some(AppWindow { feature_impl: Some(0x00107B60), feature_impl_prologue: &[ @@ -710,6 +712,7 @@ pub static BRAVE_157_55: Build = Build { omnibox: Some(super::omnibox_rows::OMNIBOX_BRAVE_157_55), page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: Some(AppWindow { feature_impl: Some(0x0017C0B0), feature_impl_prologue: &[ diff --git a/rivet/runtime/windows/src/builds/chrome.rs b/rivet/runtime/windows/src/builds/chrome.rs index c495b52..81e0e5d 100644 --- a/rivet/runtime/windows/src/builds/chrome.rs +++ b/rivet/runtime/windows/src/builds/chrome.rs @@ -52,6 +52,7 @@ pub static CHROME: Build = Build { omnibox: None, page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: None, browser: "Google Chrome", module: "chrome.dll", @@ -391,6 +392,7 @@ pub static CHROME_156_74: Build = Build { omnibox: Some(super::omnibox_rows::OMNIBOX_CHROME_156_74), page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: Some(AppWindow { feature_impl: Some(0x001BE6D0), feature_impl_prologue: &[ @@ -810,6 +812,7 @@ pub static CHROME_156_80: Build = Build { omnibox: Some(super::omnibox_rows::OMNIBOX_CHROME_156_80), page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: Some(AppWindow { feature_impl: Some(0x001E54E0), feature_impl_prologue: &[ @@ -1257,6 +1260,7 @@ pub static CHROME_157_38: Build = Build { omnibox: Some(super::omnibox_rows::OMNIBOX_CHROME_157_38), page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: Some(AppWindow { feature_impl: Some(0x0012D300), feature_impl_prologue: &[ @@ -1661,6 +1665,7 @@ pub static CHROME_150: Build = Build { omnibox: None, page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: None, browser: "Google Chrome", module: "chrome.dll", diff --git a/rivet/runtime/windows/src/builds/edge.rs b/rivet/runtime/windows/src/builds/edge.rs index bb7c4f3..7af0022 100644 --- a/rivet/runtime/windows/src/builds/edge.rs +++ b/rivet/runtime/windows/src/builds/edge.rs @@ -51,6 +51,7 @@ pub static EDGE: Build = Build { omnibox: None, page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: None, browser: "Microsoft Edge", module: "msedge.dll", diff --git a/rivet/runtime/windows/src/builds/mod.rs b/rivet/runtime/windows/src/builds/mod.rs index 4c91d89..b5335aa 100644 --- a/rivet/runtime/windows/src/builds/mod.rs +++ b/rivet/runtime/windows/src/builds/mod.rs @@ -55,6 +55,10 @@ pub struct Build { /// `BubbleDialogDelegate::GetAnchorRect` and its first bytes, when the row carries them. A /// built-in row keeps it in the bubble anchor's own list instead. pub bubble_anchor: Option<(usize, &'static [u8])>, + /// `views::View::ConvertPointToScreen(const View*, gfx::Point*)` and its first bytes, when the + /// row carries them: where Chromium itself puts a view in screen DIPs. Without it, bubbles are + /// placed from the window's monitor. + pub screen_origin: Option<(usize, &'static [u8])>, } } diff --git a/rivet/runtime/windows/src/builds/samsung.rs b/rivet/runtime/windows/src/builds/samsung.rs index 608110b..cf5ba5b 100644 --- a/rivet/runtime/windows/src/builds/samsung.rs +++ b/rivet/runtime/windows/src/builds/samsung.rs @@ -9,6 +9,7 @@ pub static SAMSUNG: Build = Build { omnibox: Some(super::omnibox_rows::OMNIBOX_SAMSUNG_29), page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: Some(AppWindow { feature_impl: Some(0x001FBB10), feature_impl_prologue: &[ diff --git a/rivet/runtime/windows/src/builds/tests.rs b/rivet/runtime/windows/src/builds/tests.rs index 754aa4d..d1d3757 100644 --- a/rivet/runtime/windows/src/builds/tests.rs +++ b/rivet/runtime/windows/src/builds/tests.rs @@ -1619,4 +1619,16 @@ fn nfr6_a_table_row_carries_the_page_tools_and_bubble_anchor_of_its_prior_row() crate::builds::bubble_anchor_of(build), crate::builds::bubble_anchor_of(&CHROME_157_38) ); + // No built-in row has `ConvertPointToScreen`; a table row found by its name brings it. + assert!(chrome["build"]["screen_origin"].is_null()); + assert_eq!(build.screen_origin, None); + row["screen_origin"] = serde_json::json!(["0x1234560", "56574883ec284889d64889cf"]); + let build = Build::from_row(&row).unwrap(); + assert_eq!( + build.screen_origin, + Some(( + 0x1234560, + &[0x56, 0x57, 0x48, 0x83, 0xEC, 0x28, 0x48, 0x89, 0xD6, 0x48, 0x89, 0xCF][..] + )) + ); } diff --git a/rivet/runtime/windows/src/builds/whale.rs b/rivet/runtime/windows/src/builds/whale.rs index 62c888c..770434a 100644 --- a/rivet/runtime/windows/src/builds/whale.rs +++ b/rivet/runtime/windows/src/builds/whale.rs @@ -19,6 +19,7 @@ pub static WHALE: Build = Build { omnibox: Some(super::omnibox_rows::OMNIBOX_WHALE_57), page_tools: None, bubble_anchor: None, + screen_origin: None, app_window: Some(AppWindow { // `WindowFeatureController::SupportsWindowFeatureImpl`, found statically and unverified: the // function `BrowserView::ShouldDrawTabStrip` asks through the thunk at 0x8104DF0 diff --git a/rivet/runtime/windows/src/chromium/mod.rs b/rivet/runtime/windows/src/chromium/mod.rs index fddfded..713c6d1 100644 --- a/rivet/runtime/windows/src/chromium/mod.rs +++ b/rivet/runtime/windows/src/chromium/mod.rs @@ -1,4 +1,5 @@ pub(crate) mod image; pub(crate) mod memory; pub(crate) mod reflect; +pub(crate) mod screen; pub(crate) mod view; diff --git a/rivet/runtime/windows/src/chromium/screen.rs b/rivet/runtime/windows/src/chromium/screen.rs new file mode 100644 index 0000000..d9672fb --- /dev/null +++ b/rivet/runtime/windows/src/chromium/screen.rs @@ -0,0 +1,69 @@ +//! Where Chromium itself puts a view on the screen. +//! +//! `views::View::ConvertPointToScreen(const View*, gfx::Point*)` is the conversion Chromium's own +//! bubbles use for their anchor, so a place found through it is the browser's own, in screen DIPs, +//! even for a window whose scale differs from that of the monitor it is on. A row may carry the +//! function with its first bytes. The bytes are checked against the loaded module once, and the +//! function is called only after they matched. + +use std::sync::atomic::{AtomicUsize, Ordering}; + +static CONVERT: AtomicUsize = AtomicUsize::new(0); +type ConvertPointToScreen = unsafe extern "system" fn(*const core::ffi::c_void, *mut [i32; 2]); + +/// The fewest bytes that tell a function start apart. +const LEAST_PROLOGUE: usize = 8; + +/// Whether the bytes found at the recorded place are the ones the row records. +fn accepts(found: Option<&[u8]>, recorded: &[u8]) -> bool { + recorded.len() >= LEAST_PROLOGUE && found == Some(recorded) +} + +/// Checks the row's `ConvertPointToScreen` against the loaded module, and says what it found. +pub(crate) fn install() -> &'static str { + let Some((at, prologue)) = crate::builds::current().and_then(|build| build.screen_origin) + else { + return "the row has no ConvertPointToScreen"; + }; + let Some(image) = crate::chromium::image::recorded() else { + return "no recorded module"; + }; + if !accepts(unsafe { image.bytes(at, prologue.len()) }, prologue) { + return "ConvertPointToScreen is not the recorded one"; + } + CONVERT.store(image.base + at, Ordering::Release); + "ConvertPointToScreen checked" +} + +/// The top left corner of `view` in screen DIPs, as Chromium puts it. `None` when the row has no +/// checked `ConvertPointToScreen`. +/// +/// # Safety +/// `view` must be a live `views::View` of the browser, used on the browser's UI thread. +pub(crate) unsafe fn origin(view: usize) -> Option<(i32, i32)> { + let at = CONVERT.load(Ordering::Acquire); + if at == 0 || view == 0 { + return None; + } + let convert: ConvertPointToScreen = std::mem::transmute(at); + let mut point = [0i32; 2]; + convert(view as *const core::ffi::c_void, &mut point); + Some((point[0], point[1])) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fr12_screen_origin_is_called_only_after_its_bytes_matched() { + let recorded: &[u8] = &[0x56, 0x57, 0x48, 0x83, 0xEC, 0x28, 0x48, 0x89, 0xD6]; + assert!(accepts(Some(recorded), recorded)); + assert!(!accepts(Some(&recorded[1..]), recorded)); + assert!(!accepts(None, recorded)); + // Too few bytes say nothing about which function lies there. + assert!(!accepts(Some(&recorded[..4]), &recorded[..4])); + // Without a checked function there is no origin, and nothing is called. + assert_eq!(unsafe { origin(0x1000) }, None); + } +} diff --git a/rivet/runtime/windows/src/features/bubble_anchor.rs b/rivet/runtime/windows/src/features/bubble_anchor.rs index c68cceb..5643710 100644 --- a/rivet/runtime/windows/src/features/bubble_anchor.rs +++ b/rivet/runtime/windows/src/features/bubble_anchor.rs @@ -240,6 +240,26 @@ fn client_to_screen_dips( ] } +/// A rect in a window's client DIPs as screen DIPs, from where Chromium puts the window's top left +/// corner. Rivet's rows sit at whole DIPs of the window, so they move with that corner as they are. +pub(crate) fn at_origin(origin: (i32, i32), [x, y, w, h]: [f32; 4]) -> Rect { + [ + origin.0 + x.round() as i32, + origin.1 + y.round() as i32, + w.round() as i32, + h.round() as i32, + ] +} + +/// The largest distance in DIPs between the same edge of two lists of rects. +pub(crate) fn largest_difference(one: &[Rect], other: &[Rect]) -> i32 { + one.iter() + .zip(other) + .flat_map(|(a, b)| a.iter().zip(b).map(|(x, y)| (x - y).abs())) + .max() + .unwrap_or(0) +} + /// Client DIPs of `window` as screen DIPs. unsafe fn to_screen(window: HWND, rect: [f32; 4]) -> Rect { let factor = crate::frame::layout_dpi(window).max(96) as f32 / 96.0; @@ -252,8 +272,8 @@ unsafe fn to_screen(window: HWND, rect: [f32; 4]) -> Rect { crate::os::win32::monitors_of(window), ) } -/// The page area, the address and the chip of a Rivet window, in screen DIPs. -unsafe fn targets(window: HWND) -> Option<(Rect, Rect, Rect)> { +/// The page area, the address and the chip of a Rivet window, in its client DIPs and in screen DIPs. +unsafe fn targets(window: HWND) -> Option<([[f32; 4]; 3], (Rect, Rect, Rect))> { let (address, chip) = crate::features::address_bar::anchors(window)?; let mut client: RECT = std::mem::zeroed(); GetClientRect(window, &mut client); @@ -266,9 +286,12 @@ unsafe fn targets(window: HWND) -> Option<(Rect, Rect, Rect)> { client.bottom as f32 / factor - below, ]; Some(( - to_screen(window, page), - to_screen(window, address), - to_screen(window, chip), + [page, address, chip], + ( + to_screen(window, page), + to_screen(window, address), + to_screen(window, chip), + ), )) } @@ -294,7 +317,7 @@ unsafe extern "system" fn get_anchor_rect( if result.is_null() { return result; } - if let Some((page, address, chip)) = targets(window) { + if let Some((client, (page, address, chip))) = targets(window) { let original = *result; crate::trace::trace(&format!( "bubble anchor: original {original:?} address {address:?} chip {chip:?} client origin {:?} layout dpi {}", @@ -305,6 +328,19 @@ unsafe extern "system" fn get_anchor_rect( }, crate::frame::layout_dpi(window) )); + // Where Chromium itself puts the same rows, beside the place derived from the monitor. + let view = crate::features::frame_layout::frame_view(window).unwrap_or(0); + if let Some(origin) = crate::chromium::screen::origin(view) { + let own = client.map(|rect| at_origin(origin, rect)); + let off = largest_difference(&own, &[page, address, chip]); + crate::trace::trace(&format!( + "bubble anchor: chromium origin {origin:?} page {:?} address {:?} chip {:?} same {} off {off}", + own[0], + own[1], + own[2], + off <= 1 + )); + } *result = choose( *result, page, @@ -345,6 +381,10 @@ pub(crate) fn install() { }) }; crate::trace::trace(&format!("bubble anchor: hook {result:?}")); + crate::trace::trace(&format!( + "bubble anchor: screen origin {}", + crate::chromium::screen::install() + )); } #[cfg(test)] @@ -389,6 +429,25 @@ mod tests { ] ); } + #[test] + fn fr12_rows_move_with_the_window_corner_chromium_reports() { + // A window parked off every monitor at a scale its nearest monitor does not have: Chromium + // put its frame view's corner at 9402,0 DIPs, and the chip 16 DIPs into the row follows it. + let chip = at_origin((9402, 0), [16.4, 16.0, 22.0, 28.0]); + assert_eq!(chip, [9418, 16, 22, 28]); + assert_eq!( + at_origin((-1920, -300), [0.0, 0.0, 0.0, 0.0]), + [-1920, -300, 0, 0] + ); + // The trace's same flag: equal places, one DIP of rounding, and the 441 DIPs once seen. + assert_eq!(largest_difference(&[chip], &[chip]), 0); + assert_eq!(largest_difference(&[chip], &[[9419, 16, 22, 28]]), 1); + assert_eq!( + largest_difference(&[ADDRESS, chip], &[ADDRESS, [8977, 16, 22, 28]]), + 441 + ); + } + #[test] fn fr12_bubble_at_the_monitor_edge_moves_to_the_address_bar() { // Brave's save bubble was seen at x=4863, past the window. From 660364354c1c8ad6b2fcb6a3a53b951d720d5f4c Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Mon, 9 Nov 2026 20:31:38 +0900 Subject: [PATCH 2/2] Fix: Name the bubble anchor's targets type for clippy Co-Authored-By: Claude Opus 5.5 --- rivet/runtime/windows/src/features/bubble_anchor.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rivet/runtime/windows/src/features/bubble_anchor.rs b/rivet/runtime/windows/src/features/bubble_anchor.rs index 5643710..f4cc57b 100644 --- a/rivet/runtime/windows/src/features/bubble_anchor.rs +++ b/rivet/runtime/windows/src/features/bubble_anchor.rs @@ -273,7 +273,10 @@ unsafe fn to_screen(window: HWND, rect: [f32; 4]) -> Rect { ) } /// The page area, the address and the chip of a Rivet window, in its client DIPs and in screen DIPs. -unsafe fn targets(window: HWND) -> Option<([[f32; 4]; 3], (Rect, Rect, Rect))> { +type Targets = ([[f32; 4]; 3], (Rect, Rect, Rect)); + +/// The `Targets` of a Rivet window. +unsafe fn targets(window: HWND) -> Option { let (address, chip) = crate::features::address_bar::anchors(window)?; let mut client: RECT = std::mem::zeroed(); GetClientRect(window, &mut client);