From cc27f60454fe6b1155cc2917f7e1ce2deae06681 Mon Sep 17 00:00:00 2001 From: FoOcean Date: Fri, 16 Oct 2026 09:41:04 -0400 Subject: [PATCH 1/2] feat(render): add a capture-visible liquid glass compatibility mode Liquid glass keeps hiding the island from screen capture by default. The new "liquid_glass_compat_capture" option instead samples the desktop through a cloaked Magnification window that excludes WinIsland's own windows, so the island stays visible in screenshots, recordings, and remote desktop at the cost of about one frame of background latency and slightly more CPU. If the magnifier cannot start, capture falls back to the excluded monitor path. Capture restarts now continue from the shared fence's completed value, so switching modes at runtime no longer fails every copy with E_INVALIDARG. Co-Authored-By: Claude Opus 5.5 --- crates/winisland-core/src/config/mod.rs | 2 + crates/winisland-platform-windows/Cargo.toml | 2 +- .../winisland-platform-windows/src/capture.rs | 136 ++++++++- .../src/capture/magnifier.rs | 270 ++++++++++++++++++ .../src/window/mod.rs | 9 +- crates/winisland-platform/src/window.rs | 7 +- resources/in_app/lang/en_us.lang | 2 + resources/in_app/lang/es_es.lang | 2 + resources/in_app/lang/zh_cn.lang | 2 + src/window/app.rs | 2 +- src/window/app/events.rs | 1 + src/window/app/system.rs | 22 +- src/window/settings/pages/general/effects.rs | 7 +- 13 files changed, 444 insertions(+), 20 deletions(-) create mode 100644 crates/winisland-platform-windows/src/capture/magnifier.rs 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/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")); From 2b273ee4b7d3bad8dd3b63a185ebb14654dd607e Mon Sep 17 00:00:00 2001 From: FoOcean Date: Fri, 16 Oct 2026 09:41:04 -0400 Subject: [PATCH 2/2] fix(settings): keep the settings window content in sync across DPI changes Moving the settings window between monitors with different scaling could leave its physical size at the old scale while the content kept the new logical size, so the panel was clipped, could not reach the screen edges, and the sidebar blur spilled past it. The window now retries the DPI resize once, adopts its real size on every redraw, and the sidebar blur is clamped to the drawn panel. Co-Authored-By: Claude Opus 5.5 --- src/window/settings/mod.rs | 11 +++++++++-- src/window/settings/renderer.rs | 24 +++++++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) 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/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, ); }