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 用错了",而是 多个合理行为在特定时序下叠加,暴露出的系统边界问题

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

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

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

相关推荐
小肥宅仙女2 小时前
限流方案
前端·后端
雲墨款哥2 小时前
从一行好奇的代码说起:Vue怎么没有React的props.children
前端·vue.js·react.js
孜孜不倦不忘初心2 小时前
Axios 常用配置及使用
前端·axios
sTone873753 小时前
vscode 二开踩坑记录
前端
用户8168694747253 小时前
Effect 执行时机与事件循环交错关系
前端·react.js
心在飞扬3 小时前
langchain学习总结-OutputParser组件及使用技巧
前端·后端
llq_3503 小时前
Ant Design v5 样式兼容性问题与解决方案
前端
triumph_passion3 小时前
React Hook Form 状态下沉最佳实践
前端·react.js
心在飞扬3 小时前
langchain学习总结-两个Runnable核心类的讲解与使用
前端·后端