-
Notifications
You must be signed in to change notification settings - Fork 151
Native UI Input
macOS 的 SwiftUI/AppKit/Metal/TextKit 和 Windows 的 React/WebView2/Monaco 都会把“点击、滚轮、焦点、布局”拆成多层事件链。很多看似随机的 UI bug,本质是不同层对同一事件拥有了不同的事实源。
#599 — 点击终端后文字仍写进编辑器
症状:点击 macOS 内置终端后,键盘焦点可能仍停留在编辑器;编辑器和终端可能同时显示/闪烁光标;窗口失焦时光标仍可见。
根因:Metal 渲染区域没有把鼠标命中交给终端;终端缓存的 focus 标记与 AppKit 窗口实际 first responder 可能不一致;SwiftTerm 1.15 的 Metal 闪烁计时器没有按焦点停止。
方案:
- 让 Metal surface 恢复键盘焦点,同时保留滚动条等子控件命中。
- 以 AppKit window 的实际键盘焦点作为终端是否聚焦的唯一依据。
- 编辑器失焦时取消 blink callback 并清除 caret;视图/窗口生命周期清理 observer。
- 在 Lithe adapter 包装 SwiftTerm 渲染:失焦时临时用透明、静止光标,绘制后恢复 shell 请求的颜色/样式;Core Graphics fallback 直接不画失焦光标。
可迁移经验:输入路由、视觉反馈和定时器不能分别维护“自己的 focus”;它们都应从同一个事实源派生。PR 报告用 7 个焦点测试和实际预览验证了终端输入、双向切换及窗口失焦。
#456 — 最后一行末尾 caret 画偏或点不到
根因:关闭系统 drawInsertionPoint 后,旧逻辑只有 location == length ? maxX : minX,不能表达换行符上的行尾、带尾换行文档的 extra line fragment、无尾换行文档末尾和 CRLF。
方案:抽出 EditorCaretGeometry,按 NSTextView 语义统一计算行尾、extra fragment 和 CRLF;有选区时不画 insertion point;补全/菜单的 caretAnchorRect 也复用同一几何。
#638 — 空行点击复用了前一行内容
根因:空行 caret calculation 继续使用前一行可见内容。
方案:计算在前一行 line ending 处停止,不拿前一行的内容作为当前空行几何。这个修复很小,但它展示了“空行是有位置语义的对象”,不能被当成缺省文本。
#112 — Windows Monaco 行尾点不到且整行拖选失效
根因:Git blame 被注入文本内容,遮住最后字符并打断 selection;CRLF 和错误的等宽宽度缓存使鼠标坐标与 Monaco 文本位置不一致。
方案:blame 移到独立 gutter;规范化 CRLF;关闭错误的 width cache,让点击、拖选和重新打开文件使用一致的文本几何。
共同经验:caret 位置、选区位置、装饰物位置和点击 hit-test 要共享字符编码/换行/宽度模型,而不是各自补一个偏移量。
#45 — Search Everywhere 无法下拉
根因:结果 ScrollView 被垂直 fixed-size constraint 限制,没有得到 bounded viewport,滚动容器无法正确接收滚轮。
方案:去掉固定高度约束,并在键盘上下移动时把 selected result scroll into view。布局约束和交互反馈同时修复,而不是只给列表加一个 scroll call。
根因:外层 AppDialog ScrollArea 在 capture 阶段劫持滚轮,内部又嵌套 overflow-y-auto;flex 子项缺少 min-h-0,动态内容短暂撑大外层 viewport,外层抢到事件却无法滚动。WebView2 中 overflow-hidden 还会创建独立滚动容器,进一步劫持事件。
方案:#193 先用 min-h-0 限制 flex 子项,属于降低复现概率的缓解;它明确把“消除双重滚动”或在 wheel handler 判断真正可滚动 target 列为后续根治。#201 使用 overflow-clip 避免生成滚动容器,并接入真实主题 registry 取代禁用占位下拉。
经验:一个组件树应明确唯一滚动 owner;修复滚轮时同时检查 CSS overflow、flex min-size、capture 阶段和 overlay 命中。#193 的“缓解而非根治”标注值得保留。
根因:Git row/file row 使用 overflow:hidden,hover 后 Chromium/WebView2 将 wheel 事件锁给不可滚动的文本节点;侧栏 resize handle 位于滚动内容上方时也会吞掉事件。
方案:将行内截断改为 overflow: clip,把 wheel delta 交给真正的 scroll container;resize handle 移出滚动条并转发事件。测试覆盖 hover 前后、停止再滚动、项目树/Git 列表和调整宽度。
#221 — React maximum update depth error #185
根因:shared scroll-area 的 viewport ref callback 每次 render 都是新函数,Base UI 反复执行 cleanup/rebind;ref 绑定本身又触发 state update,形成深度更新循环。
方案:稳定 ref callback identity,同时保留 forwarded callback ref 和 object ref。Windows Release smoke test 还确认没有 React #185、WebView send failure 或 LSP 初始化错误。
经验:DOM ref 是生命周期回调,不是普通 inline handler;任何会被组件库调用 cleanup/rebind 的 callback 都要优先稳定 identity。
#543 — 分隔条光标闪烁/消失,拖动方向不一致
根因:光标视图的 hitTest 固定返回 nil,实际命中的是外层 NSHostingView;从编辑器侧进入分隔条时光标视图被覆盖。此前测试只直接调用 cursor handler,没有覆盖真实 NSWindow + NSHostingView + native editor 的命中链。
方案:一个原生 AppKit view 统一接管 hit-test、hover 和 drag;扩大居中命中范围,使用 contentShape/zIndex 保证句柄优先;继续复用 LitheSplitPaneView 和全局坐标拖动调度器,拖动结束后再持久化。
验证经验:临时恢复 hitTest == nil 后两项真实命中测试按预期失败,说明测试锁住的是事件链而不是孤立函数。
#568 — Git 提交文件树窄面板文字重叠
根因:仓库路径、文件数和文件名被放在相互独立的固定绘制区域,窄面板时区域交叉;内容变短后旧 horizontal scroll range 还可能残留。
方案:把名称、文件数和仓库路径按顺序放入一个文本布局,保留完整内容宽度,超出面板用横向滚动;内容/外观变化时才重算尺寸,内容变短时修正滚动范围,hover 使用 AppKit 原生展开提示而非插入省略号。
#469 — 禁用 divider 后仍出现蓝色 hover/drag 高亮
根因:showsIdleDivider 只控制 idle divider,却没有限制 hover/click/drag 状态的绘制;句柄的 hit area 又因视图层级被截断。
方案:禁用时所有交互状态都不绘制;用 contentShape 和 zIndex 恢复完整横/竖句柄命中,保留通用 split pane 的默认线、约束、hover 和持久化。
#368 — WorkbenchView 冲突解决覆盖掉 macOS UI
根因:合并 #359 的 Java Debug 时,WorkbenchView.swift 的冲突解决结果采用了旧实现,项目/分支切换浮层、popover 箭头和 Git Push 对话框整体回退。
方案:保留 Run/Debug 工具栏,同时恢复原有 overlay preference/定位逻辑;增加 rendering safety regression,防止以后只看“能编译”而忽略视觉契约。
#412 的症状是新版通知、编辑器背景和无分隔线样式被旧 CSS/SwiftUI style priority 覆盖;解决方案是清理冲突旧样式、统一布局与点击区域,并用 rendering tests 固定结果。
#96 的症状是深色主题下 Maven/build/run output 对比度丢失;根因是 ANSI/语义颜色没有按 AppKit NSTextView 的原生 text attributes 传递,方案是按当前 theme 解析 native attributes,并在 appearance change 时重绘。
#584 — 点击“新建终端”没有反应
根因:按钮直接绑定了带可选 profileId 参数的创建函数。React 点击事件会把 MouseEvent 作为第一个参数传入,事件对象随后被当成 profile ID,调用 profileId.startsWith("shell:") 时抛出异常;快捷键没有鼠标事件,所以只坏点击路径。
方案:把“默认新建”和“按 profile 新建”拆成两个签名明确的回调,按钮不再把事件对象传给业务参数;用测试显式模拟额外 click event。这个 bug 的本质不是终端逻辑,而是 UI callback contract 不清晰。
#567 — 欢迎页“克隆”打开成了最近项目搜索
根因:欢迎页复用了“选择最近项目”的打开入口,而不是 clone form;首次安装没有匹配的项目时,粘贴 URL 当然无法进入克隆流程。打开模式又分散在多个 UI 状态中。
方案:克隆按钮直接进入现有仓库克隆表单;欢迎页和标题栏共享同一个 open-mode store,关闭弹窗时重置模式。回归测试覆盖无最近项目、关闭和后续切换模式,并隔离最近项目/DOM 状态。
经验:按钮的事件类型和业务意图都应在类型/API 层表达;入口复用要复用“状态机”,不能只复用一个看似相近的函数。
- 当前焦点的唯一事实源是什么?是缓存 bool、first responder、窗口 key 状态,还是 DOM active element?
- hit-test 是否覆盖真实的 view hierarchy、overlay、scrollbar、editor surface,而不只是直接调用 handler?
- caret/selection/gutter/decorations 是否共享 UTF-16、CRLF、extra line fragment 和动态字体几何?
- 滚动树是否有唯一 owner?
overflow-hidden、overflow-clip、min-h-0和 capture handler 的组合是否改变了事件路径? - ref/callback/闭包 identity 是否稳定?cleanup/rebind 会不会反向写状态?
- disabled 状态是否同时约束 idle、hover、pressed、dragged 的视觉与命中?
- 分隔条是否复用
LitheSplitPaneView/SplitHandleView、使用稳定坐标、明确 min/max,并避免高频整页重建?