diff --git a/openless-all/app/scripts/windows-capsule-acrylic-contract.test.mjs b/openless-all/app/scripts/windows-capsule-acrylic-contract.test.mjs new file mode 100644 index 00000000..da07fef9 --- /dev/null +++ b/openless-all/app/scripts/windows-capsule-acrylic-contract.test.mjs @@ -0,0 +1,53 @@ +import { readFile } from 'node:fs/promises'; + +function assertMatch(source, pattern, name) { + if (!pattern.test(source)) { + throw new Error(`${name}: pattern ${pattern} not found`); + } +} + +function assertNotMatch(source, pattern, name) { + if (pattern.test(source)) { + throw new Error(`${name}: forbidden pattern ${pattern} found`); + } +} + +const libRs = await readFile(new URL('../src-tauri/src/lib.rs', import.meta.url), 'utf-8'); +const capsuleTsx = await readFile(new URL('../src/components/Capsule.tsx', import.meta.url), 'utf-8'); +const capsuleLayoutTs = await readFile(new URL('../src/lib/capsuleLayout.ts', import.meta.url), 'utf-8'); + +assertNotMatch( + libRs, + /apply_acrylic\(&capsule,/, + 'windows capsule must not use window-vibrancy Acrylic because it paints a rectangular grey host on Win11', +); + +assertNotMatch( + libRs, + /DwmEnableBlurBehindWindow|DWMWA_SYSTEMBACKDROP_TYPE|SetWindowRgn/, + 'windows capsule must not use HWND-level DWM material or native regions; the DOM pill owns the visible shape', +); + +assertMatch( + libRs, + /apply_acrylic\(&qa,\s*Some\(\(30,\s*32,\s*38,\s*140\)\)\)/, + 'windows QA window may keep Acrylic because its panel fills the native host', +); + +assertMatch( + capsuleLayoutTs, + /return \{ width: 180, height: 44, textWidth: 88, boxSizing: 'border-box' \};[\s\S]*?const horizontalInset = 12;[\s\S]*?width: 220,[\s\S]*?height: translationActive \? 118 : 84,[\s\S]*?bottomInset: 12,/, + 'windows capsule should keep the original compact DOM pill inside a transparent native host', +); + +assertMatch( + capsuleTsx, + /const useBackdrop = os !== 'win';[\s\S]*?background: os === 'win' \? 'rgba\(255, 255, 255, 0\.96\)' : 'rgba\(255, 255, 255, 0\.85\)'/, + 'windows capsule pill should use an opaque DOM surface instead of WebView2 backdrop-filter over a transparent host', +); + +assertMatch( + capsuleTsx, + /return\s*\(\s* CapsuleWindowBounds { #[cfg(target_os = "windows")] { - const WINDOWS_CAPSULE_PILL_WIDTH: f64 = 196.0; const WINDOWS_CAPSULE_SIDE_INSET: f64 = 12.0; CapsuleWindowBounds { - // Keep the existing Windows hitbox width, but express it as - // pill width (196) + symmetric 12px side insets for shadow room. - width: WINDOWS_CAPSULE_PILL_WIDTH + WINDOWS_CAPSULE_SIDE_INSET * 2.0, + // Keep the existing Windows hitbox width while reserving transparent + // margins for the DOM pill shadow and translation badge animation. + width: 220.0, height: if translation_active { 118.0 } else { 84.0 }, - bottom_inset: 12.0, + bottom_inset: WINDOWS_CAPSULE_SIDE_INSET, } } diff --git a/openless-all/app/src/components/Capsule.tsx b/openless-all/app/src/components/Capsule.tsx index 283ce712..1cb4aed6 100644 --- a/openless-all/app/src/components/Capsule.tsx +++ b/openless-all/app/src/components/Capsule.tsx @@ -244,7 +244,7 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }: const ambient = state === 'recording' ? Math.min(1, Math.max(0, level)) : 0; const scale = os === 'win' ? 1 : 1 + ambient * 0.018; const shadowAlpha = 0.20 + ambient * 0.10; - const useBackdrop = true; + const useBackdrop = os !== 'win'; return (
(actual: T, expected: T, name: string) { } const winMetrics = getCapsulePillMetrics('win'); -assertEqual(winMetrics.width, 196, 'windows capsule widens pill'); -assertEqual(winMetrics.height, 52, 'windows capsule increases pill height'); -assertEqual(winMetrics.textWidth, 104, 'windows capsule keeps side controls clear'); +assertEqual(winMetrics.width, 180, 'windows capsule keeps original pill width'); +assertEqual(winMetrics.height, 44, 'windows capsule keeps original pill height'); +assertEqual(winMetrics.textWidth, 88, 'windows capsule keeps original text slot'); assertEqual(winMetrics.boxSizing, 'border-box', 'windows capsule pill width is an outer border-box metric'); const winHost = getCapsuleHostMetrics('win', false); @@ -22,14 +22,19 @@ assertEqual(winHost.height, 84, 'windows capsule host keeps regular height'); assertEqual(winHost.horizontalInset, 12, 'windows capsule host keeps symmetric shadow insets'); assertEqual(winHost.boxSizing, 'border-box', 'windows capsule host inset is reserved inside the native width'); assertEqual( - winHost.width, - winMetrics.width + winHost.horizontalInset * 2, - 'windows capsule host width derives from pill width plus symmetric side insets', + winHost.width - winHost.horizontalInset * 2, + 196, + 'windows capsule host leaves extra centering room around the original pill', +); +assertEqual( + winHost.width - winHost.horizontalInset * 2 - winMetrics.width, + 16, + 'windows capsule host keeps the visible pill compact inside the transparent host', ); assertEqual( - winHost.width - winHost.horizontalInset * 2, winMetrics.width, - 'windows capsule host keeps the visible pill width after reserving side insets', + 180, + 'windows capsule pill is independent from the host hitbox width', ); const winHostWithTranslation = getCapsuleHostMetrics('win', true); diff --git a/openless-all/app/src/lib/capsuleLayout.ts b/openless-all/app/src/lib/capsuleLayout.ts index cf116eb6..5663552a 100644 --- a/openless-all/app/src/lib/capsuleLayout.ts +++ b/openless-all/app/src/lib/capsuleLayout.ts @@ -26,7 +26,7 @@ export interface CapsuleMessageLayout { export function getCapsulePillMetrics(os: OS): CapsulePillMetrics { if (os === 'win') { // Windows metrics describe the visible outer footprint of the pill. - // 与 macOS pill 接近以保持视觉密度一致;保留 ~4-5% 余量适配 Windows 字体 metrics。 + // Keep the original compact capsule shape; the native host owns only transparent room. return { width: 180, height: 44, textWidth: 88, boxSizing: 'border-box' }; } @@ -41,9 +41,8 @@ export function getCapsuleHostMetrics( ): CapsuleHostMetrics { if (os === 'win') { const horizontalInset = 12; - const pill = getCapsulePillMetrics(os); return { - width: pill.width + horizontalInset * 2, + width: 220, height: translationActive ? 118 : 84, horizontalInset, bottomInset: 12,