use inset_embedder::{TextAffinity, TextEditingValue, TextRange, TextSelection};
use winit::event::Ime;
#[derive(Clone, Debug, PartialEq)]
pub(crate) enum ImeOutcome {
None,
Closed,
Value(TextEditingValue),
}
pub(crate) fn apply_ime(value: &TextEditingValue, ime: &Ime) -> ImeOutcome {
match ime {
Ime::Enabled => ImeOutcome::None,
Ime::Disabled => ImeOutcome::Closed,
Ime::Preedit(text, cursor) => ImeOutcome::Value(apply_preedit(value, text, *cursor)),
Ime::Commit(text) => ImeOutcome::Value(apply_commit(value, text)),
}
}
fn apply_preedit(
value: &TextEditingValue,
text: &str,
cursor: Option<(usize, usize)>,
) -> TextEditingValue {
let range = insertion_range(value);
let start = range.start;
let next = value.replaced(range, text);
if text.is_empty() {
return next
.composing(TextRange::EMPTY)
.selection(TextSelection::collapsed(start, TextAffinity::Downstream));
}
let inserted = utf16_len(text);
let selection = match cursor {
Some((a, b)) => TextSelection::new(start + utf16_at(text, a), start + utf16_at(text, b)),
None => TextSelection::collapsed(start + inserted, TextAffinity::Downstream),
};
next.composing(TextRange::new(start, start + inserted))
.selection(selection)
}
fn apply_commit(value: &TextEditingValue, text: &str) -> TextEditingValue {
let range = insertion_range(value);
let start = range.start;
value
.replaced(range, text)
.composing(TextRange::EMPTY)
.selection(TextSelection::collapsed(
start + utf16_len(text),
TextAffinity::Downstream,
))
}
fn insertion_range(value: &TextEditingValue) -> TextRange {
if value.composing.is_valid() {
return value.composing;
}
let range = value.selection.range();
if range.is_valid() {
range
} else {
TextRange::collapsed(0)
}
}
fn utf16_len(text: &str) -> i32 {
text.encode_utf16().count() as i32
}
fn utf16_at(text: &str, utf8: usize) -> i32 {
let mut end = utf8.min(text.len());
while end > 0 && !text.is_char_boundary(end) {
end -= 1;
}
text[..end].encode_utf16().count() as i32
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_field() -> TextEditingValue {
TextEditingValue::new().selection(TextSelection::collapsed(0, TextAffinity::Downstream))
}
#[test]
fn commit_inserts_at_the_caret() {
let value = apply_ime(&empty_field(), &Ime::Commit("hi".into()));
assert_eq!(
value,
ImeOutcome::Value(
TextEditingValue::new()
.text("hi")
.selection(TextSelection::collapsed(2, TextAffinity::Downstream))
.composing(TextRange::EMPTY)
)
);
}
#[test]
fn preedit_marks_the_composing_region() {
let value = apply_ime(&empty_field(), &Ime::Preedit("ni".into(), Some((0, 2))));
assert_eq!(
value,
ImeOutcome::Value(
TextEditingValue::new()
.text("ni")
.selection(TextSelection::new(0, 2))
.composing(TextRange::new(0, 2))
)
);
}
#[test]
fn commit_replaces_the_composing_region() {
let composing = TextEditingValue::new()
.text("ni")
.selection(TextSelection::new(0, 2))
.composing(TextRange::new(0, 2));
let value = apply_ime(&composing, &Ime::Commit("你".into()));
assert_eq!(
value,
ImeOutcome::Value(
TextEditingValue::new()
.text("你")
.selection(TextSelection::collapsed(1, TextAffinity::Downstream))
.composing(TextRange::EMPTY)
)
);
}
#[test]
fn empty_preedit_clears_composing() {
let composing = TextEditingValue::new()
.text("ni")
.selection(TextSelection::collapsed(2, TextAffinity::Downstream))
.composing(TextRange::new(0, 2));
let value = apply_ime(&composing, &Ime::Preedit(String::new(), None));
assert_eq!(
value,
ImeOutcome::Value(
TextEditingValue::new()
.text("")
.selection(TextSelection::collapsed(0, TextAffinity::Downstream))
.composing(TextRange::EMPTY)
)
);
}
#[test]
fn disabled_closes_the_connection() {
assert_eq!(
apply_ime(&empty_field(), &Ime::Disabled),
ImeOutcome::Closed
);
}
}