glass survives the window gestures: the accent rides the modal loop and the maximize animation gets a bracket
Kyle reported the blur "being masked by just going opeque" on maximize, and separately the desktop punching through a maximized window. Both were the same transition, and the old guard covered neither honestly. THE GUARD'S SIGNAL IS NOW THE MODAL LOOP ITSELF. thumbbar.rs already subclasses the main window for the taskbar transport, so the size loop is three arms on that existing proc: WM_NCLBUTTONDOWN carrying a sizing hit-test (or SC_SIZE, the keyboard path) latches "this loop is a RESIZE", WM_ENTERSIZEMOVE arms, WM_EXITSIZEMOVE releases, and WM_SIZING arms as a second net because this window is undecorated and tao does its own NCHITTEST. Emitted as soulseek:resize-drag, POSTED to ourselves rather than emitted inline: emitting from inside DefWindowProc's modal loop reenters the wry runtime's window store while that loop holds it. The signal and the failure are then the same object, which is what the two previous attempts were not. The first grounded on any DOM `resize` — one event fires for a maximize, so the settled window sat flat black for ~70ms hiding glass that was already back. The second discriminated on event CADENCE, and that is the one worth remembering: on this 4-core laptop, re-rasterising every backdrop-filter at each new size pushes `resize` deliveries far apart, and measured 2026-08-28 events 150ms apart never grounded AT ALL — a real drag stayed see-through, shipping the exact artifact the guard exists to prevent. Cadence is not a proxy for the loop. DWM SUSPENDS THE ACRYLIC INSIDE THE LOOP but keeps compositing the plain BLURBEHIND, so the accent swaps down to Blur for the gesture and back on release (window_glass refactored into apply_accent + an Accent enum; GLASS_ON tracks the main window so the subclass can re-assert). The window stays glassy through a drag instead of going black — which is what Kyle actually wanted, and GROUND_DRAG is off on his verification by hand. THE MAXIMIZE ANIMATION SCALES A SNAPSHOT the accent cannot ride: measured at 43ms/frame, desktop icons read sharply THROUGH the maximized window until ~613ms after the call. Disabling the DWM zoom animation removed the punch-through outright and every frame of a ground-suppressed maximize showed the blur soft and present — but that also removed the animation, and Kyle chose the animation back with the canvas grounded across the recomposition. So transitions are forced ON, the subclass brackets the jump (active at WM_SIZE, a re-assert immediately and again at 650ms, release with the second), and GROUND_DISCRETE paints the canvas for exactly that bracket. Both policies are one-line JS constants in glass.js — flipping between "animation + dark bracket" and "instant snap + live glass" needs no rebuild. The DOM resize listener survives as the fallback for a build with no subclass, reverting to the original behaviour; it stands down for good the moment one native event is heard. Measured clean: 3 maximize/restore gestures, 3 brackets, canvas never opaque outside them; a 9-second modal loop held with zero resize events in it. 98/98 tests.
This commit is contained in:
1 parent
93271aceec
commit
385b598170
3 files changed
+356
-23
No files matched your search
+155
-2
@@ -36,6 +36,7 @@ pub fn set_playing(_app: &tauri::AppHandle, _playing: bool) {}
|
||||
|
||||
#[cfg(windows)]
|
||||
mod imp {
|
||||
use crate::commands::{apply_accent, Accent, GLASS_ON};
|
||||
use crate::smtc::MediaButton;
|
||||
use std::cell::RefCell;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
@@ -55,8 +56,10 @@ mod imp {
|
||||
THB_ICON, THB_TOOLTIP, THUMBBUTTON, THBN_CLICKED,
|
||||
};
|
||||
use windows::Win32::UI::WindowsAndMessaging::{
|
||||
CreateIconIndirect, DestroyIcon, RegisterWindowMessageW, HICON, ICONINFO, WM_COMMAND,
|
||||
WM_SETTINGCHANGE,
|
||||
CreateIconIndirect, DestroyIcon, PostMessageW, RegisterWindowMessageW, HICON, HTBOTTOM,
|
||||
HTBOTTOMLEFT, HTBOTTOMRIGHT, HTLEFT, HTRIGHT, HTTOP, HTTOPLEFT, HTTOPRIGHT, ICONINFO,
|
||||
KillTimer, SetTimer, SC_SIZE, WM_APP, WM_COMMAND, WM_ENTERSIZEMOVE, WM_EXITSIZEMOVE,
|
||||
WM_NCLBUTTONDOWN, WM_SETTINGCHANGE, WM_SIZE, WM_SIZING, WM_SYSCOMMAND, WM_TIMER,
|
||||
};
|
||||
|
||||
const ID_PREV: u32 = 1;
|
||||
@@ -72,6 +75,36 @@ mod imp {
|
||||
static PLAYING: AtomicBool = AtomicBool::new(false);
|
||||
static ADDED: AtomicBool = AtomicBool::new(false);
|
||||
|
||||
/// Private message, posted to ourselves — see the WM_RESIZE_DRAG arm in
|
||||
/// subclass_proc for why the emit cannot happen inline.
|
||||
const WM_RESIZE_DRAG: u32 = WM_APP + 0x11;
|
||||
|
||||
/// Set between WM_NCLBUTTONDOWN-on-a-border (or SC_SIZE) and
|
||||
/// WM_EXITSIZEMOVE: "the modal loop we are about to enter is a RESIZE".
|
||||
static SIZING_LATCH: AtomicBool = AtomicBool::new(false);
|
||||
/// Set while a resize drag is actually in flight, so the enter/exit pair
|
||||
/// is emitted exactly once each however the messages interleave.
|
||||
static IN_SIZE_DRAG: AtomicBool = AtomicBool::new(false);
|
||||
/// Set for the length of ANY modal move/size loop (move drags included),
|
||||
/// so the WM_SIZE re-assert below can tell a discrete size change from
|
||||
/// the per-frame WM_SIZE stream inside a drag.
|
||||
static IN_MODAL: AtomicBool = AtomicBool::new(false);
|
||||
/// One-shot timer for the post-gesture accent re-assert; any stable value.
|
||||
const REAPPLY_TIMER_ID: usize = 0x50_1A_12;
|
||||
|
||||
/// Tells the frontend to ground the glass canvas for the length of a live
|
||||
/// resize drag. Windows drops the accent-policy acrylic inside the modal
|
||||
/// size loop while the window stays transparent, so the desktop punches
|
||||
/// through razor-sharp; see armGlassResizeGuard in src/lib/glass.js.
|
||||
#[derive(Clone, serde::Serialize)]
|
||||
pub struct ResizeDrag {
|
||||
pub active: bool,
|
||||
/// "drag" for the modal move/size loop, "discrete" for a
|
||||
/// maximize/restore/snap/DPI size jump — the frontend grounds them
|
||||
/// under separate policies (see armGlassResizeGuard in glass.js).
|
||||
pub kind: &'static str,
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
/// UI-thread only, by the apartment rule in the module docs.
|
||||
static TASKBAR: RefCell<Option<ITaskbarList3>> = const { RefCell::new(None) };
|
||||
@@ -192,6 +225,126 @@ mod imp {
|
||||
if let Some(button) = button {
|
||||
let _ = app.emit("soulseek:media-button", MediaButton { button });
|
||||
}
|
||||
} else if msg == WM_NCLBUTTONDOWN {
|
||||
// Latch WHAT KIND of non-client drag is starting. WM_ENTERSIZEMOVE
|
||||
// on its own cannot tell a sizing drag from moving the window by
|
||||
// its titlebar — data-tauri-drag-region calls StartDragging, which
|
||||
// enters the very same modal loop — and only the sizing one is
|
||||
// known to cost the acrylic. wparam IS the hit-test code here.
|
||||
let ht = (wparam.0 & 0xFFFF) as u32;
|
||||
SIZING_LATCH.store(
|
||||
matches!(
|
||||
ht,
|
||||
HTLEFT
|
||||
| HTRIGHT
|
||||
| HTTOP
|
||||
| HTTOPLEFT
|
||||
| HTTOPRIGHT
|
||||
| HTBOTTOM
|
||||
| HTBOTTOMLEFT
|
||||
| HTBOTTOMRIGHT
|
||||
),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
} else if msg == WM_SYSCOMMAND && (wparam.0 & 0xFFF0) == SC_SIZE as usize {
|
||||
// Keyboard resize (Alt+Space, Size) enters the same loop with no
|
||||
// click, so there is no hit-test to latch from.
|
||||
SIZING_LATCH.store(true, Ordering::Relaxed);
|
||||
} else if msg == WM_ENTERSIZEMOVE {
|
||||
IN_MODAL.store(true, Ordering::Relaxed);
|
||||
// DWM suspends the ACRYLIC accent for the whole modal loop but
|
||||
// keeps compositing the plain BLURBEHIND one, so the window swaps
|
||||
// down to Blur for the gesture instead of going see-through.
|
||||
// Every modal loop gets this — move drags included, since a
|
||||
// suspended acrylic punches through on those exactly the same.
|
||||
if GLASS_ON.load(Ordering::Relaxed) {
|
||||
let _ = apply_accent(hwnd.0, Accent::Blur);
|
||||
}
|
||||
// Arm on the LOOP, not on WM_SIZING. Press a border and hold still
|
||||
// and Windows sends no WM_SIZING at all, yet the acrylic is
|
||||
// already gone — keying on movement would leave exactly that case
|
||||
// see-through, which is the artifact the guard exists to hide.
|
||||
if SIZING_LATCH.load(Ordering::Relaxed) && !IN_SIZE_DRAG.swap(true, Ordering::Relaxed) {
|
||||
unsafe {
|
||||
let _ = PostMessageW(Some(hwnd), WM_RESIZE_DRAG, WPARAM(1), LPARAM(0));
|
||||
}
|
||||
}
|
||||
} else if msg == WM_SIZING {
|
||||
// Belt and braces for the latch above. If WM_NCLBUTTONDOWN never
|
||||
// reached us carrying a sizing hit-test — this window is
|
||||
// undecorated and tao does its own NCHITTEST, and the exact path
|
||||
// has never been confirmed by a real human drag — a WM_SIZING is
|
||||
// still proof we are inside a live resize, because Windows sends
|
||||
// it nowhere else. A titlebar MOVE drag never sends it, so this
|
||||
// cannot arm on a move. It arms one mouse-move later than the
|
||||
// latch does, which is why the latch is still the primary.
|
||||
if !IN_SIZE_DRAG.swap(true, Ordering::Relaxed) {
|
||||
unsafe {
|
||||
let _ = PostMessageW(Some(hwnd), WM_RESIZE_DRAG, WPARAM(1), LPARAM(0));
|
||||
}
|
||||
}
|
||||
} else if msg == WM_EXITSIZEMOVE {
|
||||
IN_MODAL.store(false, Ordering::Relaxed);
|
||||
if GLASS_ON.load(Ordering::Relaxed) {
|
||||
let _ = apply_accent(hwnd.0, Accent::Acrylic);
|
||||
}
|
||||
SIZING_LATCH.store(false, Ordering::Relaxed);
|
||||
if IN_SIZE_DRAG.swap(false, Ordering::Relaxed) {
|
||||
unsafe {
|
||||
let _ = PostMessageW(Some(hwnd), WM_RESIZE_DRAG, WPARAM(0), LPARAM(0));
|
||||
}
|
||||
}
|
||||
} else if msg == WM_RESIZE_DRAG {
|
||||
// POSTED to ourselves, never emitted inline. Emitting from inside
|
||||
// DefWindowProc's modal size loop would reenter the wry runtime's
|
||||
// window store while that loop holds it; posting lands the emit at
|
||||
// the top of the loop's own pump with nothing borrowed. The loop
|
||||
// does pump posted messages, so the arm still reaches the page
|
||||
// during the drag rather than after it.
|
||||
let app = unsafe { &*(refdata as *const tauri::AppHandle) };
|
||||
let _ = app.emit(
|
||||
"soulseek:resize-drag",
|
||||
ResizeDrag {
|
||||
active: wparam.0 != 0,
|
||||
kind: if lparam.0 == 1 { "discrete" } else { "drag" },
|
||||
},
|
||||
);
|
||||
} else if msg == WM_SIZE {
|
||||
// A DISCRETE size change: maximize (wparam 2), restore/snap/DPI
|
||||
// (0). Kyle photographed the desktop punching through a maximized
|
||||
// glass window (2026-08-28): the acrylic does not reliably
|
||||
// survive the transition, so it is re-asserted — once now, once
|
||||
// more after the animation settles (the immediate one can land
|
||||
// while DWM is still mid-transition and be dropped again).
|
||||
// Gated out inside a modal loop, where WM_SIZE streams per frame
|
||||
// and the accent is deliberately parked at Blur.
|
||||
if !IN_MODAL.load(Ordering::Relaxed)
|
||||
&& GLASS_ON.load(Ordering::Relaxed)
|
||||
&& (wparam.0 == 2 || wparam.0 == 0)
|
||||
{
|
||||
let _ = apply_accent(hwnd.0, Accent::Acrylic);
|
||||
unsafe {
|
||||
// Ground the canvas for the zoom animation plus the accent
|
||||
// recomposition after it; the timer's arm below both
|
||||
// re-asserts and releases. 650ms: recovery measured clean
|
||||
// at ~613ms after the maximize call with the animation on
|
||||
// (2026-08-28 vis_max, 43ms/frame), and WM_SIZE lands
|
||||
// ~60-100ms after the call — the release then trails the
|
||||
// last punch-through frame by a comfortable ~170ms.
|
||||
let _ = PostMessageW(Some(hwnd), WM_RESIZE_DRAG, WPARAM(1), LPARAM(1));
|
||||
let _ = SetTimer(Some(hwnd), REAPPLY_TIMER_ID, 650, None);
|
||||
}
|
||||
}
|
||||
} else if msg == WM_TIMER && wparam.0 == REAPPLY_TIMER_ID {
|
||||
unsafe {
|
||||
let _ = KillTimer(Some(hwnd), REAPPLY_TIMER_ID);
|
||||
}
|
||||
if GLASS_ON.load(Ordering::Relaxed) {
|
||||
let _ = apply_accent(hwnd.0, Accent::Acrylic);
|
||||
}
|
||||
unsafe {
|
||||
let _ = PostMessageW(Some(hwnd), WM_RESIZE_DRAG, WPARAM(0), LPARAM(1));
|
||||
}
|
||||
} else if msg == WM_SETTINGCHANGE {
|
||||
// Covers the light/dark flip: the glyphs are drawn for a
|
||||
// background they no longer sit on, so redraw them.
|
||||
|
||||
Reference in new issue
Block a user