diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-07-21 23:13:39 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-07-21 23:13:39 +0800 |
| commit | d8a403164ad67475ac082c58d11cb8d64318efa2 (patch) | |
| tree | 5844d260ae5526cdfb99f6a8f0e1c06597cb9038 /ANSI-DRAW.md | |
| parent | c84c5463f7c2c2a6bd403de121f0093e4b6063fb (diff) | |
docs(ANSI-DRAW): rewrite rendering section to clarify wide-char
skip_until mechanism
Diffstat (limited to 'ANSI-DRAW.md')
| -rw-r--r-- | ANSI-DRAW.md | 86 |
1 files changed, 71 insertions, 15 deletions
diff --git a/ANSI-DRAW.md b/ANSI-DRAW.md index 0d56f48..3ef7106 100644 --- a/ANSI-DRAW.md +++ b/ANSI-DRAW.md @@ -210,16 +210,36 @@ cargo clippy -- -D warnings 占据终端高度减去最底部一行的所有行。从左到右填满终端宽度。 -对于画布区域中的每一个终端单元格,计算其对应的画布坐标: +渲染过程不是简单的「每个格子对应一个终端列」,而是使用 `skip_until` 机制处理宽字符的视觉消费: -- 画布 x = 相机 x + 终端列号 -- 画布 y = 相机 y + 终端行号 +1. 从行首开始,维护两个游标:`grid_x`(格子索引)和 `col`(终端列索引) +2. 同时维护 `skip_until` 值,初始与 `grid_x` 相同 +3. 在每一轮: + - 如果 `grid_x < skip_until`,该格子被前一个宽字符视觉消费,跳过它:`grid_x += 1`,`col += 1` + - 但如果光标恰好落在这个被消费的格子上,则在当前位置绘制一个光标块(不读取格子数据) +4. 否则读取格子 `canvas[grid_x][row_y]`,获取其字符和宽度 W +5. 如果 W > 1,设置 `skip_until = grid_x + W`(后续 W-1 个格子将被跳过) +6. 将字符绘制到从 `col` 开始的 W 列中 +7. `col += W`,`grid_x += 1`,回到步骤 3 -然后读取画布上该坐标的格子。如果该坐标在画布中不存在,使用默认格子(空格、白字黑底)。用该格子的字符和颜色绘制到终端。 +#### 宽字符不需要清空后续格子 -对于选择模式:如果 `visual_anchor` 有值,计算以锚点和当前光标为对角的矩形范围。在该范围内的格子,渲染时应用视觉反转效果(前景色与背景色交换)。 +注意:当一个宽字符写入格子 N 时,格子 N+1 到 N+W-1 的数据保持不动。视觉遮挡由渲染器的 `skip_until` 机制处理。不要尝试清空这些格子,否则会: -如果光标位于当前视口内(即光标坐标在相机坐标到相机坐标 + 终端尺寸范围内),在光标位置绘制一个可见的光标符号。光标的视觉样式为一个半透明或高亮方块(在字符背景上叠加一个半透明色块,或在字符下方绘制下划线——选择一个在你的 TUI 库中可行的方案)。注意:光标渲染不修改画布数据。 +- 在被消费位置产生可见的空格 +- 破坏格子数据,光标离开后再回来时字符已丢失 + +#### 选择模式 + +如果 `visual_anchor` 有值,计算以锚点和当前光标为对角的矩形范围。在该范围内的格子,渲染时应用视觉反转效果(前景色与背景色交换)。跳过机制优先于选择高亮:被跳过的格子不参与选择高亮渲染。 + +#### 光标渲染 + +如果光标坐标在视口范围内,在光标位置绘制光标符号。光标样式为高亮方块(在背景色上叠加亮色)。 + +- 如果光标落在被宽字符消费的格子上(`grid_x < skip_until`),在该列绘制一个光标块,不读取格子数据 +- 如果光标落在正常格子上,在字符上叠加光标样式 +- 光标渲染不修改画布数据 ### 状态栏 @@ -420,16 +440,52 @@ cargo clippy -- -D warnings 所有涉及光标水平移动、字符插入/删除、渲染对齐的场景,必须使用 `unicode-width` 库的宽度函数计算字符实际占用的终端列数,而不是使用 `char` 的默认长度方法。 -具体规则如下: +### 格子坐标与终端列的映射 + +画布使用**格子索引**坐标系统:相邻格子坐标差值为 1,不论格子中字符的显示宽度。Unicode 宽度只在以下三个场景中影响行为: + +1. **渲染**:将格子映射到终端列时,宽度 W 的字符占据 W 列 +2. **光标移动**:移动步长以格子索引为单位,但渲染时光标位置根据字符宽度映射到终端列 +3. **选区计算**:选区边界基于格子索引,不受字符宽度影响 + +### 渲染器对宽字符的处理 + +这是整个程序中最容易踩坑的地方,必须精确理解: + +- 当一个格子中的字符宽度 W > 1 时(例如中文字符宽度为 2),该字符在终端中占据 W 列 +- 渲染器用 `skip_until` 机制追踪当前位置之后哪些格子被宽字符「视觉消费」了: + - 格子 N 的字符宽度为 W → 格子 N+1 到 N+W-1 在渲染时**跳过**(不读取、不绘制),只推进列计数 + - 跳过的格子仍然存在于画布数据中,只是不在这一帧绘制 +- 这就是视觉上「宽字符吃掉后面格子」的效果。数据不动,渲染层负责 + +#### 为什么不能清空被消费的格子数据 + +早期版本尝试在输入宽字符时手动清空后续 `width-1` 个格子的数据(设为默认空格)。这会产生两个问题: + +1. 空格在终端上可见(尤其是前景色非黑时),造成 `你_[3]456` 中的多余空格 +2. 清空数据是破坏性的:光标移走后再回来,被清空的格子丢失了原有的字符 + +正确的做法:**格子数据只记录字符本身,视觉重叠由渲染器处理**。 + +#### 光标落在被消费的格子上 + +如果用户移动光标到被宽字符消费的格子(例如宽字符在 N,光标在 N+1),渲染器应在该位置绘制一个光标块(半透明方块),因为该位置没有属于自己的字符可供高亮。光标块占据了被消费位置的终端列。 + +### 三种模式的 Backspace 与宽度 + +| 模式 | Backspace 操作 | 左移量 | 说明 | +| ---- | ---------------------------------- | ------ | ---------------------------------------------------------------------- | +| 替换 | 光标处格子 → 默认格子 | 1 格 | 当前格子变空格,光标左移 | +| 输入 | 光标**左邻**格子 → 默认格子 | 1 格 | 操作格子坐标,不关心字符宽度。宽字符在视觉上占据的中间格子逐个退格清除 | +| 插入 | 删除光标左邻格子,右方格子左移填补 | 1 格 | 内容左移,不产生空格 | + +注意:输入模式的退格在格子坐标层面只处理 1 格(相邻格子),不按字符宽度批量处理。因为宽字符被渲染器跳过,格子数据层面的操作是统一的 1 格。 + +### 输入模式下的光标右移量 -- 光标水平移动(MoveLeft / MoveRight):移动的步长 = 当前字符的显示宽度。例如,在宽度为 2 的汉字上按右箭头,光标跳 2 列而非 1 列 -- 在输入模式/插入模式下输入字符:输入完成后光标右移的距离 = 该字符的显示宽度 -- Backspace(替换模式):将光标处的字符替换为空格后,光标左移的距离 = 被替换字符的显示宽度 -- Backspace(输入模式):将光标左侧的字符替换为空格,左移的距离 = 该字符的显示宽度(清除 width 格,左移 width 格) -- BackspaceSquash(插入模式):删除光标左侧的字符后,光标左移的距离 = 被删除字符的显示宽度 -- 渲染画布时:如果一个字符的宽度为 2,它在终端中占据 2 列。渲染完该字符后,渲染位置跳过 1 列(即同一行的下一个画布坐标,而非终端下一列),以保证后续字符对齐 -- 在画布坐标计算中,所有坐标使用「格子索引」而非「终端列数」。即两个相邻格子坐标的差值是 1(不论格子中的字符宽度是多少)。Unicode 宽度只在渲染和光标移动时用于计算终端列数偏移量 -- 选区计算:矩形选区的边界基于格子索引,不受字符宽度影响 +替换模式:光标不移动。 +输入模式:光标右移 `W` 格(W = 输入字符的显示宽度)。例如输入中文(宽度 2)时光标跳 2 格。 +插入模式:光标右移 1 格(字符插入在光标位置,后续内容已经右移)。 ### 中日韩文字与 IME 输入 |
