use crate::core::{Color, Font, Point, Rect, Size};
use crate::event::{Event, EventHandler};
use crate::render::RenderContext;
use crate::signal::GenericSignal;
use crate::widget::{BaseWidget, Draw, Widget, WidgetKind};
pub struct TextArea {
base: BaseWidget,
text: String,
cursor_pos: usize,
max_length: usize,
read_only: bool,
#[allow(dead_code)]
word_wrap: bool,
placeholder: String,
focused: bool,
pub changed: GenericSignal,
}
impl TextArea {
pub fn new(text: String, rect: Rect) -> Self {
let cursor_pos = text.len();
Self {
base: BaseWidget::new(WidgetKind::TextArea, rect, "TextArea"),
text,
cursor_pos,
max_length: 0,
read_only: false,
word_wrap: true,
placeholder: String::new(),
focused: false,
changed: GenericSignal::new(),
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn set_text(&mut self, text: String) {
if self.text == text {
return;
}
let max = self.max_length;
self.text = if max > 0 && text.len() > max { text[..max].to_string() } else { text };
self.cursor_pos = self.text.len();
self.changed.emit();
}
pub fn insert(&mut self, ch: char) {
if self.max_length > 0 && self.text.len() >= self.max_length {
return;
}
self.text.insert(self.cursor_pos, ch);
self.cursor_pos += ch.len_utf8();
self.changed.emit();
}
pub fn delete_char(&mut self) {
if self.cursor_pos == 0 {
return;
}
let prev = self.text[..self.cursor_pos].char_indices().last().map(|(i, c)| {
if c == '\n' {
(i, 1)
} else {
(i, c.len_utf8())
}
});
if let Some((start, len)) = prev {
self.text.replace_range(start..start + len, "");
self.cursor_pos = start;
self.changed.emit();
}
}
pub fn cursor_pos(&self) -> usize {
self.cursor_pos
}
pub fn set_cursor_pos(&mut self, pos: usize) {
self.cursor_pos = pos.min(self.text.len());
}
pub fn set_max_length(&mut self, max: usize) {
self.max_length = max;
if max > 0 && self.text.len() > max {
self.text.truncate(max);
self.cursor_pos = self.cursor_pos.min(max);
self.changed.emit();
}
}
pub fn is_read_only(&self) -> bool {
self.read_only
}
pub fn set_read_only(&mut self, ro: bool) {
self.read_only = ro;
}
pub fn placeholder(&self) -> &str {
&self.placeholder
}
pub fn set_placeholder(&mut self, text: String) {
self.placeholder = text;
}
}
impl Widget for TextArea {
fn base(&self) -> &BaseWidget {
&self.base
}
fn base_mut(&mut self) -> &mut BaseWidget {
&mut self.base
}
fn size_hint(&self) -> Size {
let line_count = if self.text.is_empty() {
1
} else {
self.text.chars().filter(|&c| c == '\n').count() + 1
};
let max_line_width = self.text.lines().map(|l| l.len() as u32).max().unwrap_or(0);
let w = (max_line_width * 8 + 10).max(120);
let h = (line_count as u32 * 16 + 10).max(60);
Size::new(w, h)
}
}
impl EventHandler for TextArea {
fn handle_event(&mut self, event: &Event) {
self.base.handle_event(event);
if !self.base.is_enabled() {
return;
}
match event {
Event::FocusGained => {
self.focused = true;
self.request_redraw();
}
Event::FocusLost => {
self.focused = false;
self.request_redraw();
}
Event::KeyPress { key, .. } => {
if self.read_only {
return;
}
match *key {
8 => {
self.delete_char();
self.request_redraw();
}
13 => {
self.insert('\n');
self.request_redraw();
}
37 => {
if self.cursor_pos > 0 {
self.cursor_pos -= 1;
self.request_redraw();
}
}
39 => {
if self.cursor_pos < self.text.len() {
self.cursor_pos += 1;
self.request_redraw();
}
}
_ => {
if let Some(ch) = char::from_u32(*key) {
if ch.is_ascii_graphic() || ch == ' ' {
self.insert(ch);
self.request_redraw();
}
}
}
}
}
_ => {}
}
}
}
const CHAR_W: i32 = 8;
const LINE_H: i32 = 16;
impl Draw for TextArea {
fn draw(&mut self, context: &mut RenderContext) {
let rect = self.geometry();
let padding = 4;
let bg = self.style().background_color.unwrap_or(Color::from_rgb(255, 255, 255));
context.fill_rect(rect, bg);
let border = self.style().border_color.unwrap_or(Color::from_rgb(200, 200, 200));
context.draw_rect(rect, border);
let text_color = self.style().text_color.unwrap_or(Color::from_rgb(0, 0, 0));
let placeholder_color = Color::from_rgb(180, 180, 180);
if self.text.is_empty() && !self.placeholder.is_empty() && !self.focused {
context.draw_text(
Point::new(rect.x + padding, rect.y + padding),
&self.placeholder,
&Font::default(),
placeholder_color,
);
} else if !self.text.is_empty() {
let mut y = rect.y + padding;
for line in self.text.lines() {
if y + LINE_H > rect.y + rect.height as i32 {
break;
}
context.draw_text(
Point::new(rect.x + padding, y),
line,
&Font::default(),
text_color,
);
y += LINE_H;
}
if self.text.ends_with('\n') {
}
}
if self.focused {
let cursor_x = self.cursor_screen_x(rect.x + padding);
let cursor_y = self.cursor_screen_y(rect.y + padding);
context.draw_line(
Point::new(cursor_x, cursor_y),
Point::new(cursor_x, cursor_y + LINE_H),
text_color,
);
}
}
}
impl TextArea {
fn cursor_screen_x(&self, origin_x: i32) -> i32 {
let text_before = &self.text[..self.cursor_pos];
let line_start = text_before.rfind('\n').map(|i| i + 1).unwrap_or(0);
let col = self.text[line_start..self.cursor_pos].len();
origin_x + col as i32 * CHAR_W
}
fn cursor_screen_y(&self, origin_y: i32) -> i32 {
let text_before = &self.text[..self.cursor_pos];
let lines_before = text_before.chars().filter(|&c| c == '\n').count();
origin_y + lines_before as i32 * LINE_H
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::Rect;
#[test]
fn textarea_creation_defaults() {
let ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
assert_eq!(ta.text(), "");
assert_eq!(ta.cursor_pos(), 0);
assert_eq!(ta.max_length, 0);
assert!(!ta.is_read_only());
assert!(ta.placeholder().is_empty());
assert!(!ta.focused);
}
#[test]
fn textarea_set_text() {
let mut ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
ta.set_text("Hello\nWorld".to_string());
assert_eq!(ta.text(), "Hello\nWorld");
assert_eq!(ta.cursor_pos(), 11); }
#[test]
fn textarea_insert_char() {
let mut ta = TextArea::new("Helo".to_string(), Rect::new(0, 0, 300, 200));
ta.set_cursor_pos(3);
ta.insert('l');
assert_eq!(ta.text(), "Hello");
assert_eq!(ta.cursor_pos(), 4);
}
#[test]
fn textarea_delete_char() {
let mut ta = TextArea::new("Hello".to_string(), Rect::new(0, 0, 300, 200));
ta.set_cursor_pos(5);
ta.delete_char();
assert_eq!(ta.text(), "Hell");
assert_eq!(ta.cursor_pos(), 4);
}
#[test]
fn textarea_delete_char_at_start() {
let mut ta = TextArea::new("Hello".to_string(), Rect::new(0, 0, 300, 200));
ta.set_cursor_pos(0);
ta.delete_char();
assert_eq!(ta.text(), "Hello");
assert_eq!(ta.cursor_pos(), 0);
}
#[test]
fn textarea_delete_char_with_newline() {
let mut ta = TextArea::new("A\nB".to_string(), Rect::new(0, 0, 300, 200));
ta.set_cursor_pos(3);
ta.delete_char();
assert_eq!(ta.text(), "A\n");
assert_eq!(ta.cursor_pos(), 2);
}
#[test]
fn textarea_cursor_movement() {
let mut ta = TextArea::new("Hi".to_string(), Rect::new(0, 0, 300, 200));
assert_eq!(ta.cursor_pos(), 2);
ta.set_cursor_pos(0);
assert_eq!(ta.cursor_pos(), 0);
ta.set_cursor_pos(5); assert_eq!(ta.cursor_pos(), 2);
}
#[test]
fn textarea_placeholder() {
let mut ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
assert!(ta.placeholder().is_empty());
ta.set_placeholder("Enter text...".to_string());
assert_eq!(ta.placeholder(), "Enter text...");
}
#[test]
fn textarea_read_only() {
let mut ta = TextArea::new("Readable".to_string(), Rect::new(0, 0, 300, 200));
assert!(!ta.is_read_only());
ta.set_read_only(true);
assert!(ta.is_read_only());
}
#[test]
fn textarea_max_length() {
let mut ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
ta.set_max_length(5);
assert_eq!(ta.max_length, 5);
ta.set_text("Hello World".to_string());
assert_eq!(ta.text().len(), 5);
assert_eq!(ta.text(), "Hello");
}
#[test]
fn textarea_draw_does_not_panic() {
use crate::render::RenderContext;
use crate::render::SoftwarePaintBackend;
let mut ta = TextArea::new("Line1\nLine2".to_string(), Rect::new(0, 0, 200, 100));
let mut backend = SoftwarePaintBackend::new(Size::new(200, 100), 1.0);
let mut ctx = RenderContext::new(&mut backend);
ta.draw(&mut ctx);
}
#[test]
fn textarea_set_text_truncates_on_max_length() {
let mut ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
ta.set_max_length(3);
ta.set_text("Hello".to_string());
assert_eq!(ta.text(), "Hel");
assert_eq!(ta.cursor_pos(), 3);
}
#[test]
fn textarea_insert_respects_max_length() {
let mut ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
ta.set_max_length(3);
ta.set_text("ABC".to_string());
ta.insert('D');
assert_eq!(ta.text(), "ABC");
assert_eq!(ta.cursor_pos(), 3);
}
#[test]
fn textarea_empty_text_events_no_panic() {
let mut ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
ta.handle_event(&Event::KeyPress { key: 8, modifiers: 0 }); assert_eq!(ta.text(), "");
ta.handle_event(&Event::KeyPress { key: 37, modifiers: 0 }); assert_eq!(ta.cursor_pos(), 0);
ta.handle_event(&Event::KeyPress { key: 39, modifiers: 0 }); assert_eq!(ta.cursor_pos(), 0);
}
#[test]
fn textarea_focus_events() {
let mut ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
assert!(!ta.focused);
ta.handle_event(&Event::FocusGained);
assert!(ta.focused);
ta.handle_event(&Event::FocusLost);
assert!(!ta.focused);
}
#[test]
fn textarea_keypress_insertion() {
let mut ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
ta.handle_event(&Event::KeyPress { key: 72, modifiers: 0 }); ta.handle_event(&Event::KeyPress { key: 105, modifiers: 0 }); assert_eq!(ta.text(), "Hi");
}
#[test]
fn textarea_keypress_enter_inserts_newline() {
let mut ta = TextArea::new(String::new(), Rect::new(0, 0, 300, 200));
ta.handle_event(&Event::KeyPress { key: 65, modifiers: 0 }); ta.handle_event(&Event::KeyPress { key: 13, modifiers: 0 }); ta.handle_event(&Event::KeyPress { key: 66, modifiers: 0 }); assert_eq!(ta.text(), "A\nB");
}
}