use astrelis_core::math::Vec2;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextCursor {
pub position: usize,
pub visual_x: f32,
pub line: usize,
pub column: usize,
}
impl TextCursor {
pub fn new() -> Self {
Self {
position: 0,
visual_x: 0.0,
line: 0,
column: 0,
}
}
pub fn at_position(position: usize) -> Self {
Self {
position,
visual_x: 0.0,
line: 0,
column: 0,
}
}
}
impl Default for TextCursor {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct TextSelection {
pub start: usize,
pub end: usize,
}
impl TextSelection {
pub fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
pub fn range(&self) -> (usize, usize) {
if self.start <= self.end {
(self.start, self.end)
} else {
(self.end, self.start)
}
}
pub fn len(&self) -> usize {
let (min, max) = self.range();
max - min
}
pub fn is_empty(&self) -> bool {
self.start == self.end
}
pub fn contains(&self, position: usize) -> bool {
let (min, max) = self.range();
position >= min && position < max
}
}
pub struct TextEditor {
text: String,
cursor: TextCursor,
selection: Option<TextSelection>,
history: Vec<String>,
history_position: usize,
}
impl TextEditor {
pub fn new(text: impl Into<String>) -> Self {
let text = text.into();
Self {
text: text.clone(),
cursor: TextCursor::new(),
selection: None,
history: vec![text],
history_position: 0,
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn cursor(&self) -> &TextCursor {
&self.cursor
}
pub fn selection(&self) -> Option<&TextSelection> {
self.selection.as_ref()
}
pub fn has_selection(&self) -> bool {
self.selection.as_ref().is_some_and(|sel| !sel.is_empty())
}
pub fn set_cursor(&mut self, position: usize) {
self.cursor.position = position.min(self.text.len());
self.clear_selection();
self.update_cursor_position();
}
pub fn move_cursor_start(&mut self) {
self.set_cursor(0);
}
pub fn move_cursor_end(&mut self) {
self.set_cursor(self.text.len());
}
pub fn move_cursor_left(&mut self) {
if self.cursor.position > 0 {
let mut pos = self.cursor.position - 1;
while pos > 0 && !self.text.is_char_boundary(pos) {
pos -= 1;
}
self.set_cursor(pos);
}
}
pub fn move_cursor_right(&mut self) {
if self.cursor.position < self.text.len() {
let mut pos = self.cursor.position + 1;
while pos < self.text.len() && !self.text.is_char_boundary(pos) {
pos += 1;
}
self.set_cursor(pos);
}
}
pub fn select(&mut self, start: usize, end: usize) {
self.selection = Some(TextSelection::new(
start.min(self.text.len()),
end.min(self.text.len()),
));
self.cursor.position = end.min(self.text.len());
}
pub fn select_all(&mut self) {
self.select(0, self.text.len());
}
pub fn clear_selection(&mut self) {
self.selection = None;
}
pub fn insert_char(&mut self, c: char) {
if self.has_selection() {
self.delete_selection();
}
self.text.insert(self.cursor.position, c);
self.cursor.position += c.len_utf8();
self.update_cursor_position();
self.push_history();
}
pub fn insert_str(&mut self, s: &str) {
if self.has_selection() {
self.delete_selection();
}
self.text.insert_str(self.cursor.position, s);
self.cursor.position += s.len();
self.update_cursor_position();
self.push_history();
}
pub fn delete_char(&mut self) {
if self.has_selection() {
self.delete_selection();
} else if self.cursor.position > 0 {
let mut pos = self.cursor.position - 1;
while pos > 0 && !self.text.is_char_boundary(pos) {
pos -= 1;
}
self.text.drain(pos..self.cursor.position);
self.cursor.position = pos;
self.update_cursor_position();
self.push_history();
}
}
pub fn delete_char_forward(&mut self) {
if self.has_selection() {
self.delete_selection();
} else if self.cursor.position < self.text.len() {
let mut pos = self.cursor.position + 1;
while pos < self.text.len() && !self.text.is_char_boundary(pos) {
pos += 1;
}
self.text.drain(self.cursor.position..pos);
self.update_cursor_position();
self.push_history();
}
}
pub fn delete_selection(&mut self) {
if let Some(sel) = self.selection {
let (start, end) = sel.range();
self.text.drain(start..end);
self.cursor.position = start;
self.clear_selection();
self.update_cursor_position();
self.push_history();
}
}
pub fn selected_text(&self) -> Option<&str> {
self.selection.as_ref().map(|sel| {
let (start, end) = sel.range();
&self.text[start..end]
})
}
pub fn replace_selection(&mut self, text: &str) {
if self.has_selection() {
self.delete_selection();
}
self.insert_str(text);
}
pub fn undo(&mut self) -> bool {
if self.history_position > 0 {
self.history_position -= 1;
self.text = self.history[self.history_position].clone();
self.cursor.position = self.cursor.position.min(self.text.len());
self.clear_selection();
self.update_cursor_position();
true
} else {
false
}
}
pub fn redo(&mut self) -> bool {
if self.history_position < self.history.len() - 1 {
self.history_position += 1;
self.text = self.history[self.history_position].clone();
self.cursor.position = self.cursor.position.min(self.text.len());
self.clear_selection();
self.update_cursor_position();
true
} else {
false
}
}
pub fn hit_test(&self, _pos: Vec2, _char_width: f32) -> usize {
self.cursor.position
}
pub fn selection_rects(&self, _line_height: f32, _char_width: f32) -> Vec<(f32, f32, f32, f32)> {
if let Some(sel) = self.selection {
if !sel.is_empty() {
let (start, end) = sel.range();
vec![(
start as f32 * _char_width,
0.0,
(end - start) as f32 * _char_width,
_line_height,
)]
} else {
vec![]
}
} else {
vec![]
}
}
fn update_cursor_position(&mut self) {
let text_before = &self.text[..self.cursor.position];
self.cursor.line = text_before.matches('\n').count();
if let Some(line_start) = text_before.rfind('\n') {
let line_text = &text_before[line_start + 1..];
self.cursor.column = line_text.chars().count();
} else {
self.cursor.column = text_before.chars().count();
}
}
fn push_history(&mut self) {
self.history.truncate(self.history_position + 1);
self.history.push(self.text.clone());
self.history_position = self.history.len() - 1;
const MAX_HISTORY: usize = 100;
if self.history.len() > MAX_HISTORY {
self.history.remove(0);
self.history_position -= 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_cursor_default() {
let cursor = TextCursor::default();
assert_eq!(cursor.position, 0);
assert_eq!(cursor.line, 0);
assert_eq!(cursor.column, 0);
}
#[test]
fn test_text_selection_range() {
let sel = TextSelection::new(5, 10);
assert_eq!(sel.range(), (5, 10));
assert_eq!(sel.len(), 5);
assert!(!sel.is_empty());
let sel = TextSelection::new(10, 5);
assert_eq!(sel.range(), (5, 10));
assert_eq!(sel.len(), 5);
}
#[test]
fn test_text_selection_contains() {
let sel = TextSelection::new(5, 10);
assert!(sel.contains(5));
assert!(sel.contains(7));
assert!(!sel.contains(10)); assert!(!sel.contains(3));
}
#[test]
fn test_editor_new() {
let editor = TextEditor::new("Hello");
assert_eq!(editor.text(), "Hello");
assert_eq!(editor.cursor().position, 0);
assert!(!editor.has_selection());
}
#[test]
fn test_editor_insert_char() {
let mut editor = TextEditor::new("Hello");
editor.move_cursor_end();
editor.insert_char('!');
assert_eq!(editor.text(), "Hello!");
assert_eq!(editor.cursor().position, 6);
}
#[test]
fn test_editor_insert_str() {
let mut editor = TextEditor::new("Hello");
editor.move_cursor_end();
editor.insert_str(", World");
assert_eq!(editor.text(), "Hello, World");
}
#[test]
fn test_editor_delete_char() {
let mut editor = TextEditor::new("Hello");
editor.move_cursor_end();
editor.delete_char();
assert_eq!(editor.text(), "Hell");
assert_eq!(editor.cursor().position, 4);
}
#[test]
fn test_editor_delete_char_forward() {
let mut editor = TextEditor::new("Hello");
editor.set_cursor(0);
editor.delete_char_forward();
assert_eq!(editor.text(), "ello");
assert_eq!(editor.cursor().position, 0);
}
#[test]
fn test_editor_selection() {
let mut editor = TextEditor::new("Hello, World");
editor.select(0, 5);
assert!(editor.has_selection());
assert_eq!(editor.selected_text(), Some("Hello"));
}
#[test]
fn test_editor_delete_selection() {
let mut editor = TextEditor::new("Hello, World");
editor.select(0, 5);
editor.delete_selection();
assert_eq!(editor.text(), ", World");
assert!(!editor.has_selection());
}
#[test]
fn test_editor_replace_selection() {
let mut editor = TextEditor::new("Hello, World");
editor.select(7, 12);
editor.replace_selection("Universe");
assert_eq!(editor.text(), "Hello, Universe");
}
#[test]
fn test_editor_select_all() {
let mut editor = TextEditor::new("Hello");
editor.select_all();
assert_eq!(editor.selected_text(), Some("Hello"));
}
#[test]
fn test_editor_cursor_movement() {
let mut editor = TextEditor::new("Hello");
editor.move_cursor_end();
assert_eq!(editor.cursor().position, 5);
editor.move_cursor_left();
assert_eq!(editor.cursor().position, 4);
editor.move_cursor_right();
assert_eq!(editor.cursor().position, 5);
editor.move_cursor_start();
assert_eq!(editor.cursor().position, 0);
}
#[test]
fn test_editor_undo_redo() {
let mut editor = TextEditor::new("Hello");
editor.move_cursor_end();
editor.insert_char('!');
assert_eq!(editor.text(), "Hello!");
editor.undo();
assert_eq!(editor.text(), "Hello");
editor.redo();
assert_eq!(editor.text(), "Hello!");
}
#[test]
fn test_editor_utf8() {
let mut editor = TextEditor::new("Hello 世界");
editor.move_cursor_end();
editor.insert_char('!');
assert_eq!(editor.text(), "Hello 世界!");
editor.delete_char();
assert_eq!(editor.text(), "Hello 世界");
editor.delete_char();
assert_eq!(editor.text(), "Hello 世");
}
#[test]
fn test_cursor_position_multiline() {
let mut editor = TextEditor::new("Hello\nWorld");
editor.set_cursor(6); assert_eq!(editor.cursor().line, 1);
assert_eq!(editor.cursor().column, 0);
editor.move_cursor_end();
assert_eq!(editor.cursor().line, 1);
assert_eq!(editor.cursor().column, 5);
}
}