diff --git a/crates/winisland-core/src/config/mod.rs b/crates/winisland-core/src/config/mod.rs index e3d3cfee..81b6dd3f 100644 --- a/crates/winisland-core/src/config/mod.rs +++ b/crates/winisland-core/src/config/mod.rs @@ -65,6 +65,8 @@ pub struct AppConfig { pub liquid_glass_tint: f32, #[setting(toggle)] pub liquid_glass_siri: bool, + #[setting(toggle)] + pub liquid_glass_compat_capture: bool, #[setting(number(min = 0.0, max = 50.0, step = 1.0))] pub glass_blur: f32, #[educe(Default = true)] diff --git a/crates/winisland-platform-windows/Cargo.toml b/crates/winisland-platform-windows/Cargo.toml index 42d4885b..2ef2086c 100644 --- a/crates/winisland-platform-windows/Cargo.toml +++ b/crates/winisland-platform-windows/Cargo.toml @@ -8,7 +8,7 @@ winisland-platform = { path = "../winisland-platform" } windows-core = "0.62.2" windows-collections = "0.3.2" windows-registry = "0.6.1" -windows = { version = "0.62.2", features = ["Win32_Foundation", "Win32_System_Com", "Win32_System_Ole", "Win32_System_Wmi", "Win32_System_Variant", "Win32_Globalization", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_DataExchange", "Win32_System_Memory","Win32_System_SystemInformation", "Win32_System_Performance", "Win32_System_LibraryLoader", "Win32_System_EventLog", "Win32_System_WinRT", "Win32_System_WinRT_Composition", "Win32_Media_MediaFoundation", "Win32_Graphics_Direct2D", "Win32_Graphics_Direct2D_Common", "Win32_System_WinRT_Graphics_Direct2D", "Graphics", "Win32_Media_Audio", "Win32_Media_Audio_Endpoints", "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_Touch", "Win32_Storage_Packaging_Appx", "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Dwm", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", "Win32_System_WinRT_Direct3D11", "Win32_System_WinRT_Graphics_Capture", "Graphics_Capture", "Graphics_DirectX", "Security_Authorization_AppCapabilityAccess", "Graphics_DirectX_Direct3D11", "Graphics_Effects", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_Storage_FileSystem", "ApplicationModel", "Foundation", "Foundation_Metadata", "Globalization", "Foundation_Collections", "Storage_Streams", "UI_Notifications", "UI_Notifications_Management", "UI_Composition", "UI_Composition_Desktop", "Media_Control", "Graphics_Imaging", "Data_Xml_Dom", "System"] } +windows = { version = "0.62.2", features = ["Win32_Foundation", "Win32_System_Com", "Win32_System_Ole", "Win32_System_Wmi", "Win32_System_Variant", "Win32_Globalization", "Win32_System_Registry", "Win32_System_Threading", "Win32_System_DataExchange", "Win32_System_Memory","Win32_System_SystemInformation", "Win32_System_Performance", "Win32_System_LibraryLoader", "Win32_System_EventLog", "Win32_System_WinRT", "Win32_System_WinRT_Composition", "Win32_Media_MediaFoundation", "Win32_Graphics_Direct2D", "Win32_Graphics_Direct2D_Common", "Win32_System_WinRT_Graphics_Direct2D", "Graphics", "Win32_Media_Audio", "Win32_Media_Audio_Endpoints", "Win32_UI_WindowsAndMessaging", "Win32_UI_Shell", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_Input_Touch", "Win32_Storage_Packaging_Appx", "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", "Win32_Graphics_Gdi", "Win32_Graphics_Dwm", "Win32_UI_Magnification", "Win32_UI_HiDpi", "Win32_Graphics_Direct3D", "Win32_Graphics_Direct3D11", "Win32_System_WinRT_Direct3D11", "Win32_System_WinRT_Graphics_Capture", "Graphics_Capture", "Graphics_DirectX", "Security_Authorization_AppCapabilityAccess", "Graphics_DirectX_Direct3D11", "Graphics_Effects", "Win32_NetworkManagement_IpHelper", "Win32_NetworkManagement_Ndis", "Win32_Storage_FileSystem", "ApplicationModel", "Foundation", "Foundation_Metadata", "Globalization", "Foundation_Collections", "Storage_Streams", "UI_Notifications", "UI_Notifications_Management", "UI_Composition", "UI_Composition_Desktop", "Media_Control", "Graphics_Imaging", "Data_Xml_Dom", "System"] } dirs = "6.0" log = { version = "0.4", features = ["std"] } parking_lot = "0.12" diff --git a/crates/winisland-platform-windows/src/capture.rs b/crates/winisland-platform-windows/src/capture.rs index 279d8869..9af05dc7 100644 --- a/crates/winisland-platform-windows/src/capture.rs +++ b/crates/winisland-platform-windows/src/capture.rs @@ -2,6 +2,8 @@ use std::cell::{Cell, RefCell}; use std::sync::Arc; use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU64, Ordering}; +mod magnifier; + use parking_lot::Mutex; use windows::{ Foundation::Metadata::ApiInformation, @@ -15,7 +17,7 @@ use windows::{ Graphics::SizeInt32, Security::Authorization::AppCapabilityAccess::AppCapabilityAccessStatus, Win32::{ - Foundation::{HANDLE, HMODULE, HWND, LUID}, + Foundation::{HANDLE, HMODULE, HWND, LUID, RECT}, Graphics::{ Direct3D::{D3D_DRIVER_TYPE_UNKNOWN, D3D_FEATURE_LEVEL_11_0, D3D_FEATURE_LEVEL_11_1}, Direct3D11::{ @@ -38,7 +40,9 @@ use windows::{ Direct3D11::{CreateDirect3D11DeviceFromDXGIDevice, IDirect3DDxgiInterfaceAccess}, Graphics::Capture::IGraphicsCaptureItemInterop, }, - UI::WindowsAndMessaging::{SetWindowDisplayAffinity, WDA_EXCLUDEFROMCAPTURE, WDA_NONE}, + UI::WindowsAndMessaging::{ + GetWindowRect, SetWindowDisplayAffinity, WDA_EXCLUDEFROMCAPTURE, WDA_NONE, + }, }, core::{HSTRING, Interface}, }; @@ -80,12 +84,14 @@ struct Probe { struct Session { monitor: HMONITOR, - origin: (i32, i32), + origin: Cell<(i32, i32)>, + source: Cell<(i32, i32, i32, i32)>, pool: Direct3D11CaptureFramePool, session: GraphicsCaptureSession, token: i64, rate_hz: Cell, borderless: Cell, + magnifier: Option, } struct Import { @@ -98,6 +104,7 @@ struct Import { pub(crate) struct BackdropCapture { hwnd: HWND, + magnifier_mode: Cell, device: ID3D11Device5, context: ID3D11DeviceContext4, shared: Arc, @@ -110,7 +117,11 @@ pub(crate) struct BackdropCapture { } impl BackdropCapture { - pub(crate) fn new(hwnd: HWND, adapter_luid: u64) -> Result { + pub(crate) fn new( + hwnd: HWND, + adapter_luid: u64, + visible_to_capture: bool, + ) -> Result { if !GraphicsCaptureSession::IsSupported().unwrap_or(false) { return Err("Windows.Graphics.Capture is unsupported".to_string()); } @@ -122,12 +133,14 @@ impl BackdropCapture { let winrt_device: IDirect3DDevice = unsafe { CreateDirect3D11DeviceFromDXGIDevice(&dxgi) } .and_then(|device| device.cast()) .map_err(|error| format!("WinRT Direct3D device creation failed: {error}"))?; - // SAFETY: hwnd is the caller's live overlay window on this thread. - unsafe { SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE) } - .map_err(|error| format!("Capture exclusion failed: {error}"))?; + let magnifier_mode = visible_to_capture; + if !magnifier_mode { + exclude_from_capture(hwnd)?; + } let d3d = AgileContext(device.clone().into(), context.clone().into()); let capture = Self { hwnd, + magnifier_mode: Cell::new(magnifier_mode), device, context, shared: Arc::new(Shared { @@ -159,7 +172,7 @@ impl BackdropCapture { log::warn!("Backdrop capture restart failed: {error}"); return None; } - let origin = self.session.borrow().as_ref()?.origin; + let origin = self.session.borrow().as_ref()?.origin.get(); let interest = Rect { left: request.interest.left - origin.0, top: request.interest.top - origin.1, @@ -249,7 +262,7 @@ impl BackdropCapture { return None; } self.write_value.store(value, Ordering::Release); - self.serial.set(self.serial.get() + 1); + self.serial.set(value); self.copied.set(Some(key)); Some(()) } @@ -286,6 +299,12 @@ impl BackdropCapture { })(); match import { Ok(import) => { + // SAFETY: The fence was opened above and stays live inside the import. + let completed = unsafe { import.write_fence.GetCompletedValue() }; + if completed > self.write_value.load(Ordering::Acquire) { + self.write_value.store(completed, Ordering::Release); + } + self.serial.set(self.serial.get().max(completed)); *self.import.borrow_mut() = Some(import); self.copied.set(None); Some(()) @@ -298,6 +317,17 @@ impl BackdropCapture { } fn ensure_session(&self, rate_hz: u32) -> Result<(), String> { + if self.magnifier_mode.get() { + match self.ensure_magnifier_session(rate_hz) { + Ok(()) => return Ok(()), + Err(error) => { + log::warn!("Magnifier backdrop is unavailable, using monitor capture: {error}"); + self.magnifier_mode.set(false); + self.session.borrow_mut().take(); + exclude_from_capture(self.hwnd)?; + } + } + } // SAFETY: hwnd is the live overlay window; the call only reads its placement. let monitor = unsafe { MonitorFromWindow(self.hwnd, MONITOR_DEFAULTTONEAREST) }; if let Some(session) = self.session.borrow().as_ref() @@ -324,6 +354,55 @@ impl BackdropCapture { *self.session.borrow_mut() = Some(session); Ok(()) } + + fn ensure_magnifier_session(&self, rate_hz: u32) -> Result<(), String> { + let rect = window_rect(self.hwnd)?; + let source = (rect.left, rect.top, rect.right, rect.bottom); + if let Some(session) = self.session.borrow().as_ref() + && let Some(magnifier) = &session.magnifier + { + if session.rate_hz.get() != rate_hz { + session.rate_hz.set(rate_hz); + set_rate(&session.session, rate_hz); + magnifier.set_rate(rate_hz); + } + if session.source.get() != source { + session.source.set(source); + session.origin.set((rect.left, rect.top)); + magnifier.set_source(rect); + } + if !session.borderless.get() && borderless_allowed() { + session.borderless.set(true); + hide_border(&session.session); + } + if border_access_required() { + return Err("Borderless screen capture access is unavailable".to_string()); + } + return Ok(()); + } + self.session.borrow_mut().take(); + *self.shared.latest.lock() = Latest::default(); + self.copied.set(None); + let magnifier = magnifier::Magnifier::start(rect, rate_hz)?; + let interop = windows::core::factory::() + .map_err(|error| format!("Capture interop is unavailable: {error}"))?; + // SAFETY: The magnifier host is a live window owned by this process. + let item: GraphicsCaptureItem = unsafe { interop.CreateForWindow(magnifier.host()) } + .map_err(|error| format!("Magnifier capture item failed: {error}"))?; + // SAFETY: hwnd is the live overlay window; the call only reads its placement. + let monitor = unsafe { MonitorFromWindow(self.hwnd, MONITOR_DEFAULTTONEAREST) }; + let session = Session::open( + item, + &self.shared, + rate_hz, + monitor, + (rect.left, rect.top), + Some(magnifier), + )?; + session.source.set(source); + *self.session.borrow_mut() = Some(session); + Ok(()) + } } impl Session { @@ -341,6 +420,24 @@ impl Session { // SAFETY: monitor is a live display handle; WinRT returns an owned capture item. let item: GraphicsCaptureItem = unsafe { interop.CreateForMonitor(monitor) } .map_err(|error| format!("Monitor capture item failed: {error}"))?; + Self::open( + item, + shared, + rate_hz, + monitor, + (info.rcMonitor.left, info.rcMonitor.top), + None, + ) + } + + fn open( + item: GraphicsCaptureItem, + shared: &Arc, + rate_hz: u32, + monitor: HMONITOR, + origin: (i32, i32), + magnifier: Option, + ) -> Result { let size = item .Size() .map_err(|error| format!("Capture size is unavailable: {error}"))?; @@ -381,12 +478,14 @@ impl Session { .map_err(|error| format!("Capture start failed: {error}"))?; Ok(Self { monitor, - origin: (info.rcMonitor.left, info.rcMonitor.top), + origin: Cell::new(origin), + source: Cell::new((0, 0, 0, 0)), pool, session, token, rate_hz: Cell::new(rate_hz), borderless: Cell::new(borderless), + magnifier, }) } } @@ -733,3 +832,20 @@ fn hide_border(session: &GraphicsCaptureSession) { log::warn!("Screen capture border could not be hidden: {error}"); } } + +fn exclude_from_capture(hwnd: HWND) -> Result<(), String> { + // SAFETY: hwnd is the caller's live overlay window on this thread. + unsafe { SetWindowDisplayAffinity(hwnd, WDA_EXCLUDEFROMCAPTURE) } + .map_err(|error| format!("Capture exclusion failed: {error}")) +} + +fn window_rect(hwnd: HWND) -> Result { + let mut rect = RECT::default(); + // SAFETY: hwnd is the live overlay window and rect is a valid output. + unsafe { GetWindowRect(hwnd, &mut rect) } + .map_err(|error| format!("Overlay bounds are unavailable: {error}"))?; + if rect.right <= rect.left || rect.bottom <= rect.top { + return Err("Overlay bounds are empty".to_string()); + } + Ok(rect) +} diff --git a/crates/winisland-platform-windows/src/capture/magnifier.rs b/crates/winisland-platform-windows/src/capture/magnifier.rs new file mode 100644 index 00000000..c0629003 --- /dev/null +++ b/crates/winisland-platform-windows/src/capture/magnifier.rs @@ -0,0 +1,270 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::thread::JoinHandle; +use std::time::{Duration, Instant}; + +use parking_lot::Mutex; +use windows::Win32::Foundation::{COLORREF, HWND, LPARAM, LRESULT, RECT, WPARAM}; +use windows::Win32::Graphics::Dwm::{DWMWA_CLOAK, DwmSetWindowAttribute}; +use windows::Win32::Graphics::Gdi::InvalidateRect; +use windows::Win32::System::LibraryLoader::GetModuleHandleW; +use windows::Win32::System::Threading::GetCurrentProcessId; +use windows::Win32::UI::HiDpi::{ + DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2, SetThreadDpiAwarenessContext, +}; +use windows::Win32::UI::Magnification::{ + MAGTRANSFORM, MW_FILTERMODE_EXCLUDE, MagInitialize, MagSetWindowFilterList, MagSetWindowSource, + MagSetWindowTransform, MagUninitialize, WC_MAGNIFIERW, +}; +use windows::Win32::UI::WindowsAndMessaging::{ + CreateWindowExW, DefWindowProcW, DestroyWindow, DispatchMessageW, EnumWindows, + GetWindowThreadProcessId, LWA_ALPHA, MSG, PM_REMOVE, PeekMessageW, RegisterClassW, + SWP_NOACTIVATE, SWP_NOZORDER, SetLayeredWindowAttributes, SetWindowPos, TranslateMessage, + WINDOW_EX_STYLE, WNDCLASSW, WS_CHILD, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, + WS_POPUP, WS_VISIBLE, +}; +use windows::core::{BOOL, PCWSTR, w}; + +const FILTER_REFRESH: Duration = Duration::from_secs(1); + +struct State { + source: Mutex, + rate_hz: AtomicU32, + stop: AtomicBool, +} + +/// Cloaked magnifier window that DWM fills with the desktop beneath `source`, without this +/// process's windows; Windows.Graphics.Capture reads it as an ordinary window. +pub(super) struct Magnifier { + state: Arc, + host: HWND, + thread: Option>, +} + +impl Magnifier { + pub(super) fn start(source: RECT, rate_hz: u32) -> Result { + let state = Arc::new(State { + source: Mutex::new(source), + rate_hz: AtomicU32::new(rate_hz.max(1)), + stop: AtomicBool::new(false), + }); + let (sender, receiver) = std::sync::mpsc::channel(); + let thread_state = state.clone(); + let thread = std::thread::Builder::new() + .name("liquid-magnifier".into()) + .spawn(move || run(thread_state, sender)) + .map_err(|error| format!("Magnifier thread failed: {error}"))?; + match receiver.recv() { + Ok(Ok(host)) => Ok(Self { + state, + host: HWND(host as *mut _), + thread: Some(thread), + }), + Ok(Err(error)) => { + let _ = thread.join(); + Err(error) + } + Err(_) => { + let _ = thread.join(); + Err("Magnifier thread exited during setup".to_string()) + } + } + } + + pub(super) fn host(&self) -> HWND { + self.host + } + + pub(super) fn set_source(&self, source: RECT) { + *self.state.source.lock() = source; + } + + pub(super) fn set_rate(&self, rate_hz: u32) { + self.state.rate_hz.store(rate_hz.max(1), Ordering::Relaxed); + } +} + +impl Drop for Magnifier { + fn drop(&mut self) { + self.state.stop.store(true, Ordering::Release); + if let Some(thread) = self.thread.take() { + let _ = thread.join(); + } + } +} + +extern "system" fn host_proc(hwnd: HWND, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { + // SAFETY: Forwards the unmodified message to the default window procedure. + unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) } +} + +fn run(state: Arc, ready: std::sync::mpsc::Sender>) { + // SAFETY: Every handle below is created, used, and destroyed on this thread; the magnifier + // runtime is initialized before the control is created and released after it is destroyed. + unsafe { + let _ = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2); + if !MagInitialize().as_bool() { + let _ = ready.send(Err("MagInitialize failed".to_string())); + return; + } + let setup = (|| -> Result<(HWND, HWND), String> { + let instance = GetModuleHandleW(None).map_err(|error| error.to_string())?; + let class = w!("WinIslandLiquidMagnifierHost"); + RegisterClassW(&WNDCLASSW { + lpfnWndProc: Some(host_proc), + hInstance: instance.into(), + lpszClassName: class, + ..Default::default() + }); + let source = *state.source.lock(); + let host = CreateWindowExW( + WS_EX_LAYERED | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE, + class, + w!("WinIsland Liquid Backdrop"), + WS_POPUP | WS_VISIBLE, + source.left, + source.top, + (source.right - source.left).max(1), + (source.bottom - source.top).max(1), + None, + None, + Some(instance.into()), + None, + ) + .map_err(|error| format!("Magnifier host failed: {error}"))?; + let _ = SetLayeredWindowAttributes(host, COLORREF(0), 255, LWA_ALPHA); + let cloak = 1_i32; + let _ = DwmSetWindowAttribute( + host, + DWMWA_CLOAK, + (&cloak as *const i32).cast(), + size_of::() as u32, + ); + let magnifier = CreateWindowExW( + WINDOW_EX_STYLE(0), + PCWSTR(WC_MAGNIFIERW.as_ptr()), + w!("WinIsland Liquid Magnifier"), + WS_CHILD | WS_VISIBLE, + 0, + 0, + (source.right - source.left).max(1), + (source.bottom - source.top).max(1), + Some(host), + None, + Some(instance.into()), + None, + ) + .map_err(|error| { + let _ = DestroyWindow(host); + format!("Magnifier control failed: {error}") + })?; + let mut transform = MAGTRANSFORM::default(); + transform.v[0] = 1.0; + transform.v[4] = 1.0; + transform.v[8] = 1.0; + let _ = MagSetWindowTransform(magnifier, &mut transform); + Ok((host, magnifier)) + })(); + let (host, magnifier) = match setup { + Ok(handles) => handles, + Err(error) => { + let _ = MagUninitialize(); + let _ = ready.send(Err(error)); + return; + } + }; + let mut excluded = own_windows(); + apply_filter(magnifier, &mut excluded); + let _ = ready.send(Ok(host.0 as isize)); + + let mut size = (0, 0); + let mut next = Instant::now(); + let mut filter_at = Instant::now(); + let mut message = MSG::default(); + while !state.stop.load(Ordering::Acquire) { + let source = *state.source.lock(); + let current = (source.right - source.left, source.bottom - source.top); + if current != size && current.0 > 0 && current.1 > 0 { + size = current; + let _ = SetWindowPos( + host, + None, + source.left, + source.top, + size.0, + size.1, + SWP_NOZORDER | SWP_NOACTIVATE, + ); + let _ = SetWindowPos( + magnifier, + None, + 0, + 0, + size.0, + size.1, + SWP_NOZORDER | SWP_NOACTIVATE, + ); + } + if filter_at.elapsed() >= FILTER_REFRESH { + filter_at = Instant::now(); + let mut windows = own_windows(); + if windows != excluded { + excluded = windows.clone(); + apply_filter(magnifier, &mut windows); + } + } + let _ = MagSetWindowSource(magnifier, source); + let _ = InvalidateRect(Some(magnifier), None, false); + while PeekMessageW(&mut message, None, 0, 0, PM_REMOVE).as_bool() { + let _ = TranslateMessage(&message); + DispatchMessageW(&message); + } + let rate = state.rate_hz.load(Ordering::Relaxed).max(1); + next += Duration::from_nanos(1_000_000_000 / u64::from(rate)); + let now = Instant::now(); + if next > now { + std::thread::sleep(next - now); + } else { + next = now; + } + } + let _ = DestroyWindow(host); + let _ = MagUninitialize(); + } +} + +fn apply_filter(magnifier: HWND, windows: &mut [HWND]) { + if windows.is_empty() { + return; + } + // SAFETY: The magnifier control is live on this thread and the list outlives the call. + unsafe { + let _ = MagSetWindowFilterList( + magnifier, + MW_FILTERMODE_EXCLUDE, + windows.len() as i32, + windows.as_mut_ptr(), + ); + } +} + +fn own_windows() -> Vec { + extern "system" fn visit(hwnd: HWND, data: LPARAM) -> BOOL { + // SAFETY: data points to the Vec owned by own_windows for the duration of EnumWindows. + unsafe { + let windows = &mut *(data.0 as *mut Vec); + let mut process = 0; + GetWindowThreadProcessId(hwnd, Some(&mut process)); + if process == GetCurrentProcessId() { + windows.push(hwnd); + } + } + BOOL(1) + } + let mut windows = Vec::new(); + // SAFETY: The callback only appends to the local Vec passed through LPARAM. + unsafe { + let _ = EnumWindows(Some(visit), LPARAM(&mut windows as *mut Vec as isize)); + } + windows +} diff --git a/crates/winisland-platform-windows/src/window/mod.rs b/crates/winisland-platform-windows/src/window/mod.rs index fb20ccdf..3a82b033 100644 --- a/crates/winisland-platform-windows/src/window/mod.rs +++ b/crates/winisland-platform-windows/src/window/mod.rs @@ -693,11 +693,16 @@ impl WindowSystem for WindowsWindowSystem { true } - fn start_backdrop_capture(&self, id: WindowId, adapter_luid: u64) -> Result<(), PlatformError> { + fn start_backdrop_capture( + &self, + id: WindowId, + adapter_luid: u64, + visible_to_capture: bool, + ) -> Result<(), PlatformError> { let window = get_window(id).ok_or(PlatformError::Unavailable("overlay window"))?; let hwnd = window_hwnd(&window).ok_or(PlatformError::Unavailable("overlay handle"))?; let capture = Rc::new( - BackdropCapture::new(HWND(hwnd as *mut _), adapter_luid) + BackdropCapture::new(HWND(hwnd as *mut _), adapter_luid, visible_to_capture) .map_err(PlatformError::Backend)?, ); WINDOWS.with(|windows| { diff --git a/crates/winisland-platform/src/window.rs b/crates/winisland-platform/src/window.rs index 59949d63..a1f58280 100644 --- a/crates/winisland-platform/src/window.rs +++ b/crates/winisland-platform/src/window.rs @@ -132,7 +132,12 @@ pub trait WindowSystem { fn update_window_backdrop(&self, id: WindowId, width: f32, height: f32, radius: f32) -> bool; /// Starts screen sampling beneath an overlay on the adapter with `adapter_luid`, excluding the /// overlay from all screen captures until stopped; unavailable support returns an error. - fn start_backdrop_capture(&self, id: WindowId, adapter_luid: u64) -> Result<(), PlatformError>; + fn start_backdrop_capture( + &self, + id: WindowId, + adapter_luid: u64, + visible_to_capture: bool, + ) -> Result<(), PlatformError>; /// Copies the newest screen content into the shared texture; `None` means no capture is /// running or no frame has arrived yet. fn update_backdrop_capture( diff --git a/resources/in_app/lang/en_us.lang b/resources/in_app/lang/en_us.lang index 82154246..137fd6f4 100644 --- a/resources/in_app/lang/en_us.lang +++ b/resources/in_app/lang/en_us.lang @@ -122,6 +122,8 @@ glass_blur=Frosted Glass Extra Blur liquid_glass_siri=Siri AI Style (Black-to-Glass Gradient) liquid_glass_siri_note=Experimental option; performance is not guaranteed. style_liquid_note=Liquid Glass hides the island from screenshots and screen recordings. +liquid_glass_compat_capture=Compatibility mode (visible in captures) +liquid_glass_compat_capture_note=In compatibility mode the island is visible in screenshots, recordings, and remote desktop, but background latency and CPU usage increase. custom_font=Custom Font font_select=Select font_reset=Reset diff --git a/resources/in_app/lang/es_es.lang b/resources/in_app/lang/es_es.lang index 4a012b0b..2413448a 100644 --- a/resources/in_app/lang/es_es.lang +++ b/resources/in_app/lang/es_es.lang @@ -122,6 +122,8 @@ glass_blur=Desenfoque adicional del Vidrio Esmerilado liquid_glass_siri=Estilo Siri AI (degradado de negro a vidrio) liquid_glass_siri_note=Opción experimental; no se garantiza el rendimiento. style_liquid_note=El Vidrio Líquido oculta la isla en capturas y grabaciones de pantalla. +liquid_glass_compat_capture=Modo de compatibilidad (visible en capturas) +liquid_glass_compat_capture_note=En el modo de compatibilidad la isla es visible en capturas, grabaciones y escritorio remoto, pero aumentan la latencia del fondo y el uso de CPU. custom_font=Fuente Personalizada font_select=Seleccionar font_reset=Restablecer diff --git a/resources/in_app/lang/zh_cn.lang b/resources/in_app/lang/zh_cn.lang index 43730f02..1f8c4862 100644 --- a/resources/in_app/lang/zh_cn.lang +++ b/resources/in_app/lang/zh_cn.lang @@ -122,6 +122,8 @@ glass_blur=毛玻璃模糊程度 liquid_glass_siri=Siri AI 样式(黑色-玻璃渐变) liquid_glass_siri_note=实验性选项,不保证性能表现。 style_liquid_note=液态玻璃模式下,截图和录屏中不会显示灵动岛。 +liquid_glass_compat_capture=兼容模式(截图与录屏可见) +liquid_glass_compat_capture_note=兼容模式下截图、录屏和远程桌面能灵动岛可见;但会增加背景延迟与CPU占用。 custom_font=自定义字体 font_select=选择 font_reset=重置 diff --git a/src/window/app.rs b/src/window/app.rs index 80e6b910..a5fdedde 100644 --- a/src/window/app.rs +++ b/src/window/app.rs @@ -62,7 +62,7 @@ enum DragAxis { #[derive(Clone, Copy, PartialEq)] enum BackdropCaptureState { Stopped, - Running, + Running { visible: bool }, Failed(Instant), } diff --git a/src/window/app/events.rs b/src/window/app/events.rs index cb766c4b..02bea1b9 100644 --- a/src/window/app/events.rs +++ b/src/window/app/events.rs @@ -455,6 +455,7 @@ impl App { active: self.recorder.is_some() || self.config.island_style == "liquid", animating: self.recorder.is_some() || self.springs.any_animating(), + visible_to_capture: self.config.liquid_glass_compat_capture, }, ); let main_target = renderer.main_target(); diff --git a/src/window/app/system.rs b/src/window/app/system.rs index 74df9224..4ff4f4ed 100644 --- a/src/window/app/system.rs +++ b/src/window/app/system.rs @@ -757,6 +757,7 @@ pub(super) struct BackdropCaptureInput { pub(super) frost_sigma: f32, pub(super) active: bool, pub(super) animating: bool, + pub(super) visible_to_capture: bool, } pub(super) fn update_backdrop_capture( @@ -774,13 +775,26 @@ pub(super) fn update_backdrop_capture( renderer.release_backdrop(); return false; } + if let BackdropCaptureState::Running { visible } = *state + && visible != input.visible_to_capture + { + stop_backdrop_capture(state, input.window); + } match *state { - BackdropCaptureState::Running => {} + BackdropCaptureState::Running { .. } => {} BackdropCaptureState::Failed(at) if at.elapsed() < BACKDROP_CAPTURE_RETRY => { return false; } - _ => match window().start_backdrop_capture(window_ref.id(), renderer.adapter_luid()) { - Ok(()) => *state = BackdropCaptureState::Running, + _ => match window().start_backdrop_capture( + window_ref.id(), + renderer.adapter_luid(), + input.visible_to_capture, + ) { + Ok(()) => { + *state = BackdropCaptureState::Running { + visible: input.visible_to_capture, + } + } Err(error) => { log::warn!("Liquid glass backdrop capture is unavailable: {error}"); *state = BackdropCaptureState::Failed(Instant::now()); @@ -831,7 +845,7 @@ pub(super) fn update_backdrop_capture( } fn stop_backdrop_capture(state: &mut BackdropCaptureState, window_ref: Option) { - if *state != BackdropCaptureState::Running { + if !matches!(state, BackdropCaptureState::Running { .. }) { return; } if let Some(window_ref) = window_ref { diff --git a/src/window/settings/mod.rs b/src/window/settings/mod.rs index 132702f7..7a954aa3 100644 --- a/src/window/settings/mod.rs +++ b/src/window/settings/mod.rs @@ -221,6 +221,8 @@ pub struct SettingsApp { logical_win_w: f64, logical_win_h: f64, pending_dpi_size: Option, + pending_dpi_retried: bool, + dpi_size_check: bool, target_monitor: Option, pub(crate) focused: bool, pub(crate) dots_hovered: bool, @@ -357,6 +359,8 @@ impl SettingsApp { logical_win_w: WIN_W as f64, logical_win_h: WIN_H as f64, pending_dpi_size: None, + pending_dpi_retried: false, + dpi_size_check: false, target_monitor: None, focused: true, dots_hovered: false, @@ -689,11 +693,12 @@ impl SettingsApp { return; } if let Some(expected) = self.pending_dpi_size.take() { - if expected.width.abs_diff(size.width) > 2 || expected.height.abs_diff(size.height) > 2 + if (expected.width.abs_diff(size.width) > 2 + || expected.height.abs_diff(size.height) > 2) + && !std::mem::replace(&mut self.pending_dpi_retried, true) { self.pending_dpi_size = Some(expected); let _ = window.request_inner_size(expected); - return; } } else if size.width > 0 && size.height > 0 { let scale = window.scale_factor(); @@ -718,6 +723,8 @@ impl SettingsApp { .is_some_and(|window_ref| window_ref.request_inner_size(size)) { self.pending_dpi_size = Some(size); + self.pending_dpi_retried = false; + self.dpi_size_check = true; } } diff --git a/src/window/settings/pages/general/effects.rs b/src/window/settings/pages/general/effects.rs index a74aff36..ee59b661 100644 --- a/src/window/settings/pages/general/effects.rs +++ b/src/window/settings/pages/general/effects.rs @@ -45,7 +45,12 @@ impl SettingsApp { page.setting(&self.config, AppConfigField::LiquidGlassTint, true); page.setting(&self.config, AppConfigField::LiquidGlassSiri, true); page.row_label(tr("liquid_glass_siri_note")); - page.row_label(tr("style_liquid_note")); + page.setting(&self.config, AppConfigField::LiquidGlassCompatCapture, true); + page.row_label(tr(if self.config.liquid_glass_compat_capture { + "liquid_glass_compat_capture_note" + } else { + "style_liquid_note" + })); } if !host_backdrop { page.row_label(tr("platform_unavailable")); diff --git a/src/window/settings/renderer.rs b/src/window/settings/renderer.rs index 6f0d965a..4aa76745 100644 --- a/src/window/settings/renderer.rs +++ b/src/window/settings/renderer.rs @@ -98,7 +98,7 @@ impl SettingsApp { } pub(crate) fn draw(&mut self, renderer: &mut Renderer) { - let Some(win) = self.window.as_ref() else { + let Some(win) = self.window else { return; }; if win.is_minimized() == Some(true) { @@ -116,6 +116,24 @@ impl SettingsApp { if p_w <= 0 || p_h <= 0 { return; } + if (p_w as f32 - self.win_w).abs() > 0.5 || (p_h as f32 - self.win_h).abs() > 0.5 { + self.win_w = p_w as f32; + self.win_h = p_h as f32; + self.mark_items_dirty(); + self.resize_renderer_target( + renderer, + winisland_platform::WindowSize::new(p_w as u32, p_h as u32), + ); + } + if std::mem::take(&mut self.dpi_size_check) { + let expected = winisland_platform::WindowSize::new( + (self.logical_win_w * f64::from(scale)).round() as u32, + (self.logical_win_h * f64::from(scale)).round() as u32, + ); + if expected.width.abs_diff(p_w as u32) > 2 || expected.height.abs_diff(p_h as u32) > 2 { + let _ = win.request_inner_size(expected); + } + } self.ensure_items_cache(); let plugin_frames = self @@ -143,8 +161,8 @@ impl SettingsApp { if self.window_backdrop { self.window_backdrop = crate::platform::window().update_window_backdrop( window_id, - (SIDEBAR_W + WINDOW_RADIUS) * scale, - p_h as f32, + ((SIDEBAR_W + WINDOW_RADIUS) * scale).min(p_w as f32), + (win_h * scale).min(p_h as f32), WINDOW_RADIUS * scale, ); }