Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions openless-all/app/scripts/windows-capsule-acrylic-contract.test.mjs
Original file line number Diff line number Diff line change
@@ -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*<div\s*style=\{\{[\s\S]*?width:\s*'100%',[\s\S]*?height:\s*'100%',[\s\S]*?paddingLeft:\s*hostMetrics\.horizontalInset,[\s\S]*?paddingRight:\s*hostMetrics\.horizontalInset,[\s\S]*?background:\s*'transparent'/,
'capsule host should remain transparent outside the visible pill',
);
8 changes: 6 additions & 2 deletions openless-all/app/scripts/windows-ui-config.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,11 @@ if (!/export function getCapsuleHostMetrics\(\s*os: OS,\s*translationActive: boo
throw new Error('capsule layout should define explicit host metrics separate from the visible pill metrics');
}

if (!/if \(os === 'win'\)\s*\{[\s\S]*?const horizontalInset = 12;[\s\S]*?const pill = getCapsulePillMetrics\(os\);[\s\S]*?width: pill\.width \+ horizontalInset \* 2,[\s\S]*?height: translationActive \? 118 : 84,[\s\S]*?horizontalInset,[\s\S]*?bottomInset: 12,[\s\S]*?badgeGap: 8,[\s\S]*?boxSizing: 'border-box',[\s\S]*?\}/.test(capsuleLayoutTs)) {
if (!/if \(os === 'win'\)\s*\{[\s\S]*?return \{ width: 180, height: 44, textWidth: 88, boxSizing: 'border-box' \};[\s\S]*?\}/.test(capsuleLayoutTs)) {
throw new Error('windows capsule should keep the original compact visible pill metrics');
}

if (!/if \(os === 'win'\)\s*\{[\s\S]*?const horizontalInset = 12;[\s\S]*?width: 220,[\s\S]*?height: translationActive \? 118 : 84,[\s\S]*?horizontalInset,[\s\S]*?bottomInset: 12,[\s\S]*?badgeGap: 8,[\s\S]*?boxSizing: 'border-box',[\s\S]*?\}/.test(capsuleLayoutTs)) {
throw new Error('windows capsule host metrics should leave room for shadow and badge geometry');
}

Expand All @@ -132,7 +136,7 @@ if (!/hostMetrics\.bottomInset \+ metrics\.height \+ hostMetrics\.badgeGap/.test
throw new Error('windows translation badge should anchor from the shared host inset instead of a fixed center-based offset');
}

if (!/#\[cfg\(target_os = "windows"\)\][\s\S]*?const WINDOWS_CAPSULE_PILL_WIDTH: f64 = 196\.0;[\s\S]*?const WINDOWS_CAPSULE_SIDE_INSET: f64 = 12\.0;[\s\S]*?width: WINDOWS_CAPSULE_PILL_WIDTH \+ WINDOWS_CAPSULE_SIDE_INSET \* 2\.0,[\s\S]*?height: if translation_active \{ 118\.0 \} else \{ 84\.0 \},[\s\S]*?bottom_inset: 12\.0,/.test(libRs)) {
if (!/#\[cfg\(target_os = "windows"\)\]\s*\{[\s\S]*?const WINDOWS_CAPSULE_SIDE_INSET: f64 = 12\.0;[\s\S]*?width: 220\.0,[\s\S]*?height: if translation_active \{ 118\.0 \} else \{ 84\.0 \},[\s\S]*?bottom_inset: WINDOWS_CAPSULE_SIDE_INSET,/.test(libRs)) {
throw new Error('windows runtime capsule bounds should leave room for the native shadow while keeping a fixed visual pill');
}

Expand Down
30 changes: 9 additions & 21 deletions openless-all/app/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,9 @@ pub fn run() {
if let Err(e) = position_capsule_bottom_center(&capsule, false) {
log::warn!("[capsule] position failed: {e}");
}
// Windows 上 transparent:true 让窗口对桌面完全透明,Webview2 的
// backdrop-filter 只能模糊 DOM 内部,模糊不到 OS 桌面 → 胶囊背景
// 看起来就是「透到桌面」。这里给 Win10/Win11 都支持的 Acrylic 做兜底,
// 让 OS 提供毛玻璃材质,胶囊 rgba(255,255,255,0.85) 上面再叠 DOM 模糊。
// 失败不阻塞(老 Win10 / Win7 上 Acrylic 不可用),仅 warn。
#[cfg(target_os = "windows")]
{
use window_vibrancy::apply_acrylic;
// 中性偏冷的浅灰半透,与胶囊白底叠合后保持可读性。
if let Err(e) = apply_acrylic(&capsule, Some((30, 32, 38, 140))) {
log::warn!("[capsule] acrylic failed: {e}");
}
}
// Keep the Windows capsule host fully transparent. The native host is larger
// than the visible pill to reserve shadow, badge, and animation room; applying
// a HWND-level material here paints those transparent margins as a gray box.
let _ = capsule.hide();
}

Expand All @@ -155,9 +145,8 @@ pub fn run() {
}
#[cfg(target_os = "macos")]
make_qa_window_draggable_macos(&qa);
// 同 capsule:Windows 下 QA 浮窗也走 Acrylic 兜底。QA 面板自身
// 已经把 alpha 拉到 0.97(QaPanel.tsx:623),主要是防止 0.03 缝隙
// 透到桌面、以及 Win10 上完全无毛玻璃感的兜底。
// QA fills its native host, so Windows Acrylic remains a useful fallback here.
// Capsule is different: its host has transparent margins around a smaller pill.
#[cfg(target_os = "windows")]
{
use window_vibrancy::apply_acrylic;
Expand Down Expand Up @@ -1223,14 +1212,13 @@ struct CapsuleWindowBounds {
fn capsule_window_bounds(translation_active: bool) -> 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,
}
}

Expand Down
6 changes: 3 additions & 3 deletions openless-all/app/src/components/Capsule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div
Expand All @@ -258,10 +258,10 @@ function Pill({ os, state, level, insertedChars, message, onCancel, onConfirm }:
height: metrics.height,
boxSizing: metrics.boxSizing,
borderRadius: 999,
background: 'rgba(255, 255, 255, 0.85)',
background: os === 'win' ? 'rgba(255, 255, 255, 0.96)' : 'rgba(255, 255, 255, 0.85)',
backdropFilter: useBackdrop ? 'blur(28px) saturate(180%)' : 'none',
WebkitBackdropFilter: useBackdrop ? 'blur(28px) saturate(180%)' : 'none',
border: '1px solid rgba(255, 255, 255, 0.55)',
border: os === 'win' ? '1px solid rgba(255, 255, 255, 0.78)' : '1px solid rgba(255, 255, 255, 0.55)',
boxShadow: os === 'win'
? `0 10px 24px -14px rgba(0, 0, 0, ${(0.24 + ambient * 0.06).toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 0.5px 0 rgba(255, 255, 255, 0.55)`
: `0 18px 50px -10px rgba(0, 0, 0, ${shadowAlpha.toFixed(3)}), 0 0 0 0.5px rgba(0, 0, 0, 0.08), inset 0 0.5px 0 rgba(255, 255, 255, 0.55)`,
Expand Down
21 changes: 13 additions & 8 deletions openless-all/app/src/lib/capsuleLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ function assertEqual<T>(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);
Expand All @@ -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);
Expand Down
5 changes: 2 additions & 3 deletions openless-all/app/src/lib/capsuleLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' };
}

Expand All @@ -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,
Expand Down
Loading