From 1974d295355f65c1fddf57b2213ec0cb88664cf4 Mon Sep 17 00:00:00 2001 From: rgdevment Date: Fri, 16 Oct 2026 23:37:20 -0300 Subject: [PATCH 1/3] fix(picker): Windows without transparency shows an opaque sheet, without the black box or the bear --- .github/oversized.txt | 2 +- crates/linkunbound-shell/src/look.rs | 85 +++++++++++++++++++++++-- crates/linkunbound-shell/src/main.rs | 31 +++++---- crates/linkunbound-shell/src/mascot.rs | 9 ++- crates/linkunbound-shell/ui/parts.slint | 2 + crates/linkunbound-shell/ui/shell.slint | 2 +- crates/linkunbound-win/Cargo.toml | 2 + crates/linkunbound-win/src/lib.rs | 6 +- crates/linkunbound-win/src/native.rs | 73 +++++++++++++++++---- 9 files changed, 168 insertions(+), 44 deletions(-) diff --git a/.github/oversized.txt b/.github/oversized.txt index 0ba4410..e9849bb 100644 --- a/.github/oversized.txt +++ b/.github/oversized.txt @@ -1,2 +1,2 @@ 1893 app/src-tauri/src/lib.rs -1965 crates/linkunbound-shell/src/main.rs +1964 crates/linkunbound-shell/src/main.rs diff --git a/crates/linkunbound-shell/src/look.rs b/crates/linkunbound-shell/src/look.rs index 79c692d..2cbd6fb 100644 --- a/crates/linkunbound-shell/src/look.rs +++ b/crates/linkunbound-shell/src/look.rs @@ -5,15 +5,27 @@ use linkunbound_shell::{CLASSIC_CORNER, CORNER, HALO, Look, Palette, Picker, pla use slint::ComponentHandle; pub fn style(picker: &Picker, chosen: PickerStyle) { - let look = match chosen { + picker.set_look(match chosen { PickerStyle::Classic => Look::List, PickerStyle::Sheet => Look::Tiles, PickerStyle::Compact => Look::Compact, PickerStyle::Bear => Look::Bear, - }; - let clear = native_glass(); - picker.set_look(look); + }); + settle(picker); +} + +/// Read again at every showing: a remote session or energy saver can start between two links. +pub fn settle(picker: &Picker) { + dress_for(picker, !blends()); +} + +/// Where the system blends no window, the shadow and the bear would come out as black boxes, so +/// the sheet goes opaque, the size of its window, and DWM rounds it and casts the shadow. +fn dress_for(picker: &Picker, solid: bool) { + let look = picker.get_look(); + let clear = !solid && native_glass(); let palette = picker.global::(); + palette.set_solid(solid); palette.set_clear(clear); palette.set_tint(tint()); palette.set_glass_corner(if cfg!(target_os = "macos") { @@ -21,8 +33,8 @@ pub fn style(picker: &Picker, chosen: PickerStyle) { } else { DWM_CORNER }); - palette.set_see_through(see_through()); - picker.set_halo(if look == Look::List || clear { + palette.set_see_through(!solid && see_through()); + picker.set_halo(if look == Look::List || clear || solid { 0.0 } else { HALO @@ -59,6 +71,10 @@ pub fn corner_of(picker: &Picker) -> f64 { /// painted sheet and its shadow come back. Returns whether the window changed size. pub fn wear_backdrop(picker: &Picker, handle: isize) -> bool { let palette = picker.global::(); + if palette.get_solid() { + wear_solid(handle, !palette.get_light()); + return false; + } if !palette.get_clear() { take_off_glass(handle); return false; @@ -156,6 +172,25 @@ pub fn put_away(picker: &Picker) { } } +#[cfg(windows)] +fn blends() -> bool { + linkunbound_win::transparency_is_on() && linkunbound_win::blends_windows() +} + +/// macOS blends every window whatever the person asks for; only the material can go. +#[cfg(not(windows))] +fn blends() -> bool { + true +} + +#[cfg(windows)] +fn wear_solid(window: isize, dark: bool) { + linkunbound_win::wear_solid(window, dark); +} + +#[cfg(not(windows))] +fn wear_solid(_window: isize, _dark: bool) {} + #[cfg(windows)] fn see_through() -> bool { linkunbound_win::transparency_is_on() @@ -216,7 +251,7 @@ fn wear_glass(_window: isize, _dark: bool, _corner: f64) -> bool { #[cfg(test)] mod tests { - use super::{corner_of, native_glass, place_near, style, wear_backdrop}; + use super::{corner_of, dress_for, native_glass, place_near, style, wear_backdrop}; use linkunbound_core::PickerStyle; use linkunbound_shell::{CLASSIC_CORNER, CORNER, HALO, Palette, Picker}; use slint::ComponentHandle; @@ -289,4 +324,40 @@ mod tests { "a painted sheet asks for nothing" ); } + + /// Without blending the sheet fills its window: no shadow of its own, no material, no bear, + /// whatever the look. + #[test] + fn a_system_that_blends_nothing_gets_an_opaque_sheet() { + let picker = picker(); + let palette = picker.global::(); + + for chosen in [ + PickerStyle::Classic, + PickerStyle::Sheet, + PickerStyle::Compact, + PickerStyle::Bear, + ] { + style(&picker, chosen); + dress_for(&picker, true); + assert!(palette.get_solid()); + assert!(!palette.get_clear()); + assert!(!palette.get_see_through()); + assert_eq!(picker.get_halo(), 0.0); + assert_eq!( + crate::mascot::rise(&picker), + 0.0, + "{chosen:?} shows no bear" + ); + } + assert!( + !wear_backdrop(&picker, 0), + "the opaque sheet keeps its size" + ); + + style(&picker, PickerStyle::Bear); + dress_for(&picker, false); + assert!(!palette.get_solid()); + assert!(crate::mascot::rise(&picker) > 0.0, "the bear is back"); + } } diff --git a/crates/linkunbound-shell/src/main.rs b/crates/linkunbound-shell/src/main.rs index 97826ab..ecaa258 100644 --- a/crates/linkunbound-shell/src/main.rs +++ b/crates/linkunbound-shell/src/main.rs @@ -499,6 +499,18 @@ fn attended(picker: &Picker) -> bool { picker.window().is_visible() && native_handle(picker.window()).is_some_and(host::is_in_front) } +fn dress_the_window(picker: &Picker) { + let Some(handle) = native_handle(picker.window()) else { + return; + }; + host::keep_off_the_taskbar(handle, look::corner_of(picker)); + if look::wear_backdrop(picker, handle) { + look::beside_the_pointer(picker); + } + look::follow(picker); + host::take_the_keyboard(handle); +} + /// Whether the link went on screen, rather than behind one already being answered. fn present(picker: &Picker, words: &Strings, shown: &Rc>, arrival: Arrival) -> bool { let Some(Arrival { url, source }) = @@ -506,6 +518,7 @@ fn present(picker: &Picker, words: &Strings, shown: &Rc>, arrival else { return false; }; + look::settle(picker); let label = source.as_ref().map(|origin| origin.label.as_str()); let listed = rows(physical_icon_side(picker)); if listed.is_empty() { @@ -522,14 +535,7 @@ fn present(picker: &Picker, words: &Strings, shown: &Rc>, arrival } look::beside_the_pointer(picker); let _ = picker.show(); - if let Some(handle) = native_handle(picker.window()) { - host::keep_off_the_taskbar(handle, look::corner_of(picker)); - if look::wear_backdrop(picker, handle) { - look::beside_the_pointer(picker); - } - look::follow(picker); - host::take_the_keyboard(handle); - } + dress_the_window(picker); return true; } dress(picker, words, &url, label, &listed); @@ -548,14 +554,7 @@ fn present(picker: &Picker, words: &Strings, shown: &Rc>, arrival if let Some(ui) = ui() { ui.appeared.set(Some(std::time::Instant::now())); } - if let Some(handle) = native_handle(picker.window()) { - host::keep_off_the_taskbar(handle, look::corner_of(picker)); - if look::wear_backdrop(picker, handle) { - look::beside_the_pointer(picker); - } - look::follow(picker); - host::take_the_keyboard(handle); - } + dress_the_window(picker); // The window reports a scale of one until it exists, which is only now: the first picker of // a session on a dense display was placed for a window two thirds its size, and drew icons // asked for at the wrong side. diff --git a/crates/linkunbound-shell/src/mascot.rs b/crates/linkunbound-shell/src/mascot.rs index e63306c..b98539c 100644 --- a/crates/linkunbound-shell/src/mascot.rs +++ b/crates/linkunbound-shell/src/mascot.rs @@ -1,6 +1,6 @@ //! The bear that peeks over the picker: its head under the glass, its paws on the edge. -use linkunbound_shell::{BearHead, BearPaws, Look, Picker}; +use linkunbound_shell::{BearHead, BearPaws, Look, Palette, Picker}; use slint::ComponentHandle; /// Sizes in logical pixels: the head, how far it rises over the edge, a paw, the room between @@ -25,10 +25,13 @@ const COMPACT: Pose = Pose { gap: 46.0, }; -/// The pose the look calls for, if the bear shows at all: only over the glass, since on a painted /// The pose the look calls for, if the bear shows at all. Its head sits under the picker, so on -/// glass the hidden half shows blurred and on a painted sheet it is simply behind it. +/// glass the hidden half shows blurred and on a painted sheet it is simply behind it; where the +/// system blends no window it would stand in a black box. fn pose(picker: &Picker) -> Option<&'static Pose> { + if picker.global::().get_solid() { + return None; + } match picker.get_look() { Look::Bear => Some(&BEAR), Look::Compact => Some(&COMPACT), diff --git a/crates/linkunbound-shell/ui/parts.slint b/crates/linkunbound-shell/ui/parts.slint index 3a129a7..11138fb 100644 --- a/crates/linkunbound-shell/ui/parts.slint +++ b/crates/linkunbound-shell/ui/parts.slint @@ -24,6 +24,8 @@ export global Palette { in property tint: 0.77; // DWM rounds Acrylic at its own 8px; on macOS the glass view is cut to the sheet's corner. in property glass-corner: 8px; + // Windows blends no window: the opaque sheet fills it and takes DWM's corner and shadow. + in property solid: false; out property veil: light ? @linear-gradient(180deg, #FFFFFF.with-alpha(tint) 0%, #F3F5F9.with-alpha(tint + 0.03) 100%) : @linear-gradient(180deg, #282B36.with-alpha(tint) 0%, #1A1C23.with-alpha(tint + 0.03) 100%); diff --git a/crates/linkunbound-shell/ui/shell.slint b/crates/linkunbound-shell/ui/shell.slint index 1f4bb24..5afb082 100644 --- a/crates/linkunbound-shell/ui/shell.slint +++ b/crates/linkunbound-shell/ui/shell.slint @@ -237,7 +237,7 @@ export component Picker inherits Window { y: root.halo; width: root.sheet-width; height: root.sheet-height; - border-radius: Palette.clear ? Palette.glass-corner : (root.look == Look.list ? 10px : 14px); + border-radius: Palette.clear || Palette.solid ? Palette.glass-corner : (root.look == Look.list ? 10px : 14px); background: Palette.clear ? Palette.veil : Palette.pane; border-width: Palette.clear && !root.private-on ? 0px : 1px; border-color: root.private-on ? Palette.secret : Palette.glow; diff --git a/crates/linkunbound-win/Cargo.toml b/crates/linkunbound-win/Cargo.toml index dc109c9..1e21925 100644 --- a/crates/linkunbound-win/Cargo.toml +++ b/crates/linkunbound-win/Cargo.toml @@ -29,6 +29,7 @@ windows = { version = "0.62", features = [ "Win32_Security_Authorization", "Win32_System_IO", "Win32_System_Pipes", + "Win32_System_Power", "Win32_System_RemoteDesktop", "Win32_System_Threading", "Win32_UI_Shell", @@ -38,6 +39,7 @@ windows = { version = "0.62", features = [ "Win32_System_Memory", "Win32_System_Ole", "Win32_Storage_FileSystem", + "Win32_UI_Accessibility", "Win32_UI_Controls", "Win32_UI_Input_KeyboardAndMouse", "Win32_UI_WindowsAndMessaging", diff --git a/crates/linkunbound-win/src/lib.rs b/crates/linkunbound-win/src/lib.rs index 9bde191..85f24eb 100644 --- a/crates/linkunbound-win/src/lib.rs +++ b/crates/linkunbound-win/src/lib.rs @@ -15,11 +15,11 @@ pub use detect::installed_browsers; pub use icons::cached_or_extract as icon_for; #[cfg(windows)] pub use native::{ - attach_parent_console, copy_text, current_session, current_user_sid, cursor, digit_behind, - front_is_ours, front_window, is_in_front, keep_off_the_taskbar, + attach_parent_console, blends_windows, copy_text, current_session, current_user_sid, cursor, + digit_behind, front_is_ours, front_window, is_in_front, keep_off_the_taskbar, let_whoever_opens_next_come_forward, never_activates, notify_associations_changed, shift_is_down, source_app, strip_title_icon, take_the_keyboard, tuck_under, wear_acrylic, - work_area_at, write_line_to_pipe, + wear_solid, work_area_at, write_line_to_pipe, }; #[cfg(windows)] pub use registration::{ diff --git a/crates/linkunbound-win/src/native.rs b/crates/linkunbound-win/src/native.rs index 8995680..f6648cc 100644 --- a/crates/linkunbound-win/src/native.rs +++ b/crates/linkunbound-win/src/native.rs @@ -11,7 +11,7 @@ use windows::Win32::Foundation::{ LocalFree, POINT, WPARAM, }; use windows::Win32::Graphics::Dwm::{ - DWM_SYSTEMBACKDROP_TYPE, DWM_WINDOW_CORNER_PREFERENCE, DWMSBT_TRANSIENTWINDOW, + DWM_SYSTEMBACKDROP_TYPE, DWM_WINDOW_CORNER_PREFERENCE, DWMSBT_NONE, DWMSBT_TRANSIENTWINDOW, DWMWA_SYSTEMBACKDROP_TYPE, DWMWA_USE_IMMERSIVE_DARK_MODE, DWMWA_WINDOW_CORNER_PREFERENCE, DWMWCP_ROUND, DwmExtendFrameIntoClientArea, DwmSetWindowAttribute, }; @@ -38,22 +38,25 @@ use windows::Win32::System::DataExchange::{ use windows::Win32::System::Memory::{GMEM_MOVEABLE, GlobalAlloc, GlobalLock, GlobalUnlock}; use windows::Win32::System::Ole::CF_UNICODETEXT; use windows::Win32::System::Pipes::{GetNamedPipeServerProcessId, WaitNamedPipeW}; +use windows::Win32::System::Power::{GetSystemPowerStatus, SYSTEM_POWER_STATUS}; use windows::Win32::System::RemoteDesktop::ProcessIdToSessionId; use windows::Win32::System::Threading::{ AttachThreadInput, GetCurrentProcess, GetCurrentProcessId, GetCurrentThreadId, OpenProcess, OpenProcessToken, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION, QueryFullProcessImageNameW, }; +use windows::Win32::UI::Accessibility::{HCF_HIGHCONTRASTON, HIGHCONTRASTW}; use windows::Win32::UI::Controls::MARGINS; use windows::Win32::UI::Input::KeyboardAndMouse::{GetKeyState, SetFocus, VK_SHIFT, VkKeyScanW}; use windows::Win32::UI::Shell::{SHCNE_ASSOCCHANGED, SHCNF_IDLIST, SHChangeNotify}; use windows::Win32::UI::Shell::{SHFILEINFOW, SHGFI_ICON, SHGFI_LARGEICON, SHGetFileInfoW}; use windows::Win32::UI::WindowsAndMessaging::{ ASFW_ANY, AllowSetForegroundWindow, GWL_EXSTYLE, GWL_STYLE, GetCursorPos, GetForegroundWindow, - GetWindowLongPtrW, GetWindowThreadProcessId, HWND_TOPMOST, ICON_BIG, ICON_SMALL, - IsHungAppWindow, SWP_FRAMECHANGED, SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, - SendMessageW, SetForegroundWindow, SetWindowLongPtrW, SetWindowPos, WM_SETICON, - WS_EX_DLGMODALFRAME, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_SYSMENU, + GetSystemMetrics, GetWindowLongPtrW, GetWindowThreadProcessId, HWND_TOPMOST, ICON_BIG, + ICON_SMALL, IsHungAppWindow, SM_REMOTESESSION, SPI_GETHIGHCONTRAST, SWP_FRAMECHANGED, + SWP_NOACTIVATE, SWP_NOMOVE, SWP_NOSIZE, SWP_NOZORDER, SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS, + SendMessageW, SetForegroundWindow, SetWindowLongPtrW, SetWindowPos, SystemParametersInfoW, + WM_SETICON, WS_EX_DLGMODALFRAME, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_SYSMENU, }; use windows::Win32::UI::WindowsAndMessaging::{DestroyIcon, GetIconInfo, HICON, ICONINFO}; @@ -123,19 +126,38 @@ pub fn keep_off_the_taskbar(window: isize) { } /// The picker wears the material of Windows 11's menus and flyouts, drawn by DWM behind a sheet -/// left clear. DWM also rounds the corners and casts the shadow a frameless window lacks, and -/// with transparency turned off in Settings it paints the solid colour on its own. +/// left clear. DWM also rounds the corners and casts the shadow a frameless window lacks. pub fn wear_acrylic(window: isize, dark: bool) -> bool { - let hwnd = HWND(window as *mut std::ffi::c_void); - let dark = windows::core::BOOL::from(dark); - let corner = DWMWCP_ROUND; - let backdrop = DWMSBT_TRANSIENTWINDOW; - let margins = MARGINS { + let whole = MARGINS { cxLeftWidth: -1, cxRightWidth: -1, cyTopHeight: -1, cyBottomHeight: -1, }; + framed_by_dwm(window, dark, &whole, DWMSBT_TRANSIENTWINDOW) +} + +/// An opaque sheet with no material: DWM still rounds the window and casts its shadow, which +/// needs some frame in the client area, and the material a previous showing wore comes off. +pub fn wear_solid(window: isize, dark: bool) -> bool { + let edge = MARGINS { + cxLeftWidth: 1, + cxRightWidth: 1, + cyTopHeight: 1, + cyBottomHeight: 1, + }; + framed_by_dwm(window, dark, &edge, DWMSBT_NONE) +} + +fn framed_by_dwm( + window: isize, + dark: bool, + margins: &MARGINS, + backdrop: DWM_SYSTEMBACKDROP_TYPE, +) -> bool { + let hwnd = HWND(window as *mut std::ffi::c_void); + let dark = windows::core::BOOL::from(dark); + let corner = DWMWCP_ROUND; // winit keeps the system menu on a frameless window, and over an extended frame DWM draws // its close button. let style = unsafe { GetWindowLongPtrW(hwnd, GWL_STYLE) }; @@ -164,7 +186,7 @@ pub fn wear_acrylic(window: isize, dark: bool) -> bool { (&raw const corner).cast(), size_of::() as u32, ); - DwmExtendFrameIntoClientArea(hwnd, &margins).is_ok() + DwmExtendFrameIntoClientArea(hwnd, margins).is_ok() && DwmSetWindowAttribute( hwnd, DWMWA_SYSTEMBACKDROP_TYPE, @@ -175,6 +197,31 @@ pub fn wear_acrylic(window: isize, dark: bool) -> bool { } } +/// Over Remote Desktop, in energy saver or in high contrast DWM may leave the material out and +/// the see-through pixels of a window black, without the switch in Settings saying so. +#[must_use] +pub fn blends_windows() -> bool { + let remote = unsafe { GetSystemMetrics(SM_REMOTESESSION) } != 0; + let mut power = SYSTEM_POWER_STATUS::default(); + let saving = + unsafe { GetSystemPowerStatus(&raw mut power) }.is_ok() && power.SystemStatusFlag == 1; + let mut contrast = HIGHCONTRASTW { + cbSize: size_of::() as u32, + ..Default::default() + }; + let high = unsafe { + SystemParametersInfoW( + SPI_GETHIGHCONTRAST, + contrast.cbSize, + Some((&raw mut contrast).cast()), + SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0), + ) + } + .is_ok() + && contrast.dwFlags.contains(HCF_HIGHCONTRASTON); + !(remote || saving || high) +} + /// Slides `window` just below `above` in the topmost band, so what `above` draws covers it. pub fn tuck_under(window: isize, above: isize) { let _ = unsafe { From 5a2d13245f58d57d9b886124ad06585c1c7cd441 Mon Sep 17 00:00:00 2001 From: rgdevment Date: Fri, 16 Oct 2026 23:41:11 -0300 Subject: [PATCH 2/3] fix(picker): Windows 10 keeps square corners in the opaque sheet and the tests no longer read the machine --- .github/oversized.txt | 2 +- crates/linkunbound-shell/src/look.rs | 50 +++++++++++++++++++--- crates/linkunbound-shell/src/main.rs | 16 +------ crates/linkunbound-shell/src/mascot.rs | 3 +- crates/linkunbound-shell/ui/parts.slint | 2 +- crates/linkunbound-win/src/lib.rs | 6 +-- crates/linkunbound-win/src/native.rs | 6 +-- crates/linkunbound-win/src/registration.rs | 12 +++++- 8 files changed, 64 insertions(+), 33 deletions(-) diff --git a/.github/oversized.txt b/.github/oversized.txt index e9849bb..87b1dcf 100644 --- a/.github/oversized.txt +++ b/.github/oversized.txt @@ -1,2 +1,2 @@ 1893 app/src-tauri/src/lib.rs -1964 crates/linkunbound-shell/src/main.rs +1952 crates/linkunbound-shell/src/main.rs diff --git a/crates/linkunbound-shell/src/look.rs b/crates/linkunbound-shell/src/look.rs index 2cbd6fb..dc90941 100644 --- a/crates/linkunbound-shell/src/look.rs +++ b/crates/linkunbound-shell/src/look.rs @@ -14,13 +14,12 @@ pub fn style(picker: &Picker, chosen: PickerStyle) { settle(picker); } -/// Read again at every showing: a remote session or energy saver can start between two links. +/// A remote session or energy saver can start between two links. pub fn settle(picker: &Picker) { dress_for(picker, !blends()); } -/// Where the system blends no window, the shadow and the bear would come out as black boxes, so -/// the sheet goes opaque, the size of its window, and DWM rounds it and casts the shadow. +/// Unblended, the shadow and the bear would come out as black boxes. fn dress_for(picker: &Picker, solid: bool) { let look = picker.get_look(); let clear = !solid && native_glass(); @@ -30,6 +29,8 @@ fn dress_for(picker: &Picker, solid: bool) { palette.set_tint(tint()); palette.set_glass_corner(if cfg!(target_os = "macos") { corner_of(picker) as f32 + } else if solid && !dwm_rounds() { + 0.0 } else { DWM_CORNER }); @@ -90,6 +91,18 @@ pub fn wear_backdrop(picker: &Picker, handle: isize) -> bool { true } +pub fn dress_the_window(picker: &Picker) { + let Some(handle) = crate::native_handle(picker.window()) else { + return; + }; + crate::host::keep_off_the_taskbar(handle, corner_of(picker)); + if wear_backdrop(picker, handle) { + beside_the_pointer(picker); + } + follow(picker); + crate::host::take_the_keyboard(handle); +} + /// The picker belongs to the click that summoned it, not to the middle of a screen. pub fn beside_the_pointer(picker: &Picker) { let Some(pointer) = crate::host::cursor() else { @@ -177,15 +190,27 @@ fn blends() -> bool { linkunbound_win::transparency_is_on() && linkunbound_win::blends_windows() } -/// macOS blends every window whatever the person asks for; only the material can go. +/// macOS blends every window; only the material can go. #[cfg(not(windows))] fn blends() -> bool { true } +#[cfg(windows)] +fn dwm_rounds() -> bool { + linkunbound_win::dwm_rounds_corners() +} + +#[cfg(not(windows))] +fn dwm_rounds() -> bool { + false +} + #[cfg(windows)] fn wear_solid(window: isize, dark: bool) { - linkunbound_win::wear_solid(window, dark); + if dwm_rounds() { + linkunbound_win::wear_solid(window, dark); + } } #[cfg(not(windows))] @@ -269,11 +294,15 @@ mod tests { let clear = native_glass(); style(&picker, PickerStyle::Sheet); + + dress_for(&picker, false); assert_eq!(picker.global::().get_clear(), clear); assert_eq!(picker.get_halo(), if clear { 0.0 } else { HALO }); assert!((corner_of(&picker) - CORNER).abs() < f64::EPSILON); style(&picker, PickerStyle::Classic); + + dress_for(&picker, false); assert_eq!(picker.get_halo(), 0.0); assert!((corner_of(&picker) - CLASSIC_CORNER).abs() < f64::EPSILON); } @@ -287,9 +316,13 @@ mod tests { let near_the_top = (900, 2); style(&picker, PickerStyle::Classic); + + dress_for(&picker, false); let (_, list_y) = place_near(&picker, near_the_top, screen); style(&picker, PickerStyle::Bear); + + dress_for(&picker, false); picker.set_halo(0.0); let (_, bear_y) = place_near(&picker, near_the_top, screen); let rise = crate::mascot::rise(&picker); @@ -308,6 +341,8 @@ mod tests { let palette = picker.global::(); style(&picker, PickerStyle::Sheet); + + dress_for(&picker, false); palette.set_clear(true); picker.set_halo(0.0); assert!(wear_backdrop(&picker, 0), "the tiles grow by their shadow"); @@ -315,6 +350,8 @@ mod tests { assert_eq!(picker.get_halo(), HALO); style(&picker, PickerStyle::Classic); + + dress_for(&picker, false); palette.set_clear(true); assert!(!wear_backdrop(&picker, 0), "the list has no shadow to add"); assert!(!palette.get_clear()); @@ -325,8 +362,7 @@ mod tests { ); } - /// Without blending the sheet fills its window: no shadow of its own, no material, no bear, - /// whatever the look. + /// Unblended, every look fills its window with no shadow, material or bear. #[test] fn a_system_that_blends_nothing_gets_an_opaque_sheet() { let picker = picker(); diff --git a/crates/linkunbound-shell/src/main.rs b/crates/linkunbound-shell/src/main.rs index ecaa258..e88f44e 100644 --- a/crates/linkunbound-shell/src/main.rs +++ b/crates/linkunbound-shell/src/main.rs @@ -499,18 +499,6 @@ fn attended(picker: &Picker) -> bool { picker.window().is_visible() && native_handle(picker.window()).is_some_and(host::is_in_front) } -fn dress_the_window(picker: &Picker) { - let Some(handle) = native_handle(picker.window()) else { - return; - }; - host::keep_off_the_taskbar(handle, look::corner_of(picker)); - if look::wear_backdrop(picker, handle) { - look::beside_the_pointer(picker); - } - look::follow(picker); - host::take_the_keyboard(handle); -} - /// Whether the link went on screen, rather than behind one already being answered. fn present(picker: &Picker, words: &Strings, shown: &Rc>, arrival: Arrival) -> bool { let Some(Arrival { url, source }) = @@ -535,7 +523,7 @@ fn present(picker: &Picker, words: &Strings, shown: &Rc>, arrival } look::beside_the_pointer(picker); let _ = picker.show(); - dress_the_window(picker); + look::dress_the_window(picker); return true; } dress(picker, words, &url, label, &listed); @@ -554,7 +542,7 @@ fn present(picker: &Picker, words: &Strings, shown: &Rc>, arrival if let Some(ui) = ui() { ui.appeared.set(Some(std::time::Instant::now())); } - dress_the_window(picker); + look::dress_the_window(picker); // The window reports a scale of one until it exists, which is only now: the first picker of // a session on a dense display was placed for a window two thirds its size, and drew icons // asked for at the wrong side. diff --git a/crates/linkunbound-shell/src/mascot.rs b/crates/linkunbound-shell/src/mascot.rs index b98539c..6f958b4 100644 --- a/crates/linkunbound-shell/src/mascot.rs +++ b/crates/linkunbound-shell/src/mascot.rs @@ -26,8 +26,7 @@ const COMPACT: Pose = Pose { }; /// The pose the look calls for, if the bear shows at all. Its head sits under the picker, so on -/// glass the hidden half shows blurred and on a painted sheet it is simply behind it; where the -/// system blends no window it would stand in a black box. +/// glass the hidden half shows blurred and on a painted sheet it is simply behind it. fn pose(picker: &Picker) -> Option<&'static Pose> { if picker.global::().get_solid() { return None; diff --git a/crates/linkunbound-shell/ui/parts.slint b/crates/linkunbound-shell/ui/parts.slint index 11138fb..a40035f 100644 --- a/crates/linkunbound-shell/ui/parts.slint +++ b/crates/linkunbound-shell/ui/parts.slint @@ -24,7 +24,7 @@ export global Palette { in property tint: 0.77; // DWM rounds Acrylic at its own 8px; on macOS the glass view is cut to the sheet's corner. in property glass-corner: 8px; - // Windows blends no window: the opaque sheet fills it and takes DWM's corner and shadow. + // Windows blends no window: the opaque sheet fills it. in property solid: false; out property veil: light ? @linear-gradient(180deg, #FFFFFF.with-alpha(tint) 0%, #F3F5F9.with-alpha(tint + 0.03) 100%) diff --git a/crates/linkunbound-win/src/lib.rs b/crates/linkunbound-win/src/lib.rs index 85f24eb..659c618 100644 --- a/crates/linkunbound-win/src/lib.rs +++ b/crates/linkunbound-win/src/lib.rs @@ -23,9 +23,9 @@ pub use native::{ }; #[cfg(windows)] pub use registration::{ - Registration, acrylic_is_available, association_report, default_apps_page, is_build_tree, - is_default_browser, sweep_legacy_edge_capture, taskbar_is_light, transparency_is_on, - windows_are_light, + Registration, acrylic_is_available, association_report, default_apps_page, dwm_rounds_corners, + is_build_tree, is_default_browser, sweep_legacy_edge_capture, taskbar_is_light, + transparency_is_on, windows_are_light, }; #[cfg(windows)] pub use startup::{ diff --git a/crates/linkunbound-win/src/native.rs b/crates/linkunbound-win/src/native.rs index f6648cc..cdbe4a4 100644 --- a/crates/linkunbound-win/src/native.rs +++ b/crates/linkunbound-win/src/native.rs @@ -137,8 +137,7 @@ pub fn wear_acrylic(window: isize, dark: bool) -> bool { framed_by_dwm(window, dark, &whole, DWMSBT_TRANSIENTWINDOW) } -/// An opaque sheet with no material: DWM still rounds the window and casts its shadow, which -/// needs some frame in the client area, and the material a previous showing wore comes off. +/// DWM shadows only a window with some frame in its client area. pub fn wear_solid(window: isize, dark: bool) -> bool { let edge = MARGINS { cxLeftWidth: 1, @@ -197,8 +196,7 @@ fn framed_by_dwm( } } -/// Over Remote Desktop, in energy saver or in high contrast DWM may leave the material out and -/// the see-through pixels of a window black, without the switch in Settings saying so. +/// Remote Desktop, energy saver and high contrast can leave see-through pixels black. #[must_use] pub fn blends_windows() -> bool { let remote = unsafe { GetSystemMetrics(SM_REMOTESESSION) } != 0; diff --git a/crates/linkunbound-win/src/registration.rs b/crates/linkunbound-win/src/registration.rs index 26c9bbd..59e2803 100644 --- a/crates/linkunbound-win/src/registration.rs +++ b/crates/linkunbound-win/src/registration.rs @@ -293,12 +293,22 @@ pub fn transparency_is_on() -> bool { /// refuse the attribute or draw it black. #[must_use] pub fn acrylic_is_available() -> bool { + build_is_at_least(22621) +} + +/// DWM rounds and shadows a frameless window from Windows 11, build 22000. +#[must_use] +pub fn dwm_rounds_corners() -> bool { + build_is_at_least(22000) +} + +fn build_is_at_least(wanted: u32) -> bool { RegKey::predef(HKEY_LOCAL_MACHINE) .open_subkey(r"SOFTWARE\Microsoft\Windows NT\CurrentVersion") .and_then(|k| k.get_value::("CurrentBuildNumber")) .ok() .and_then(|build| build.parse::().ok()) - .is_some_and(|build| build >= 22621) + .is_some_and(|build| build >= wanted) } /// Matches exactly, never by substring: a third-party ProgId embedding our name From 2a032e23c64ba2f96baf806596ef3eb734db7cd8 Mon Sep 17 00:00:00 2001 From: rgdevment Date: Fri, 16 Oct 2026 23:44:17 -0300 Subject: [PATCH 3/3] build(deps): ignore the zip advisories the updater cannot reach --- deny.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/deny.toml b/deny.toml index 70ecd8e..e854189 100644 --- a/deny.toml +++ b/deny.toml @@ -47,6 +47,10 @@ ignore = [ "RUSTSEC-2025-0081", "RUSTSEC-2025-0098", "RUSTSEC-2025-0100", + # zip 4 through tauri-plugin-updater, which extracts only on Windows and only signed updates. + "RUSTSEC-2026-0367", + "RUSTSEC-2026-0368", + "RUSTSEC-2026-0369", ] [sources]