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, ); }