背景
在桌面端应用中,我们为 SearchChat 设计了一种「紧凑模式」:
- 正常状态下窗口高度较大
- 当一段时间无操作后,窗口会自动收起到紧凑高度(例如 84px)
- 收起动作由
compactModeAutoCollapseDelay控制,比如 5 秒后触发
整体体验在大多数情况下是正常的,但在一次使用中发现了一个非常隐蔽却影响体验的问题。
问题现象
问题只会出现在一个特定时序下:
- 窗口处于紧凑模式的 "延迟收起倒计时" 中(例如还剩 2~3 秒)
- 用户通过快捷键 主动隐藏窗口
- 延迟计时器仍然在后台触发
- 计时结束后,执行了
setWindowSize收起逻辑 - 用户再次用快捷键唤起窗口
结果是:
窗口位置发生了漂移,不再出现在隐藏前的位置。
这个问题在某些平台或窗口管理器上尤为明显。
问题根因分析
拆开来看,核心原因其实并不复杂:
- 延迟收起逻辑是一个 纯前端的定时器
- 窗口被隐藏后,计时器并不会自动停止
- 计时器触发时,仍然会调用
setWindowSize - 某些平台在「窗口不可见」状态下修改窗口尺寸时,会重新计算窗口位置
- 这个重算过程不是我们可控的
因此,真正的问题不是"收起"本身,而是:
在窗口不可见时发生了尺寸变化,导致系统偷偷帮我们改了位置。
核心设计目标
我们希望做到一件事:
即使窗口在隐藏状态下被触发了尺寸变更,也要保证它在再次显示时,仍然回到隐藏前的位置。
并且要满足几个约束:
- 不侵入现有窗口尺寸策略
- 不依赖平台特性 hack
- 能正确处理高 DPI 场景
- 修改范围尽量小
解决思路(前端侧)
整体方案分为两步。
一、在窗口失焦 / 隐藏时,记录当前位置
当窗口即将被隐藏时,我们可以认为此刻的位置是"用户认可的位置"。
在 SearchChat 中:
- 通过
useTauriFocus的onBlur回调 - 调用
outerPosition()获取当前窗口位置 - 将结果保存到
windowPositionRef
关键点在于:
outerPosition()返回的是 physical position- 这个坐标不受 DPI / scale factor 影响
ts
const pos = await window.outerPosition()
windowPositionRef.current = { x: pos.x, y: pos.y }
代码位置:
bash
src/components/SearchChat/index.tsx:113-119
二、延迟收起触发时,如果窗口不可见,强制恢复位置
在自动收起的定时器中:
-
正常执行
setWindowSize -
紧接着判断窗口当前是否可见
-
如果窗口是隐藏状态,并且我们之前记录过位置:
- 主动把窗口位置设回去
伪代码逻辑如下:
javascript
await platformAdapter.setWindowSize(width, height)
if (!(await window.isVisible()) && windowPositionRef.current) {
const { x, y } = windowPositionRef.current
await platformAdapter.setWindowPhysicalPosition(x, y)
}
代码位置:
bash
src/components/SearchChat/index.tsx:158-179
这样即使系统在隐藏期间偷偷"动了手脚",也会被我们立刻纠正。
为什么要用 Physical Position
这里有一个非常容易踩坑的点:DPI 缩放。
outerPosition()返回的是 physical position- 项目中原有的
setWindowPosition(x, y)使用的是 logical position - 如果存的是 physical,却用 logical 去设,高 DPI 下会产生明显偏移
因此,我们补充了一个明确的 API:
setWindowPhysicalPosition
Tauri 实现
javascript
import { PhysicalPosition, getCurrentWebviewWindow } from '@tauri-apps/api/window'
const win = getCurrentWebviewWindow()
await win.setPosition(new PhysicalPosition(x, y))
代码位置:
bash
src/utils/tauriAdapter.ts:85-89
Web 实现(占位)
Web 模式下不需要真实移动窗口,只保留日志即可:
bash
src/utils/webAdapter.ts:88-90
最终效果
这个方案带来的收益非常明确:
- ✅ 修复隐藏期间自动收起导致的窗口位置漂移
- ✅ 正确处理高 DPI 场景,避免 logical / physical 混用
- ✅ 改动范围小,只在
SearchChat的定时收起路径兜底 - ✅ 不影响其他窗口尺寸或动画策略
手动验证步骤
建议按以下流程验证:
- 设置
compactModeAutoCollapseDelay = 5 - 打开窗口,确保满足进入紧凑模式的条件
- 在 5 秒倒计时期间,使用快捷键隐藏窗口
- 等待超过 5 秒
- 再次用快捷键唤起窗口
预期结果:
窗口应出现在隐藏前的位置,不应发生任何跳动或漂移。
小结
这个问题本质上不是 "窗口 API 用错了",而是 多个合理行为在特定时序下叠加,暴露出的系统边界问题。
解决它的关键,不是阻止自动收起,而是:
尊重用户最后一次看到的窗口状态,并在必要时为系统行为兜底。
这类问题在桌面端应用中非常常见,也非常容易被忽略,希望这次的整理能对你有所帮助。