docs: explain developer tool painting and source selection

This commit is contained in:
2026-11-12 19:15:28 +08:00
parent bb6e78ad04
commit a0ff16e23f
3 changed files with 78 additions and 2 deletions
+38 -2
View File
@@ -5,19 +5,24 @@ use super::*;
/// 开发工具的绘制助手,把布局结果写入同帧显示列表。
pub(super) struct Painter {
// 所有工具页共用宿主 Ui,保存显示命令和同帧 Widget 命中;不是业务 Endor 的对象树。
pub(super) ui: Ui<Action>,
// 除 Widget 外还保留输入、源码、滚动条和分隔条几何,交回 DeveloperToolsState 处理后续事件。
pub(super) layout: DeveloperToolsLayout,
}
impl Painter {
/// 向宿主显示列表追加填充矩形,不登记点击或滚动目标。
// console/network/settings/sources 画背景或高亮时调用,只委托底层 Ui 填充。
pub(super) fn fill(&mut self, r: Rect, c: Color) {
self.ui.fill(r, c);
}
/// 绘制工具文字,code 选择等宽风格;不在此处截断文本或登记目标。
// 工具页标题、文本行和状态提示使用,text 的测量/字形绘制由显示列表路径统一完成。
pub(super) fn label(&mut self, x: f32, y: f32, value: &str, c: Color, code: bool) {
self.ui.label(x, y, value, c, code);
}
/// 创建普通或切换按钮,active 作为选中状态参与外观。
// Console 和 Sources 的普通/切换按钮共用,Action 同时提供稳定控件身份及后续激活语义。
pub(super) fn button(&mut self, r: Rect, label: &str, action: Action, active: bool) {
// Follow 与数据源切换使用 Toggle 语义,其余动作使用普通按钮角色;身份仍由 action 决定。
let role = match action {
@@ -28,17 +33,20 @@ impl Painter {
.button(r, action, Button::new(label).selected(active).role(role));
}
/// 创建带启用条件的按钮,禁用项仍可显示但不能激活动作。
// Console/Network 清屏按钮依据是否有记录启用;disabled 的交互规则由底层 Ui 统一处理。
pub(super) fn enabled_button(&mut self, r: Rect, label: &str, action: Action, enabled: bool) {
self.ui
.button(r, action, Button::new(label).enabled(enabled));
}
/// 记录待在最终覆盖层绘制的分隔条及其拖动类别,此处不立即画线。
// Sources 与 Network 布局调用,父 build 最后遍历 splits 以覆盖在内容之上。
pub(super) fn split(&mut self, r: Rect, kind: Split) {
// Paint and register splitters after all pane content, in one overlay pass.
// 此处只记录分隔条布局,外层最后统一覆盖绘制/注册,保证线条与命中目标位于内容之上。
self.layout.splits.push((r, kind));
}
/// 测量并缓存完整行宽,钳制滚动,仅绘制可见行,同时登记区域、选择和两轴滚动条。
// 各工具页共享行面板入口;Region 决定独立滚动/宽度缓存位置,Source 另外维护只读文字选择。
pub(super) fn lines(
&mut self,
area: Rect,
@@ -51,12 +59,14 @@ impl Painter {
let content_h = lines.len() as f32 * ROW + 12.0;
use std::hash::{Hash, Hasher};
let mut hash = std::collections::hash_map::DefaultHasher::new();
// 散列完整行文本,滚动到屏幕外的行变化也会使整段最大宽度缓存失效。
for line in lines {
line.value.hash(&mut hash);
}
// 宽度缓存只依赖行文本指纹,颜色/选择变化无需重新测量;文字样式在此固定为 code。
let fingerprint = hash.finish();
let cache = &mut state.widths[region as usize];
// 匹配指纹时复用宽度,否则重新测量所有行;颜色和行选择不影响这里的排版宽度。
let content_w = match *cache {
Some((previous, width)) if previous == fingerprint => width,
_ => {
@@ -69,6 +79,7 @@ impl Painter {
width
}
};
// 按 Region 取出本面板状态,之后使用同一份钳制结果定位文本、选择和滚动手柄。
let s = &mut state.scroll[region as usize];
// Follow 跳到内容末端;否则只钳掉内容缩短后越界的旧偏移,保留用户当前阅读位置。
s.y = if follow {
@@ -86,6 +97,7 @@ impl Painter {
)));
// 源码行前七字符为显示行号与间隔,选择数据去掉这段;输入法/Console 编辑使用其他状态。
if region == Region::Source {
// 选择与复制按显示副本工作;此处固定去掉七字符前缀,并非根据 AST/source 字节范围回读原文。
let values = lines
.iter()
.map(|line| line.value.chars().skip(7).collect::<String>())
@@ -103,8 +115,11 @@ impl Painter {
// 按滚动量只遍历可见行,并多取两行覆盖边缘部分行;完整文本仍保留供测量和复制。
let first = (s.y / ROW).floor() as usize;
let count = (area.height / ROW).ceil() as usize + 2;
// i 保留完整文本的原始行号,不能把 skip 之后的可见序号当成选择或输入行号。
for (i, line) in lines.iter().enumerate().skip(first).take(count) {
// 把完整行的 y 减去内容滚动,部分行也可进入裁剪;布局中仍保存其原行下标。
let y = area.y + 5.0 + i as f32 * ROW - s.y;
// 行分隔使用主题强调色或中性色,与行文字共用当前 clip,不能盖到面板外。
if let Some(divider) = line.divider {
self.fill(
Rect::new(area.x, y, area.width, 1.0),
@@ -114,7 +129,9 @@ impl Painter {
},
);
}
// paint 用完整行区域绘制背景,后面的交互区域另按可见上下界收窄。
let paint = Rect::new(area.x, y, area.width, ROW);
// 有动作的行登记可点击 row 并传递选中态;无动作但 selected 的行只画静态高亮。
if let Some(action) = &line.action {
let top = y.max(area.y);
let bottom = (y + ROW).min(area.y + area.height - 9.0);
@@ -146,6 +163,7 @@ impl Painter {
}
self.label(area.x + 8.0 - s.x, y, &line.value, line.color, true);
}
// 行内容绘制结束必须恢复外层 clip,再在外层画滚动条,否则会被刚扣除的手柄区域裁掉。
self.ui.list.push(DrawCommand::PopClip);
// 记录完整内容范围给滚轮/拖动逻辑,再在裁剪弹出后绘制两轴滚动条,避免手柄被内容裁剪掉。
self.layout
@@ -155,6 +173,7 @@ impl Painter {
self.scrollbar(area, region, true, s.x, content_w);
}
/// 按内容/视口比例绘制单轴滚动手柄,并登记拖动到内容位移的换算倍率。
// lines 对横纵轴各调用一次,拖动倍率连同手柄几何存入 layout.thumbs,供 pointer_moved 反算内容偏移。
pub(super) fn scrollbar(
&mut self,
r: Rect,
@@ -165,6 +184,7 @@ impl Painter {
) {
// h 统一复用横纵轴公式;先扣除另一轴手柄占用,内容不溢出或视口无面积时不创建滚动条。
let viewport = if h { r.width } else { r.height } - 10.0;
// 无溢出或无有效轨道时跳过,不登记无法移动的虚假手柄。
if content <= viewport || viewport <= 0.0 {
return;
}
@@ -174,12 +194,14 @@ impl Painter {
let maximum = (content - viewport).max(1.0);
// 内容偏移归一化后映射到手柄可移动距离;两者并非一比一像素关系。
let position = (offset / maximum).clamp(0.0, 1.0) * travel;
// 横条在面板底部,竖条在右侧,轴选择只影响几何摆放,内容换算使用同一套比例。
let track = if h {
Rect::new(r.x, r.y + r.height - 8.0, viewport, 8.0)
} else {
Rect::new(r.x + r.width - 8.0, r.y, 8.0, viewport)
};
self.fill(track, BAR);
// 手柄沿轨道移动,另外一轴保留一单位内缩,视觉大小与命中矩形采用同一 thumb。
let thumb = if h {
Rect::new(track.x + position, track.y + 1.0, length, 6.0)
} else {
@@ -201,12 +223,14 @@ impl Painter {
pub(super) struct Line {
pub(super) value: String,
pub(super) color: Color,
// 可选执行分隔线,与文字颜色分离;Console 请求标题和详情分组使用它。
pub(super) divider: Option<Divider>,
pub(super) selected: bool,
// Some 使该行可激活请求/节点等动作,None 仅展示;selected 本身不赋予点击行为。
pub(super) action: Option<Action>,
}
#[derive(Clone, Copy)]
/// 管理画面内分隔条方向/位置描述。
/// 文字行分隔线的配色类别,区别执行强调线与普通中性线。
pub(super) enum Divider {
Accent,
Neutral,
@@ -224,6 +248,7 @@ impl Line {
}
}
/// 将文本按换行分成显示行并展开 Tab,追加到 out;不直接绘制,也不返回布局范围。
// console_result、request_details 及工具空态共用,把展示文本追加为行而不提交任何程序输入。
pub(super) fn multiline(out: &mut Vec<Line>, value: &str, color: Color) {
out.extend(
value
@@ -232,7 +257,8 @@ pub(super) fn multiline(out: &mut Vec<Line>, value: &str, color: Color) {
.map(|s| Line::new(s, color)),
);
}
/// 把一条 Console 结果投影为可滚动、可选择的文字画面。
/// 把一条 Console 响应解析成显示行,供 transcript 面板统一滚动展示。
// Console 遍历已完成 Exchange 时调用,按响应字段优先级选一种主要结果,成功执行不等于页面重新渲染。
pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
// 响应若为 JSON,按 reset、value、diagnostics、error、revision 优先提取可读结果;不满足时退回原文。
if let Ok(value) = serde_json::from_str::<serde_json::Value>(body) {
@@ -253,6 +279,7 @@ pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
MUTED,
));
}
// 会话重置消息优先于普通 value/diagnostics,显示后立即返回,避免重复呈现同一响应的其他字段。
if value["session_reset"] == true {
multiline(
out,
@@ -264,6 +291,7 @@ pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
);
return;
}
// 只有字符串 value 被当作结果文本,其他 JSON 值类型继续尝试诊断、错误或版本分支。
if let Some(result) = value["value"].as_str() {
multiline(out, result, color);
return;
@@ -271,6 +299,7 @@ pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
if let Some(diagnostics) = value["diagnostics"].as_array()
&& !diagnostics.is_empty()
{
// 每条诊断可选显示位置,再显示 message;缺 message 用固定失败提示,不调用源对象格式化。
for d in diagnostics {
if let Some(line) = d["line"].as_u64() {
out.push(Line::new(
@@ -286,27 +315,33 @@ pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
}
return;
}
// 没有更高优先级结果/诊断时显示 error 文本,同样直接结束本次主要结果提取。
if let Some(error) = value["error"].as_str() {
multiline(out, error, color);
return;
}
// 只有可转为 u64 的 revision 才生成提交提示;不符合已知字段形状的 JSON 仍会退回正文。
if let Some(revision) = value["revision"].as_u64() {
out.push(Line::new(format!("Committed revision {revision}"), color));
return;
}
}
// 非 JSON、被预览截断的 JSON 或无已知结果字段都按原预览显示,不因此丢失诊断内容。
multiline(out, body, color);
}
/// 将代码层次展开为工具可绘制的行数据,保留节点与源范围关联。
// Sources 在构造 AST 树行时调用,返回带深度的借用节点序列,节点 ID 和源位置由原树提供。
pub(super) fn flatten<'a>(node: &'a CodeNode, depth: usize, out: &mut Vec<(usize, &'a CodeNode)>) {
// 先序展开保持 AST 子节点顺序,depth 供显示缩进;借用原节点而不复制整棵树。
out.push((depth, node));
// 按子节点原顺序递归,使显示树次序与发布 AST 一致;只增加显示深度,不修改节点。
for child in &node.children {
flatten(child, depth + 1, out);
}
}
/// 绘制选中请求的头部、正文预览与响应信息,不重新执行该请求。
// Network 为当前选中 Exchange 构造详情行,只有记录中已有的受限头部和正文能显示。
pub(super) fn request_details(out: &mut Vec<Line>, r: &Exchange) {
multiline(
out,
@@ -330,6 +365,7 @@ pub(super) fn request_details(out: &mut Vec<Line>, r: &Exchange) {
("RESPONSE BODY", &r.response),
] {
out.push(Line::new("", MUTED));
// 分组标题用强调分隔线,正文沿原换行展开;空字段以 (empty) 明示,并非继续发起网络读取。
let mut l = Line::new(title, MUTED);
l.divider = Some(Divider::Accent);
out.push(l);