//! 开发工具画面的绘制辅助:文字行、按钮、分隔线和滚动条。 //! 绘制命令与可交互区域同步登记,保证屏幕上的管理控件能用同一几何命中。 //! use super::*; /// 开发工具的绘制助手,把布局结果写入同帧显示列表。 pub(super) struct Painter { pub(super) ui: Ui, 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::()) .collect::>(); 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, pub(super) selected: bool, pub(super) action: Option, } #[derive(Clone, Copy)] /// 管理画面内分隔条方向/位置描述。 pub(super) enum Divider { Accent, Neutral, } impl Line { /// 构造本类型并初始化其状态;资源建立和默认字段以函数体为准。 pub(super) fn new(value: impl Into, color: Color) -> Self { Self { value: value.into(), color, divider: None, selected: false, action: None, } } } /// 按行绘制工具文本,返回后续布局需要的占用范围。 pub(super) fn multiline(out: &mut Vec, 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, body: &str, color: Color) { if let Ok(value) = serde_json::from_str::(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, 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 }, ); } }