use std::rc::Rc;
use crate::base::{Point, Rgba};
use crate::layout::Style as LayoutStyle;
use crate::render::rich::RichLine;
use crate::render::Style;
use crate::text::{CLikeLexer, Highlighter, TokenKind};
use crate::theme::TokenSet;
use crate::ui::Element;
pub fn code_token_color(kind: TokenKind, t: &TokenSet) -> Rgba {
match kind {
TokenKind::Keyword => t.syntax_keyword,
TokenKind::String => t.syntax_string,
TokenKind::Number => t.syntax_number,
TokenKind::Comment => t.syntax_comment,
TokenKind::Punct => t.syntax_punct,
TokenKind::Ident => t.text,
}
}
pub struct CodeView {
source: String,
lexer: Rc<dyn Highlighter>,
line_numbers: bool,
scroll_offset: i32,
layout: Option<LayoutStyle>,
}
impl CodeView {
pub fn new(source: impl Into<String>) -> CodeView {
CodeView {
source: source.into(),
lexer: Rc::new(CLikeLexer::default()),
line_numbers: true,
scroll_offset: 0,
layout: None,
}
}
pub fn lexer(mut self, lexer: impl Highlighter + 'static) -> CodeView {
self.lexer = Rc::new(lexer);
self
}
pub fn line_numbers(mut self, on: bool) -> CodeView {
self.line_numbers = on;
self
}
pub fn scroll_offset(mut self, lines: i32) -> CodeView {
self.scroll_offset = lines.max(0);
self
}
pub fn layout(mut self, layout: LayoutStyle) -> CodeView {
self.layout = Some(layout);
self
}
pub fn line_count(source: &str) -> usize {
source.lines().count()
}
pub fn element(self, t: &TokenSet) -> Element {
let tokens = *t;
let ground = t.surface_raised;
let gutter_fg = t.text_faint;
let rule_fg = t.border;
let base = Style::new().fg(t.text);
let lexer = self.lexer;
let line_numbers = self.line_numbers;
let offset = self.scroll_offset as usize;
let lines: Vec<String> = self.source.lines().map(|s| s.to_string()).collect();
let layout = self
.layout
.unwrap_or_else(|| LayoutStyle::default().grow(1.0));
Element::new().style(layout).draw(move |canvas, rect| {
if rect.w <= 0 || rect.h <= 0 {
return;
}
canvas.fill(rect, ' ', tokens.text, ground);
let total = lines.len();
let gutter_w = if line_numbers {
(digits(total) + 1).min(rect.w - 2)
} else {
0
};
let offset = offset.min(total.saturating_sub(1));
for (row, line) in lines.iter().skip(offset).enumerate() {
let y = rect.y + row as i32;
if y >= rect.bottom() {
break;
}
let mut x = rect.x;
if gutter_w > 0 {
let num = format!("{:>w$} ", offset + row + 1, w = (gutter_w - 1) as usize);
x += canvas.print(Point::new(x, y), &num, gutter_fg, ground);
canvas.put(Point::new(x, y), '│', rule_fg, ground);
x += 2;
}
let rich = RichLine::from_highlighted(line, &*lexer, base, |kind| {
Style::new().fg(code_token_color(kind, &tokens))
});
for span in &rich.spans {
x += crate::widgets::richtext::print_span_clipped(
canvas,
x,
y,
rect.right(),
&span.text,
&span.style,
);
if x >= rect.right() {
break;
}
}
}
})
}
}
fn digits(n: usize) -> i32 {
n.max(1).ilog10() as i32 + 1
}
#[cfg(test)]
mod tests {
use super::*;
use crate::base::Size;
use crate::theme::default_theme;
use crate::widgets::test_util::{draw_into, row};
const SRC: &str = "fn main() {\n // greet\n print(\"hi\", 42);\n}";
fn cell_of(row: &str, needle: &str) -> i32 {
let byte = row.find(needle).unwrap();
row[..byte].chars().count() as i32
}
#[test]
fn token_kinds_map_to_syntax_inks() {
let t = default_theme().tokens;
assert_eq!(code_token_color(TokenKind::Keyword, &t), t.syntax_keyword);
assert_eq!(code_token_color(TokenKind::String, &t), t.syntax_string);
assert_eq!(code_token_color(TokenKind::Number, &t), t.syntax_number);
assert_eq!(code_token_color(TokenKind::Comment, &t), t.syntax_comment);
assert_eq!(code_token_color(TokenKind::Punct, &t), t.syntax_punct);
assert_eq!(code_token_color(TokenKind::Ident, &t), t.text);
}
#[test]
fn renders_highlighted_cells_on_the_code_ground() {
let t = default_theme().tokens;
let c = draw_into(CodeView::new(SRC).element(&t), Size::new(28, 4));
assert!(row(&c, 0).starts_with("1 │ fn main"), "{:?}", row(&c, 0));
assert_eq!(c.cell(Point::new(0, 0)).unwrap().1, t.text_faint);
assert_eq!(c.cell(Point::new(2, 0)).unwrap().1, t.border);
let (ch, fg, bg) = c.cell(Point::new(4, 0)).unwrap();
assert_eq!(ch, 'f');
assert_eq!(fg, t.syntax_keyword);
assert_eq!(bg, t.surface_raised);
let comment_x = cell_of(&row(&c, 1), "//");
assert_eq!(
c.cell(Point::new(comment_x, 1)).unwrap().1,
t.syntax_comment
);
let quote_x = cell_of(&row(&c, 2), "\"");
assert_eq!(c.cell(Point::new(quote_x, 2)).unwrap().1, t.syntax_string);
let num_x = cell_of(&row(&c, 2), "42");
assert_eq!(c.cell(Point::new(num_x, 2)).unwrap().1, t.syntax_number);
}
#[test]
fn scroll_clamps_and_numbers_track() {
let t = default_theme().tokens;
let c = draw_into(
CodeView::new(SRC).scroll_offset(2).element(&t),
Size::new(24, 2),
);
assert!(row(&c, 0).starts_with("3 │"), "{:?}", row(&c, 0));
let c = draw_into(
CodeView::new(SRC).scroll_offset(999).element(&t),
Size::new(24, 2),
);
assert!(row(&c, 0).starts_with("4 │"), "{:?}", row(&c, 0));
assert_eq!(CodeView::line_count(SRC), 4);
}
#[test]
fn no_gutter_mode_and_tiny_rects() {
let t = default_theme().tokens;
let c = draw_into(
CodeView::new(SRC).line_numbers(false).element(&t),
Size::new(16, 2),
);
assert!(row(&c, 0).starts_with("fn main"), "{:?}", row(&c, 0));
for size in [Size::new(0, 0), Size::new(2, 1), Size::new(5, 1)] {
let _ = draw_into(CodeView::new(SRC).element(&t), size);
}
}
}