use std::borrow::Cow;
use ratatui::Frame;
use ratatui::buffer::Buffer;
use ratatui::layout::{Position, Rect};
use ratatui::style::Style;
use ratatui::text::{Line, Span};
use super::app::App;
use super::editor::{Mode, RowSelection};
use super::help;
use super::highlight::highlight_line;
use super::style;
use super::theme;
use crate::eval::format_number;
use crate::lang::parse_line;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorShape {
Bar,
Block,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CursorPlacement {
pub x: u16,
pub y: u16,
pub shape: CursorShape,
}
pub fn gutter_width(line_count: usize) -> usize {
line_count.to_string().len().max(2) + 3
}
fn text_width(s: &str) -> usize {
Span::raw(s).width()
}
pub fn draw(frame: &mut Frame, app: &App) -> Option<CursorPlacement> {
let area = frame.area();
let placement = render(app, area, frame.buffer_mut());
if let Some(p) = placement {
frame.set_cursor_position(Position::new(p.x, p.y));
}
placement
}
pub fn render(app: &App, area: Rect, buf: &mut Buffer) -> Option<CursorPlacement> {
if area.width == 0 || area.height == 0 {
return None;
}
let lines = app.lines();
let (cursor_row, cursor_col) = app.cursor();
let width = usize::from(area.width);
let gutter = gutter_width(lines.len());
let nr_width = gutter - 3;
let input_width = width * 60 / 100;
let content_width = input_width.saturating_sub(gutter);
let result_width = width - input_width;
let text_rows = usize::from(area.height - 1);
let selection = app.editor.selection();
let visible = lines
.iter()
.enumerate()
.skip(app.viewport_start)
.take(text_rows);
for (screen_row, (i, line)) in visible.enumerate() {
let y = area.y + screen_row as u16;
let x = area.x;
let number = format!("{:>nr_width$}", i + 1);
let (x, _) = buf.set_stringn(x, y, number, width, style::line_number(i == cursor_row));
let (x, _) = buf.set_stringn(x, y, " ", width, Style::new());
let (x, _) = buf.set_stringn(x, y, "│", width, style::separator());
let (x, _) = buf.set_stringn(x, y, " ", width, Style::new());
let room = (input_width.saturating_sub(usize::from(x - area.x))).min(content_width);
let parsed = match app.parsed.get(i) {
Some(p) if p.raw == *line => Cow::Borrowed(p),
_ => Cow::Owned(parse_line(line)),
};
let mut spans = highlight_line(line, &parsed);
if i == cursor_row
&& app.mode() == Mode::Insert
&& let Some(c) = &app.completion
{
spans = with_ghost(spans, line, cursor_col, c.ghost_text(), room);
}
let text = Line::from(spans);
let (text_end, _) = buf.set_line(x, y, &text, u16::try_from(room).unwrap_or(u16::MAX));
if let Some(sel) = selection.as_ref().and_then(|s| s.on_row(i)) {
paint_selection(buf, line, sel, x, text_end, room, y);
}
if let Some((text, st)) = result_text(app, i) {
render_result(buf, area, y, input_width, result_width, &text, st);
}
}
render_status_bar(app, area, buf);
if app.help {
help::render(area, buf);
return None;
}
if let Some(command) = &app.command {
return Some(command_cursor(area, command));
}
cursor_placement(app, area, gutter, cursor_row, cursor_col)
}
fn paint_selection(
buf: &mut Buffer,
line: &str,
sel: RowSelection,
x: u16,
text_end: u16,
room: usize,
y: u16,
) {
let (first, last) = match sel {
RowSelection::Whole => {
let w = u16::try_from(room).unwrap_or(u16::MAX);
buf.set_style(Rect::new(x, y, w, 1), style::visual());
return;
}
RowSelection::Chars { first, last } => (first, last),
};
let mut at = 0usize;
let mut from = None;
let mut to = 0usize;
for (col, c) in line.chars().enumerate() {
if col > last {
break;
}
let w = text_width(c.encode_utf8(&mut [0; 4]));
if col >= first {
from.get_or_insert(at);
to = at + w;
}
at += w;
}
let Some(from) = from else {
return;
};
let start = usize::from(x) + from;
let end = (usize::from(x) + to).min(usize::from(text_end));
if end > start {
let rect = Rect::new(start as u16, y, (end - start) as u16, 1);
buf.set_style(rect, style::visual());
}
}
fn with_ghost(
spans: Vec<Span<'static>>,
line: &str,
col: usize,
ghost: &str,
room: usize,
) -> Vec<Span<'static>> {
let mut avail = room.saturating_sub(text_width(line));
let ghost: String = ghost
.chars()
.take_while(|&c| {
let w = text_width(c.encode_utf8(&mut [0; 4]));
let fits = w <= avail;
avail = avail.saturating_sub(w);
fits
})
.collect();
if ghost.is_empty() {
return spans;
}
let mut out = Vec::with_capacity(spans.len() + 2);
let mut ghost = Some(Span::styled(ghost, style::ghost()));
let mut at = 0;
for span in spans {
let n = span.content.chars().count();
if ghost.is_some() && col < at + n {
let split = col - at;
let head: String = span.content.chars().take(split).collect();
let tail: String = span.content.chars().skip(split).collect();
if !head.is_empty() {
out.push(Span::styled(head, span.style));
}
out.extend(ghost.take());
out.push(Span::styled(tail, span.style));
} else {
out.push(span);
}
at += n;
}
out.extend(ghost);
out
}
fn command_cursor(area: Rect, command: &str) -> CursorPlacement {
let x = (1 + 1 + text_width(command)).min(usize::from(area.width) - 1);
CursorPlacement {
x: area.x + x as u16,
y: area.bottom() - 1,
shape: CursorShape::Bar,
}
}
fn result_text(app: &App, i: usize) -> Option<(String, Style)> {
let r = app.results.get(i)?;
if let Some(e) = &r.error {
Some((e.short(), style::error()))
} else {
r.value.map(|v| (format_number(v), style::result()))
}
}
fn render_result(
buf: &mut Buffer,
area: Rect,
y: u16,
input_width: usize,
result_width: usize,
text: &str,
st: Style,
) {
let room = result_width.saturating_sub(1);
let w = text_width(text).min(room);
let x = input_width + room - w;
buf.set_stringn(area.x + x as u16, y, text, w, st);
}
fn render_status_bar(app: &App, area: Rect, buf: &mut Buffer) {
let y = area.bottom() - 1;
let row = Rect::new(area.x, y, area.width, 1);
buf.set_style(row, style::status_bar());
let width = usize::from(area.width);
let x = area.x + 1;
let room = width.saturating_sub(1);
if let Some(command) = &app.command {
buf.set_stringn(x, y, format!(":{command}"), room, style::status_bar());
return;
}
if let Some(msg) = &app.status_msg {
buf.set_stringn(x, y, msg, room, style::status_bar());
return;
}
let (label, bg) = badge(app.mode());
let (x, _) = buf.set_stringn(x, y, format!(" {label} "), room, style::badge(bg));
let used = usize::from(x - area.x);
let rest = format!(
"{} │ total: {} │ {} lines",
file_indicator(app),
format_number(app.total()),
app.lines().len()
);
buf.set_stringn(x, y, rest, width.saturating_sub(used), style::status_bar());
}
fn badge(mode: Mode) -> (&'static str, ratatui::style::Color) {
let t = theme::active();
match mode {
Mode::Normal => ("NORMAL", t.badge_normal),
Mode::Insert => ("INSERT", t.badge_insert),
Mode::Visual => ("VISUAL", t.badge_visual),
Mode::VisualLine => ("VISUAL LINE", t.badge_visual),
Mode::VisualBlock => ("VISUAL BLOCK", t.badge_visual),
}
}
fn file_indicator(app: &App) -> String {
let modified = if app.modified { " [+]" } else { "" };
if let Some(path) = &app.file_path {
format!(" {}{modified}", path.display())
} else if app.session_mode && app.lines().iter().any(|l| !l.trim().is_empty()) {
format!(" [session]{modified}")
} else {
modified.to_string()
}
}
fn cursor_placement(
app: &App,
area: Rect,
gutter: usize,
cursor_row: usize,
cursor_col: usize,
) -> Option<CursorPlacement> {
let row = cursor_row.checked_sub(app.viewport_start)?;
if row >= usize::from(area.height - 1) {
return None;
}
let prefix: String = app.current_line().chars().take(cursor_col).collect();
let x = (gutter + text_width(&prefix)).min(usize::from(area.width) - 1);
let shape = if app.mode() == Mode::Insert {
CursorShape::Bar
} else {
CursorShape::Block
};
Some(CursorPlacement {
x: area.x + x as u16,
y: area.y + row as u16,
shape,
})
}