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
+122
-14
@@ -9,7 +9,7 @@
|
||||
// The mini must never import this — its applier would re-emit into its own
|
||||
// listener.
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import { windowGlass } from "./api.js";
|
||||
import { onEvent, windowGlass } from "./api.js";
|
||||
|
||||
let current = false;
|
||||
export const glassOn = () => current;
|
||||
@@ -21,7 +21,7 @@ export function applyGlass(on) {
|
||||
void emit("mini:glass", { on: current });
|
||||
}
|
||||
|
||||
/** Ground the canvas while the window is being resized.
|
||||
/** Ground the canvas while the window is being DRAGGED to a new size.
|
||||
*
|
||||
* WHY THIS EXISTS. The OS half of glass mode is the LEGACY accent-policy
|
||||
* acrylic — `SetWindowCompositionAttribute` with ACCENT_ENABLE_ACRYLICBLURBEHIND
|
||||
@@ -31,8 +31,9 @@ export function applyGlass(on) {
|
||||
* it is not something the app can switch off. The moment it drops, the window
|
||||
* is still TRANSPARENT — so the desktop behind it stops being a soft wash and
|
||||
* punches through razor-sharp, wallpaper and icons and all. Measured
|
||||
* 2026-08-27 by toggling `window_glass` alone: with the acrylic off you can
|
||||
* read desktop icon labels through the titlebar and the sidebar.
|
||||
* 2026-08-27 by toggling `window_glass` alone; the reference shot is
|
||||
* research/scroll-diag/glass-refs/c-acrylic-off.png, and it is genuinely worse
|
||||
* than the grounded state beside it.
|
||||
*
|
||||
* So the fix is not to keep the blur — Windows will not let us. It is to stop
|
||||
* relying on it for the length of the gesture: the canvas goes opaque, and
|
||||
@@ -40,24 +41,131 @@ export function applyGlass(on) {
|
||||
* window reads as an ordinary solid dark window while you drag, and comes back
|
||||
* to glass when you let go.
|
||||
*
|
||||
* `resize` fires continuously through a drag and once for a maximize, a
|
||||
* restore or a DPI change, so all of those are covered by the same guard. The
|
||||
* class goes on unconditionally and the CSS gates it on `.glass`, which keeps
|
||||
* this correct if the mode is toggled mid-gesture.
|
||||
* HOW GLASS SURVIVES A GESTURE (2026-08-28, three layers, Rust-first):
|
||||
*
|
||||
* 1. Modal move/size loops (border drags, titlebar drags): the subclass in
|
||||
* src-tauri/src/thumbbar.rs swaps the accent from ACRYLIC down to plain
|
||||
* BLURBEHIND for the length of the loop and back on release. DWM
|
||||
* suspends the acrylic inside the loop but keeps compositing the plain
|
||||
* blur, so the window stays glassy instead of see-through or black.
|
||||
*
|
||||
* 2. Discrete jumps (maximize, restore, snap, DPI): the DWM zoom animation
|
||||
* scales a SNAPSHOT of the window, not the window — the accent visual
|
||||
* detaches for its length and recomposes only ~613ms after the call
|
||||
* (measured 2026-08-28 at 43ms/frame: desktop icons read sharply THROUGH
|
||||
* the maximized window until then). Disabling the animation removed the
|
||||
* punch-through outright, but Kyle chose keeping the animation and
|
||||
* grounding across the recomposition instead (2026-08-29) — so the
|
||||
* subclass brackets the jump: active:true at WM_SIZE, a re-assert of the
|
||||
* accent immediately and again at 650ms, active:false with the second
|
||||
* re-assert. GROUND_DISCRETE below paints the canvas opaque for exactly
|
||||
* that bracket.
|
||||
*
|
||||
* 3. This file: the subclass reports both kinds as soulseek:resize-drag
|
||||
* ({active, kind: "drag"|"discrete"}), and the policy constants below
|
||||
* decide whether the canvas grounds opaque for the bracket. DRAG is off
|
||||
* (the blur swap carries it — verified by Kyle by hand, 2026-08-29);
|
||||
* DISCRETE is on, covering the animation trade above.
|
||||
*
|
||||
* HISTORY, kept because both failures looked right when they shipped.
|
||||
* `resize` fires ONCE for a discrete change
|
||||
* (measured 2026-08-27 driving Tauri's own maximize) and the first guard
|
||||
* grounded on it, which bought nothing: the settled maximized window sat flat
|
||||
* black for ~70ms AFTER the animation had finished, hiding glass that was
|
||||
* already back. Kyle reported it as the blur "being masked by just going
|
||||
* opaque".
|
||||
*
|
||||
* The attempt in between is recorded because it looked right and was not. It
|
||||
* discriminated on event CADENCE — ground on the second `resize` inside a
|
||||
* short window, since a drag streams and a maximize does not. On a 4-core
|
||||
* laptop dragging this app, re-rasterising every backdrop-filter at each new
|
||||
* size pushes `resize` deliveries far apart: measured 2026-08-28, events
|
||||
* 150ms apart never grounded AT ALL, so a real drag stayed see-through and
|
||||
* shipped the exact artifact the guard exists to prevent. Cadence is not a
|
||||
* proxy for the loop. Do not try it again.
|
||||
*
|
||||
* FALLBACK. If the native signal never arrives — the subclass failed to
|
||||
* install (thumbbar.rs only warns), or a build without it — the DOM `resize`
|
||||
* listener reverts to the ORIGINAL behaviour: ground on any resize, release
|
||||
* after RESIZE_SETTLE_MS. That grounds maximize again, which is the complaint
|
||||
* this file exists to fix, but it fails toward "too opaque" rather than toward
|
||||
* "see-through", and see-through is the one that is actually ugly. The moment
|
||||
* a single native event is heard the fallback stands down for good.
|
||||
*
|
||||
* The class goes on unconditionally and the CSS gates it on `.glass`, which
|
||||
* keeps this correct if the mode is toggled mid-gesture.
|
||||
*/
|
||||
const RESIZE_SETTLE_MS = 180;
|
||||
// The acrylic is back by WM_EXITSIZEMOVE, but the repaint at the final size
|
||||
// can trail it by a frame or two. Long enough to cover that, short enough that
|
||||
// nobody reads it as a black tail hanging off mouse-up.
|
||||
const EXIT_SETTLE_MS = 60;
|
||||
// Insurance only, and deliberately far longer than any real gesture: if an
|
||||
// EXITSIZEMOVE is ever lost the canvas must not stay grounded for the rest of
|
||||
// the session. Nothing should ever reach this.
|
||||
const DRAG_HARD_CAP_MS = 20_000;
|
||||
// Ground policy per native gesture kind: true = opaque canvas for the
|
||||
// bracket (deterministic, dark), false = live glass, trusting the Rust
|
||||
// layers above. DRAG is off: the blur swap carries border drags, verified
|
||||
// by Kyle by hand (2026-08-29). DISCRETE is ON, by choice, same day: the
|
||||
// alternative was disabling the DWM zoom animation (measured clean,
|
||||
// t2_noground) but Kyle wanted the animation, and the animation scales a
|
||||
// snapshot the accent cannot ride — so the canvas grounds for the ~650ms
|
||||
// bracket instead. Flipping DISCRETE off without also disabling
|
||||
// transitions in window_glass reintroduces the photographed punch-through.
|
||||
const GROUND_DRAG = false;
|
||||
const GROUND_DISCRETE = true;
|
||||
|
||||
export function armGlassResizeGuard() {
|
||||
if (typeof window === "undefined") return () => {};
|
||||
let timer = 0;
|
||||
const root = document.documentElement;
|
||||
let settle = 0;
|
||||
let cap = 0;
|
||||
// Latches on the first native event and never unlatches: one report is
|
||||
// proof the subclass is alive, and the fallback must not fight it.
|
||||
let native = false;
|
||||
|
||||
const ground = () => {
|
||||
clearTimeout(settle);
|
||||
root.classList.add("resizing");
|
||||
};
|
||||
const release = (delay) => {
|
||||
clearTimeout(settle);
|
||||
settle = setTimeout(() => {
|
||||
clearTimeout(cap);
|
||||
root.classList.remove("resizing");
|
||||
}, delay);
|
||||
};
|
||||
|
||||
const stopNative = onEvent("soulseek:resize-drag", (p) => {
|
||||
native = true;
|
||||
// Separate policies per gesture kind, both JS-side so they can be
|
||||
// tuned without a Rust rebuild. "drag" is the modal move/size loop;
|
||||
// "discrete" is a maximize/restore/snap/DPI jump, bracketed by the
|
||||
// subclass around the accent recomposition window.
|
||||
const wanted = p?.kind === "discrete" ? GROUND_DISCRETE : GROUND_DRAG;
|
||||
if (!wanted) return;
|
||||
if (p?.active) {
|
||||
ground();
|
||||
clearTimeout(cap);
|
||||
cap = setTimeout(() => release(0), DRAG_HARD_CAP_MS);
|
||||
} else {
|
||||
release(EXIT_SETTLE_MS);
|
||||
}
|
||||
});
|
||||
|
||||
const onResize = () => {
|
||||
document.documentElement.classList.add("resizing");
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => document.documentElement.classList.remove("resizing"), RESIZE_SETTLE_MS);
|
||||
if (native) return;
|
||||
ground();
|
||||
release(RESIZE_SETTLE_MS);
|
||||
};
|
||||
window.addEventListener("resize", onResize, { passive: true });
|
||||
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
clearTimeout(settle);
|
||||
clearTimeout(cap);
|
||||
stopNative();
|
||||
window.removeEventListener("resize", onResize);
|
||||
document.documentElement.classList.remove("resizing");
|
||||
root.classList.remove("resizing");
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user