Tauri (24)——窗口在隐藏期间自动收起导致了位置漂移

背景

在桌面端应用中,我们为 SearchChat 设计了一种「紧凑模式」:

  • 正常状态下窗口高度较大
  • 当一段时间无操作后,窗口会自动收起到紧凑高度(例如 84px)
  • 收起动作由 compactModeAutoCollapseDelay 控制,比如 5 秒后触发

整体体验在大多数情况下是正常的,但在一次使用中发现了一个非常隐蔽却影响体验的问题


问题现象

问题只会出现在一个特定时序下:

  1. 窗口处于紧凑模式的 "延迟收起倒计时" 中(例如还剩 2~3 秒)
  2. 用户通过快捷键 主动隐藏窗口
  3. 延迟计时器仍然在后台触发
  4. 计时结束后,执行了 setWindowSize 收起逻辑
  5. 用户再次用快捷键唤起窗口

结果是:

窗口位置发生了漂移,不再出现在隐藏前的位置。

这个问题在某些平台或窗口管理器上尤为明显。


问题根因分析

拆开来看,核心原因其实并不复杂:

  • 延迟收起逻辑是一个 纯前端的定时器
  • 窗口被隐藏后,计时器并不会自动停止
  • 计时器触发时,仍然会调用 setWindowSize
  • 某些平台在「窗口不可见」状态下修改窗口尺寸时,会重新计算窗口位置
  • 这个重算过程不是我们可控的

因此,真正的问题不是"收起"本身,而是:

在窗口不可见时发生了尺寸变化,导致系统偷偷帮我们改了位置。


核心设计目标

我们希望做到一件事:

即使窗口在隐藏状态下被触发了尺寸变更,也要保证它在再次显示时,仍然回到隐藏前的位置。

并且要满足几个约束:

  • 不侵入现有窗口尺寸策略
  • 不依赖平台特性 hack
  • 能正确处理高 DPI 场景
  • 修改范围尽量小

解决思路(前端侧)

整体方案分为两步。

一、在窗口失焦 / 隐藏时,记录当前位置

当窗口即将被隐藏时,我们可以认为此刻的位置是"用户认可的位置"。

SearchChat 中:

  • 通过 useTauriFocusonBlur 回调
  • 调用 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

二、延迟收起触发时,如果窗口不可见,强制恢复位置

在自动收起的定时器中:

  1. 正常执行 setWindowSize

  2. 紧接着判断窗口当前是否可见

  3. 如果窗口是隐藏状态,并且我们之前记录过位置:

    • 主动把窗口位置设回去

伪代码逻辑如下:

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 的定时收起路径兜底
  • ✅ 不影响其他窗口尺寸或动画策略

手动验证步骤

建议按以下流程验证:

  1. 设置 compactModeAutoCollapseDelay = 5
  2. 打开窗口,确保满足进入紧凑模式的条件
  3. 在 5 秒倒计时期间,使用快捷键隐藏窗口
  4. 等待超过 5 秒
  5. 再次用快捷键唤起窗口

预期结果:

窗口应出现在隐藏前的位置,不应发生任何跳动或漂移。


小结

这个问题本质上不是 "窗口 API 用错了",而是 多个合理行为在特定时序下叠加,暴露出的系统边界问题

解决它的关键,不是阻止自动收起,而是:

尊重用户最后一次看到的窗口状态,并在必要时为系统行为兜底。

这类问题在桌面端应用中非常常见,也非常容易被忽略,希望这次的整理能对你有所帮助。

相关推荐
. . . . .11 分钟前
shadcn组件库
前端
2501_9447114319 分钟前
JS 对象遍历全解析
开发语言·前端·javascript
发现一只大呆瓜1 小时前
虚拟列表:支持“向上加载”的历史消息(Vue 3 & React 双版本)
前端·javascript·面试
css趣多多1 小时前
ctx 上下文对象控制新增 / 编辑表单显示隐藏的逻辑
前端
lbb 小魔仙1 小时前
【HarmonyOS实战】React Native 表单实战:自定义 useReactHookForm 高性能验证
javascript·react native·react.js
_codemonster1 小时前
Vue的三种使用方式对比
前端·javascript·vue.js
寻找奶酪的mouse1 小时前
30岁技术人对职业和生活的思考
前端·后端·年终总结
梦想很大很大1 小时前
使用 Go + Gin + Fx 构建工程化后端服务模板(gin-app 实践)
前端·后端·go
We་ct1 小时前
LeetCode 56. 合并区间:区间重叠问题的核心解法与代码解析
前端·算法·leetcode·typescript
张3蜂2 小时前
深入理解 Python 的 frozenset:为什么要有“不可变集合”?
前端·python·spring