Files
MailUI4Agents/client
jianf 1094154f26 feat(ele): 自绘窗口标题栏 —— 对齐 HarmonyOS 的 PC/2in1 窗口外观
## 问题

用户:「现在窗口外观还是 ele 默认外观,很原始」。

查证了两端在 PC 上的实际做法,差距是**结构性的**:

| | HarmonyOS (PC/2in1) | Electron(改之前)|
|---|---|---|
| 窗口装饰 | `setWindowDecorVisible(false)` 隐掉系统标题栏 | 系统默认标题栏 |
| 顶栏 | 自绘 `AppHeader`(圆角/材质/标题/可选返回键) | 无 |
| 避让 | `Insets` 三量:`statusBar`/`navIndicator`/`windowDecor` | 无这个概念 |
| 拖动 | 系统 | 系统 |

⇒ `grep app-region|titleBarStyle` 在整个 `client/electron/` 命中 **0**。
鸿蒙那边早就走完「内容铺满 + 自绘顶栏」,Electron 还停在最原始的系统窗口。

## 修法

* `frame: false` + `titleBarStyle: 'hidden'` —— 两个**一起**。
  只给 `titleBarStyle` 在 Win/Linux 上仍留着系统边框(可拖动、可双击),
  观感还是「系统窗口 + 一条自己画的头」;既然自绘窗口按钮 = 框架整个接管,
  那圈系统边框就是多余的一层。
  ★ 不用 `titleBarOverlay`(官方「留系统按钮」那条):它留的是**系统**按钮,
  观感仍由系统决定,与「自绘」目标相反,且 Linux 支持不齐。
  本项目只有 win/linux target(无 mac)⇒ 统一一条路,不做两套形态。

* 新增 `src/components/TitleBar.tsx`:固定顶栏 + 左标题 + 右三键。
  ★ 挂点选在 `main.tsx`、与 `.app-backdrop` 同层,**不在 App 里面** ——
    App 的根节点有四个 return 分支(宽屏/窄屏/独页/…),
    塞进去就得改四处,漏一处就是「某个页面没有标题栏」。
    它自己是 `position: fixed`,与 App 布局零耦合。

## 三条实现纪律(都写进注释了)

① **拖动靠 CSS `-webkit-app-region: drag`,不靠 JS 鼠标事件。**
   `drag` 区域由浏览器/系统处理,不受页面重排影响(sandbox 下那种
   mousemove 算窗口位置的写法既慢又脆)。
   ★ 代价:**drag 区域里的交互元素收不到点击** ⇒ 按钮与标题文字都显式
   `no-drag`。实测 `elementFromPoint` 命中 `BUTTON.titlebar-btn`(不是拖拽层)。

② **最大化状态是「订阅」来的,不是「查」来的。**
   最大化有三条**不经过按钮**的路径:双击拖拽区(系统处理,JS 收不到事件)、
   `Win+↑↓`、拖到屏幕边缘的 Snap Layouts ⇒ 只在点按钮时查一次,
   图标必然与真实状态脱节。所以主进程用 `pushMaxState` 主动推
   (`maximize`/`unmaximize`/进退全屏四个事件),渲染层只订阅。
   另:`toggleMaximize` 刻意**不**拆成 maximize/unmaximize ——
   双击时序上会多一次异步往返,IPC 往返期间用户可能又双击了一次。
   ⇒ 读状态与决定动作在主进程侧原子完成。

③ **浏览器里整条不渲染。**
   没有 bridge 时 `TitleBar` 返回 `null`;高度占位(`html.titlebar-on`)
   由 `main.tsx` 用**同一个** `__AGENTMAIL_SHELL__` 判据挂上 ——
   CSS 不会看 bridge,不挂则网页端白丢 36px。

## 尺寸为什么是 36px

鸿蒙 PC/2in1 实测 `windowDecor=37`(见 `MainPage.ets` 的 insets 日志
statusBar=38.6 navIndicator=27.8 windowDecor=37)⇒ 两端窗口控件高度对齐同一量级,
免得并排摆两个应用时一个头厚一个头薄。这里取 36:桌面端按物理像素算,
1x 下更接近常见做法,且 12px 字号不出血。**要改就两端一起改。**

## 顺带记一条踩过的坑(它就在这批代码里)

`preload.cjs` 是 `.cjs`,**不能写 TS 类型标注**。第一版写了
`(cb: (maximized: boolean) => void)` ⇒ 整个 preload **静默**加载失败 ⇒
`window.agentmail === undefined` ⇒ 账号读不到(回退到网页版登录页,
显示用户名+密码,桌面壳里注定失败)+ 标题栏不渲染。
★ 而 `npm run typecheck`(`tsc --noEmit`)**不检查 .cjs**,照常全绿。
已在 preload 注释里写明,并在 `main-process-security.test.mjs` 加了加载闸门
(下一个提交)。

## 实测

`DISPLAY` 起真窗口 + CDP 取证:
  shell=desktop  hasBridge=true  hasWin=true  titlebar=true  h=36  appRegion=drag
  btns=[最小化, 还原, 关闭]  btnHitTarget=BUTTON.titlebar-btn
  errs=[](渲染层零异常)
标题栏实测截图含深浅两态,面板圆角与阴影不变。

## 未验(本机无 GUI 交互,只能取证不能点)

最大化/还原按钮点击、双击标题栏、**关闭进托盘**(这个最需要小心,
点错会把应用整个退出)。上一条留待有人手上有真桌面时验。
2026-10-04 11:07:56 +08:00
..