From 8e38c151899741ee7424a6c54831375a6492d94b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 29 Aug 2026 10:18:37 +0000 Subject: [PATCH 1/2] fix: make Liquid Glass button highlights follow the pointer The inner glint was a radial gradient pinned at top-center. Drive its position from the cursor on interactive hosts so illumination starts under the pointer, matching Apple / the rdev reference. Co-authored-by: Gao Yu --- tauri-app/src/components/LiquidGlass.tsx | 65 ++++++++++++++++++------ tauri-app/src/styles.css | 6 ++- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/tauri-app/src/components/LiquidGlass.tsx b/tauri-app/src/components/LiquidGlass.tsx index 4427095..a864d23 100644 --- a/tauri-app/src/components/LiquidGlass.tsx +++ b/tauri-app/src/components/LiquidGlass.tsx @@ -21,7 +21,8 @@ import { buildLiquidGlassMap, type LiquidGlassMode } from "../lib/liquidGlass"; // backdrop is composited back into the center (edge-only lensing), // - dual gradient border layers (screen + overlay) + inset specular that // pick up the light of whatever sits behind the glass, -// - hover / press inner illumination, optional elastic stretch on small +// - hover / press inner illumination that follows the pointer (Apple: +// light starts under the cursor), optional elastic stretch on small // controls only (large surfaces never warp). // Unlike the reference, layout is fill-parent (no centered translate) and the // material renders as plain children of the host element, so existing chrome @@ -48,7 +49,8 @@ import { buildLiquidGlassMap, type LiquidGlassMode } from "../lib/liquidGlass"; // - video wallpaper: cheap frost only, no displacement. // - prefers-reduced-transparency / prefers-contrast / forced-colors: the // component renders nothing and the CSS-only fallback rules own the look. -// - prefers-reduced-motion: no elasticity, no animated illumination. +// - prefers-reduced-motion: no elasticity, no pointer-tracking illumination +// (the hover fade stays at the static top-center rest pose). // Refractive surfaces are deliberately scarce (Apple reserves the material // for floating chrome, and each lens costs three displacement passes on the @@ -196,7 +198,7 @@ export type LiquidGlassLayersProps = { blur?: number; /** "chrome" = neutral material, "accent" = colored glass for primary CTAs. */ tint?: "chrome" | "accent"; - /** Hover / press inner illumination (Apple: the material energizes). */ + /** Hover / press inner illumination that follows the pointer. */ interactive?: boolean; /** * Mouse-following stretch. Small controls only (buttons, chips, tiles) — @@ -322,11 +324,13 @@ export function LiquidGlassLayers({ }; }, [glass.lensed, mode]); - // Elasticity: subtle mouse-following stretch, opt-in for small controls. - // Direct style writes on the host (rAF-throttled) so React never re-renders - // on mousemove. + // Pointer tracking: hover illumination follows the cursor (Apple: light + // starts under the pointer), plus optional elastic stretch on small + // controls. Direct style writes on the host (rAF-throttled) so React never + // re-renders on mousemove. Coarse pointers and prefers-reduced-motion keep + // the static top-center fade from CSS. useEffect(() => { - if (!glass.active || !elastic || glass.reducedMotion) { + if (!glass.active || glass.reducedMotion || (!interactive && !elastic)) { return; } if ( @@ -340,7 +344,9 @@ export function LiquidGlassLayers({ if (!host) { return; } - host.classList.add("lg-elastic"); + if (elastic) { + host.classList.add("lg-elastic"); + } let raf = 0; let hovered = false; let pressed = false; @@ -348,8 +354,25 @@ export function LiquidGlassLayers({ let ty = 0; let sx = 1; let sy = 1; + let glintX = 50; + let glintY = 0; + const resetGlint = () => { + host.style.removeProperty("--lg-glint-x"); + host.style.removeProperty("--lg-glint-y"); + }; const apply = () => { raf = 0; + if (interactive) { + if (!hovered && !pressed) { + resetGlint(); + } else { + host.style.setProperty("--lg-glint-x", `${glintX.toFixed(1)}%`); + host.style.setProperty("--lg-glint-y", `${glintY.toFixed(1)}%`); + } + } + if (!elastic) { + return; + } if (!hovered && !pressed) { host.style.transform = ""; return; @@ -363,17 +386,26 @@ export function LiquidGlassLayers({ } }; const onMove = (event: MouseEvent) => { + if (host instanceof HTMLButtonElement && host.disabled) { + return; + } const rect = host.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) { return; } - // Normalized offset from center, [-0.5, 0.5] while inside the control. - const nx = (event.clientX - (rect.left + rect.width / 2)) / rect.width; - const ny = (event.clientY - (rect.top + rect.height / 2)) / rect.height; - tx = Math.max(-4, Math.min(4, nx * 7)); - ty = Math.max(-3, Math.min(3, ny * 7)); - sx = 1 + Math.min(0.03, Math.abs(nx) * 0.05); - sy = 1 + Math.min(0.04, Math.abs(ny) * 0.08); + if (elastic) { + // Normalized offset from center, [-0.5, 0.5] while inside the control. + const nx = (event.clientX - (rect.left + rect.width / 2)) / rect.width; + const ny = (event.clientY - (rect.top + rect.height / 2)) / rect.height; + tx = Math.max(-4, Math.min(4, nx * 7)); + ty = Math.max(-3, Math.min(3, ny * 7)); + sx = 1 + Math.min(0.03, Math.abs(nx) * 0.05); + sy = 1 + Math.min(0.04, Math.abs(ny) * 0.08); + } + if (interactive) { + glintX = Math.max(0, Math.min(100, ((event.clientX - rect.left) / rect.width) * 100)); + glintY = Math.max(0, Math.min(100, ((event.clientY - rect.top) / rect.height) * 100)); + } hovered = true; schedule(); }; @@ -403,9 +435,10 @@ export function LiquidGlassLayers({ cancelAnimationFrame(raf); } host.style.transform = ""; + resetGlint(); host.classList.remove("lg-elastic"); }; - }, [glass.active, glass.reducedMotion, elastic]); + }, [glass.active, glass.reducedMotion, elastic, interactive]); if (!glass.active) { return null; diff --git a/tauri-app/src/styles.css b/tauri-app/src/styles.css index 5bf0c38..cd5812d 100644 --- a/tauri-app/src/styles.css +++ b/tauri-app/src/styles.css @@ -6015,12 +6015,14 @@ button:disabled { } /* Hover / press inner illumination — the material energizes under the - pointer (rendered only for interactive hosts). */ + pointer (rendered only for interactive hosts). Position is written by + LiquidGlassLayers as --lg-glint-x / --lg-glint-y; the rest pose is the + Apple-style top-center catch light. */ .lg-glint { opacity: 0; mix-blend-mode: overlay; background: radial-gradient( - circle at 50% 0%, + circle at var(--lg-glint-x, 50%) var(--lg-glint-y, 0%), rgba(255, 255, 255, 0.65) 0%, rgba(255, 255, 255, 0) 62% ); From c4b55d2e228f0146670ff5c5106f6d36ad8ff5ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sat, 29 Aug 2026 10:30:42 +0000 Subject: [PATCH 2/2] fix: make Liquid Glass highlights track the pointer Drive the inner glint and rim shine from the cursor on interactive hosts. Keep elasticity on fine pointers only, skip tracking when motion is reduced, and light the hotspot with a compact radial so the highlight is actually visible as it moves. Co-authored-by: Gao Yu --- tauri-app/src/components/LiquidGlass.tsx | 26 +++++++++++++++--------- tauri-app/src/styles.css | 22 ++++++++++++-------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/tauri-app/src/components/LiquidGlass.tsx b/tauri-app/src/components/LiquidGlass.tsx index a864d23..7ad0fe9 100644 --- a/tauri-app/src/components/LiquidGlass.tsx +++ b/tauri-app/src/components/LiquidGlass.tsx @@ -327,24 +327,26 @@ export function LiquidGlassLayers({ // Pointer tracking: hover illumination follows the cursor (Apple: light // starts under the pointer), plus optional elastic stretch on small // controls. Direct style writes on the host (rAF-throttled) so React never - // re-renders on mousemove. Coarse pointers and prefers-reduced-motion keep - // the static top-center fade from CSS. + // re-renders on mousemove. Stretch stays pointer:fine-only so a tap does + // not warp the control; the glint follows any mousemove. Reduced motion + // keeps the static top-center fade from CSS. useEffect(() => { if (!glass.active || glass.reducedMotion || (!interactive && !elastic)) { return; } - if ( - typeof window === "undefined" || - typeof window.matchMedia !== "function" || - !window.matchMedia("(pointer: fine)").matches - ) { + const finePointer = + typeof window !== "undefined" && + typeof window.matchMedia === "function" && + window.matchMedia("(pointer: fine)").matches; + if (!interactive && !finePointer) { return; } const host = anchorRef.current?.parentElement as HTMLElement | null; if (!host) { return; } - if (elastic) { + const stretch = elastic && finePointer; + if (stretch) { host.classList.add("lg-elastic"); } let raf = 0; @@ -359,6 +361,8 @@ export function LiquidGlassLayers({ const resetGlint = () => { host.style.removeProperty("--lg-glint-x"); host.style.removeProperty("--lg-glint-y"); + host.style.removeProperty("--lg-shine-angle"); + host.classList.remove("lg-hot"); }; const apply = () => { raf = 0; @@ -368,9 +372,11 @@ export function LiquidGlassLayers({ } else { host.style.setProperty("--lg-glint-x", `${glintX.toFixed(1)}%`); host.style.setProperty("--lg-glint-y", `${glintY.toFixed(1)}%`); + host.style.setProperty("--lg-shine-angle", `${(180 + (glintX - 50) * 0.72).toFixed(1)}deg`); + host.classList.add("lg-hot"); } } - if (!elastic) { + if (!stretch) { return; } if (!hovered && !pressed) { @@ -393,7 +399,7 @@ export function LiquidGlassLayers({ if (rect.width <= 0 || rect.height <= 0) { return; } - if (elastic) { + if (stretch) { // Normalized offset from center, [-0.5, 0.5] while inside the control. const nx = (event.clientX - (rect.left + rect.width / 2)) / rect.width; const ny = (event.clientY - (rect.top + rect.height / 2)) / rect.height; diff --git a/tauri-app/src/styles.css b/tauri-app/src/styles.css index cd5812d..9c9696a 100644 --- a/tauri-app/src/styles.css +++ b/tauri-app/src/styles.css @@ -5986,7 +5986,7 @@ button:disabled { linear-gradient(#000 0 0); mask-composite: exclude; background: linear-gradient( - 180deg, + var(--lg-shine-angle, 180deg), rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0.14) 34%, rgba(255, 255, 255, 0) 62% @@ -5997,7 +5997,7 @@ button:disabled { mix-blend-mode: overlay; opacity: 0.55; background: linear-gradient( - 180deg, + var(--lg-shine-angle, 180deg), rgba(255, 255, 255, 0.42) 0%, rgba(255, 255, 255, 0.1) 36%, rgba(255, 255, 255, 0) 64% @@ -6017,19 +6017,23 @@ button:disabled { /* Hover / press inner illumination — the material energizes under the pointer (rendered only for interactive hosts). Position is written by LiquidGlassLayers as --lg-glint-x / --lg-glint-y; the rest pose is the - Apple-style top-center catch light. */ + Apple-style top-center catch light. The radius stays compact so the + hotspot actually travels with the cursor instead of washing the whole + control. */ .lg-glint { opacity: 0; - mix-blend-mode: overlay; + mix-blend-mode: normal; background: radial-gradient( - circle at var(--lg-glint-x, 50%) var(--lg-glint-y, 0%), - rgba(255, 255, 255, 0.65) 0%, - rgba(255, 255, 255, 0) 62% + circle 70px at var(--lg-glint-x, 50%) var(--lg-glint-y, 0%), + rgba(255, 255, 255, 0.42) 0%, + rgba(255, 255, 255, 0.1) 38%, + rgba(255, 255, 255, 0) 68% ); } -.lg-on:hover > .lg-layers .lg-glint { - opacity: 0.6; +.lg-on:hover > .lg-layers .lg-glint, +.lg-on.lg-hot > .lg-layers .lg-glint { + opacity: 1; } .lg-on:active > .lg-layers .lg-glint {