diff --git a/client/electron/test/harmony-appearance.test.mjs b/client/electron/test/harmony-appearance.test.mjs index 4805821..b967e94 100644 --- a/client/electron/test/harmony-appearance.test.mjs +++ b/client/electron/test/harmony-appearance.test.mjs @@ -380,7 +380,12 @@ test('★ 页面真的把背景画出来了(这一条是补漏:P4 第一版 assert.match(main, /Image\(this\.wallpaperImage\)/, '图片档要真的把图渲染出来'); assert.match(main, /\.objectFit\(ImageFit\.Cover\)/, '图片要铺满(不是拉伸变形或留白)'); // 压暗用系统遮罩色 + 服务端浓度 - assert.match(main, /\.backgroundColor\(Theme\.overlay\)[\s\S]{0,80}?\.opacity\(this\.bgPlan\.scrim\)/, '压暗层要用系统遮罩色与算出来的浓度'); + /* + * 遮盖层:用**页面底色系**的 `Theme.wallpaperScrim`(不是模态遮罩 mask)—— + * 浅色主题下 mask 是深色(#99182431),会把预设压暗,而 WebUI 是把预设洗淡(--bg-scrim 是白)。 + * 这一条与下面那条"遮盖色方向"是同一件事的两个面:这里钉"层在不在、浓度接没接上"。 + */ + assert.match(main, /\.backgroundColor\(Theme\.wallpaperScrim\)[\s\S]{0,80}?\.opacity\(this\.bgPlan\.scrim\)/, '遮盖层要用页面底色系令牌与算出来的浓度'); // 背景在主界面这一层:Tabs 之上不该再有不透明的底色把背景盖死 assert.match(main, /Stack\(\) \{[\s\S]{0,120}?this\.WallpaperLayer\(\)/, '背景要铺在内容之下(Stack 的底层)'); }); @@ -585,7 +590,7 @@ test('★ 预设档的遮盖:两档同一个浓度(WebUI 的 --bg-dim 不区 assert.match(bgStore, /--bg-dim|setProperty\('--bg-dim'/, 'WebUI 要无条件写 --bg-dim(这是"两档都压"的依据)'); const main = read('pages/MainPage.ets'); // 两档各有一处遮盖层(都用系统遮罩色 + 算出来的浓度) - const scrims = [...main.matchAll(/\.backgroundColor\(Theme\.overlay\)\s*\n\s*\.opacity\(this\.bgPlan\.scrim\)/g)]; + const scrims = [...main.matchAll(/\.backgroundColor\(Theme\.wallpaperScrim\)\s*\n\s*\.opacity\(this\.bgPlan\.scrim\)/g)]; assert.ok(scrims.length >= 2, `预设档与图片档各要有一层遮盖(实际 ${scrims.length} 处)`); }); @@ -647,3 +652,89 @@ test('★ 主题变化时**我们自己算的值**要跟着重算(pi 的规则 const sdk = readFileSync(join(CLT, 'sdk/default/openharmony/ets/api/@ohos.app.ability.EnvironmentCallback.d.ts'), 'utf8'); assert.match(sdk, /onConfigurationUpdated\(config: Configuration\): void/, 'SDK 里回调是 onConfigurationUpdated(别写错名字)'); }); + +// ───────── 遮盖色方向:从 SDK 两张表读出真值再判(pi 2026-09-14 的方向性疑问) ───────── + +/** 从 SDK 读出 `sys.color.ohos_id_color_*` 的真实值(名字→id 取编译器那张表,id→值取预览器那张) */ +function sdkSystemColors() { + const sysRes = readFileSync(join(CLT, 'sdk/default/openharmony/ets/build-tools/ets-loader/sysResource.js'), 'utf8'); + const resTxt = readFileSync(join(CLT, 'sdk/default/openharmony/previewer/common/resources/entry/resources.txt'), 'utf8'); + const name2id = new Map(); + for (const m of sysRes.matchAll(/'?(ohos_id_color_[a-z_]+)'?:\s*(\d+)/g)) { + if (!name2id.has(m[1])) name2id.set(m[1], Number(m[2])); + } + const id2val = new Map(); + for (const m of resTxt.matchAll(/id:(\d+),\s*'([^']*)'/g)) { + const id = Number(m[1]); + if (!id2val.has(id)) id2val.set(id, m[2]); + } + const out = {}; + for (const [name, id] of name2id) if (id2val.has(id)) out[name] = id2val.get(id); + return out; +} + +/** '#AARRGGBB' → 亮度(0=黑 1=白);只用于判"深还是浅" */ +function luminanceOf(argb) { + const r = parseInt(argb.slice(3, 5), 16) / 255; + const g = parseInt(argb.slice(5, 7), 16) / 255; + const b = parseInt(argb.slice(7, 9), 16) / 255; + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +} + +test('★ 遮盖色方向:`mask_*` 两套主题下都是**深色**(模态遮罩),壁纸遮盖必须用**页面底色系**', () => { + /* + * pi 的疑问原话:「系统那三个 mask —— 名字里的 light/regular/thick 是**浓度档** + * (同一个色的三个 alpha),不是深浅主题的两套值。它们的用途是**模态遮罩**(弹层背后压暗), + * 所以两套主题下通常都是深色。如果预设/图片的遮盖层用 mask 色,**浅色主题下会把预设压暗, + * 而 WebUI 是把预设洗淡**:方向相反,而且这是"机制上确定不同",不是观感。」 + * + * 他让我先把值读出来再定 —— 这里就是那次读数,做成判据(免得以后凭记忆选令牌)。 + */ + let colors; + try { + colors = sdkSystemColors(); + } catch (e) { + assert.fail(`读不到 SDK 的系统色表(这条判据无从判起):${e.message}`); + } + const maskRegular = colors['ohos_id_color_mask_regular']; + const maskDark = colors['ohos_id_color_mask_regular_dark']; + const bg = colors['ohos_id_color_background']; + const bgDark = colors['ohos_id_color_background_dark']; + assert.ok(maskRegular && maskDark && bg && bgDark, '系统色表里这几个令牌都要有值'); + + // 事实一:mask 在浅色主题下也是深色(#99182431)—— 所以它是模态遮罩语义 + assert.ok(luminanceOf(maskRegular) < 0.2, + `mask_regular 在浅色主题下应当是深色(实测 ${maskRegular})—— 若是浅色,这条判据的前提要重写`); + assert.ok(luminanceOf(maskDark) < 0.2, `mask 深色主题下也应当是深色(实测 ${maskDark})`); + + // 事实二:页面底色系随主题换向(浅色白、深色近黑)—— 这才是"朝底色淡化" + assert.ok(luminanceOf(bg) > 0.8, `ohos_id_color_background 浅色主题下应当是白(实测 ${bg})`); + assert.ok(luminanceOf(bgDark) < 0.2, `ohos_id_color_background_dark 应当近黑(实测 ${bgDark})`); + + // 事实三:WebUI 的遮罩方向是"朝底色淡化"(浅色白、深色黑)—— 与页面底色系同向、与 mask 反向 + const webCss = readFileSync(join(ROOT, 'client/electron/src/index.css'), 'utf8'); + const scrimLight = /--bg-scrim:\s*(\d+)\s+(\d+)\s+(\d+);/.exec(webCss); + assert.ok(scrimLight, 'CSS 里要有 --bg-scrim'); + assert.deepEqual([scrimLight[1], scrimLight[2], scrimLight[3]], ['255', '255', '255'], + 'WebUI 浅色下的遮罩是**白**(把图案洗淡);这是"必须用页面底色系"的依据'); + + // 结论落到代码:壁纸遮盖用页面底色系令牌,且**不是** mask + const theme = readFileSync(join(HARMONY_ETS, 'common/Theme.ets'), 'utf8'); + assert.match(theme, /static readonly wallpaperScrim: Resource = \$r\('sys\.color\.ohos_id_color_background'\)/, + '壁纸遮盖色要用页面底色系(ohos_id_color_background)'); + assert.match(theme, /static readonly overlay: Resource = \$r\('sys\.color\.ohos_id_color_mask_regular'\)/, + 'overlay(模态遮罩)保持 mask'); + const main = read('pages/MainPage.ets'); + const scrims = [...main.matchAll(/\.backgroundColor\(Theme\.(\w+)\)\s*\n\s*\.opacity\(this\.bgPlan\.scrim\)/g)]; + assert.equal(scrims.length, 2, '预设档与图片档各一层遮盖'); + for (const s of scrims) { + assert.equal(s[1], 'wallpaperScrim', + '壁纸遮盖层的颜色必须用 wallpaperScrim(页面底色系)。用 mask 会在浅色主题下把预设压暗,与 WebUI 反向'); + } + // 两个语义不许共用一个令牌(这个仓库撞过四次的那个模式) + assert.notEqual('wallpaperScrim', 'overlay'); + // 模态弹层仍然用 mask(那里的语义确实是"压暗背后") + const settings = read('pages/SettingsPage.ets'); + assert.match(settings, /Theme\.overlay/, '自绘弹层的遮罩仍然用 mask(那里的语义是压暗背后)'); + assert.ok(!/wallpaperScrim/.test(settings), '弹层不该用壁纸遮盖色(两个语义别混)'); +}); diff --git a/client/harmony/entry/src/main/ets/common/Theme.ets b/client/harmony/entry/src/main/ets/common/Theme.ets index c0ec6cc..9e4a214 100644 --- a/client/harmony/entry/src/main/ets/common/Theme.ets +++ b/client/harmony/entry/src/main/ets/common/Theme.ets @@ -62,6 +62,34 @@ export class Theme { */ static readonly overlay: Resource = $r('sys.color.ohos_id_color_mask_regular'); + /** + * 壁纸遮盖层的颜色 —— **不是** `overlay`(那是模态遮罩),两者语义不同。 + * + * pi 2026-09-14 提出的方向性疑问,我按他说的去 SDK 里把值读出来了 + * (`ets/build-tools/ets-loader/sysResource.js` 给 名字→id, + * `previewer/common/resources/entry/resources.txt` 给 id→值,两张表交叉验证): + * + * | 令牌 | 浅色主题 | 深色主题 | + * |---|---|---| + * | `ohos_id_color_mask_regular`(= `overlay`) | `#99182431` **深蓝灰** | `#b2000000` 黑 | + * | `ohos_id_color_background` | `#ffffffff` **白** | `#ff18181a` 近黑 | + * + * 也就是说 **`mask_*` 在两套主题下都是深色**(它的 light/regular/thick 是**浓度档**, + * 不是深浅两套值),用途是**模态遮罩**(弹层背后压暗)。 + * 而 WebUI 的 `--bg-scrim` 在 `:root` 是**白**、`.dark` 才是黑,注释原话是 + * 「浅色下用白把花哨的图案洗淡,深色下用黑压暗」—— 它做的是"**朝页面底色淡化**"。 + * + * 所以壁纸遮盖若用 `mask`:**浅色主题下会把预设压暗,而 WebUI 是把它洗淡 —— 方向相反**。 + * 这是"机制上确定不同",不是观感。改用**页面底色系**:那才是"朝底色淡化"的系统对应物, + * 浅色=白底方向、深色=深底方向,自动换向,与 WebUI 是同一个意图。 + * + * ⚠️ 为什么不干脆复用 `pageBg`(值一样是 `ohos_id_color_background`): + * 页面底在背景开启时会被换成语义上的**透明**(`bgActive ? Color.Transparent : Theme.pageBg`), + * 而遮盖层**永远需要一个真实颜色**。两个用途的生命周期不同,共用一个名字迟早坏一头 + * —— 这正是这个仓库撞过四次的模式(`text-white` vs `bg-white`、`--c-*` vs `--s-*`)。 + */ + static readonly wallpaperScrim: Resource = $r('sys.color.ohos_id_color_background'); + /** * 导航/浮层的材质档次。**不再有 `#B8FFFFFF` / `#B80F172A` 这种手写玻璃 alpha** —— * 那两个值等于"我们替系统猜了深色该怎么做",与"用系统方案"直接冲突; diff --git a/client/harmony/entry/src/main/ets/pages/MainPage.ets b/client/harmony/entry/src/main/ets/pages/MainPage.ets index 038aebe..b990447 100644 --- a/client/harmony/entry/src/main/ets/pages/MainPage.ets +++ b/client/harmony/entry/src/main/ets/pages/MainPage.ets @@ -1652,7 +1652,8 @@ struct MainPage { */ Column() .width('100%').height('100%') - .backgroundColor(Theme.overlay) + // 遮盖色 = **页面底色系**(朝底色淡化),不是模态遮罩 mask(浅色下那会压暗,方向相反) + .backgroundColor(Theme.wallpaperScrim) .opacity(this.bgPlan.scrim) } .width('100%').height('100%') @@ -1664,7 +1665,8 @@ struct MainPage { // 压暗用**系统遮罩色** + 服务端给的浓度:换向(浅色洗白/深色压黑)由系统负责 Column() .width('100%').height('100%') - .backgroundColor(Theme.overlay) + // 遮盖色 = **页面底色系**(朝底色淡化),不是模态遮罩 mask(浅色下那会压暗,方向相反) + .backgroundColor(Theme.wallpaperScrim) .opacity(this.bgPlan.scrim) } .width('100%').height('100%') diff --git a/docs/HARMONY-ALIGN-PLAN.md b/docs/HARMONY-ALIGN-PLAN.md index 7ef63de..f39537d 100644 --- a/docs/HARMONY-ALIGN-PLAN.md +++ b/docs/HARMONY-ALIGN-PLAN.md @@ -517,6 +517,7 @@ deb 也不必从 targets 里摘。已写进 `client/electron/BUILD.md`(含排 | 遮罩 | 自声明 `--bg-scrim` + `--bg-dim` 两段式 | 系统 `sys.color.ohos_id_color_mask_regular` | 遮罩要随主题换向(浅色洗白/深色压黑),这件事系统已经做了 | | **品牌色** | `--c-blue-600: 37 99 235` | `Theme.accent = '#2563EB'` | **不允许差异** —— 两个客户端是同一个产品 | | **本地外观缓存的键** | 全局常量 `agentmail.background` | **按账号**:`appearance.` | **鸿蒙是对的,不许为"对齐"退回全局键** —— 全局键的后果是切到服务端没有记录的账号时,`saved=false` 分支会把**上一个账号的外观** push 上去(新账号"继承"了外观,而且写进了服务端)。pi 2026-09-14 确认这条记在 WebUI 侧为开项(换键,或至少在把继承来的值当"本地的"推给从未有记录的账号前停一下) | +| **遮盖色的令牌** | `--bg-scrim`(浅色白 / 深色黑,"朝底色淡化") | `Theme.wallpaperScrim` = `sys.color.ohos_id_color_background`(同向);`Theme.overlay` = mask **只用于模态弹层** | **不允许混用** —— mask 两套主题下都是深色(浅色 `#99182431`),拿它当壁纸遮盖会在浅色主题下压暗(与 WebUI 反向)。两个语义两个令牌,理由与实测值见 §7.17b-2 | | **遮罩浓度的默认值** | 两套:store 的 `dim=24/blur=8` 与 `lib/appearance.ts` 的 `clamp(...,12,4)` | 12 / 4(只有一套) | 服务端**缺字段**时真正落地的是 `clamp` 的默认值,所以按 12/4 对齐;WebUI 那两套值迟早要统一(pi 记在他那边) | **关于 `overlayColor` / `overlayAlpha` 消失**(pi 要求把删除理由记在这里,否则下一个人会当成漏改补回来): @@ -826,6 +827,40 @@ SDK 锚点(免得后人凭记忆写):回调接口是 `@ohos.app.ability.En 踩过两个编译错:`Configuration.colorMode` 是**枚举 | undefined**,字段写成 `number` 直接报错; `ApplicationContext.on('environment')` 返回的是 **number 型 callbackId**(不是 void)。 +### 7.17b-2 遮盖色的**方向**:模态遮罩 ≠ 壁纸遮盖(pi 的方向性疑问,按实测改) + +pi 的疑问:「系统那三个 `mask_*` —— light/regular/thick 是**浓度档**(同一个色的三个 alpha), +不是深浅两套值;用途是**模态遮罩**(弹层背后压暗),所以两套主题下通常都是深色。 +如果壁纸遮盖用 mask 色,**浅色主题下会把预设压暗,而 WebUI 是把预设洗淡**:方向相反。」 + +他让我先把值读出来再定。读数方式(两张 SDK 表交叉验证,不靠记忆): +`ets/build-tools/ets-loader/sysResource.js` 给**名字→id**, +`previewer/common/resources/entry/resources.txt` 给 **id→值**: + +| 令牌 | 浅色主题 | 深色主题 | +|---|---|---| +| `ohos_id_color_mask_regular`(= 原 `Theme.overlay`) | `#99182431` **深蓝灰** | `#b2000000` 黑 | +| `ohos_id_color_mask_thick` / `_light` | `#cc182431` / `#66182431` 深 | 同为深 | +| `ohos_id_color_background` | `#ffffffff` **白** | `#ff18181a` 近黑 | + +⇒ **pi 是对的,而且这是"机制上确定不同"**:mask 在两套主题下都是深色(它是模态遮罩), +而 WebUI 的 `--bg-scrim` 浅色是**白**(把花哨的图案洗淡)、深色才是黑。 +壁纸遮盖用 mask = 浅色下把预设压暗,方向与 WebUI 相反。 + +**改法**:新增 `Theme.wallpaperScrim = $r('sys.color.ohos_id_color_background')` +(页面底色系:浅色白、深色近黑,自动换向,与"朝底色淡化"同一意图), +壁纸两档的遮盖层都改用它;`Theme.overlay`(mask)**只留给模态弹层**(那里的语义确实是压暗背后)。 + +**为什么不再复用 `pageBg`**(值一样是 `ohos_id_color_background`):页面底在背景开启时会被换成 +语义上的**透明**(`bgActive ? Color.Transparent : Theme.pageBg`),而遮盖层**永远要一个真实颜色**。 +两个用途生命周期不同,共用一个名字迟早坏一头 —— 就是这个仓库撞过四次的模式 +(`text-white` vs `bg-white`、`--c-*` vs `--s-*`、掩码 vs 遮罩)。 + +**判据**:从 SDK 两张表读出真值,断言「mask 两套主题都深」「页面底色系浅白深黑」 +「WebUI 的 `--bg-scrim` 浅色是白」,再落到代码(两处遮盖层都必须用 `wallpaperScrim`、 +弹层仍用 `overlay`)。变异:遮盖退回 mask → 红 1 条;只改一层 → 红 2 条。 +**这样"令牌选错方向"以后不能再靠记性避免。** + ### 7.17c 手写色清册升级成**跨文件按类扫**(pi 建议) 原来的 A2 只保护 `Theme.ets`;而 `Wallpaper.ts` 也有手写色(14 个预设色)。