extern crate self as escriba_render;
pub mod gpu;
pub mod langs;
pub mod splash;
pub use gpu::{GpuRenderer, SharedState};
pub use splash::render_splash_ansi;
use escriba_runtime::EditorState;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum RenderTarget {
Text,
Gpu,
}
pub trait Renderer {
fn render_frame(&mut self, state: &EditorState) -> String;
}
pub struct TextRenderer;
impl Renderer for TextRenderer {
fn render_frame(&mut self, state: &EditorState) -> String {
let Some(win) = state.layout.active_window() else {
return "<no window>\n".to_string();
};
let Some(buf) = state.buffers.get(win.buffer_id) else {
return "<no buffer>\n".to_string();
};
let chrome = state.chrome();
let cursor = state.cursor();
let world = state.world();
let line_count = buf.line_count();
let gutter_cols = escriba_ui::gutter::gutter_width(line_count);
let mut out = String::new();
let top = win.viewport.top_line;
let left = win.viewport.left_column as usize;
let vis_cols = (win.viewport.visible_columns as usize).saturating_sub(gutter_cols);
let height = win.viewport.visible_lines.max(10);
for row in 0..height {
let ln = top + row;
if ln >= line_count {
break;
}
let line = buf.line(ln).unwrap_or_default();
let line = line.trim_end_matches('\n').trim_end_matches('\r');
let mark = state.results.worst_on_line(&world, state.active, ln);
for cell in escriba_ui::gutter::gutter_cells(ln, mark, line_count) {
match cell.role {
escriba_ui::gutter::GutterRole::Mark(sev) => {
push_fg(&mut out, escriba_ui::chrome::severity_color(&chrome, sev));
out.push_str(&cell.text);
out.push_str(RESET);
}
_ => out.push_str(&cell.text),
}
}
let visible: Vec<char> = line.chars().skip(left).take(vis_cols).collect();
if ln == cursor.line && cursor.column as usize >= left {
let rel = cursor.column as usize - left;
out.extend(visible.iter().take(rel));
out.push_str(INVERT);
out.push(visible.get(rel).copied().unwrap_or(' '));
out.push_str(RESET);
out.extend(visible.iter().skip(rel + 1));
} else {
out.extend(visible.iter());
}
out.push('\n');
}
out.push_str(INVERT);
out.push_str(" escriba · ");
out.push_str(&chrome.info.hex());
out.push_str(" · ");
out.push_str(&(cursor.line + 1).to_string());
out.push(':');
out.push_str(&(cursor.column + 1).to_string());
out.push(' ');
out.push_str(RESET);
out.push('\n');
out
}
}
const INVERT: &str = "\x1b[7m";
const RESET: &str = "\x1b[0m";
fn push_fg(out: &mut String, c: ishou_tokens::Rgb) {
use std::fmt::Write as _;
let _ = write!(out, "\x1b[38;2;{};{};{}m", c.r, c.g, c.b);
}
#[cfg(test)]
mod tests {
use super::*;
use escriba_buffer::BufferSet;
fn editor(text: &str) -> EditorState {
let mut bufs = BufferSet::new();
let id = bufs.scratch(text);
let mut st = EditorState::new_with_buffer(bufs, id);
st.dismiss_splash();
if let Some(w) = st.layout.windows.first_mut() {
w.viewport.visible_lines = 20;
w.viewport.visible_columns = 80;
}
st
}
#[test]
fn renders_buffer_lines() {
let mut st = editor("hello\nworld\nfoo");
st.on_key(&escriba_keymap::Key::Char('j'));
st.on_key(&escriba_keymap::Key::Char('j'));
let frame = TextRenderer.render_frame(&st);
assert!(frame.contains("hello"), "{frame:?}");
assert!(frame.contains("world"), "{frame:?}");
}
#[test]
fn cursor_is_highlighted() {
let frame = TextRenderer.render_frame(&editor("hello world"));
assert!(frame.contains(INVERT), "{frame:?}");
}
#[test]
fn the_cursor_is_drawn_where_the_editor_actually_has_it() {
let mut st = editor("alpha\nbravo\ncharlie\n");
st.on_key(&escriba_keymap::Key::Char('j'));
st.on_key(&escriba_keymap::Key::Char('j'));
assert_eq!(st.cursor().line, 2, "precondition: the cursor moved");
let frame = TextRenderer.render_frame(&st);
assert!(
frame.contains("3:1"),
"the status line must report the REAL cursor: {frame:?}",
);
let body: Vec<&str> = frame.lines().filter(|l| l.contains('\u{2502}')).collect();
let cursor_row = body
.iter()
.position(|l| l.contains(INVERT))
.expect("the cursor is painted on some line");
assert_eq!(cursor_row, 2, "{frame:?}");
}
#[test]
fn the_gutter_matches_the_shared_model() {
let st = editor("one\ntwo\n");
let frame = TextRenderer.render_frame(&st);
let first = frame.lines().next().expect("a line");
let rule = first
.chars()
.position(|c| c == '\u{2502}')
.expect("the gutter rule");
assert_eq!(rule + 2, escriba_ui::gutter::gutter_width(2), "{first:?}");
}
#[test]
fn the_status_line_reports_the_theme_the_editor_paints() {
let st = editor("x");
let frame = TextRenderer.render_frame(&st);
assert!(
frame.contains(&st.chrome().info.hex()),
"{frame:?} should carry the editor's own accent",
);
}
}