use ratatui::{
buffer::Buffer as TuiBuffer,
style::{Color, Modifier, Style},
};
use crate::editor::highlight::{StyledSpan, to_ratatui_style};
use unicode_width::UnicodeWidthChar;
pub(crate) const CURRENT_LINE_BG: Color = Color::Rgb(40, 40, 55);
pub(crate) const CURSOR_FG: Color = Color::Black;
pub(crate) const CURSOR_BG: Color = Color::White;
pub(crate) const BRACE_MATCH_FG: Color = Color::Yellow;
pub(crate) const BRACE_MATCH_BG: Color = Color::Rgb(80, 80, 0);
pub(crate) const GUTTER_NORMAL: Style = Style::new().fg(Color::DarkGray);
pub(crate) const GUTTER_CURRENT: Style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
pub(crate) const FOLD_OPEN_FG: Color = Color::Cyan;
pub(crate) const FOLD_CLOSED_FG: Color = Color::Yellow;
pub(crate) const FOLD_PLACEHOLDER: Style = Style::new()
.fg(Color::DarkGray)
.add_modifier(Modifier::ITALIC);
pub(crate) const MARKER_STYLE: Style = Style::new()
.fg(Color::LightRed)
.add_modifier(Modifier::BOLD);
pub(crate) const TILDE_STYLE: Style = Style::new().fg(Color::DarkGray);
const SEARCH_MATCH_BG: Color = Color::Rgb(100, 80, 0);
const SEARCH_CURRENT_BG: Color = Color::Rgb(230, 160, 0);
const SEARCH_CURRENT_FG: Color = Color::Black;
const SELECTION_BG: Color = Color::Rgb(50, 60, 100);
pub trait TextContent {
fn line_count(&self) -> usize;
fn line_text(&self, idx: usize) -> &str;
fn line_highlights(&self, idx: usize) -> Option<&[StyledSpan]>;
}
#[derive(Debug, Clone)]
pub struct GutterMarker {
pub line: usize,
pub symbol: char,
pub style: Style,
}
#[derive(Clone, Copy)]
pub enum RenderMode {
Clip { scroll_x: usize },
Wrap { max_rows: u16 },
}
pub fn gutter_width(show_line_numbers: bool, line_count: usize) -> usize {
if show_line_numbers {
let digit_w = line_count.to_string().len().max(3);
digit_w + 2
} else {
4
}
}
#[allow(clippy::too_many_arguments)]
pub fn render_line_content(
buf: &mut TuiBuffer,
x: u16,
start_y: u16,
width: usize,
line_text: &str,
spans: &[StyledSpan],
row_bg: Color,
logical: usize,
cursor_row: usize,
cursor_col: usize,
brace_match: Option<(usize, usize)>,
search_matches: &[(usize, usize, usize)],
search_current: Option<usize>,
selection_spans: &[(usize, usize, usize)],
mode: RenderMode,
tab_width: usize,
) -> usize {
let n = line_text.len();
let mut styles = vec![Style::default().fg(Color::Reset).bg(row_bg); n + 1];
for sp in spans {
let start = sp.range.start.min(n);
if start >= n {
break;
}
let end = sp.range.end.min(n);
if end <= start {
continue;
}
let sty = to_ratatui_style(&sp.style).bg(row_bg);
styles[start..end].fill(sty);
}
for &(row, start, end) in selection_spans {
if row != logical {
continue;
}
let sty = Style::default().bg(SELECTION_BG);
for s in styles.iter_mut().take(end.min(n)).skip(start) {
*s = sty;
}
}
for (idx, &(row, start, end)) in search_matches.iter().enumerate() {
if row != logical {
continue;
}
let is_current = Some(idx) == search_current;
let (fg, bg) = if is_current {
(SEARCH_CURRENT_FG, SEARCH_CURRENT_BG)
} else {
(Color::White, SEARCH_MATCH_BG)
};
let sty = Style::default().fg(fg).bg(bg);
for s in styles.iter_mut().take(end.min(n)).skip(start) {
*s = sty;
}
}
let brace_style = Style::default()
.fg(BRACE_MATCH_FG)
.bg(BRACE_MATCH_BG)
.add_modifier(Modifier::BOLD);
let cursor_style = Style::default()
.fg(CURSOR_FG)
.bg(CURSOR_BG)
.add_modifier(Modifier::BOLD);
let cursor_col_byte = line_text
.char_indices()
.nth(cursor_col)
.map(|(i, _)| i)
.unwrap_or(n);
let cursor_is_brace = logical == cursor_row
&& cursor_col_byte < n
&& matches!(
line_text.as_bytes()[cursor_col_byte],
b'{' | b'}' | b'(' | b')' | b'[' | b']'
);
if cursor_is_brace {
styles[cursor_col_byte] = brace_style;
}
if let Some((br, bc)) = brace_match
&& br == logical
&& bc < n
{
styles[bc] = brace_style;
}
match mode {
RenderMode::Clip { scroll_x } => clip_render(
buf, x, start_y, width, line_text, &styles, row_bg, n,
logical, cursor_row, cursor_col, scroll_x, cursor_style, tab_width,
),
RenderMode::Wrap { max_rows } => wrap_render(
buf, x, start_y, width, line_text, &styles, row_bg, n,
logical, cursor_row, cursor_col, max_rows, cursor_style, tab_width,
),
}
}
#[allow(clippy::too_many_arguments)]
pub fn clip_render(
buf: &mut TuiBuffer,
x: u16,
y: u16,
width: usize,
line_text: &str,
styles: &[Style],
row_bg: Color,
n: usize,
logical: usize,
cursor_row: usize,
cursor_col: usize,
scroll_x: usize,
cursor_style: Style,
tab_width: usize,
) -> usize {
let mut screen_col = 0usize;
let mut byte_idx = 0usize;
let mut char_idx = 0usize;
while screen_col < scroll_x + width {
if byte_idx >= n {
if logical == cursor_row && cursor_col >= char_idx && screen_col >= scroll_x {
let vc = screen_col - scroll_x;
buf[(x + vc as u16, y)]
.set_char(' ')
.set_style(cursor_style);
screen_col += 1;
}
break;
}
let ch = line_text[byte_idx..].chars().next().unwrap_or(' ');
if ch == '\n' || ch == '\r' {
byte_idx += ch.len_utf8();
continue;
}
if ch == '\t' {
let spaces = tab_width - (screen_col % tab_width);
let sty = if logical == cursor_row && char_idx == cursor_col {
cursor_style
} else {
styles[byte_idx]
};
for s in 0..spaces {
let sc = screen_col + s;
if sc >= scroll_x && sc < scroll_x + width {
let vc = sc - scroll_x;
buf[(x + vc as u16, y)].set_char(' ').set_style(sty);
}
}
screen_col += spaces;
char_idx += 1;
} else {
let sty = if logical == cursor_row && char_idx == cursor_col {
cursor_style
} else {
styles[byte_idx]
};
if screen_col >= scroll_x {
let vc = screen_col - scroll_x;
buf[(x + vc as u16, y)]
.set_char(ch)
.set_style(sty);
}
screen_col += 1;
char_idx += 1;
}
byte_idx += ch.len_utf8();
}
let start_pad = screen_col.saturating_sub(scroll_x).min(width);
for sc in start_pad..width {
buf[(x + sc as u16, y)].set_char(' ').set_bg(row_bg);
}
1
}
#[allow(clippy::too_many_arguments)]
pub fn wrap_render(
buf: &mut TuiBuffer,
x: u16,
start_y: u16,
width: usize,
line_text: &str,
styles: &[Style],
row_bg: Color,
n: usize,
logical: usize,
cursor_row: usize,
cursor_col: usize,
max_rows: u16,
cursor_style: Style,
tab_width: usize,
) -> usize {
let mut col_in_row = 0usize;
let mut visual_row = 0usize;
let mut byte_idx = 0usize;
let mut char_idx = 0usize;
while byte_idx < n {
if visual_row >= max_rows as usize {
break;
}
let ch = line_text[byte_idx..].chars().next().unwrap_or(' ');
if ch == '\n' || ch == '\r' {
byte_idx += ch.len_utf8();
continue;
}
let char_w = if ch == '\t' { tab_width - (col_in_row % tab_width) } else { 1 };
if col_in_row + char_w > width {
for pc in col_in_row..width {
buf[(x + pc as u16, start_y + visual_row as u16)]
.set_char(' ')
.set_bg(row_bg);
}
visual_row += 1;
col_in_row = 0;
if visual_row >= max_rows as usize {
break;
}
let char_w2 = if ch == '\t' { 4 } else { 1 };
if ch == '\t' {
let sty = if logical == cursor_row && char_idx == cursor_col {
cursor_style
} else {
styles[byte_idx]
};
for s in 0..char_w2 {
if s < width {
buf[(x + s as u16, start_y + visual_row as u16)]
.set_char(' ')
.set_style(sty);
}
}
col_in_row += char_w2;
} else {
let sty = if logical == cursor_row && char_idx == cursor_col {
cursor_style
} else {
styles[byte_idx]
};
buf[(x + col_in_row as u16, start_y + visual_row as u16)]
.set_char(ch)
.set_style(sty);
col_in_row += 1;
}
} else if ch == '\t' {
let sty = if logical == cursor_row && char_idx == cursor_col {
cursor_style
} else {
styles[byte_idx]
};
for s in 0..char_w {
if col_in_row + s < width {
buf[(x + (col_in_row + s) as u16, start_y + visual_row as u16)]
.set_char(' ')
.set_style(sty);
}
}
col_in_row += char_w;
} else {
let sty = if logical == cursor_row && char_idx == cursor_col {
cursor_style
} else {
styles[byte_idx]
};
buf[(x + col_in_row as u16, start_y + visual_row as u16)]
.set_char(ch)
.set_style(sty);
col_in_row += 1;
}
byte_idx += ch.len_utf8();
char_idx += 1;
}
if logical == cursor_row
&& cursor_col >= char_idx
&& visual_row < max_rows as usize
&& col_in_row < width
{
buf[(x + col_in_row as u16, start_y + visual_row as u16)]
.set_char(' ')
.set_style(cursor_style);
col_in_row += 1;
}
for pc in col_in_row..width {
buf[(x + pc as u16, start_y + visual_row as u16)]
.set_char(' ')
.set_bg(row_bg);
}
visual_row + 1
}
pub fn render_plain(
buf: &mut TuiBuffer,
x: u16,
y: u16,
width: usize,
text: &str,
style: Style,
bg: Color,
) {
let s = style.bg(bg);
for (ci, ch) in text.chars().enumerate() {
if ci >= width {
break;
}
buf[(x + ci as u16, y)].set_char(ch).set_style(s);
}
for ci in text.chars().count().min(width)..width {
buf[(x + ci as u16, y)].set_char(' ').set_bg(bg);
}
}
pub fn indent_level(line: &str) -> usize {
line.chars().take_while(|c| *c == ' ' || *c == '\t').count()
}
pub fn byte_to_screen_col(line_text: &str, byte_idx: usize, tab_width: usize) -> usize {
let n = line_text.len();
let byte_idx = byte_idx.min(n);
let mut col: usize = 0;
for (pos, ch) in line_text.char_indices() {
if pos >= byte_idx { break; }
if ch == '\t' {
let rem = tab_width - (col % tab_width);
col += rem;
} else {
col += UnicodeWidthChar::width(ch).unwrap_or(1);
}
}
col
}