use crossterm::style::Color;
use crate::{
graphics::char_width,
layout::{LayoutBox, Rect},
tdom::node::NodeArena,
};
use super::{
PaintCanvas, SELECTION_BG, caret_cell, cell_index, control_background_at, control_clip,
paint_control_chrome, put,
};
pub(crate) struct TextareaLine {
pub char_start: usize,
pub text: String,
}
pub(crate) fn textarea_visual_lines(value: &str, content_width: usize) -> Vec<TextareaLine> {
let cw = content_width.max(1);
let mut out = Vec::new();
let mut char_idx = 0usize;
for logical in value.split('\n') {
let chars: Vec<char> = logical.chars().collect();
let mut line_start = 0usize;
let mut width = 0usize;
for (i, &c) in chars.iter().enumerate() {
let w = char_width(c).max(1);
if width + w > cw && i > line_start {
out.push(TextareaLine {
char_start: char_idx.saturating_add(line_start),
text: chars[line_start..i].iter().collect(),
});
line_start = i;
width = 0;
}
width += w;
}
out.push(TextareaLine {
char_start: char_idx.saturating_add(line_start),
text: chars[line_start..].iter().collect(),
});
char_idx = char_idx
.saturating_add(logical.chars().count())
.saturating_add(1);
}
out
}
pub(crate) fn textarea_caret_position(
value: &str,
content_width: usize,
cursor: usize,
) -> (usize, usize) {
let lines = textarea_visual_lines(value, content_width);
for (row, line) in lines.iter().enumerate() {
let end = line.char_start.saturating_add(line.text.chars().count());
if cursor <= end {
let col: usize = line
.text
.chars()
.take(cursor.saturating_sub(line.char_start))
.map(|c| char_width(c).max(1))
.sum();
return (row, col);
}
}
(lines.len().saturating_sub(1), 0)
}
pub(crate) fn textarea_char_at(
value: &str,
content_width: usize,
row: usize,
col_cells: usize,
) -> usize {
let lines = textarea_visual_lines(value, content_width);
let Some(line) = lines.get(row) else {
return value.chars().count();
};
let mut w = 0usize;
for (i, c) in line.text.chars().enumerate() {
if w >= col_cells {
return line.char_start.saturating_add(i);
}
w += char_width(c).max(1);
}
line.char_start.saturating_add(line.text.chars().count())
}
pub(crate) fn textarea_scroll_row(
value: &str,
content_width: usize,
rows: usize,
cursor: usize,
) -> usize {
let (caret_row, _) = textarea_caret_position(value, content_width, cursor);
let rows = rows.max(1);
if caret_row.saturating_add(1) > rows {
caret_row.saturating_add(1).saturating_sub(rows)
} else {
0
}
}
pub(crate) fn paint_textarea(
lb: &LayoutBox,
arena: &NodeArena,
canvas: &mut PaintCanvas<'_>,
clip: Rect,
) {
let Some(r) = control_clip(lb, canvas, clip) else {
return;
};
let node = &arena[lb.node_id];
let (value, cursor, focused, selection) = match canvas.input_states.get(&lb.node_id) {
Some(st) => (st.value.clone(), st.cursor, st.focused, st.selection),
None => (node.text_content(arena).into(), 0, false, None),
};
let fg = node.css_attr.get_color();
let (content, background) = paint_control_chrome(lb, arena, canvas, r, fg);
let content_width = content.width.max(1);
let content_rows = content.height.max(1);
let lines = textarea_visual_lines(&value, content_width);
let scroll = textarea_scroll_row(&value, content_width, content_rows, cursor);
let (caret_row, caret_col) = textarea_caret_position(&value, content_width, cursor);
for vis_row in 0..content_rows {
let y = content.y.saturating_add(vis_row);
if y < r.y || y >= r.y.saturating_add(r.height) {
continue;
}
let Some(line) = lines.get(scroll.saturating_add(vis_row)) else {
continue;
};
let line_row = scroll.saturating_add(vis_row);
let mut col = 0usize;
for (i, c) in line.text.chars().enumerate() {
let w = char_width(c).max(1);
let global_char = line.char_start.saturating_add(i);
let is_caret = focused && line_row == caret_row && col == caret_col;
for sub in 0..w {
let abs_col = content.x.saturating_add(col).saturating_add(sub);
if abs_col >= content.x.saturating_add(content_width) {
break;
}
if abs_col < r.x || abs_col >= r.x.saturating_add(r.width) {
continue;
}
let sampled_bg = control_background_at(&background, abs_col, y, r);
let (cell_fg, cell_bg) = if is_caret {
caret_cell(node, fg, sampled_bg)
} else if selection.is_some_and(|(s, e)| global_char >= s && global_char < e) {
(Some(Color::White), Some(SELECTION_BG))
} else {
(fg, sampled_bg)
};
put(
canvas,
cell_index(canvas, abs_col, y),
if sub == 0 { Some(c) } else { None },
cell_fg,
cell_bg,
);
}
col += w;
}
if focused && line_row == caret_row && caret_col >= col && caret_col < content_width {
let abs_col = content.x.saturating_add(caret_col);
if abs_col < content.x.saturating_add(content_width) {
let sampled_bg = control_background_at(&background, abs_col, y, r);
let (cf, cb) = caret_cell(node, fg, sampled_bg);
put(canvas, cell_index(canvas, abs_col, y), Some(' '), cf, cb);
}
}
}
}