use crate::render::{RenderContext, TextAlign, TextBaseline};
use crate::types::{Rect, WidgetState};
use super::settings::TextInputSettings;
use super::types::InputType;
fn rgba_to_hex(c: [u8; 4]) -> String {
if c[3] == 255 {
format!("#{:02X}{:02X}{:02X}", c[0], c[1], c[2])
} else {
format!("#{:02X}{:02X}{:02X}{:02X}", c[0], c[1], c[2], c[3])
}
}
fn char_idx_to_byte_idx(s: &str, char_idx: usize) -> usize {
s.char_indices()
.nth(char_idx)
.map(|(b, _)| b)
.unwrap_or(s.len())
}
fn safe_char_slice(s: &str, char_start: usize, char_end: usize) -> &str {
let a = char_idx_to_byte_idx(s, char_start);
let b = char_idx_to_byte_idx(s, char_end);
&s[a..b]
}
pub struct InputView<'a> {
pub text: &'a str,
pub placeholder: &'a str,
pub cursor: usize,
pub selection: Option<(usize, usize)>,
pub focused: bool,
pub disabled: bool,
pub input_type: InputType,
}
#[derive(Debug, Default, Clone)]
pub struct InputResult {
pub text_rect: Rect,
pub hovered: bool,
pub cursor_x: f64,
pub cursor_y: f64,
pub cursor_height: f64,
pub char_x_positions: Vec<f64>,
}
pub fn draw_input(
ctx: &mut dyn RenderContext,
rect: Rect,
state: WidgetState,
view: &InputView,
settings: &TextInputSettings,
) -> InputResult {
let style = settings.style.as_ref();
let theme = settings.theme.as_ref();
let effective_state = if view.disabled {
WidgetState::Disabled
} else {
state
};
let (bg, border, text_color) = match effective_state {
WidgetState::Disabled => {
(theme.bg_disabled(), theme.border_normal(), theme.text_disabled())
}
_ if view.focused => {
(theme.bg_normal(), theme.border_focused(), theme.text_normal())
}
WidgetState::Hovered | WidgetState::Pressed => {
(theme.bg_normal(), theme.border_hover(), theme.text_normal())
}
_ => (theme.bg_normal(), theme.border_normal(), theme.text_normal()),
};
let radius = style.radius();
ctx.set_fill_color(&rgba_to_hex(bg));
ctx.fill_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
let border_width = if view.focused {
style.border_width_focused()
} else {
style.border_width_normal()
};
ctx.set_stroke_color(&rgba_to_hex(border));
ctx.set_stroke_width(border_width);
ctx.stroke_rounded_rect(rect.x, rect.y, rect.width, rect.height, radius);
let text_rect = rect.inset(style.padding());
ctx.set_font(&format!("{}px sans-serif", style.font_size()));
ctx.set_text_align(TextAlign::Left);
ctx.set_text_baseline(TextBaseline::Middle);
let text_y = rect.center_y();
let display_text = view.text;
let char_count = display_text.chars().count();
let available_width = text_rect.width.max(0.0);
let text_width = if display_text.is_empty() {
0.0
} else {
ctx.measure_text(display_text)
};
let safe_cursor = view.cursor.min(char_count);
let text_before_cursor = safe_char_slice(display_text, 0, safe_cursor);
let cursor_offset_from_text_start = ctx.measure_text(text_before_cursor);
let cursor_margin = style.cursor_margin();
let scroll_offset_x = if text_width <= available_width {
0.0
} else {
let ideal = cursor_offset_from_text_start - (available_width - cursor_margin);
let max_scroll = (text_width - available_width).max(0.0);
ideal.max(0.0).min(max_scroll)
};
if !view.disabled {
if let Some((sel_start, sel_end)) = view.selection {
let s = sel_start.min(char_count);
let e = sel_end.min(char_count);
if s != e {
let before = safe_char_slice(display_text, 0, s);
let selected = safe_char_slice(display_text, s, e);
let sel_x = text_rect.x - scroll_offset_x + ctx.measure_text(before);
let sel_w = ctx.measure_text(selected);
ctx.save();
ctx.clip_rect(text_rect.x, rect.y, available_width, rect.height);
ctx.set_fill_color(&rgba_to_hex(theme.selection()));
ctx.fill_rect(sel_x, rect.y, sel_w, rect.height);
ctx.restore();
}
}
}
ctx.save();
ctx.clip_rect(text_rect.x, rect.y, available_width, rect.height);
if display_text.is_empty() && !view.placeholder.is_empty() {
ctx.set_fill_color(&rgba_to_hex(theme.placeholder()));
ctx.fill_text(view.placeholder, text_rect.x, text_y);
} else {
ctx.set_fill_color(&rgba_to_hex(text_color));
ctx.fill_text(display_text, text_rect.x - scroll_offset_x, text_y);
}
ctx.restore();
let cursor_x = text_rect.x - scroll_offset_x + cursor_offset_from_text_start;
let cursor_height = style.font_size() * 1.2;
let cursor_y = text_y - cursor_height / 2.0;
let mut char_x_positions = Vec::with_capacity(char_count + 1);
for i in 0..=char_count {
let slice = safe_char_slice(display_text, 0, i);
char_x_positions.push(text_rect.x - scroll_offset_x + ctx.measure_text(slice));
}
InputResult {
text_rect,
hovered: effective_state.is_hovered(),
cursor_x,
cursor_y,
cursor_height,
char_x_positions,
}
}
pub fn draw_input_cursor(
ctx: &mut dyn RenderContext,
cursor_x: f64,
cursor_y: f64,
cursor_height: f64,
width: f64,
color: [u8; 4],
) {
ctx.set_fill_color(&rgba_to_hex(color));
ctx.fill_rect(cursor_x, cursor_y, width, cursor_height);
}
pub fn cursor_from_char_positions(char_x_positions: &[f64], click_x: f64) -> usize {
if char_x_positions.is_empty() {
return 0;
}
let mut best_idx = 0;
let mut best_dist = f64::MAX;
for (i, &x) in char_x_positions.iter().enumerate() {
let d = (click_x - x).abs();
if d < best_dist {
best_dist = d;
best_idx = i;
}
}
best_idx
}