feat(input): implement IME preedit state machine and cursor area tracker
This commit is contained in:
parent
be35eaaffd
commit
8a7a05ec9a
4 changed files with 113 additions and 0 deletions
88
crates/molotov-core/src/input/ime.rs
Normal file
88
crates/molotov-core/src/input/ime.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||
pub struct Rect {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
pub width: i32,
|
||||
pub height: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ImeState {
|
||||
Inactive,
|
||||
Active,
|
||||
}
|
||||
|
||||
pub struct ImeManager {
|
||||
state: ImeState,
|
||||
cursor_area: Rect,
|
||||
preedit_text: String,
|
||||
cursor_pos: usize,
|
||||
}
|
||||
|
||||
impl Default for ImeManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ImeManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: ImeState::Inactive,
|
||||
cursor_area: Rect::default(),
|
||||
preedit_text: String::new(),
|
||||
cursor_pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_active(&self) -> bool {
|
||||
self.state == ImeState::Active
|
||||
}
|
||||
|
||||
pub fn start_text_input(&mut self, area: Rect) {
|
||||
self.state = ImeState::Active;
|
||||
self.cursor_area = area;
|
||||
self.preedit_text.clear();
|
||||
self.cursor_pos = 0;
|
||||
}
|
||||
|
||||
pub fn stop_text_input(&mut self) {
|
||||
self.state = ImeState::Inactive;
|
||||
self.preedit_text.clear();
|
||||
self.cursor_pos = 0;
|
||||
}
|
||||
|
||||
pub fn set_cursor_area(&mut self, area: Rect) {
|
||||
self.cursor_area = area;
|
||||
}
|
||||
|
||||
pub fn cursor_area(&self) -> Rect {
|
||||
self.cursor_area
|
||||
}
|
||||
|
||||
pub fn update_preedit(&mut self, text: &str, cursor_pos: usize) {
|
||||
self.preedit_text = text.to_string();
|
||||
self.cursor_pos = cursor_pos.min(self.preedit_text.chars().count());
|
||||
}
|
||||
|
||||
pub fn preedit_text(&self) -> &str {
|
||||
&self.preedit_text
|
||||
}
|
||||
|
||||
pub fn cursor_pos(&self) -> usize {
|
||||
self.cursor_pos
|
||||
}
|
||||
|
||||
pub fn commit_text(&mut self, text: &str) -> String {
|
||||
self.preedit_text.clear();
|
||||
self.cursor_pos = 0;
|
||||
text.to_string()
|
||||
}
|
||||
|
||||
pub fn cancel_composition(&mut self) {
|
||||
self.preedit_text.clear();
|
||||
self.cursor_pos = 0;
|
||||
}
|
||||
}
|
||||
2
crates/molotov-core/src/input/mod.rs
Normal file
2
crates/molotov-core/src/input/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
pub mod ime;
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
pub mod audio;
|
||||
pub mod dynamic;
|
||||
pub mod elf;
|
||||
pub mod input;
|
||||
pub mod jni;
|
||||
pub mod linker;
|
||||
pub mod mmap;
|
||||
|
|
|
|||
22
crates/molotov-core/tests/ime_preedit_test.rs
Normal file
22
crates/molotov-core/tests/ime_preedit_test.rs
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
use molotov_core::input::ime::{ImeManager, Rect};
|
||||
|
||||
#[test]
|
||||
fn test_ime_preedit_composition_flow() {
|
||||
let mut ime = ImeManager::new();
|
||||
ime.start_text_input(Rect { x: 100, y: 200, width: 20, height: 24 });
|
||||
assert!(ime.is_active());
|
||||
|
||||
// User types in Japanese IME: preedit string "とうきょう"
|
||||
ime.update_preedit("とうきょう", 4);
|
||||
assert_eq!(ime.preedit_text(), "とうきょう");
|
||||
assert_eq!(ime.cursor_pos(), 4);
|
||||
|
||||
// User selects candidate: commit "東京"
|
||||
let committed = ime.commit_text("東京");
|
||||
assert_eq!(committed, "東京");
|
||||
assert_eq!(ime.preedit_text(), "", "Preedit must clear on commit");
|
||||
|
||||
ime.stop_text_input();
|
||||
assert!(!ime.is_active());
|
||||
}
|
||||
Reference in a new issue