use std::ops::Range;
use unicode_segmentation::UnicodeSegmentation;
use crate::keyboard::{Key, KeyEvent};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct Edit {
pub(crate) text: String,
pub(crate) cursor: usize,
pub(crate) anchor: Option<usize>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) enum Keyed {
Edited,
Moved,
Accept,
Copy(String),
Cut(String),
Paste,
Unused,
}
impl Edit {
pub(crate) fn set_text(&mut self, text: String) {
let text = if text.contains(['\n', '\r']) {
text.replace(['\n', '\r'], " ")
} else {
text
};
if text == self.text {
return;
}
self.text = text;
self.cursor = self.text.len();
self.anchor = None;
}
pub(crate) fn selection(&self) -> Option<Range<usize>> {
let anchor = self.anchor.filter(|&anchor| anchor != self.cursor)?;
Some(anchor.min(self.cursor)..anchor.max(self.cursor))
}
pub(crate) fn insert(&mut self, text: &str) {
let range = self.selection().unwrap_or(self.cursor..self.cursor);
self.text.replace_range(range.clone(), text);
self.cursor = range.start + text.len();
self.anchor = None;
}
pub(crate) fn key(&mut self, event: &KeyEvent, copyable: bool) -> Keyed {
let modifiers = event.modifiers;
let word = modifiers.ctrl;
let command = modifiers.ctrl && !modifiers.alt;
let before = self.clone();
match &event.key {
Key::Enter => return Keyed::Accept,
Key::Character('c' | 'C' | 'x' | 'X') if command && !copyable => return Keyed::Moved,
Key::Character('c' | 'C') if command => {
return match self.selection() {
Some(range) => Keyed::Copy(self.text[range].to_owned()),
None => Keyed::Unused,
};
}
Key::Character('x' | 'X') if command => {
let Some(range) = self.selection() else {
return Keyed::Unused;
};
let cut = self.text[range].to_owned();
self.insert("");
return Keyed::Cut(cut);
}
Key::Character('v' | 'V') if command => return Keyed::Paste,
Key::Character('a' | 'A') if modifiers.ctrl && !modifiers.alt => {
self.anchor = Some(0);
self.cursor = self.text.len();
}
Key::Backspace => self.erase(|edit| edit.back(edit.cursor, word)),
Key::Delete => self.erase(|edit| edit.forward(edit.cursor, word)),
Key::Left => self.motion(modifiers.shift, |edit, at| edit.back(at, word), true),
Key::Right => self.motion(modifiers.shift, |edit, at| edit.forward(at, word), false),
Key::Home => self.motion(modifiers.shift, |_, _| 0, true),
Key::End => self.motion(modifiers.shift, |edit, _| edit.text.len(), false),
_ => match &event.text {
Some(text) if !modifiers.ctrl && !modifiers.alt && !modifiers.logo => self.insert(text),
_ => return Keyed::Unused,
},
}
if self.text != before.text {
Keyed::Edited
} else {
Keyed::Moved
}
}
fn erase(&mut self, to: impl FnOnce(&Edit) -> usize) {
let range = match self.selection() {
Some(range) => range,
None => {
let other = to(self);
other.min(self.cursor)..other.max(self.cursor)
}
};
self.text.replace_range(range.clone(), "");
self.cursor = range.start;
self.anchor = None;
}
fn motion(&mut self, extend: bool, to: impl FnOnce(&Edit, usize) -> usize, start: bool) {
if extend {
self.anchor.get_or_insert(self.cursor);
self.cursor = to(self, self.cursor);
return;
}
if let Some(range) = self.selection() {
self.cursor = if start { range.start } else { range.end };
} else {
self.cursor = to(self, self.cursor);
}
self.anchor = None;
}
fn back(&self, at: usize, word: bool) -> usize {
let before = &self.text[..at];
if word {
before
.split_word_bound_indices()
.rev()
.find(|(_, part)| !part.trim().is_empty())
.map_or(0, |(index, _)| index)
} else {
before
.grapheme_indices(true)
.next_back()
.map_or(0, |(index, _)| index)
}
}
fn forward(&self, at: usize, word: bool) -> usize {
let after = &self.text[at..];
let length = if word {
after
.split_word_bound_indices()
.find(|(_, part)| !part.trim().is_empty())
.map_or(after.len(), |(index, part)| index + part.len())
} else {
after.graphemes(true).next().map_or(0, str::len)
};
at + length
}
}
pub(crate) fn graphemes_before(text: &str, at: usize) -> usize {
text[..at].graphemes(true).count()
}
pub(crate) fn grapheme_start(text: &str, count: usize) -> usize {
text
.grapheme_indices(true)
.nth(count)
.map_or(text.len(), |(index, _)| index)
}