Skip to content

Commit dc19942

Browse files
committed
Merge remote-tracking branch 'origin/main' into cal/worlds-switching-page
2 parents f69fc28 + d118541 commit dc19942

6 files changed

Lines changed: 1591 additions & 1312 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/app/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,12 @@ tauri-plugin-updater = { workspace = true, optional = true }
5353

5454
[target.'cfg(windows)'.dependencies]
5555
webview2-com.workspace = true
56+
windows = { workspace = true, features = [
57+
"Win32_Foundation",
58+
"Win32_Graphics_Dwm",
59+
"Win32_Graphics_Gdi",
60+
"Win32_UI_WindowsAndMessaging",
61+
] }
5662
windows-core.workspace = true
5763

5864
[features]

apps/app/src/api/ads.rs

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use tokio::sync::RwLock;
1111
pub struct AdsState {
1212
pub shown: bool,
1313
pub modal_shown: bool,
14+
pub occluded: bool,
1415
pub last_click: Option<Instant>,
1516
pub malicious_origins: HashSet<String>,
1617
}
@@ -60,8 +61,8 @@ fn configure_ads_cookie_settings(
6061
core_webview2: &webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2,
6162
) {
6263
use webview2_com::Microsoft::Web::WebView2::Win32::{
63-
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2,
64-
ICoreWebView2_13, ICoreWebView2Profile3,
64+
COREWEBVIEW2_TRACKING_PREVENTION_LEVEL_NONE, ICoreWebView2_13,
65+
ICoreWebView2Profile3,
6566
};
6667
use windows_core::Interface;
6768

@@ -119,7 +120,55 @@ fn set_webview_visible_for_window<R: Runtime>(
119120
.and_then(|window| window.is_minimized().ok())
120121
.unwrap_or(false);
121122

122-
set_webview_visible(webview, visible && !is_minimized);
123+
let is_occluded = app
124+
.state::<RwLock<AdsState>>()
125+
.try_read()
126+
.map(|state| state.occluded)
127+
.unwrap_or(false);
128+
129+
set_webview_visible(webview, visible && !is_minimized && !is_occluded);
130+
}
131+
132+
#[cfg(windows)]
133+
fn compute_ads_webview_occlusion<R: Runtime>(
134+
app: &tauri::AppHandle<R>,
135+
) -> Option<bool> {
136+
let main_window = app.get_window("main")?;
137+
let webviews = app.webviews();
138+
let webview = webviews.get("ads-window")?;
139+
let position = webview.position().ok()?;
140+
let size = webview.size().ok()?;
141+
let hwnd = main_window.hwnd().ok()?;
142+
143+
Some(crate::api::ads_occlusion_windows::is_ads_webview_occluded(
144+
hwnd,
145+
position.x,
146+
position.y,
147+
size.width,
148+
size.height,
149+
))
150+
}
151+
152+
#[cfg(windows)]
153+
async fn sync_ads_occlusion<R: Runtime>(app: &tauri::AppHandle<R>) {
154+
let Some(occluded) = compute_ads_webview_occlusion(app) else {
155+
return;
156+
};
157+
158+
let state = app.state::<RwLock<AdsState>>();
159+
let mut state = state.write().await;
160+
161+
if state.occluded == occluded {
162+
return;
163+
}
164+
165+
state.occluded = occluded;
166+
let visible = state.shown && !state.modal_shown;
167+
drop(state);
168+
169+
if let Some(webview) = app.webviews().get("ads-window") {
170+
set_webview_visible_for_window(app, webview, visible);
171+
}
123172
}
124173

125174
fn sync_webview_visibility_for_main_window<R: Runtime>(
@@ -140,7 +189,7 @@ fn sync_webview_visibility_for_main_window<R: Runtime>(
140189
false
141190
} else {
142191
match app.state::<RwLock<AdsState>>().try_read() {
143-
Ok(state) => state.shown && !state.modal_shown,
192+
Ok(state) => state.shown && !state.modal_shown && !state.occluded,
144193
Err(_) => false,
145194
}
146195
};
@@ -173,20 +222,28 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
173222
app.manage(RwLock::new(AdsState {
174223
shown: true,
175224
modal_shown: false,
225+
occluded: false,
176226
last_click: None,
177227
malicious_origins: HashSet::new(),
178228
}));
179229

180-
// We refresh the ads window every 5 minutes to mitigate memory leak issues.
181-
// While this loop doesn't include explicit checks to see if the window is still
182-
// visible when we refresh, the Aditude wrapper will not make any ad requests
183-
// unless Chromium reports the page as visible. The refresh does not reset the
184-
// visibility state.
230+
// We refresh the ads window periodically to mitigate memory leak issues.
231+
// Skip refreshes when app state has hidden the ads WebView. The refresh does
232+
// not reset the visibility state.
185233
let refresh_app = app.clone();
186234
tauri::async_runtime::spawn(async move {
187235
loop {
188-
if let Some(webview) =
189-
refresh_app.webviews().get_mut("ads-window")
236+
let should_refresh = refresh_app
237+
.state::<RwLock<AdsState>>()
238+
.try_read()
239+
.map(|state| {
240+
state.shown && !state.modal_shown && !state.occluded
241+
})
242+
.unwrap_or(false);
243+
244+
if should_refresh
245+
&& let Some(webview) =
246+
refresh_app.webviews().get_mut("ads-window")
190247
{
191248
let _ = webview.navigate(AD_LINK.parse().unwrap());
192249
}
@@ -224,6 +281,19 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
224281
});
225282
}
226283

284+
#[cfg(windows)]
285+
{
286+
let app_handle = app.clone();
287+
288+
tauri::async_runtime::spawn(async move {
289+
loop {
290+
sync_ads_occlusion(&app_handle).await;
291+
292+
tokio::time::sleep(Duration::from_millis(200)).await;
293+
}
294+
});
295+
}
296+
227297
Ok(())
228298
})
229299
.invoke_handler(tauri::generate_handler![
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
use windows::Win32::Foundation::{HWND, POINT, RECT};
2+
use windows::Win32::Graphics::Dwm::{
3+
DWMWA_CLOAKED, DWMWA_EXTENDED_FRAME_BOUNDS, DwmGetWindowAttribute,
4+
};
5+
use windows::Win32::Graphics::Gdi::ClientToScreen;
6+
use windows::Win32::UI::WindowsAndMessaging::{
7+
GA_ROOT, GW_HWNDNEXT, GetAncestor, GetTopWindow, GetWindow, GetWindowRect,
8+
GetWindowThreadProcessId, IsIconic, IsWindowVisible,
9+
};
10+
11+
const OCCLUDED_AREA_THRESHOLD: f64 = 1.0;
12+
13+
pub fn is_ads_webview_occluded(
14+
main_hwnd: HWND,
15+
x: i32,
16+
y: i32,
17+
width: u32,
18+
height: u32,
19+
) -> bool {
20+
let Some(ad_rect) = ad_rect_in_screen(main_hwnd, x, y, width, height)
21+
else {
22+
return false;
23+
};
24+
25+
if is_empty_rect(&ad_rect) {
26+
return false;
27+
}
28+
29+
let ad_area = rect_area(&ad_rect);
30+
if ad_area == 0 {
31+
return false;
32+
}
33+
34+
let mut occluded_area = 0u64;
35+
let app_root = unsafe { GetAncestor(main_hwnd, GA_ROOT) };
36+
let app_process_id = std::process::id();
37+
let mut hwnd = match unsafe { GetTopWindow(None) } {
38+
Ok(hwnd) => hwnd,
39+
Err(_) => return false,
40+
};
41+
42+
while !hwnd.is_invalid() {
43+
let window_root = unsafe { GetAncestor(hwnd, GA_ROOT) };
44+
45+
if window_root == app_root {
46+
return false;
47+
}
48+
49+
if window_process_id(hwnd) == Some(app_process_id) {
50+
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
51+
Ok(hwnd) => hwnd,
52+
Err(_) => break,
53+
};
54+
continue;
55+
}
56+
57+
if window_counts_as_occluder(hwnd)
58+
&& let Some(occluder_rect) = window_rect(hwnd)
59+
&& let Some(intersection) =
60+
intersect_rects(&ad_rect, &occluder_rect)
61+
{
62+
occluded_area =
63+
occluded_area.saturating_add(rect_area(&intersection));
64+
65+
if (occluded_area as f64 / ad_area as f64)
66+
>= OCCLUDED_AREA_THRESHOLD
67+
{
68+
return true;
69+
}
70+
}
71+
72+
hwnd = match unsafe { GetWindow(hwnd, GW_HWNDNEXT) } {
73+
Ok(hwnd) => hwnd,
74+
Err(_) => break,
75+
};
76+
}
77+
78+
false
79+
}
80+
81+
fn ad_rect_in_screen(
82+
main_hwnd: HWND,
83+
x: i32,
84+
y: i32,
85+
width: u32,
86+
height: u32,
87+
) -> Option<RECT> {
88+
let mut origin = POINT { x: 0, y: 0 };
89+
90+
if !unsafe { ClientToScreen(main_hwnd, &mut origin).as_bool() } {
91+
return None;
92+
}
93+
94+
let left = origin.x.saturating_add(x);
95+
let top = origin.y.saturating_add(y);
96+
let right = left.saturating_add(width as i32);
97+
let bottom = top.saturating_add(height as i32);
98+
99+
Some(RECT {
100+
left,
101+
top,
102+
right,
103+
bottom,
104+
})
105+
}
106+
107+
fn window_counts_as_occluder(hwnd: HWND) -> bool {
108+
if !unsafe { IsWindowVisible(hwnd).as_bool() } {
109+
return false;
110+
}
111+
112+
if unsafe { IsIconic(hwnd).as_bool() } {
113+
return false;
114+
}
115+
116+
if is_dwm_cloaked(hwnd) {
117+
return false;
118+
}
119+
120+
true
121+
}
122+
123+
fn window_process_id(hwnd: HWND) -> Option<u32> {
124+
let mut process_id = 0u32;
125+
126+
unsafe {
127+
GetWindowThreadProcessId(hwnd, Some(&mut process_id));
128+
}
129+
130+
(process_id != 0).then_some(process_id)
131+
}
132+
133+
fn is_dwm_cloaked(hwnd: HWND) -> bool {
134+
let mut cloaked = 0u32;
135+
136+
unsafe {
137+
DwmGetWindowAttribute(
138+
hwnd,
139+
DWMWA_CLOAKED,
140+
&mut cloaked as *mut u32 as *mut _,
141+
std::mem::size_of::<u32>() as u32,
142+
)
143+
}
144+
.is_ok()
145+
&& cloaked != 0
146+
}
147+
148+
fn window_rect(hwnd: HWND) -> Option<RECT> {
149+
let mut rect = RECT::default();
150+
151+
if unsafe {
152+
DwmGetWindowAttribute(
153+
hwnd,
154+
DWMWA_EXTENDED_FRAME_BOUNDS,
155+
&mut rect as *mut RECT as *mut _,
156+
std::mem::size_of::<RECT>() as u32,
157+
)
158+
}
159+
.is_err()
160+
&& unsafe { GetWindowRect(hwnd, &mut rect) }.is_err()
161+
{
162+
return None;
163+
}
164+
165+
if is_empty_rect(&rect) {
166+
return None;
167+
}
168+
169+
Some(rect)
170+
}
171+
172+
fn is_empty_rect(rect: &RECT) -> bool {
173+
rect.right <= rect.left || rect.bottom <= rect.top
174+
}
175+
176+
fn rect_area(rect: &RECT) -> u64 {
177+
if is_empty_rect(rect) {
178+
return 0;
179+
}
180+
181+
(rect.right - rect.left) as u64 * (rect.bottom - rect.top) as u64
182+
}
183+
184+
fn intersect_rects(a: &RECT, b: &RECT) -> Option<RECT> {
185+
let rect = RECT {
186+
left: a.left.max(b.left),
187+
top: a.top.max(b.top),
188+
right: a.right.min(b.right),
189+
bottom: a.bottom.min(b.bottom),
190+
};
191+
192+
(!is_empty_rect(&rect)).then_some(rect)
193+
}

apps/app/src/api/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ pub mod tags;
1818
pub mod utils;
1919

2020
pub mod ads;
21+
#[cfg(windows)]
22+
mod ads_occlusion_windows;
2123
pub mod cache;
2224
pub mod files;
2325
pub mod friends;

0 commit comments

Comments
 (0)