use crossterm::style::Color;
use crate::{
graphics::{FontStyle, char_width},
layout::{LayoutBox, Rect},
tdom::node::NodeArena,
};
use super::button::paint_button;
use super::{
PaintCanvas, cell_index, clamp_rect_to_canvas, intersect_rects, paint_control_chrome, put,
};
const SELECTION_BG: Color = Color::Rgb {
r: 0x3b,
g: 0x42,
b: 0x61,
};
pub(crate) fn value_window(value: &str, caret: usize, inner_width: usize) -> (usize, usize) {
let v: Vec<char> = value.chars().collect();
let total = v.len();
let caret = caret.min(total);
let widths: Vec<usize> = v.iter().map(|&c| char_width(c).max(1)).collect();
let caret_w: usize = widths.get(caret).copied().unwrap_or(1);
let mut start = caret;
let mut before_w = 0usize;
while start > 0
&& before_w
.saturating_add(widths[start - 1])
.saturating_add(caret_w)
<= inner_width
{
before_w += widths[start - 1];
start -= 1;
}
let mut end = caret.saturating_add(1).min(total);
let mut used = before_w.saturating_add(caret_w);
while end < total && used.saturating_add(widths[end]) <= inner_width {
used += widths[end];
end += 1;
}
(start, end)
}
pub(crate) fn paint_input(
lb: &LayoutBox,
arena: &NodeArena,
canvas: &mut PaintCanvas<'_>,
clip: Rect,
) {
let r = lb.rect;
let canvas_cols = canvas.columns as usize;
if r.x >= canvas_cols || r.width == 0 || r.height == 0 {
return;
}
let Some(r) = clamp_rect_to_canvas(r, canvas_cols) else {
return;
};
let Some(visible) = intersect_rects(Rect::new(r.x, r.y, r.width, r.height), clip) else {
return;
};
let node = &arena[lb.node_id];
let input_type = node
.get_type()
.map(|t| t.to_ascii_lowercase())
.unwrap_or_default();
if node.input_button_label().is_some() {
paint_button(lb, arena, canvas, clip);
return;
}
let fg = node.css_attr.get_color();
if matches!(input_type.as_str(), "checkbox" | "radio") {
let checked = node.attributes.contains_key("checked");
let disabled = node.attributes.contains_key("disabled");
let glyph = match (input_type.as_str(), checked) {
("checkbox", true) => '\u{25A0}', ("checkbox", false) => '\u{25A1}', ("radio", true) => '\u{25CF}', _ => '\u{25CB}', };
let toggle_fg = if disabled {
Color::DarkGrey
} else {
fg.unwrap_or(Color::White)
};
put_bare(
canvas,
cell_index(canvas, r.x, r.y),
Some(glyph),
Some(toggle_fg),
);
put_bare(
canvas,
cell_index(canvas, r.x.saturating_add(1), r.y),
Some(' '),
Some(toggle_fg),
);
return;
}
let content = paint_control_chrome(lb, arena, canvas, r, fg);
let bg = node
.css_attr
.get_bg_color()
.or(Some(super::CONTROL_DEFAULT_BG));
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.attributes.get("value").cloned().unwrap_or_default(),
0,
false,
None,
),
};
let placeholder = node.attributes.get("placeholder").cloned();
let placeholder = placeholder.as_deref().filter(|p| !p.is_empty());
let showing_placeholder = value.is_empty() && !focused && placeholder.is_some();
let display: &str = match (showing_placeholder, placeholder) {
(true, Some(p)) => p,
_ => value.as_str(),
};
let disabled = node.attributes.contains_key("disabled");
let text_fg = if disabled { Some(Color::DarkGrey) } else { fg };
let caret_color = node.css_attr.get_caret_color();
let caret_cell = |fg: Option<Color>, bg: Option<Color>| match caret_color {
Some(cc) => (Some(bg.unwrap_or(Color::Black)), Some(cc)),
None => (
Some(bg.unwrap_or(Color::Black)),
Some(fg.unwrap_or(Color::White)),
),
};
let inner_width = content.width;
let inner_x = content.x;
let inner_y = content.y;
let v: Vec<char> = display.chars().collect();
let cursor = cursor.min(value.chars().count());
let (start, end) = value_window(display, cursor, inner_width);
let is_password = input_type == "password" && !showing_placeholder;
let mut col = 0usize;
for (i, &ch) in v.iter().enumerate().take(end).skip(start) {
let w = char_width(ch).max(1);
let glyph = if is_password { '\u{2022}' } else { ch };
let inverted = focused && i == cursor;
for sub in 0..w {
let abs_col = inner_x.saturating_add(col).saturating_add(sub);
if abs_col >= inner_x.saturating_add(inner_width) {
break;
}
if abs_col < visible.x || abs_col >= visible.x.saturating_add(visible.width) {
continue;
}
let (cell_fg, cell_bg) = if inverted {
caret_cell(fg, bg)
} else if selection.is_some_and(|(s, e)| i >= s && i < e) {
(Some(Color::White), Some(SELECTION_BG))
} else if showing_placeholder {
(Some(Color::DarkGrey), bg)
} else {
(text_fg, bg)
};
put(
canvas,
cell_index(canvas, abs_col, inner_y),
if sub == 0 { Some(glyph) } else { None },
cell_fg,
cell_bg,
);
}
col += w;
}
if focused && cursor == value.chars().count() && col < inner_width {
let abs_col = inner_x.saturating_add(col);
if abs_col < inner_x.saturating_add(inner_width) {
let (cf, cb) = caret_cell(fg, bg);
put(
canvas,
cell_index(canvas, abs_col, inner_y),
Some(' '),
cf,
cb,
);
}
}
}
fn put_bare(canvas: &mut PaintCanvas<'_>, idx: usize, word: Option<char>, fg: Option<Color>) {
if idx >= canvas.layout.len() || !canvas.in_region(idx) {
return;
}
let cell = &mut canvas.layout[idx];
cell.word.clear();
if let Some(c) = word {
cell.word.push(c);
}
cell.fcolor = fg;
cell.style = FontStyle::empty();
}