144 lines
6.0 KiB
Rust
144 lines
6.0 KiB
Rust
//! 绘制已观察到的 HTTP 活动、请求列表与详情。
|
||
//! 筛选、选择和滚动是工具视图状态;展示一条请求不会重发该请求。
|
||
//!
|
||
use super::*;
|
||
|
||
/// 将本进程 HTTP 活动投影为可筛选请求列表和独立滚动详情,并生成选择/分隔条命中区域。
|
||
// developer_tools::build 选择 Network 时调用,共享日志已在父入口克隆,过滤和文字测量不持活动日志锁。
|
||
pub(super) fn build(
|
||
p: &mut Painter,
|
||
state: &mut DeveloperToolsState,
|
||
records: &[Exchange],
|
||
viewport: Rect,
|
||
) {
|
||
// 工具页以 viewport 横向原点和宽度布局;底部状态栏高度留给父入口统一绘制。
|
||
let x = viewport.x;
|
||
let w = viewport.width;
|
||
let bottom = viewport.height - 22.0;
|
||
|
||
p.fill(Rect::new(x, 32.0, w, 30.0), BAR);
|
||
// 水位后存在记录才允许 Clear,Action::ClearNetwork 更新工具水位并清除选择,不删除 Console 共用日志。
|
||
p.enabled_button(
|
||
Rect::new(x + 3.0, 32.0, 62.0, 29.0),
|
||
"Clear",
|
||
Action::ClearNetwork,
|
||
records.iter().any(|r| r.id > state.network_after),
|
||
);
|
||
// 过滤框注册 Filter 焦点,按键编辑在 DeveloperToolsState 中处理;此处只展示当前文本与焦点。
|
||
let filter_area = Rect::new(x + 72.0, 35.0, (w - 85.0).max(100.0), 24.0);
|
||
p.ui.text_field(
|
||
filter_area,
|
||
Action::Focus(Focus::Filter),
|
||
state.focus == Some(Focus::Filter),
|
||
);
|
||
p.label(
|
||
filter_area.x + 7.0,
|
||
filter_area.y + 3.0,
|
||
&format!(
|
||
"Filter: {}{}",
|
||
state.filter,
|
||
// 竖线是焦点提示字符,不是 Console 那种按字节光标定位,也不修改过滤字符串本身。
|
||
if state.focus == Some(Focus::Filter) {
|
||
"│"
|
||
} else {
|
||
""
|
||
}
|
||
),
|
||
MUTED,
|
||
false,
|
||
);
|
||
|
||
p.label(
|
||
x + 10.0,
|
||
69.0,
|
||
"METHOD STATUS TIME PATH",
|
||
MUTED,
|
||
true,
|
||
);
|
||
// 上下区分隔位置取自工具状态比例,拖动只改变显示分配,不改变记录本身。
|
||
let divider = 92.0 + (bottom - 92.0) * state.network_fraction;
|
||
// 对方法、路径和状态的组合字符串做大小写不敏感子串匹配;未响应记录可用 pending 搜索。
|
||
let filter = state.filter.to_lowercase();
|
||
// 保留原记录顺序及引用,只建立本次展示候选,不复制/更改每条 Exchange 的响应状态。
|
||
let visible = records
|
||
.iter()
|
||
// 清屏按 ID 水位隐藏旧记录,不删除共享 ActivityLog,Console 仍可独立展示同一条请求。
|
||
.filter(|r| r.id > state.network_after)
|
||
// 方法、路径与状态合并后统一小写匹配,origin 和正文不在当前过滤范围内。
|
||
.filter(|r| {
|
||
format!(
|
||
"{} {} {}",
|
||
r.method,
|
||
r.path,
|
||
r.status.map_or("pending".into(), |s| s.to_string())
|
||
)
|
||
.to_lowercase()
|
||
.contains(&filter)
|
||
})
|
||
.collect::<Vec<_>>();
|
||
let mut lines = Vec::new();
|
||
// 逐匹配记录生成一行,pending 状态用省略号;耗时直接取日志快照,不在绘制时重新计时。
|
||
for r in &visible {
|
||
// 先创建带状态颜色的行,再附加稳定 Request 动作和选中标记,筛选改变行号也不会改变目标身份。
|
||
let mut l = Line::new(
|
||
format!(
|
||
"{:<7} {:<6} {:>5} ms {}",
|
||
r.method,
|
||
r.status.map_or("…".into(), |s| s.to_string()),
|
||
r.elapsed_ms,
|
||
r.path
|
||
),
|
||
if r.status.is_some_and(|s| s >= 400) {
|
||
ERROR
|
||
} else {
|
||
TEXT
|
||
},
|
||
);
|
||
// 请求 ID 是选择身份,不依赖筛选后行号;点击行仅生成 Request 动作,不重新发送 HTTP。
|
||
l.selected = state.selected_request == Some(r.id);
|
||
l.action = Some(Action::Request(r.id));
|
||
lines.push(l);
|
||
}
|
||
// 空列表可能来自无记录、清屏或过滤无匹配,统一展示本进程 API 的捕获范围提示。
|
||
if lines.is_empty() {
|
||
lines.push(Line::new(
|
||
"No matching requests. Listening to Tessera's local API.",
|
||
MUTED,
|
||
));
|
||
}
|
||
// 列表使用 Requests Region 保存独立滚动,false 表示不因为新增请求自动跟随到底部。
|
||
p.lines(
|
||
Rect::new(x + 1.0, 92.0, w - 2.0, divider - 92.0),
|
||
Region::Requests,
|
||
&lines,
|
||
state,
|
||
false,
|
||
);
|
||
// 只登记横向分隔条,父绘制入口最后覆盖绘制并注册 Split::Network 命中目标。
|
||
p.split(Rect::new(x + 1.0, divider, w - 2.0, 1.0), Split::Network);
|
||
// 详情从完整保留记录中查找,因此筛选隐藏某行后可继续查看已选详情;记录淘汰后才回提示。
|
||
let selected = records
|
||
.iter()
|
||
.find(|r| Some(r.id) == state.selected_request);
|
||
// 详情另建行列表,不依赖上面的 visible;筛选无匹配并不自动取消已选请求。
|
||
let mut details = Vec::new();
|
||
// 仍保留的已选记录交给 request_details;ID 已淘汰或从未选择则显示捕获范围说明。
|
||
if let Some(r) = selected {
|
||
request_details(&mut details, r);
|
||
} else {
|
||
multiline(
|
||
&mut details,
|
||
"Select a request to inspect headers and bodies.\n\nCapture scope: 127.0.0.1:4737 in this Tessera process.\n80 most recent requests; body previews up to 32 KiB.\nOnly Content-Type and Content-Length request headers are recorded.\nExternal browser / OS traffic is not captured.",
|
||
MUTED,
|
||
);
|
||
}
|
||
// 请求列表与详情使用不同 Region,各自维护滚动量,避免切换详情影响列表定位。
|
||
p.lines(
|
||
Rect::new(x + 1.0, divider + 5.0, w - 2.0, bottom - divider - 5.0),
|
||
Region::Details,
|
||
&details,
|
||
state,
|
||
false,
|
||
);
|
||
}
|