feat(terminal): mouse reporting (P2)

Implement xterm-compatible mouse reporting so vim, htop, btm, tmux, etc.
can receive click / drag / wheel events.

Core:
- Expose `mouse_protocol_mode()` and `mouse_protocol_encoding()` on
  `TerminalCore`, re-exporting the vt100 enums.

Component:
- Add `MouseAction`, `mouse_button_base`, `with_mouse_modifiers`,
  `encode_mouse_report`, and `pixel_to_grid` pure helpers.
- Support SGR (1006), Default, and UTF-8 encodings.  SGR is fully correct
  and ASCII-only; Default/UTF-8 are correct for the common ≤94 col/row
  range.
- Wire `on:mousedown` / `on:mouseup` / `on:mousemove` to report mouse
  events when the app enabled mouse reporting; otherwise fall back to
  local text selection.
- Shift+click bypasses mouse reporting and forces local selection (xterm
  behaviour).
- Throttle motion reports to one per grid cell.
- Report vertical/horizontal wheel ticks as buttons 64/65/66/67 when
  mouse reporting is active, otherwise keep the existing alt-screen
  arrow-key paging.

Tests: 23/23 pass, clippy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
zhangheng
2026-06-18 14:57:36 +08:00
parent c3ddd9df4f
commit b9cc578d28
3 changed files with 461 additions and 16 deletions

View File

@@ -31,8 +31,9 @@
| 基础按键Ctrl-C/D/L、方向键、Enter/Tab/Esc/Backspace | `component.rs::map_key_to_terminal_input` |
| 完整键表F1F12/Home/End/PageUp/Down/Ctrl AZ/Alt-key | `component.rs::key_to_bytes` |
| application-cursor 模式DECCKM方向键切 SS3 | `component.rs::cursor_seq` + `TerminalCore::application_cursor` |
| 粘贴Ctrl-V / 浏览器菜单,含 bracketed paste | `component.rs::handle_paste` / `prepare_paste` |
| 文本选区 + 复制自建模型Ctrl-Shift-C跨滚动 | `component.rs` 鼠标处理 / `core.rs::selection_text` |
| 粘贴Ctrl-V / Ctrl-Shift-V / 右键菜单,含 bracketed paste | `component.rs::handle_paste` / `prepare_paste` |
| 文本选区 + 复制自建模型Ctrl-Shift-C / 选区存在时 Ctrl+C,跨滚动) | `component.rs` 鼠标处理 / `core.rs::selection_text` |
| 鼠标上报vim/htop/tmux 鼠标SGR/Default/UTF-8 编码,滚轮) | `component.rs` 鼠标处理 / `encode_mouse_report` |
---
@@ -83,21 +84,28 @@
---
### P2 — 鼠标上报mouse reporting
### P2 — 鼠标上报mouse reporting ✅ 已完成
**现状**没有。点击 / 拖拽位置不会发给应用vim 鼠标、tmux 选区、btm 点击都用不了
**现状**已实现并验证。`vim`/`htop`/`btm`/`tmux` 的鼠标交互、tmux 选区、vim 滚轮均可以工作
**计划实现**
1. vt100 已解析鼠标模式:`screen().mouse_protocol_mode()`None/Press/PressRelease/ButtonMotion/AnyMotion`mouse_protocol_encoding()`Default/Utf8/Sgr
2. `terminal-screen` 上监听 `on:mousedown` / `on:mouseup` / `on:mousemove`
- 把像素坐标换算成终端 **行列**(用已测量的 cell 宽高 + 容器 rect
- 按当前 encoding 编码:
- **SGR**(最常用):`ESC[<b;col;row;M`(按下)/ `m`(释放)。
- Default`ESC[M` + 三字节
- 只在 `mouse_protocol_mode != None` 时拦截,否则放行(不破坏选区/原生行为)。
3. 与 P0 选区冲突处理:应用开启鼠标上报时,鼠标事件优先发给应用;否则用于本地选区
**实现**
1. **vt100 mouse-mode accessors**`core.rs::mouse_protocol_mode` / `mouse_protocol_encoding`)—— 暴露 `None/Press/PressRelease/ButtonMotion/AnyMotion` `Default/Utf8/Sgr`
2. **事件监听与坐标换算**`component.rs`
- `on:mousedown` / `on:mouseup` / `on:mousemove` 复用现有的 DOM 事件
- `pixel_to_grid``(clientX, clientY)` 换算成 0-based 可见网格 `(row, col)`(注意:与选区用的 `point_to_cell` 不同,这里**不加 scroll_top**,因为鼠标报告坐标是相对于应用正在绘制的当前屏幕,而非 scrollback 缓冲区)。
3. **编码**`encode_mouse_report` 纯函数)
- **SGR1006**`ESC[<b;col;row;M`(按下/移动)/ `ESC[<b;col;row;m`释放。ASCII-only任意屏幕尺寸都正确
- **Default / UTF-8**`ESC[M` + 三字节(各 +32。坐标 ≤94 时完全正确并 ASCII-safe更大坐标被钳位到 94大多数终端 likewise现代应用都走 SGR)。
- **滚轮**:垂直/水平滚轮映射为按钮 64/65/66/67 的 `Press` 事件
4. **模式仲裁**
- 应用开启 mouse reporting`mode != None`)时,鼠标事件优先发给应用;否则走本地选区。
- 按住 **Shift** 点击可绕过鼠标上报、强制本地选区xterm 行为)。
- 鼠标移动按网格单元去重,避免每像素都发报告。
5. **测试**`encode_mouse_report``pixel_to_grid``with_mouse_modifiers``mouse_button_base` 均有单测23 个 terminal 测试全过clippy 干净。
**风险**:中。坐标换算边界 + 与选区的优先级仲裁需要仔细测。
**已知限制**
- 主屏幕(非 alt-screen滚动后鼠标报告坐标仍相对于可见网格顶部因为开启鼠标上报的应用通常运行在 alt-screen这覆盖主要使用场景。
- 与浏览器 `Ctrl+W/T/N` 类似,浏览器可能保留某些鼠标手势(如右键菜单、拖拽文本),这些在网页终端内无法 100% 拦截。应用内鼠标操作应正常。
---