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,