327 lines
12 KiB
Rust
327 lines
12 KiB
Rust
//! 开发工具画面的绘制辅助:文字行、按钮、分隔线和滚动条。
|
|
//! 绘制命令与可交互区域同步登记,保证屏幕上的管理控件能用同一几何命中。
|
|
//!
|
|
use super::*;
|
|
|
|
/// 开发工具的绘制助手,把布局结果写入同帧显示列表。
|
|
pub(super) struct Painter {
|
|
pub(super) ui: Ui<Action>,
|
|
pub(super) layout: DeveloperToolsLayout,
|
|
}
|
|
impl Painter {
|
|
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
|
pub(super) fn fill(&mut self, r: Rect, c: Color) {
|
|
self.ui.fill(r, c);
|
|
}
|
|
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
|
pub(super) fn label(&mut self, x: f32, y: f32, value: &str, c: Color, code: bool) {
|
|
self.ui.label(x, y, value, c, code);
|
|
}
|
|
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
|
pub(super) fn button(&mut self, r: Rect, label: &str, action: Action, active: bool) {
|
|
let role = match action {
|
|
Action::Follow | Action::Snapshot | Action::Cloud => Role::Toggle,
|
|
_ => Role::Button,
|
|
};
|
|
self.ui
|
|
.button(r, action, Button::new(label).selected(active).role(role));
|
|
}
|
|
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
|
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));
|
|
}
|
|
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
|
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));
|
|
}
|
|
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
|
pub(super) fn lines(
|
|
&mut self,
|
|
area: Rect,
|
|
region: Region,
|
|
lines: &[Line],
|
|
state: &mut DeveloperToolsState,
|
|
follow: bool,
|
|
) {
|
|
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);
|
|
}
|
|
let fingerprint = hash.finish();
|
|
let cache = &mut state.widths[region as usize];
|
|
let content_w = match *cache {
|
|
Some((previous, width)) if previous == fingerprint => width,
|
|
_ => {
|
|
let width = lines
|
|
.iter()
|
|
.map(|line| text::measure(&line.value, TextStyle::code()).width + 20.0)
|
|
.fold(0.0_f32, f32::max);
|
|
*cache = Some((fingerprint, width));
|
|
width
|
|
}
|
|
};
|
|
let s = &mut state.scroll[region as usize];
|
|
s.y = if follow {
|
|
(content_h - area.height + 12.0).max(0.0)
|
|
} else {
|
|
s.y.clamp(0.0, (content_h - area.height + 12.0).max(0.0))
|
|
};
|
|
s.x = s.x.clamp(0.0, (content_w - area.width + 12.0).max(0.0));
|
|
self.ui.list.push(DrawCommand::PushClip(Rect::new(
|
|
area.x,
|
|
area.y,
|
|
(area.width - 9.0).max(0.0),
|
|
(area.height - 9.0).max(0.0),
|
|
)));
|
|
if region == Region::Source {
|
|
let values = lines
|
|
.iter()
|
|
.map(|line| line.value.chars().skip(7).collect::<String>())
|
|
.collect::<Vec<_>>();
|
|
state.source_selection.set_lines(&values);
|
|
self.layout.source = Some((
|
|
area,
|
|
Point::new(
|
|
area.x + 8.0 - s.x + text::measure(" ", TextStyle::code()).width,
|
|
area.y + 5.0 - s.y,
|
|
),
|
|
values,
|
|
));
|
|
}
|
|
let first = (s.y / ROW).floor() as usize;
|
|
let count = (area.height / ROW).ceil() as usize + 2;
|
|
for (i, line) in lines.iter().enumerate().skip(first).take(count) {
|
|
let y = area.y + 5.0 + i as f32 * ROW - s.y;
|
|
if let Some(divider) = line.divider {
|
|
self.fill(
|
|
Rect::new(area.x, y, area.width, 1.0),
|
|
match divider {
|
|
Divider::Accent => self.ui.theme.accent,
|
|
Divider::Neutral => BORDER,
|
|
},
|
|
);
|
|
}
|
|
let paint = Rect::new(area.x, y, area.width, ROW);
|
|
if let Some(action) = &line.action {
|
|
let top = y.max(area.y);
|
|
let bottom = (y + ROW).min(area.y + area.height - 9.0);
|
|
if bottom > top {
|
|
self.ui.row(
|
|
paint,
|
|
Rect::new(area.x, top, area.width - 9.0, bottom - top),
|
|
action.clone(),
|
|
line.selected,
|
|
);
|
|
}
|
|
} else if line.selected {
|
|
self.fill(paint, self.ui.theme.soft());
|
|
}
|
|
if region == Region::Source
|
|
&& let Some((_, origin, values)) = &self.layout.source
|
|
&& let Some((left, right)) = state.source_selection.span(
|
|
i,
|
|
state.selection && state.focus == Some(Focus::Source),
|
|
values,
|
|
)
|
|
{
|
|
self.fill(
|
|
Rect::new(origin.x + left, y, right - left, ROW),
|
|
self.ui.theme.hover(),
|
|
);
|
|
}
|
|
self.label(area.x + 8.0 - s.x, y, &line.value, line.color, true);
|
|
}
|
|
self.ui.list.push(DrawCommand::PopClip);
|
|
self.layout
|
|
.regions
|
|
.push((area, region, content_w, content_h));
|
|
self.scrollbar(area, region, false, s.y, content_h);
|
|
self.scrollbar(area, region, true, s.x, content_w);
|
|
}
|
|
/// 绘制对应管理界面原语,并按控件需要同步登记命中和滚动区域。
|
|
pub(super) fn scrollbar(
|
|
&mut self,
|
|
r: Rect,
|
|
region: Region,
|
|
h: bool,
|
|
offset: f32,
|
|
content: f32,
|
|
) {
|
|
let viewport = if h { r.width } else { r.height } - 10.0;
|
|
if content <= viewport || viewport <= 0.0 {
|
|
return;
|
|
}
|
|
let length = (viewport * viewport / content).max(24.0).min(viewport);
|
|
let travel = (viewport - length).max(1.0);
|
|
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);
|
|
let thumb = if h {
|
|
Rect::new(track.x + position, track.y + 1.0, length, 6.0)
|
|
} else {
|
|
Rect::new(track.x + 1.0, track.y + position, 6.0, length)
|
|
};
|
|
self.ui.scrollbar(
|
|
thumb,
|
|
WidgetId::new(("scroll", region, h)),
|
|
if h { Axis::Horizontal } else { Axis::Vertical },
|
|
);
|
|
self.layout
|
|
.thumbs
|
|
.push((thumb, region, h, maximum / travel));
|
|
}
|
|
}
|
|
#[derive(Clone)]
|
|
/// 一行可绘制、可关联源码位置的工具文本。
|
|
pub(super) struct Line {
|
|
pub(super) value: String,
|
|
pub(super) color: Color,
|
|
pub(super) divider: Option<Divider>,
|
|
pub(super) selected: bool,
|
|
pub(super) action: Option<Action>,
|
|
}
|
|
#[derive(Clone, Copy)]
|
|
/// 管理画面内分隔条方向/位置描述。
|
|
pub(super) enum Divider {
|
|
Accent,
|
|
Neutral,
|
|
}
|
|
impl Line {
|
|
/// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。
|
|
pub(super) fn new(value: impl Into<String>, color: Color) -> Self {
|
|
Self {
|
|
value: value.into(),
|
|
color,
|
|
divider: None,
|
|
selected: false,
|
|
action: None,
|
|
}
|
|
}
|
|
}
|
|
/// 按行绘制工具文本,返回后续布局需要的占用范围。
|
|
pub(super) fn multiline(out: &mut Vec<Line>, value: &str, color: Color) {
|
|
out.extend(
|
|
value
|
|
.replace('\t', " ")
|
|
.split('\n')
|
|
.map(|s| Line::new(s, color)),
|
|
);
|
|
}
|
|
/// 把一条 Console 结果投影为可滚动、可选择的文字画面。
|
|
pub(super) fn console_result(out: &mut Vec<Line>, body: &str, color: Color) {
|
|
if let Ok(value) = serde_json::from_str::<serde_json::Value>(body) {
|
|
if value["execution"] == "repl" {
|
|
out.push(Line::new(
|
|
if value["rendered"] == true {
|
|
format!(
|
|
"Rendered revision {} · session {}",
|
|
value["revision"], value["session"]
|
|
)
|
|
} else {
|
|
format!(
|
|
"Session {} · retained in memory; page unchanged",
|
|
value["session"]
|
|
)
|
|
},
|
|
MUTED,
|
|
));
|
|
}
|
|
if value["session_reset"] == true {
|
|
multiline(
|
|
out,
|
|
value["message"]
|
|
.as_str()
|
|
.or(value["error"].as_str())
|
|
.unwrap_or("Session reset"),
|
|
color,
|
|
);
|
|
return;
|
|
}
|
|
if let Some(result) = value["value"].as_str() {
|
|
multiline(out, result, color);
|
|
return;
|
|
}
|
|
if let Some(diagnostics) = value["diagnostics"].as_array()
|
|
&& !diagnostics.is_empty()
|
|
{
|
|
for d in diagnostics {
|
|
if let Some(line) = d["line"].as_u64() {
|
|
out.push(Line::new(
|
|
format!("Error · line {line}, column {}", d["column"]),
|
|
color,
|
|
));
|
|
}
|
|
multiline(
|
|
out,
|
|
d["message"].as_str().unwrap_or("Execution failed"),
|
|
color,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
if let Some(error) = value["error"].as_str() {
|
|
multiline(out, error, color);
|
|
return;
|
|
}
|
|
if let Some(revision) = value["revision"].as_u64() {
|
|
out.push(Line::new(format!("Committed revision {revision}"), color));
|
|
return;
|
|
}
|
|
}
|
|
multiline(out, body, color);
|
|
}
|
|
/// 将代码层次展开为工具可绘制的行数据,保留节点与源范围关联。
|
|
pub(super) fn flatten<'a>(node: &'a CodeNode, depth: usize, out: &mut Vec<(usize, &'a CodeNode)>) {
|
|
out.push((depth, node));
|
|
for child in &node.children {
|
|
flatten(child, depth + 1, out);
|
|
}
|
|
}
|
|
|
|
/// 绘制选中请求的头部、正文预览与响应信息,不重新执行该请求。
|
|
pub(super) fn request_details(out: &mut Vec<Line>, r: &Exchange) {
|
|
multiline(
|
|
out,
|
|
&format!(
|
|
"#{} {} {}\n{} · {} · {} ms\nStatus: {}",
|
|
r.id,
|
|
r.method,
|
|
r.path,
|
|
r.timestamp,
|
|
r.origin,
|
|
r.elapsed_ms,
|
|
r.status.map_or("pending".into(), |s| s.to_string())
|
|
),
|
|
TEXT,
|
|
);
|
|
for (title, body) in [
|
|
("REQUEST HEADERS", &r.request_headers),
|
|
("REQUEST BODY", &r.request),
|
|
("RESPONSE HEADERS", &r.response_headers),
|
|
("RESPONSE BODY", &r.response),
|
|
] {
|
|
out.push(Line::new("", MUTED));
|
|
let mut l = Line::new(title, MUTED);
|
|
l.divider = Some(Divider::Accent);
|
|
out.push(l);
|
|
multiline(
|
|
out,
|
|
if body.is_empty() { "(empty)" } else { body },
|
|
if title == "RESPONSE BODY" && r.status.is_some_and(|s| s >= 400) {
|
|
ERROR
|
|
} else {
|
|
TEXT
|
|
},
|
|
);
|
|
}
|
|
}
|