docs: explain developer tool painting and source selection
This commit is contained in:
@@ -11,14 +11,17 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
#[derive(Debug, Default)]
|
||||
/// 行文本、锚点与选择端点,既供鼠标拖选也供键盘导航使用。
|
||||
pub(super) struct SourceSelection {
|
||||
// Painter::lines 同步去行号的完整显示文本;只读 Sources 选择与 Console 的可编辑 input 分开维护。
|
||||
lines: Vec<String>,
|
||||
// 端点是 (行号, UTF-8 字节偏移),字节位置始终由完整字素边界产生,不用字符数直接切片。
|
||||
anchor: (usize, usize),
|
||||
end: (usize, usize),
|
||||
// DeveloperToolsState 的按下/移动/释放路径使用此标记,决定指针移动是否扩展选区并触发行滚动。
|
||||
pub dragging: bool,
|
||||
}
|
||||
impl SourceSelection {
|
||||
/// 同步展示文本;内容变化时重置选择和拖动,内容完全相同时保留原选区。
|
||||
// Painter 在绘制 Source Region 时调用,传完整内容而非仅可见行,让滚出视口的选择仍可复制。
|
||||
pub fn set_lines(&mut self, lines: &[String]) {
|
||||
// 文本变化时重置两端与拖选状态;当前实现不是把旧选区钳到新文本,避免对应内容已经改变。
|
||||
if self.lines != lines {
|
||||
@@ -29,19 +32,24 @@ impl SourceSelection {
|
||||
}
|
||||
}
|
||||
/// 把工具指针位置换算成合法行号与文本字符边界。
|
||||
// begin/extend 共用坐标换算;origin 已包含当前滚动量和去行号偏移,不在此再次扣除滚动。
|
||||
fn locate(p: Point, origin: Point, lines: &[String]) -> (usize, usize) {
|
||||
// 按固定行高求行号,向上下界钳制;空文本通过 saturating_sub 与后续 get 安全返回原点。
|
||||
let row = (((p.y - origin.y) / super::ROW).floor().max(0.0) as usize)
|
||||
.min(lines.len().saturating_sub(1));
|
||||
// 空 lines 没有可切片文本,返回 (0,0) 与默认选择一致,避免继续访问无效行。
|
||||
let Some(line) = lines.get(row) else {
|
||||
return (0, 0);
|
||||
};
|
||||
// 横向坐标相对正文原点并钳到非负,行号区或左侧留白点击按行首处理。
|
||||
let x = (p.x - origin.x).max(0.0);
|
||||
// 先假定落在行尾,扫描到第一个位于点击右侧的字素中点时,改取该字素起点。
|
||||
let mut byte = line.len();
|
||||
// 逐个字素测量左右边界,以中点决定插入位置,点击超过行尾则保留 byte=line.len()。
|
||||
for (i, g) in line.grapheme_indices(true) {
|
||||
let left = text::measure(&line[..i], TextStyle::code()).width;
|
||||
let right = text::measure(&line[..i + g.len()], TextStyle::code()).width;
|
||||
// 严格小于中点时选字素前边界;在中点或之后继续扫描,相当于倾向该字素右边界。
|
||||
if x < (left + right) * 0.5 {
|
||||
byte = i;
|
||||
break;
|
||||
@@ -50,6 +58,7 @@ impl SourceSelection {
|
||||
(row, byte)
|
||||
}
|
||||
/// 在指针位置建立文本选择锚点,开始一次拖动选择。
|
||||
// DeveloperToolsState 在源码区按下时调用,折叠到新锚点并开启拖选;这里不修改文本行内容。
|
||||
pub fn begin(&mut self, p: Point, origin: Point, lines: &[String]) {
|
||||
// 调用方提供的 lines 与当前展示一致;开始拖动先折叠旧选择,后续 extend 只移动活动端。
|
||||
self.anchor = Self::locate(p, origin, lines);
|
||||
@@ -57,10 +66,12 @@ impl SourceSelection {
|
||||
self.dragging = true;
|
||||
}
|
||||
/// 保持原锚点,随指针更新选择另一端。
|
||||
// 源码拖选的 pointer_moved 调用;锚点固定,端点按当前显示快照重新定位,越界行由 locate 钳制。
|
||||
pub fn extend(&mut self, p: Point, origin: Point, lines: &[String]) {
|
||||
self.end = Self::locate(p, origin, lines);
|
||||
}
|
||||
/// 用键盘移动文本选择端点,按扩展模式决定是否保留锚点。
|
||||
// DeveloperToolsState::key 在 Source 焦点下调用,返回活动端行号供外层调整滚动;非导航键返回 None。
|
||||
pub fn navigate(
|
||||
&mut self,
|
||||
key: &winit::keyboard::NamedKey,
|
||||
@@ -68,11 +79,14 @@ impl SourceSelection {
|
||||
command: bool,
|
||||
) -> Option<usize> {
|
||||
use winit::keyboard::NamedKey::*;
|
||||
// 从活动端移动,不从已排序范围起点移动,因此 Shift 可以跨过原锚点形成反向选区。
|
||||
let (mut row, mut byte) = self.end;
|
||||
// 没有当前行时不导航;后面行数减一依赖这里已证明 lines 非空。
|
||||
let line = self.lines.get(row)?;
|
||||
match key {
|
||||
// 水平移动按字素跨越,组合音标和 emoji 视作整体;到行边界后跨到相邻行的末尾/开头。
|
||||
ArrowLeft => {
|
||||
// 左移使用当前位置之前的最后一个字素起点,行内切片边界来自此前定位/导航的不变量。
|
||||
if byte > 0 {
|
||||
byte = line[..byte]
|
||||
.grapheme_indices(true)
|
||||
@@ -84,6 +98,7 @@ impl SourceSelection {
|
||||
}
|
||||
}
|
||||
ArrowRight => {
|
||||
// 右移取后缀第一个完整字素长度,行尾才跨到下一行开头,不将换行当作行内字节。
|
||||
if byte < line.len() {
|
||||
byte += line[byte..].graphemes(true).next().map_or(0, str::len);
|
||||
} else if row + 1 < self.lines.len() {
|
||||
@@ -93,12 +108,15 @@ impl SourceSelection {
|
||||
}
|
||||
// 垂直移动保持的是字素列数,不是像素 x;目标行较短时取尽其字素,落到行尾。
|
||||
ArrowUp | ArrowDown => {
|
||||
// 记录当前前缀字素数量,在目标行最多消费同样数量,不能直接复用 UTF-8 字节偏移。
|
||||
let col = line[..byte].graphemes(true).count();
|
||||
// 向上使用饱和减法,向下钳到最后一行;到文件边界继续按键仍停在有效行。
|
||||
row = if *key == ArrowUp {
|
||||
row.saturating_sub(1)
|
||||
} else {
|
||||
(row + 1).min(self.lines.len() - 1)
|
||||
};
|
||||
// 目标行逐字素累加字节长度,较短行自然停在行尾,结果仍是安全切片边界。
|
||||
byte = self.lines[row]
|
||||
.graphemes(true)
|
||||
.take(col)
|
||||
@@ -120,6 +138,7 @@ impl SourceSelection {
|
||||
}
|
||||
_ => return None,
|
||||
}
|
||||
// 所有受支持导航完成后统一更新活动端,再按 Shift 决定保留或同步锚点。
|
||||
self.end = (row, byte);
|
||||
// 未按 Shift 时同步锚点以折叠选区;按 Shift 则仅移动 end,保持原锚点扩展选择。
|
||||
if !shift {
|
||||
@@ -128,7 +147,9 @@ impl SourceSelection {
|
||||
Some(row)
|
||||
}
|
||||
/// 返回排序后的有效选择范围,隐藏拖动方向差异。
|
||||
// text 与 span 共用范围归一化;all 是外层全选标记,不需要重写保存的 anchor/end。
|
||||
fn range(&self, all: bool) -> ((usize, usize), (usize, usize)) {
|
||||
// 全选从首行首字节到末行末字节,空内容通过饱和索引和缺省末行长度保持零范围。
|
||||
if all {
|
||||
return (
|
||||
(0, 0),
|
||||
@@ -142,7 +163,9 @@ impl SourceSelection {
|
||||
(self.anchor.min(self.end), self.anchor.max(self.end))
|
||||
}
|
||||
/// 拼接当前选择范围内的实际文本,保持多行换行与 UTF-8 边界。
|
||||
// DeveloperToolsState::selected_text 在复制路径调用,返回自有字符串;不直接写系统剪贴板。
|
||||
pub fn text(&self, all: bool) -> String {
|
||||
// 先统一正向范围,随后选中行顺序按文档正序输出,拖选方向不改变复制文本次序。
|
||||
let (a, b) = self.range(all);
|
||||
self.lines
|
||||
.iter()
|
||||
@@ -158,12 +181,14 @@ impl SourceSelection {
|
||||
.join("\n")
|
||||
}
|
||||
/// 取得某行的可见选择片段,供高亮绘制复用。
|
||||
// Painter::lines 给可见源码行调用,将保存的字节端点转成真实等宽字体的两个横向位置。
|
||||
pub fn span(&self, row: usize, all: bool, lines: &[String]) -> Option<(f32, f32)> {
|
||||
let (a, b) = self.range(all);
|
||||
// 未选中的行或折叠选择不画高亮;有选择时返回两个真实字体测量的横向位置,不是宽度值。
|
||||
if row < a.0 || row > b.0 || a == b {
|
||||
return None;
|
||||
}
|
||||
// 使用本帧展示行测量,越界行直接 None;下面前缀 get 也会拒绝不一致字节端点。
|
||||
let line = lines.get(row)?;
|
||||
let start = if row == a.0 { a.1 } else { 0 };
|
||||
let end = if row == b.0 { b.1 } else { line.len() };
|
||||
|
||||
Reference in New Issue
Block a user