use alloc::boxed::Box;
use alloc::string::String;
use crate::widget::Rect;
pub const DEFAULT_CHAR_WIDTH: i32 = 8;
pub const DEFAULT_LINE_HEIGHT: i32 = 16;
pub const CARET_WIDTH: i32 = 2;
pub type ChangeCallback = Box<dyn FnMut(&str)>;
pub type AcceptFn = Box<dyn Fn(char) -> bool>;
pub struct EditCore {
pub bounds: Rect,
pub buffer: String,
pub caret: usize,
pub active: bool,
pub multi_line: bool,
pub max_len: Option<usize>,
pub accept: Option<AcceptFn>,
pub on_change: Option<ChangeCallback>,
pub char_width: i32,
pub line_height: i32,
}
impl EditCore {
pub fn new(text: &str, bounds: Rect, multi_line: bool) -> Self {
let buffer = String::from(text);
let caret = buffer.chars().count();
Self {
bounds,
buffer,
caret,
active: false,
multi_line,
max_len: None,
accept: None,
on_change: None,
char_width: DEFAULT_CHAR_WIDTH,
line_height: DEFAULT_LINE_HEIGHT,
}
}
pub fn set_text(&mut self, text: &str) {
self.buffer = String::from(text);
self.caret = self.caret.min(self.buffer.chars().count());
if let Some(cb) = self.on_change.as_mut() {
cb(&self.buffer);
}
}
pub fn committed(&mut self) {
if let Some(cb) = self.on_change.as_mut() {
cb(&self.buffer);
}
}
pub fn byte_index(&self, char_index: usize) -> usize {
self.buffer
.char_indices()
.nth(char_index)
.map(|(i, _)| i)
.unwrap_or(self.buffer.len())
}
pub fn char_count(&self) -> usize {
self.buffer.chars().count()
}
pub fn try_insert(&mut self, c: char) -> bool {
let printable = ('\u{20}'..='\u{7e}').contains(&c);
if !(printable || (c == '\n' && self.multi_line)) {
return false;
}
if c != '\n'
&& let Some(accept) = self.accept.as_ref()
&& !accept(c)
{
return false;
}
if let Some(max) = self.max_len
&& self.char_count() >= max
{
return false;
}
let at = self.byte_index(self.caret);
self.buffer.insert(at, c);
self.caret += 1;
self.committed();
true
}
pub fn try_backspace(&mut self) -> bool {
if self.caret == 0 {
return false;
}
let at = self.byte_index(self.caret - 1);
self.buffer.remove(at);
self.caret -= 1;
self.committed();
true
}
pub fn handle_key(&mut self, key: &crate::event::Key) -> bool {
use crate::event::Key;
match key {
Key::Character(c) => {
self.try_insert(*c);
true }
Key::Space => {
self.try_insert(' ');
true
}
Key::Backspace => {
self.try_backspace();
true
}
Key::ArrowLeft => {
self.caret = self.caret.saturating_sub(1);
true
}
Key::ArrowRight => {
self.caret = (self.caret + 1).min(self.char_count());
true
}
_ => false,
}
}
pub fn caret_row_col(&self) -> (i32, i32) {
let mut row = 0i32;
let mut col = 0i32;
for c in self.buffer.chars().take(self.caret) {
if c == '\n' {
row += 1;
col = 0;
} else {
col += 1;
}
}
(row, col)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Key;
use crate::widget::Rect;
const BOUNDS: Rect = Rect {
x: 0,
y: 0,
width: 200,
height: 100,
};
fn core(multi: bool) -> EditCore {
EditCore::new("", BOUNDS, multi)
}
#[test]
fn insert_printable_ascii() {
let mut ec = core(false);
ec.active = true;
assert!(ec.try_insert('A'));
assert_eq!(ec.buffer, "A");
assert_eq!(ec.caret, 1);
}
#[test]
fn insert_newline_only_on_multiline() {
let mut single = core(false);
let mut multi = core(true);
assert!(!single.try_insert('\n'));
assert!(multi.try_insert('\n'));
}
#[test]
fn backspace_removes_char_and_decrements_caret() {
let mut ec = core(false);
ec.try_insert('X');
assert!(ec.try_backspace());
assert_eq!(ec.buffer, "");
assert_eq!(ec.caret, 0);
assert!(!ec.try_backspace());
}
#[test]
fn handle_key_arrow_navigation() {
let mut ec = core(false);
ec.try_insert('A');
ec.try_insert('B');
ec.handle_key(&Key::ArrowLeft);
assert_eq!(ec.caret, 1);
ec.handle_key(&Key::ArrowLeft);
assert_eq!(ec.caret, 0);
ec.handle_key(&Key::ArrowLeft);
assert_eq!(ec.caret, 0);
ec.handle_key(&Key::ArrowRight);
assert_eq!(ec.caret, 1);
ec.handle_key(&Key::ArrowRight);
ec.handle_key(&Key::ArrowRight);
assert_eq!(ec.caret, 2, "clamped at end");
}
#[test]
fn max_len_gate() {
let mut ec = core(false);
ec.max_len = Some(2);
ec.try_insert('A');
ec.try_insert('B');
assert!(!ec.try_insert('C'));
assert_eq!(ec.buffer, "AB");
}
#[test]
fn accept_gate() {
let mut ec = core(false);
ec.accept = Some(Box::new(|c: char| c.is_ascii_digit()));
assert!(ec.try_insert('5'));
assert!(!ec.try_insert('x'));
assert_eq!(ec.buffer, "5");
}
#[test]
fn set_text_bypasses_ascii_gate_and_fires_callback() {
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec::Vec;
use core::cell::RefCell;
let log: Rc<RefCell<Vec<String>>> = Rc::new(RefCell::new(Vec::new()));
let log2 = log.clone();
let mut ec = core(false);
ec.on_change = Some(Box::new(move |s| log2.borrow_mut().push(String::from(s))));
ec.set_text("héllo");
assert_eq!(ec.buffer, "héllo");
assert_eq!(log.borrow()[0], "héllo");
}
#[test]
fn caret_row_col_multiline() {
let mut ec = core(true);
ec.try_insert('A');
ec.try_insert('B');
ec.try_insert('\n');
ec.try_insert('C');
assert_eq!(ec.caret_row_col(), (1, 1));
}
#[test]
fn byte_index_is_char_aware() {
let mut ec = core(false);
ec.set_text("abc");
assert_eq!(ec.byte_index(0), 0);
assert_eq!(ec.byte_index(1), 1);
assert_eq!(ec.byte_index(3), 3);
}
}