use super::{EditBuffer, EditKind};
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct Preedit {
pub(super) text: String,
pub(super) cursor: Option<(usize, usize)>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DisplayText {
pub text: String,
pub caret: usize,
pub selection: Option<(usize, usize)>,
pub preedit: Option<(usize, usize)>,
}
fn floor_boundary(s: &str, i: usize) -> usize {
let mut i = i.min(s.len());
while !s.is_char_boundary(i) {
i -= 1;
}
i
}
impl EditBuffer {
pub fn set_preedit(&mut self, text: &str, cursor: Option<(usize, usize)>) {
if text.is_empty() {
self.preedit = None;
return;
}
if self.preedit.is_none() {
self.delete_selection();
}
let cursor = cursor.map(|(s, e)| {
let (s, e) = (floor_boundary(text, s), floor_boundary(text, e));
(s.min(e), s.max(e))
});
self.preedit = Some(Preedit {
text: text.to_string(),
cursor,
});
self.seal();
}
pub fn clear_preedit(&mut self) {
self.preedit = None;
}
pub fn preedit(&self) -> Option<&str> {
self.preedit.as_ref().map(|p| p.text.as_str())
}
pub(super) fn commit_text(&mut self, text: &str) {
self.preedit = None;
self.insert(text);
}
pub fn delete_surrounding(&mut self, before: usize, after: usize) {
self.anchor = None;
let start = floor_boundary(&self.text, self.caret.saturating_sub(before));
let mut end = (self.caret + after).min(self.text.len());
while !self.text.is_char_boundary(end) {
end += 1;
}
self.replace(start..end, "", EditKind::Other);
self.seal();
}
pub fn display(&self) -> DisplayText {
let Some(p) = &self.preedit else {
return DisplayText {
text: self.text.clone(),
caret: self.caret,
selection: self.selection(),
preedit: None,
};
};
let at = self.caret;
let mut text = String::with_capacity(self.text.len() + p.text.len());
text.push_str(&self.text[..at]);
text.push_str(&p.text);
text.push_str(&self.text[at..]);
let (caret, selection) = match p.cursor {
Some((s, e)) if e > s => (at + e, Some((at + s, at + e))),
Some((_, e)) => (at + e, None),
None => (at + p.text.len(), None),
};
DisplayText {
text,
caret,
selection,
preedit: Some((at, at + p.text.len())),
}
}
pub fn display_to_text(&self, i: usize) -> usize {
match &self.preedit {
Some(p) if i > self.caret + p.text.len() => i - p.text.len(),
Some(_) if i > self.caret => self.caret,
_ => i,
}
}
}
#[cfg(test)]
mod tests {
use crate::core::KeyInput;
use crate::widgets::EditBuffer;
fn preedit(text: &str, cursor: Option<(usize, usize)>) -> KeyInput {
KeyInput::Preedit {
text: text.into(),
cursor,
}
}
#[test]
fn preedit_shows_at_the_caret_without_editing_the_text() {
let mut b = EditBuffer::from_text("ab");
b.apply(&KeyInput::Left);
b.apply(&preedit("にほ", Some((3, 3))));
assert_eq!(b.text(), "ab");
assert_eq!(b.preedit(), Some("にほ"));
let d = b.display();
assert_eq!(d.text, "aにほb");
assert_eq!(d.preedit, Some((1, 7)));
assert_eq!(d.caret, 4); assert_eq!(d.selection, None);
assert!(!b.can_undo());
b.apply(&preedit("日本", Some((0, 3))));
assert_eq!(b.display().selection, Some((1, 4)));
assert_eq!(b.display_to_text(0), 0);
assert_eq!(b.display_to_text(4), 1);
assert_eq!(b.display_to_text(8), 2);
}
#[test]
fn commit_replaces_the_preedit_as_one_undo_step() {
let mut b = EditBuffer::from_text("x");
b.apply(&preedit("´", Some((2, 2))));
b.apply(&preedit("ni", Some((2, 2))));
b.apply(&KeyInput::Text("日".into()));
assert_eq!(b.text(), "x日");
assert_eq!(b.preedit(), None);
assert_eq!(b.display().text, "x日");
assert!(b.undo());
assert_eq!(b.text(), "x");
assert_eq!(b.preedit(), None);
assert!(!b.can_undo());
}
#[test]
fn empty_preedit_cancels_and_a_selection_is_replaced() {
let mut b = EditBuffer::from_text("hello");
b.apply(&preedit("k", None));
b.apply(&preedit("", None));
assert_eq!(b.display().text, "hello");
assert!(!b.can_undo());
b.select_all();
b.apply(&preedit("か", Some((3, 3))));
assert_eq!(b.text(), "");
assert_eq!(b.display().text, "か");
b.apply(&KeyInput::Text("火".into()));
assert_eq!(b.text(), "火");
assert!(b.undo());
assert!(b.undo());
assert_eq!(b.text(), "hello");
}
#[test]
fn delete_surrounding_text() {
let mut b = EditBuffer::from_text("héllo");
b.place_caret(3); b.apply(&KeyInput::DeleteSurrounding {
before: 2,
after: 1,
});
assert_eq!(b.text(), "hlo");
assert_eq!(b.caret(), 1);
assert!(b.undo());
assert_eq!(b.text(), "héllo");
}
}