You've already forked AstralRinth
Ad webview occlusion handling (#6116)
* wip: ad webview occlusion * Ad webview window occlusion testing * revert refresh test
This commit is contained in:
@@ -53,6 +53,12 @@ tauri-plugin-updater = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
webview2-com.workspace = true
|
||||
windows = { workspace = true, features = [
|
||||
"Win32_Foundation",
|
||||
"Win32_Graphics_Dwm",
|
||||
"Win32_Graphics_Gdi",
|
||||
"Win32_UI_WindowsAndMessaging",
|
||||
] }
|
||||
windows-core.workspace = true
|
||||
|
||||
[features]
|
||||
|
||||
+81
-11
@@ -11,6 +11,7 @@ use tokio::sync::RwLock;
|
||||
pub struct AdsState {
|
||||
pub shown: bool,
|
||||
pub modal_shown: bool,
|
||||
pub occluded: bool,
|
||||
pub last_click: Option<Instant>,
|
||||
pub malicious_origins: HashSet<String>,
|
||||
}
|
||||
@@ -60,8 +61,8 @@ fn configure_ads_cookie_settings(
|
||||
core_webview2: &webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2,
|
||||
) {
|
||||
use webview2_com::Microsoft::Web::WebView2::Win32::{
|
||||
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2,
|
||||
ICoreWebView2_13, ICoreWebView2Profile3,
|
||||
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2_13,
|
||||
ICoreWebView2Profile3,
|
||||
};
|
||||
use windows_core::Interface;
|
||||
|
||||
@@ -119,7 +120,55 @@ fn set_webview_visible_for_window<R: Runtime>(
|
||||
.and_then(|window| window.is_minimized().ok())
|
||||
.unwrap_or(false);
|
||||
|
||||
set_webview_visible(webview, visible && !is_minimized);
|
||||
let is_occluded = app
|
||||
.state::<RwLock<AdsState>>()
|
||||
.try_read()
|
||||
.map(|state| state.occluded)
|
||||
.unwrap_or(false);
|
||||
|
||||
set_webview_visible(webview, visible && !is_minimized && !is_occluded);
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
fn compute_ads_webview_occlusion<R: Runtime>(
|
||||
app: &tauri::AppHandle<R>,
|
||||
) -> Option<bool> {
|
||||
let main_window = app.get_window("main")?;
|
||||
let webviews = app.webviews();
|
||||
let webview = webviews.get("ads-window")?;
|
||||
let position = webview.position().ok()?;
|
||||
let size = webview.size().ok()?;
|
||||
let hwnd = main_window.hwnd().ok()?;
|
||||
|
||||
Some(crate::api::ads_occlusion_windows::is_ads_webview_occluded(
|
||||
hwnd,
|
||||
position.x,
|
||||
position.y,
|
||||
size.width,
|
||||
size.height,
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
async fn sync_ads_occlusion<R: Runtime>(app: &tauri::AppHandle<R>) {
|
||||
let Some(occluded) = compute_ads_webview_occlusion(app) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let state = app.state::<RwLock<AdsState>>();
|
||||
let mut state = state.write().await;
|
||||
|
||||
if state.occluded == occluded {
|
||||
return;
|
||||
}
|
||||
|
||||
state.occluded = occluded;
|
||||
let visible = state.shown && !state.modal_shown;
|
||||
drop(state);
|
||||
|
||||
if let Some(webview) = app.webviews().get("ads-window") {
|
||||
set_webview_visible_for_window(app, webview, visible);
|
||||
}
|
||||
}
|
||||
|
||||
fn sync_webview_visibility_for_main_window<R: Runtime>(
|
||||
@@ -140,7 +189,7 @@ fn sync_webview_visibility_for_main_window<R: Runtime>(
|
||||
false
|
||||
} else {
|
||||
match app.state::<RwLock<AdsState>>().try_read() {
|
||||
Ok(state) => state.shown && !state.modal_shown,
|
||||
Ok(state) => state.shown && !state.modal_shown && !state.occluded,
|
||||
Err(_) => false,
|
||||
}
|
||||
};
|
||||
@@ -173,20 +222,28 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
app.manage(RwLock::new(AdsState {
|
||||
shown: true,
|
||||
modal_shown: false,
|
||||
occluded: false,
|
||||
last_click: None,
|
||||
malicious_origins: HashSet::new(),
|
||||
}));
|
||||
|
||||
// We refresh the ads window every 5 minutes to mitigate memory leak issues.
|
||||
// While this loop doesn't include explicit checks to see if the window is still
|
||||
// visible when we refresh, the Aditude wrapper will not make any ad requests
|
||||
// unless Chromium reports the page as visible. The refresh does not reset the
|
||||
// visibility state.
|
||||
// We refresh the ads window periodically to mitigate memory leak issues.
|
||||
// Skip refreshes when app state has hidden the ads WebView. The refresh does
|
||||
// not reset the visibility state.
|
||||
let refresh_app = app.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
if let Some(webview) =
|
||||
refresh_app.webviews().get_mut("ads-window")
|
||||
let should_refresh = refresh_app
|
||||
.state::<RwLock<AdsState>>()
|
||||
.try_read()
|
||||
.map(|state| {
|
||||
state.shown && !state.modal_shown && !state.occluded
|
||||
})
|
||||
.unwrap_or(false);
|
||||
|
||||
if should_refresh
|
||||
&& let Some(webview) =
|
||||
refresh_app.webviews().get_mut("ads-window")
|
||||
{
|
||||
let _ = webview.navigate(AD_LINK.parse().unwrap());
|
||||
}
|
||||
@@ -224,6 +281,19 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let app_handle = app.clone();
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
loop {
|
||||
sync_ads_occlusion(&app_handle).await;
|
||||
|
||||
tokio::time::sleep(Duration::from_millis(200)).await;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
use windows::Win32::Foundation::{HWND, POINT, RECT};
|
||||
use windows::Win32::Graphics::Dwm::{
|
||||
DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute,
|
||||
};
|
||||
use windows::Win32::Graphics::Gdi::ClientToScreen;
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
GA_ROOT, GW_HWNDNEXT, GetAncestor, GetTopWindow, GetWindow, GetWindowRect,
|
||||
GetWindowThreadProcessId, IsIconic, IsWindowVisible,
|
||||
};
|
||||
|
||||
const OCCLUDED_AREA_THRESHOLD: f64 = 1.0;
|
||||
|
||||
pub fn is_ads_webview_occluded(
|
||||
main_hwnd: HWND,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> bool {
|
||||
let Some(ad_rect) = ad_rect_in_screen(main_hwnd, x, y, width, height)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if is_empty_rect(&ad_rect) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let ad_area = rect_area(&ad_rect);
|
||||
if ad_area == 0 {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut occluded_area = 0u64;
|
||||
let app_root = unsafe { GetAncestor(main_hwnd, GA_ROOT) };
|
||||
let app_process_id = std::process::id();
|
||||
let mut hwnd = match unsafe { GetTopWindow(None) } {
|
||||
Ok(hwnd) => hwnd,
|
||||
Err(_) => return false,
|
||||
};
|
||||
|
||||
while !hwnd.is_invalid() {
|
||||
let window_root = unsafe { GetAncestor(hwnd, GA_ROOT) };
|
||||
|
||||
if window_root == app_root {
|
||||
return false;
|
||||
}
|
||||
|
||||
if window_process_id(hwnd) == Some(app_process_id) {
|
||||
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
|
||||
Ok(hwnd) => hwnd,
|
||||
Err(_) => break,
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if window_counts_as_occluder(hwnd)
|
||||
&& let Some(occluder_rect) = window_rect(hwnd)
|
||||
&& let Some(intersection) =
|
||||
intersect_rects(&ad_rect, &occluder_rect)
|
||||
{
|
||||
occluded_area =
|
||||
occluded_area.saturating_add(rect_area(&intersection));
|
||||
|
||||
if (occluded_area as f64 / ad_area as f64)
|
||||
>= OCCLUDED_AREA_THRESHOLD
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
|
||||
Ok(hwnd) => hwnd,
|
||||
Err(_) => break,
|
||||
};
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
fn ad_rect_in_screen(
|
||||
main_hwnd: HWND,
|
||||
x: i32,
|
||||
y: i32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
) -> Option<RECT> {
|
||||
let mut origin = POINT { x: 0, y: 0 };
|
||||
|
||||
if !unsafe { ClientToScreen(main_hwnd, &mut origin).as_bool() } {
|
||||
return None;
|
||||
}
|
||||
|
||||
let left = origin.x.saturating_add(x);
|
||||
let top = origin.y.saturating_add(y);
|
||||
let right = left.saturating_add(width as i32);
|
||||
let bottom = top.saturating_add(height as i32);
|
||||
|
||||
Some(RECT {
|
||||
left,
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
})
|
||||
}
|
||||
|
||||
fn window_counts_as_occluder(hwnd: HWND) -> bool {
|
||||
if !unsafe { IsWindowVisible(hwnd).as_bool() } {
|
||||
return false;
|
||||
}
|
||||
|
||||
if unsafe { IsIconic(hwnd).as_bool() } {
|
||||
return false;
|
||||
}
|
||||
|
||||
if is_dwm_cloaked(hwnd) {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
fn window_process_id(hwnd: HWND) -> Option<u32> {
|
||||
let mut process_id = 0u32;
|
||||
|
||||
unsafe {
|
||||
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
|
||||
}
|
||||
|
||||
(process_id != 0).then_some(process_id)
|
||||
}
|
||||
|
||||
fn is_dwm_cloaked(hwnd: HWND) -> bool {
|
||||
let mut cloaked = 0u32;
|
||||
|
||||
unsafe {
|
||||
DwmGetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_CLOAKED,
|
||||
&mut cloaked as *mut u32 as *mut _,
|
||||
std::mem::size_of::<u32>() as u32,
|
||||
)
|
||||
}
|
||||
.is_ok()
|
||||
&& cloaked != 0
|
||||
}
|
||||
|
||||
fn window_rect(hwnd: HWND) -> Option<RECT> {
|
||||
let mut rect = RECT::default();
|
||||
|
||||
if unsafe {
|
||||
DwmGetWindowAttribute(
|
||||
hwnd,
|
||||
DWMWA_EXTENDED_FRAME_BOUNDS,
|
||||
&mut rect as *mut RECT as *mut _,
|
||||
std::mem::size_of::<RECT>() as u32,
|
||||
)
|
||||
}
|
||||
.is_err()
|
||||
&& unsafe { GetWindowRect(hwnd, &mut rect) }.is_err()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
if is_empty_rect(&rect) {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(rect)
|
||||
}
|
||||
|
||||
fn is_empty_rect(rect: &RECT) -> bool {
|
||||
rect.right <= rect.left || rect.bottom <= rect.top
|
||||
}
|
||||
|
||||
fn rect_area(rect: &RECT) -> u64 {
|
||||
if is_empty_rect(rect) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
(rect.right - rect.left) as u64 * (rect.bottom - rect.top) as u64
|
||||
}
|
||||
|
||||
fn intersect_rects(a: &RECT, b: &RECT) -> Option<RECT> {
|
||||
let rect = RECT {
|
||||
left: a.left.max(b.left),
|
||||
top: a.top.max(b.top),
|
||||
right: a.right.min(b.right),
|
||||
bottom: a.bottom.min(b.bottom),
|
||||
};
|
||||
|
||||
(!is_empty_rect(&rect)).then_some(rect)
|
||||
}
|
||||
@@ -18,6 +18,8 @@ pub mod tags;
|
||||
pub mod utils;
|
||||
|
||||
pub mod ads;
|
||||
#[cfg(windows)]
|
||||
mod ads_occlusion_windows;
|
||||
pub mod cache;
|
||||
pub mod files;
|
||||
pub mod friends;
|
||||
|
||||
Reference in New Issue
Block a user