docs: explain console, network and settings view projections
This commit is contained in:
@@ -4,18 +4,21 @@
|
||||
use super::*;
|
||||
|
||||
/// 将有界 HTTP 活动、当前编辑输入和输入法预编辑投影为 Console 显示行与命中布局,并按需滚动到光标。
|
||||
// developer_tools::build 在 Console 标签选中时调用;records 是父入口克隆的活动快照,后续绘制不持活动日志锁。
|
||||
pub(super) fn build(
|
||||
p: &mut Painter,
|
||||
state: &mut DeveloperToolsState,
|
||||
records: &[Exchange],
|
||||
viewport: Rect,
|
||||
) {
|
||||
// x 是工具画布横向原点,w 为可用宽度;本页标题及行区纵坐标沿用父工具布局约定。
|
||||
let x = viewport.x;
|
||||
let w = viewport.width;
|
||||
// 此工具页使用其既定标题栏/底栏布局坐标,输出区与输入区共享下方 transcript 滚动面板。
|
||||
let bottom = viewport.height - 22.0;
|
||||
|
||||
p.fill(Rect::new(x, 32.0, w, 30.0), BAR);
|
||||
// Clear 的可用性与下面输出记录过滤条件一致,仅存在清屏水位之后的可展示记录才启用。
|
||||
p.enabled_button(
|
||||
Rect::new(x + 3.0, 32.0, 62.0, 29.0),
|
||||
"Clear",
|
||||
@@ -28,6 +31,7 @@ pub(super) fn build(
|
||||
);
|
||||
p.button(
|
||||
Rect::new(x + 68.0, 32.0, 106.0, 29.0),
|
||||
// 按钮文本反映当前跟随设置,点击 Action::Follow 后由状态处理器切换,不在绘制时直接改 follow。
|
||||
if state.follow {
|
||||
"Follow: on"
|
||||
} else {
|
||||
@@ -42,6 +46,7 @@ pub(super) fn build(
|
||||
Action::ResetSession,
|
||||
false,
|
||||
);
|
||||
// 窄窗口省略辅助会话说明,保留清屏、跟随和重置按钮,不改变真实会话状态。
|
||||
if w > 520.0 {
|
||||
p.label(x + 309.0, 39.0, "app.main · live objects", MUTED, false);
|
||||
}
|
||||
@@ -53,7 +58,9 @@ pub(super) fn build(
|
||||
&& (r.method == "PUT" || r.method == "POST" || r.method == "DELETE")
|
||||
&& !r.path.ends_with("/events")
|
||||
}) {
|
||||
// 按路径及方法给观察记录选择显示标签,分类不执行 API,也不决定 worker 的真实请求处理。
|
||||
let kind = if r.path.ends_with("/repl") {
|
||||
// 同一 /repl 的 DELETE 显示会话重置,其他方法显示持续会话执行,区别于独立 /evaluate。
|
||||
if r.method == "DELETE" {
|
||||
"RESET SESSION"
|
||||
} else {
|
||||
@@ -67,12 +74,15 @@ pub(super) fn build(
|
||||
"CLEAR DOCUMENT"
|
||||
};
|
||||
// Console 自己发起的 REPL 请求省略冗余标题和 HTTP 状态行,外部请求保留来源与诊断入口。
|
||||
// 只简化本工具发出的 POST /repl;重置和外部请求仍保留来源与状态,避免混淆不同操作。
|
||||
let local = r.origin == "Console" && r.path.ends_with("/repl") && r.method == "POST";
|
||||
// 非本地简化输出加可点击标题,Action::Request 根据稳定请求 ID 打开 Network 详情。
|
||||
if !local {
|
||||
let mut title = Line::new(
|
||||
format!("#{} {} {} {}", r.id, r.timestamp, r.origin, kind),
|
||||
MUTED,
|
||||
);
|
||||
// 强调分隔线标记一次执行起点,标题动作与文本一起交给 Painter 注册命中目标。
|
||||
title.divider = Some(Divider::Accent);
|
||||
title.action = Some(Action::Request(r.id));
|
||||
lines.push(title);
|
||||
@@ -84,6 +94,7 @@ pub(super) fn build(
|
||||
TEXT,
|
||||
));
|
||||
}
|
||||
// 颜色只按已记录 HTTP 状态判定,pending 尚无失败码时暂用成功色变量,但实际分支显示灰色 Executing。
|
||||
let color = if r.status.is_some_and(|s| s >= 400) {
|
||||
ERROR
|
||||
} else {
|
||||
@@ -97,14 +108,17 @@ pub(super) fn build(
|
||||
color,
|
||||
));
|
||||
}
|
||||
// 响应格式化由 painter 模块解析已记录正文;预览截断导致 JSON 无法解析时会回退原文显示。
|
||||
console_result(&mut lines, &r.response, color);
|
||||
} else {
|
||||
lines.push(Line::new(" Executing…", MUTED));
|
||||
}
|
||||
// 每条请求结束添加中性分隔线,包括仍执行中的记录,让后续输入与相邻历史有明确边界。
|
||||
let mut separator = Line::new("", MUTED);
|
||||
separator.divider = Some(Divider::Neutral);
|
||||
lines.push(separator);
|
||||
}
|
||||
// 没有可展示历史时补欢迎说明,这些行也属于 transcript 输出,后面的输入起始偏移包含它们。
|
||||
if lines.is_empty() {
|
||||
multiline(
|
||||
&mut lines,
|
||||
@@ -115,37 +129,47 @@ pub(super) fn build(
|
||||
use std::hash::{Hash, Hasher};
|
||||
// 只对输出行文本求 stamp,用来判断 Follow 是否需要跳到底部;当前编辑输入不参与输出变化检测。
|
||||
let mut hash = std::collections::hash_map::DefaultHasher::new();
|
||||
// 只散列 value,不把选择、颜色或分隔线样式变化当成新输出,避免无关重绘触发跟随到底部。
|
||||
for line in &lines {
|
||||
line.value.hash(&mut hash);
|
||||
}
|
||||
let stamp = hash.finish();
|
||||
// 保存本次指纹作下一帧比较,Follow 开关只决定是否采用变化信号,不影响指纹更新。
|
||||
let output_changed = stamp != state.console_stamp;
|
||||
state.console_stamp = stamp;
|
||||
// 记录输入开始的行号,后续光标和点击区域都需加上 transcript 输出行偏移。
|
||||
let first_input = lines.len();
|
||||
// 输出历史与可编辑输入共用一个 Transcript 区域,底部留出状态提示和滚动条空间。
|
||||
let area = Rect::new(x + 4.0, 63.0, w - 5.0, (bottom - 88.0).max(1.0));
|
||||
// 输入法 preedit 只插入显示副本,不提交到实际 input;只有输入框持焦点时展示组合文本。
|
||||
let mut input = state.input.clone();
|
||||
// 只在输入框持焦点时把 preedit 插到显示副本,编辑字符串本身仍等最终 IME Commit。
|
||||
if state.focus == Some(Focus::Input) {
|
||||
input.insert_str(state.cursor, &state.preedit);
|
||||
}
|
||||
// 显示输入保留换行并加提示符;全选高亮只跟随 Input 焦点,不把历史输出当成可替换输入。
|
||||
for (i, line) in input.split('\n').enumerate() {
|
||||
let mut line = Line::new(format!("{}{line}", if i == 0 { "> " } else { " " }), TEXT);
|
||||
line.selected = state.selection && state.focus == Some(Focus::Input);
|
||||
lines.push(line);
|
||||
}
|
||||
// 提示符用真实等宽字体度量,后续光标和鼠标定位共享同一文本起点。
|
||||
let prefix = text::measure("> ", TextStyle::code()).width;
|
||||
// cursor 是输入字符串的字节边界,由编辑逻辑维护;通过换行计数定位行,再用真实字体宽度计算列位置。
|
||||
let before = &state.input[..state.cursor];
|
||||
// cursor 以原输入的 UTF-8 字节位置计,按其之前换行数求行号,最后一段就是当前行已有正文。
|
||||
let row = before.matches('\n').count() as f32;
|
||||
let column = before.rsplit('\n').next().unwrap_or("");
|
||||
// 列正文与 preedit 一起测量,光标画在组合输入后;提示符宽度另加而不参与待编辑字符串。
|
||||
let caret_x =
|
||||
prefix + text::measure(&format!("{column}{}", state.preedit), TextStyle::code()).width;
|
||||
// 当前输入行加上输出行数得到整个 transcript 中的纵向位置,滚动量稍后统一扣除。
|
||||
let caret_y = (first_input as f32 + row) * ROW;
|
||||
// 新输出触发 Follow,但显式 reveal_input 优先,避免执行后或编辑时光标被自动滚动隐藏。
|
||||
let follow = state.follow && output_changed && !state.reveal_input;
|
||||
// 把现有滚动量钳入“光标可见”的允许范围,而非每次将光标强制居中;横向预留 24 单位空间。
|
||||
if state.reveal_input {
|
||||
// 编辑触发的 reveal 优先保住光标,不跳到历史尾部;校正后清标记,只消费一次请求。
|
||||
let s = &mut state.scroll[Region::Transcript as usize];
|
||||
s.y = s.y.clamp(
|
||||
(caret_y + ROW + 16.0 - area.height).max(0.0).min(caret_y),
|
||||
@@ -154,8 +178,10 @@ pub(super) fn build(
|
||||
s.x = s.x.clamp((caret_x + 24.0 - area.width).max(0.0), caret_x);
|
||||
state.reveal_input = false;
|
||||
}
|
||||
// Painter 测量完整内容并钳制最终滚动,必须在此之后再读取 scroll 计算输入命中布局。
|
||||
p.lines(area, Region::Transcript, &lines, state, follow);
|
||||
let scroll = state.scroll[Region::Transcript as usize];
|
||||
// 保存输入正文第一行的实际屏幕原点,包含内容边距、提示符、输出高度及最终两轴滚动。
|
||||
let origin = Point::new(
|
||||
area.x + 8.0 + prefix - scroll.x,
|
||||
area.y + 5.0 + first_input as f32 * ROW - scroll.y,
|
||||
@@ -165,10 +191,12 @@ pub(super) fn build(
|
||||
viewport: area,
|
||||
origin,
|
||||
});
|
||||
// 输入可见竖向片段与 transcript 相交,下面只为交集注册 Focus(Input),不覆盖历史文本区域。
|
||||
let input_top = origin.y.max(area.y);
|
||||
let input_bottom =
|
||||
(origin.y + state.input.split('\n').count() as f32 * ROW).min(area.y + area.height - 9.0);
|
||||
// 仅为仍可见的输入片段注册文本输入命中区,扣除滚动条空间,避免历史输出区误抢输入焦点。
|
||||
// 整个输入已滚出视口时不登记输入矩形,避免不可见编辑区域拦截鼠标。
|
||||
if input_bottom > input_top {
|
||||
p.ui.inline_input(
|
||||
Rect::new(
|
||||
@@ -181,6 +209,7 @@ pub(super) fn build(
|
||||
);
|
||||
}
|
||||
// 光标单独绘制并限制在文本视口内,不能穿过滚动条或工具页边缘;随后配对 PopClip。
|
||||
// 光标只是绘制覆盖层,先压入扣除滚动条的 clip,再画一像素竖线并弹出,不扩大输入命中范围。
|
||||
if state.focus == Some(Focus::Input) {
|
||||
p.ui.list.push(DrawCommand::PushClip(Rect::new(
|
||||
area.x,
|
||||
|
||||
Reference in New Issue
Block a user