Files
endorphin-tessera-gui/src/shell/developer_tools/console.rs
T

203 lines
6.6 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 绘制 Console 输入、结果历史和对应命中区域。
//! 这里消费工具状态并生成显示列表;用户程序的编译执行由提交路径和 worker 完成。
//!
use super::*;
/// 根据当前输入构造本模块的派生结果,具体的验证与资源边界见函数内步骤。
pub(super) fn build(
p: &mut Painter,
state: &mut DeveloperToolsState,
records: &[Exchange],
viewport: Rect,
) {
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);
p.enabled_button(
Rect::new(x + 3.0, 32.0, 62.0, 29.0),
"Clear",
Action::ClearConsole,
records.iter().any(|r| {
r.id > state.console_after
&& matches!(r.method.as_str(), "PUT" | "POST" | "DELETE")
&& !r.path.ends_with("/events")
}),
);
p.button(
Rect::new(x + 68.0, 32.0, 106.0, 29.0),
if state.follow {
"Follow: on"
} else {
"Follow: off"
},
Action::Follow,
state.follow,
);
p.button(
Rect::new(x + 181.0, 32.0, 119.0, 29.0),
"Reset session",
Action::ResetSession,
false,
);
if w > 520.0 {
p.label(x + 309.0, 39.0, "app.main · live objects", MUTED, false);
}
let mut lines = Vec::new();
for r in records.iter().filter(|r| {
r.id > state.console_after
&& (r.method == "PUT" || r.method == "POST" || r.method == "DELETE")
&& !r.path.ends_with("/events")
}) {
let kind = if r.path.ends_with("/repl") {
if r.method == "DELETE" {
"RESET SESSION"
} else {
"REPL · live objects"
}
} else if r.path.ends_with("/evaluate") {
"ISOLATED API EVALUATION"
} else if r.method == "PUT" {
"FULL SOURCE · live program"
} else {
"CLEAR DOCUMENT"
};
let local = r.origin == "Console" && r.path.ends_with("/repl") && r.method == "POST";
if !local {
let mut title = Line::new(
format!("#{} {} {} {}", r.id, r.timestamp, r.origin, kind),
MUTED,
);
title.divider = Some(Divider::Accent);
title.action = Some(Action::Request(r.id));
lines.push(title);
}
for (i, line) in r.request.split('\n').enumerate() {
lines.push(Line::new(
format!("{}{line}", if i == 0 { "> " } else { " " }),
TEXT,
));
}
let color = if r.status.is_some_and(|s| s >= 400) {
ERROR
} else {
OK
};
if let Some(status) = r.status {
if !local {
lines.push(Line::new(
format!("‹ {status} · {} ms", r.elapsed_ms),
color,
));
}
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);
}
if lines.is_empty() {
multiline(
&mut lines,
"Endor · live session\nCreate objects here; call page.render(...) to display.\n",
MUTED,
);
}
use std::hash::{Hash, Hasher};
let mut hash = std::collections::hash_map::DefaultHasher::new();
for line in &lines {
line.value.hash(&mut hash);
}
let stamp = hash.finish();
let output_changed = stamp != state.console_stamp;
state.console_stamp = stamp;
let first_input = lines.len();
let area = Rect::new(x + 4.0, 63.0, w - 5.0, (bottom - 88.0).max(1.0));
let mut input = state.input.clone();
if state.focus == Some(Focus::Input) {
input.insert_str(state.cursor, &state.preedit);
}
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;
let before = &state.input[..state.cursor];
let row = before.matches('\n').count() as f32;
let column = before.rsplit('\n').next().unwrap_or("");
let caret_x =
prefix + text::measure(&format!("{column}{}", state.preedit), TextStyle::code()).width;
let caret_y = (first_input as f32 + row) * ROW;
let follow = state.follow && output_changed && !state.reveal_input;
if state.reveal_input {
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),
caret_y,
);
s.x = s.x.clamp((caret_x + 24.0 - area.width).max(0.0), caret_x);
state.reveal_input = false;
}
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,
);
p.layout.input = Some(ConsoleInputLayout {
viewport: area,
origin,
});
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(
area.x,
input_top,
(area.width - 9.0).max(0.0),
input_bottom - input_top,
),
Action::Focus(Focus::Input),
);
}
if state.focus == Some(Focus::Input) {
p.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),
)));
p.fill(
Rect::new(
area.x + 8.0 + caret_x - scroll.x,
origin.y + row * ROW + 1.0,
1.0,
ROW - 2.0,
),
TEXT,
);
p.ui.list.push(DrawCommand::PopClip);
}
p.label(
x + 10.0,
bottom - 23.0,
if state.input_error.is_empty() {
"Enter: run · Shift+Enter: newline · ⌘/Ctrl+↑↓: history"
} else {
&state.input_error
},
if state.input_error.is_empty() {
MUTED
} else {
ERROR
},
false,
);
}