You've already forked AstralRinth
Window occlusion checks on MacOS (#6135)
* wip: window occlusion checks on MacOS * wip: occlusion works * occlusion notification * fix ci * fix * wire in hiding into macos occlusion * remove debug logs * fix * clean up
This commit is contained in:
Generated
+3
@@ -10565,6 +10565,8 @@ version = "1.0.0-local"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"async_zip",
|
"async_zip",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"core-foundation 0.10.1",
|
||||||
|
"core-graphics",
|
||||||
"daedalus",
|
"daedalus",
|
||||||
"dashmap",
|
"dashmap",
|
||||||
"either",
|
"either",
|
||||||
@@ -10572,6 +10574,7 @@ dependencies = [
|
|||||||
"hyper 1.7.0",
|
"hyper 1.7.0",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"native-dialog",
|
"native-dialog",
|
||||||
|
"objc2-app-kit",
|
||||||
"paste",
|
"paste",
|
||||||
"path-util",
|
"path-util",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ clickhouse = "0.14.0"
|
|||||||
color-eyre = "0.6.5"
|
color-eyre = "0.6.5"
|
||||||
color-thief = "0.2.2"
|
color-thief = "0.2.2"
|
||||||
const_format = "0.2.34"
|
const_format = "0.2.34"
|
||||||
|
core-foundation = "0.10.1"
|
||||||
|
core-graphics = "0.24.0"
|
||||||
daedalus = { path = "packages/daedalus" }
|
daedalus = { path = "packages/daedalus" }
|
||||||
darling = { version = "0.23" }
|
darling = { version = "0.23" }
|
||||||
dashmap = "6.1.0"
|
dashmap = "6.1.0"
|
||||||
@@ -123,6 +125,7 @@ murmur2 = "0.1.0"
|
|||||||
native-dialog = "0.9.2"
|
native-dialog = "0.9.2"
|
||||||
notify = { version = "8.2.0", default-features = false }
|
notify = { version = "8.2.0", default-features = false }
|
||||||
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
notify-debouncer-mini = { version = "0.7.0", default-features = false }
|
||||||
|
objc2-app-kit = { version = "0.3.2", default-features = false }
|
||||||
p256 = "0.13.2"
|
p256 = "0.13.2"
|
||||||
parking_lot = "0.12.5"
|
parking_lot = "0.12.5"
|
||||||
paste = "1.0.15"
|
paste = "1.0.15"
|
||||||
|
|||||||
@@ -51,6 +51,13 @@ tauri-build = { workspace = true, features = ["codegen"] }
|
|||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
tauri-plugin-updater = { workspace = true, optional = true }
|
tauri-plugin-updater = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "macos")'.dependencies]
|
||||||
|
core-foundation.workspace = true
|
||||||
|
core-graphics.workspace = true
|
||||||
|
objc2-app-kit = { workspace = true, features = [
|
||||||
|
"NSWindow",
|
||||||
|
] }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
webview2-com.workspace = true
|
webview2-com.workspace = true
|
||||||
windows = { workspace = true, features = [
|
windows = { workspace = true, features = [
|
||||||
|
|||||||
+50
-18
@@ -17,6 +17,8 @@ pub struct AdsState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
|
const AD_LINK: &str = "https://modrinth.com/wrapper/app-ads-cookie";
|
||||||
|
#[cfg(any(windows, target_os = "macos"))]
|
||||||
|
pub(super) const OCCLUDED_AREA_THRESHOLD: f64 = 1.0;
|
||||||
#[cfg(not(target_os = "linux"))]
|
#[cfg(not(target_os = "linux"))]
|
||||||
const ADS_USER_AGENT: &str = concat!(
|
const ADS_USER_AGENT: &str = concat!(
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ",
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ",
|
||||||
@@ -92,10 +94,12 @@ fn configure_ads_cookie_settings(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_webview_visible<R: Runtime>(
|
fn set_webview_visible<R: Runtime>(webview: &tauri::Webview<R>, visible: bool) {
|
||||||
webview: &tauri::Webview<R>,
|
#[cfg(not(any(windows, target_os = "macos")))]
|
||||||
_visible: bool,
|
{
|
||||||
) {
|
_ = visible;
|
||||||
|
}
|
||||||
|
|
||||||
webview
|
webview
|
||||||
.with_webview(
|
.with_webview(
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
@@ -103,11 +107,18 @@ fn set_webview_visible<R: Runtime>(
|
|||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let controller = wv.controller();
|
let controller = wv.controller();
|
||||||
unsafe { controller.SetIsVisible(_visible) }.ok();
|
unsafe { controller.SetIsVisible(visible) }.ok();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
if visible {
|
||||||
|
webview.show().ok();
|
||||||
|
} else {
|
||||||
|
webview.hide().ok();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_webview_visible_for_window<R: Runtime>(
|
fn set_webview_visible_for_window<R: Runtime>(
|
||||||
@@ -129,27 +140,48 @@ fn set_webview_visible_for_window<R: Runtime>(
|
|||||||
set_webview_visible(webview, visible && !is_minimized && !is_occluded);
|
set_webview_visible(webview, visible && !is_minimized && !is_occluded);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(any(windows, target_os = "macos"))]
|
||||||
fn compute_ads_webview_occlusion<R: Runtime>(
|
fn compute_ads_webview_occlusion<R: Runtime>(
|
||||||
app: &tauri::AppHandle<R>,
|
app: &tauri::AppHandle<R>,
|
||||||
) -> Option<bool> {
|
) -> Option<bool> {
|
||||||
let main_window = app.get_window("main")?;
|
let main_window = app.get_window("main")?;
|
||||||
let webviews = app.webviews();
|
let webviews = app.webviews();
|
||||||
let webview = webviews.get("ads-window")?;
|
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(
|
#[cfg(target_os = "macos")]
|
||||||
hwnd,
|
{
|
||||||
position.x,
|
let position = webview.position().ok()?;
|
||||||
position.y,
|
let size = webview.size().ok()?;
|
||||||
size.width,
|
|
||||||
size.height,
|
Some(
|
||||||
))
|
crate::api::ads_occlusion_macos::is_ads_webview_occluded(
|
||||||
|
&main_window,
|
||||||
|
position.x,
|
||||||
|
position.y,
|
||||||
|
size.width,
|
||||||
|
size.height,
|
||||||
|
)
|
||||||
|
.unwrap_or(false),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
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)]
|
#[cfg(any(windows, target_os = "macos"))]
|
||||||
async fn sync_ads_occlusion<R: Runtime>(app: &tauri::AppHandle<R>) {
|
async fn sync_ads_occlusion<R: Runtime>(app: &tauri::AppHandle<R>) {
|
||||||
let Some(occluded) = compute_ads_webview_occlusion(app) else {
|
let Some(occluded) = compute_ads_webview_occlusion(app) else {
|
||||||
return;
|
return;
|
||||||
@@ -281,7 +313,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(any(windows, target_os = "macos"))]
|
||||||
{
|
{
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,236 @@
|
|||||||
|
use std::ptr::NonNull;
|
||||||
|
|
||||||
|
use core_foundation::array::CFArray;
|
||||||
|
use core_foundation::base::{CFType, TCFType};
|
||||||
|
use core_foundation::dictionary::CFDictionary;
|
||||||
|
use core_foundation::number::CFNumber;
|
||||||
|
use core_foundation::string::CFString;
|
||||||
|
use core_graphics::display::CGDisplay;
|
||||||
|
use core_graphics::geometry::{CGPoint, CGRect, CGSize};
|
||||||
|
use core_graphics::window::{
|
||||||
|
CGWindowID, kCGWindowAlpha, kCGWindowBounds, kCGWindowLayer,
|
||||||
|
kCGWindowListExcludeDesktopElements,
|
||||||
|
kCGWindowListOptionOnScreenAboveWindow, kCGWindowListOptionOnScreenOnly,
|
||||||
|
kCGWindowNumber, kCGWindowOwnerName, kCGWindowOwnerPID,
|
||||||
|
};
|
||||||
|
use objc2_app_kit::NSWindow;
|
||||||
|
|
||||||
|
pub fn is_ads_webview_occluded<R: tauri::Runtime>(
|
||||||
|
main_window: &tauri::Window<R>,
|
||||||
|
ad_x: i32,
|
||||||
|
ad_y: i32,
|
||||||
|
ad_width: u32,
|
||||||
|
ad_height: u32,
|
||||||
|
) -> Option<bool> {
|
||||||
|
let scale_factor = main_window.scale_factor().ok()?;
|
||||||
|
let maybe_ns_window = main_window.ns_window().ok()?;
|
||||||
|
let ns_window = NonNull::new(maybe_ns_window)?.cast::<NSWindow>();
|
||||||
|
let ns_window = unsafe { ns_window.as_ref() };
|
||||||
|
let window_number = ns_window.windowNumber();
|
||||||
|
let main_window_id = if window_number > 0 {
|
||||||
|
window_number as CGWindowID
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
let window_infos = CGDisplay::window_list_info(
|
||||||
|
kCGWindowListOptionOnScreenOnly | kCGWindowListExcludeDesktopElements,
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let window_infos = unsafe {
|
||||||
|
CFArray::<CFDictionary<CFString, CFType>>::wrap_under_get_rule(
|
||||||
|
window_infos.as_concrete_TypeRef(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let main_window_rect = window_infos
|
||||||
|
.iter()
|
||||||
|
.find(|window_info| window_id(window_info) == Some(main_window_id))
|
||||||
|
.and_then(|window_info| window_rect(&window_info))?;
|
||||||
|
let ad_rect = ad_rect_from_main_window(
|
||||||
|
&main_window_rect,
|
||||||
|
ad_x,
|
||||||
|
ad_y,
|
||||||
|
ad_width,
|
||||||
|
ad_height,
|
||||||
|
scale_factor,
|
||||||
|
);
|
||||||
|
|
||||||
|
if is_empty_rect(&ad_rect) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ad_area = rect_area(&ad_rect);
|
||||||
|
|
||||||
|
if ad_area == 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_process_id = std::process::id() as i32;
|
||||||
|
let windows_above_main = CGDisplay::window_list_info(
|
||||||
|
kCGWindowListOptionOnScreenAboveWindow
|
||||||
|
| kCGWindowListExcludeDesktopElements,
|
||||||
|
Some(main_window_id),
|
||||||
|
)?;
|
||||||
|
let windows_above_main = unsafe {
|
||||||
|
CFArray::<CFDictionary<CFString, CFType>>::wrap_under_get_rule(
|
||||||
|
windows_above_main.as_concrete_TypeRef(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let mut occluded_area = 0.0;
|
||||||
|
|
||||||
|
for window_info in windows_above_main.iter() {
|
||||||
|
if window_id(&window_info) == Some(main_window_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if window_process_id(&window_info) == Some(app_process_id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let owner_name = window_owner_name(&window_info);
|
||||||
|
|
||||||
|
if owner_name.as_deref().is_some_and(is_system_window_owner) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let layer = window_layer(&window_info);
|
||||||
|
|
||||||
|
if layer != Some(0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let alpha = window_alpha(&window_info);
|
||||||
|
|
||||||
|
if alpha.is_some_and(|alpha| alpha <= 0.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(rect) = window_rect(&window_info) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(intersection) = intersect_rects(&ad_rect, &rect) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
|
occluded_area += rect_area(&intersection);
|
||||||
|
let occluded_ratio = occluded_area / ad_area;
|
||||||
|
|
||||||
|
if occluded_ratio >= super::ads::OCCLUDED_AREA_THRESHOLD {
|
||||||
|
return Some(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ad_rect_from_main_window(
|
||||||
|
main_window_rect: &CGRect,
|
||||||
|
x: i32,
|
||||||
|
y: i32,
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
scale_factor: f64,
|
||||||
|
) -> CGRect {
|
||||||
|
let scale_factor = if scale_factor > 0.0 {
|
||||||
|
scale_factor
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
|
||||||
|
CGRect::new(
|
||||||
|
&CGPoint::new(
|
||||||
|
main_window_rect.origin.x + x as f64 / scale_factor,
|
||||||
|
main_window_rect.origin.y + y as f64 / scale_factor,
|
||||||
|
),
|
||||||
|
&CGSize::new(width as f64 / scale_factor, height as f64 / scale_factor),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_id(
|
||||||
|
window_info: &CFDictionary<CFString, CFType>,
|
||||||
|
) -> Option<CGWindowID> {
|
||||||
|
number_value(window_info, unsafe { kCGWindowNumber })
|
||||||
|
.and_then(|value| u32::try_from(value).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_process_id(
|
||||||
|
window_info: &CFDictionary<CFString, CFType>,
|
||||||
|
) -> Option<i32> {
|
||||||
|
number_value(window_info, unsafe { kCGWindowOwnerPID })
|
||||||
|
.and_then(|value| i32::try_from(value).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_layer(window_info: &CFDictionary<CFString, CFType>) -> Option<i32> {
|
||||||
|
number_value(window_info, unsafe { kCGWindowLayer })
|
||||||
|
.and_then(|value| i32::try_from(value).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_alpha(window_info: &CFDictionary<CFString, CFType>) -> Option<f64> {
|
||||||
|
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowAlpha) };
|
||||||
|
|
||||||
|
window_info.find(&key)?.downcast::<CFNumber>()?.to_f64()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_owner_name(
|
||||||
|
window_info: &CFDictionary<CFString, CFType>,
|
||||||
|
) -> Option<String> {
|
||||||
|
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowOwnerName) };
|
||||||
|
|
||||||
|
Some(window_info.find(&key)?.downcast::<CFString>()?.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_system_window_owner(owner_name: &str) -> bool {
|
||||||
|
matches!(owner_name, "WindowManager")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number_value(
|
||||||
|
window_info: &CFDictionary<CFString, CFType>,
|
||||||
|
key: core_foundation::string::CFStringRef,
|
||||||
|
) -> Option<i64> {
|
||||||
|
let key = unsafe { CFString::wrap_under_get_rule(key) };
|
||||||
|
|
||||||
|
window_info.find(&key)?.downcast::<CFNumber>()?.to_i64()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn window_rect(window_info: &CFDictionary<CFString, CFType>) -> Option<CGRect> {
|
||||||
|
let key = unsafe { CFString::wrap_under_get_rule(kCGWindowBounds) };
|
||||||
|
let bounds = window_info.find(&key)?.downcast::<CFDictionary>()?;
|
||||||
|
let rect = CGRect::from_dict_representation(&bounds)?;
|
||||||
|
|
||||||
|
if is_empty_rect(&rect) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(rect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty_rect(rect: &CGRect) -> bool {
|
||||||
|
rect.size.width <= 0.0 || rect.size.height <= 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rect_area(rect: &CGRect) -> f64 {
|
||||||
|
if is_empty_rect(rect) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
rect.size.width * rect.size.height
|
||||||
|
}
|
||||||
|
|
||||||
|
fn intersect_rects(a: &CGRect, b: &CGRect) -> Option<CGRect> {
|
||||||
|
let left = a.origin.x.max(b.origin.x);
|
||||||
|
let top = a.origin.y.max(b.origin.y);
|
||||||
|
let right = (a.origin.x + a.size.width).min(b.origin.x + b.size.width);
|
||||||
|
let bottom = (a.origin.y + a.size.height).min(b.origin.y + b.size.height);
|
||||||
|
let rect = CGRect::new(
|
||||||
|
&CGPoint::new(left, top),
|
||||||
|
&CGSize::new(right - left, bottom - top),
|
||||||
|
);
|
||||||
|
|
||||||
|
if is_empty_rect(&rect) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(rect)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,6 @@ use windows::Win32::UI::WindowsAndMessaging::{
|
|||||||
GetWindowThreadProcessId, IsIconic, IsWindowVisible,
|
GetWindowThreadProcessId, IsIconic, IsWindowVisible,
|
||||||
};
|
};
|
||||||
|
|
||||||
const OCCLUDED_AREA_THRESHOLD: f64 = 1.0;
|
|
||||||
|
|
||||||
pub fn is_ads_webview_occluded(
|
pub fn is_ads_webview_occluded(
|
||||||
main_hwnd: HWND,
|
main_hwnd: HWND,
|
||||||
x: i32,
|
x: i32,
|
||||||
@@ -63,7 +61,7 @@ pub fn is_ads_webview_occluded(
|
|||||||
occluded_area.saturating_add(rect_area(&intersection));
|
occluded_area.saturating_add(rect_area(&intersection));
|
||||||
|
|
||||||
if (occluded_area as f64 / ad_area as f64)
|
if (occluded_area as f64 / ad_area as f64)
|
||||||
>= OCCLUDED_AREA_THRESHOLD
|
>= super::ads::OCCLUDED_AREA_THRESHOLD
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ pub mod tags;
|
|||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
pub mod ads;
|
pub mod ads;
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
mod ads_occlusion_macos;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
mod ads_occlusion_windows;
|
mod ads_occlusion_windows;
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
|
|||||||
Reference in New Issue
Block a user