#![cfg_attr(
not(target_os = "windows"),
allow(dead_code, reason = "only the Windows hook completes, for now")
)]
pub(crate) const GCS_COMPSTR: u32 = 0x0008;
pub(crate) const GCS_RESULTSTR: u32 = 0x0800;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum WinitState {
Disabled,
Enabled,
Preedit,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ImmMessage {
Composition {
flags: u32,
result: Option<String>,
composition: Option<String>,
},
End { result: Option<String> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ImmCompletion {
state: WinitState,
results: String,
composing: Option<String>,
winit_commits: Vec<String>,
}
impl Default for ImmCompletion {
fn default() -> Self {
Self::new()
}
}
impl ImmCompletion {
pub(crate) fn new() -> Self {
Self {
state: WinitState::Preedit,
results: String::new(),
composing: None,
winit_commits: Vec::new(),
}
}
pub(crate) fn message(&mut self, message: &ImmMessage) {
match message {
ImmMessage::Composition {
flags,
result,
composition,
} => {
if flags & GCS_RESULTSTR != 0
&& let Some(result) = result
{
self.results.push_str(result);
}
self.composing = composition.clone().filter(|_| flags & GCS_COMPSTR != 0);
if self.state == WinitState::Disabled {
return;
}
if flags & GCS_RESULTSTR != 0
&& let Some(result) = result
{
self.winit_commits.push(result.clone());
self.state = WinitState::Enabled;
}
if flags & GCS_COMPSTR != 0 && composition.is_some() {
self.state = WinitState::Preedit;
}
}
ImmMessage::End { result } => {
if self.state == WinitState::Preedit
&& let Some(result) = result
{
self.winit_commits.push(result.clone());
}
self.state = WinitState::Disabled;
self.composing = None;
}
}
}
pub(crate) fn left_composing(&self) -> Option<&str> {
(self.state == WinitState::Preedit).then(|| self.composing.as_deref().unwrap_or(""))
}
pub(crate) fn committed(&self, left: Option<&str>) -> Option<String> {
if self.results.is_empty() {
return None;
}
let mut committed = self.results.clone();
committed.push_str(left.unwrap_or(""));
Some(committed)
}
pub(crate) fn winit_commits(&self) -> &[String] {
&self.winit_commits
}
}
#[cfg(test)]
mod tests {
use super::*;
const PINYIN_COMMIT: u32 = 0x1c00;
const PINYIN_COMPOSITION: u32 = 0x1b8;
fn result(flags: u32, text: &str) -> ImmMessage {
ImmMessage::Composition {
flags,
result: Some(text.to_owned()),
composition: None,
}
}
fn end(text: &str) -> ImmMessage {
ImmMessage::End {
result: Some(text.to_owned()),
}
}
fn cleared() -> ImmMessage {
ImmMessage::Composition {
flags: 0,
result: None,
composition: None,
}
}
fn completed(messages: &[ImmMessage]) -> ImmCompletion {
let mut completion = ImmCompletion::new();
for message in messages {
completion.message(message);
}
completion
}
#[test]
fn a_result_then_the_end_is_committed_once_by_winit() {
let completion = completed(&[result(PINYIN_COMMIT, "nihao"), end("nihao")]);
assert_eq!(completion.committed(None).as_deref(), Some("nihao"));
assert_eq!(
completion.winit_commits(),
["nihao"],
"the end finds winit out of its preedit state and commits nothing more"
);
assert_eq!(completion.left_composing(), None);
}
#[test]
fn a_cleared_composition_before_the_result_changes_nothing() {
let completion = completed(&[cleared(), result(0x1e00, "にほん"), end("")]);
assert_eq!(completion.committed(None).as_deref(), Some("にほん"));
assert_eq!(completion.winit_commits(), ["にほん"]);
}
#[test]
fn a_result_after_the_end_is_the_input_methods_but_winit_commits_at_the_end() {
let completion = completed(&[end("\u{AE00}"), result(GCS_RESULTSTR, "\u{AE00}")]);
assert_eq!(completion.committed(None).as_deref(), Some("\u{AE00}"));
assert_eq!(completion.winit_commits(), ["\u{AE00}"]);
let completion = completed(&[end(""), result(GCS_RESULTSTR, "\u{AE00}")]);
assert_eq!(completion.committed(None).as_deref(), Some("\u{AE00}"));
assert_eq!(completion.winit_commits(), [""]);
}
#[test]
fn an_end_with_no_result_commits_nothing_for_the_field() {
let completion = completed(&[cleared(), end("")]);
assert_eq!(completion.committed(None), None);
assert_eq!(completion.winit_commits(), [""]);
assert_eq!(completion.left_composing(), None);
}
#[test]
fn an_empty_result_is_no_commit_for_the_field() {
let completion = completed(&[result(PINYIN_COMMIT, ""), end("")]);
assert_eq!(completion.committed(None), None);
assert_eq!(completion.winit_commits(), [""]);
}
#[test]
fn several_results_are_kept_in_order_and_dropped_one_by_one() {
let completion = completed(&[
result(GCS_RESULTSTR, "\u{65E5}"),
result(GCS_RESULTSTR, "\u{672C}"),
end("\u{672C}"),
]);
assert_eq!(
completion.committed(None).as_deref(),
Some("\u{65E5}\u{672C}")
);
assert_eq!(completion.winit_commits(), ["\u{65E5}", "\u{672C}"]);
}
#[test]
fn nothing_sent_leaves_the_input_method_composing() {
let completion = completed(&[]);
assert_eq!(completion.left_composing(), Some(""));
assert_eq!(completion.committed(None), None);
assert!(completion.winit_commits().is_empty());
}
#[test]
fn a_composition_left_after_the_result_is_kept_after_it() {
let mut completion = completed(&[ImmMessage::Composition {
flags: PINYIN_COMMIT | PINYIN_COMPOSITION,
result: Some("\u{4F60}".to_owned()),
composition: Some("hao".to_owned()),
}]);
let left = completion.left_composing().map(str::to_owned);
assert_eq!(left.as_deref(), Some("hao"));
completion.message(&cleared());
completion.message(&end("\u{4F60}"));
assert_eq!(completion.left_composing(), None);
assert_eq!(
completion.committed(left.as_deref()).as_deref(),
Some("\u{4F60}hao")
);
assert_eq!(completion.winit_commits(), ["\u{4F60}", "\u{4F60}"]);
}
#[test]
fn a_string_winit_cannot_read_is_not_committed() {
let completion = completed(&[
ImmMessage::Composition {
flags: GCS_RESULTSTR,
result: None,
composition: None,
},
ImmMessage::End { result: None },
]);
assert_eq!(completion.committed(None), None);
assert!(completion.winit_commits().is_empty());
}
#[test]
fn a_message_after_the_end_is_ignored_by_winit() {
let completion = completed(&[
result(GCS_RESULTSTR, "a"),
end("a"),
ImmMessage::Composition {
flags: GCS_COMPSTR,
result: None,
composition: Some("b".to_owned()),
},
]);
assert_eq!(completion.winit_commits(), ["a"]);
assert_eq!(completion.left_composing(), None, "winit is disabled");
}
}