ohos_ime/
lib.rs

1//! Safe abstractions to use IME (inputmethods) from Rust on OpenHarmony
2//!
3//! This crate provides an [`ImeProxy`], which allows interacting with the Input method on OpenHarmony
4//! devices. The user needs to implement the [`Ime`] trait
5//!
6//! This crate is still under active development and based on the
7//! [InputMethod C-API] of OpenHarmony.
8//!
9//! [InputMethod C-API]: https://docs.openharmony.cn/pages/v5.0/zh-cn/application-dev/reference/apis-ime-kit/_input_method.md
10//!
11//! ## Usage
12//!
13//! 1. Implement the Ime trait
14//! 2. call `ImeProxy::new()`
15mod text_config;
16mod text_editor;
17
18pub use crate::text_config::{TextConfig, TextConfigBuilder, TextSelection};
19use crate::text_editor::DISPATCHER;
20use log::{error, warn};
21use ohos_ime_sys::attach_options::{
22    InputMethod_AttachOptions, OH_AttachOptions_Create, OH_AttachOptions_Destroy,
23    OH_AttachOptions_IsShowKeyboard,
24};
25use ohos_ime_sys::controller::{OH_InputMethodController_Attach, OH_InputMethodController_Detach};
26use ohos_ime_sys::inputmethod_proxy::{
27    InputMethod_InputMethodProxy, OH_InputMethodProxy_HideKeyboard,
28    OH_InputMethodProxy_ShowKeyboard,
29};
30use ohos_ime_sys::text_editor_proxy::{
31    InputMethod_TextEditorProxy, OH_TextEditorProxy_Create, OH_TextEditorProxy_Destroy,
32    OH_TextEditorProxy_SetDeleteBackwardFunc, OH_TextEditorProxy_SetDeleteForwardFunc,
33    OH_TextEditorProxy_SetFinishTextPreviewFunc, OH_TextEditorProxy_SetGetLeftTextOfCursorFunc,
34    OH_TextEditorProxy_SetGetRightTextOfCursorFunc, OH_TextEditorProxy_SetGetTextConfigFunc,
35    OH_TextEditorProxy_SetGetTextIndexAtCursorFunc, OH_TextEditorProxy_SetHandleExtendActionFunc,
36    OH_TextEditorProxy_SetHandleSetSelectionFunc, OH_TextEditorProxy_SetInsertTextFunc,
37    OH_TextEditorProxy_SetMoveCursorFunc, OH_TextEditorProxy_SetReceivePrivateCommandFunc,
38    OH_TextEditorProxy_SetSendEnterKeyFunc, OH_TextEditorProxy_SetSendKeyboardStatusFunc,
39    OH_TextEditorProxy_SetSetPreviewTextFunc,
40};
41use ohos_ime_sys::types::{
42    InputMethodErrorCode, InputMethodResult, InputMethod_EnterKeyType, InputMethod_KeyboardStatus,
43};
44use std::fmt::Debug;
45use std::ptr::NonNull;
46
47// Todo: Well, honestly we really need to clarify the required sematics on the IME.
48/// User implementation of required Inputmethod functionality
49pub trait Ime: Send + Sync {
50    /// Insert `text` at the current cursor position.
51    fn insert_text(&self, text: String);
52    /// Delete the next `len` `char`s(?) starting at the current cursor position
53    fn delete_forward(&self, len: usize);
54
55    /// Delete the previous `len` `char`s(?) before the current cursor position
56    fn delete_backward(&self, len: usize);
57
58    /// Return the text configuration associated with the current IME
59    fn get_text_config(&self) -> &TextConfig;
60
61    /// Process the enter key variant pressed by the user.
62    ///
63    /// Depending on the configuration (applied by the implementation of [`get_text_config()`])
64    /// the enterkey label displayed to the user varies.
65    /// This function will be called when the enter key is pressed and the associated label
66    /// is passed, so that the application can handle it accordingly.
67    fn send_enter_key(&self, enter_key: InputMethod_EnterKeyType);
68
69    /// Called when the status of IME virtual keyboard changes.
70    fn keyboard_status_changed(&self, status: KeyboardStatus) {
71        log::debug!("Keyboard status changed to {:?}", status);
72    }
73    // ...
74}
75
76pub struct ImeProxy {
77    raw: NonNull<InputMethod_InputMethodProxy>,
78    // keep the text editor alive.
79    #[allow(dead_code)]
80    editor: RawTextEditorProxy,
81}
82
83impl Drop for ImeProxy {
84    fn drop(&mut self) {
85        // We must first detach the InputMethodProxy, before dropping the TextEditorProxy.
86        // SAFETY: We own the raw pointer, with now oher references.
87        if let Err(e) = unsafe { OH_InputMethodController_Detach(self.raw.as_ptr()) } {
88            error!("IME: Detach failed for InputMethodController {:?}", e);
89        }
90        let res = DISPATCHER.unregister(self.editor.raw);
91        #[cfg(debug_assertions)]
92        if let Err(e) = res {
93            error!("IME: ImeProxy destroy failed {:?}", e);
94        }
95        #[cfg(not(debug_assertions))]
96        drop(res)
97    }
98}
99
100pub struct CreateImeProxyError {
101    pub editor: RawTextEditorProxy,
102    pub options: AttachOptions,
103    pub error_code: InputMethodErrorCode,
104}
105
106impl Debug for CreateImeProxyError {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        f.write_fmt(format_args!("InputMethodErrorCode: {:?}", self.error_code))
109    }
110}
111
112impl ImeProxy {
113    // todo: maybe use builder pattern instead.
114    pub fn new(
115        editor: RawTextEditorProxy,
116        options: AttachOptions,
117    ) -> Result<Self, CreateImeProxyError> {
118        unsafe {
119            let mut ime_proxy: *mut InputMethod_InputMethodProxy = core::ptr::null_mut();
120            if let Err(error_code) = OH_InputMethodController_Attach(
121                editor.raw.as_ptr(),
122                options.raw.as_ptr(),
123                &raw mut ime_proxy,
124            ) {
125                return Err(CreateImeProxyError {
126                    editor,
127                    options,
128                    error_code,
129                });
130            }
131
132            Ok(Self {
133                // We checked the returncode above, so the pointer should be valid and non-null now.
134                raw: NonNull::new(ime_proxy).expect("Wrong Errorcode"),
135                editor,
136            })
137        }
138    }
139
140    pub fn show_keyboard(&self) -> InputMethodResult {
141        unsafe { OH_InputMethodProxy_ShowKeyboard(self.raw.as_ptr()) }
142    }
143
144    pub fn hide_keyboard(&self) -> InputMethodResult {
145        unsafe { OH_InputMethodProxy_HideKeyboard(self.raw.as_ptr()) }
146    }
147}
148
149pub struct AttachOptions {
150    raw: NonNull<InputMethod_AttachOptions>,
151}
152
153pub enum KeyboardVisibility {
154    Hide,
155    Show,
156}
157
158impl AttachOptions {
159    pub fn new(show_keyboard: bool) -> Self {
160        // SAFETY: No particular safety or other requirements.
161        // Only documented failure reason is insufficient Memory
162        let raw = unsafe {
163            let raw = OH_AttachOptions_Create(show_keyboard);
164            NonNull::new(raw).expect("OOM?")
165        };
166        Self { raw }
167    }
168
169    pub fn get_visibility(&self) -> KeyboardVisibility {
170        let mut show_keyboard: u8 = 0;
171        const _: () = assert!(size_of::<u8>() == size_of::<bool>());
172        // SAFETY: We can guarantee self.raw is valid (neither copy, nor clone, private).
173        // We also asserted that bool and `u8` have the same layout, and do not rely on the
174        // C-side writing a valid bool.
175        unsafe {
176            let err =
177                OH_AttachOptions_IsShowKeyboard(self.raw.as_ptr(), (&raw mut show_keyboard).cast());
178            // The only documented failure condition is passing a nullpointer, which is impossible for
179            // us since we use NonNull, so we don't check the result in release mode.
180            debug_assert!(err.is_ok());
181            // We don't want to rely on OH_AttachOptions_IsShowKeyboard writing a valid bool,
182            // so we check the raw `u8` value.
183            if show_keyboard == 0 {
184                KeyboardVisibility::Hide
185            } else {
186                KeyboardVisibility::Show
187            }
188        }
189    }
190}
191
192impl Drop for AttachOptions {
193    fn drop(&mut self) {
194        // SAFETY: Type is neither copy nor clone, raw is private, so our pointer is unique
195        // and had no opportunity to leak.
196        unsafe {
197            OH_AttachOptions_Destroy(self.raw.as_ptr());
198        }
199    }
200}
201
202// Very raw bindings. To be replaced with something better!
203// Ideally we want to provide a Rust trait, user provides a rust implementation,
204// and we somehow create a C-ABI wrapper around the trait implementations.
205// Brain-storming: We could make One generic C-ABI implementation here, and then lookup
206// the Rust impl based on the TextEditorProxy pointer.
207pub struct RawTextEditorProxy {
208    raw: NonNull<InputMethod_TextEditorProxy>,
209}
210
211#[derive(Debug)]
212pub enum CreateTextEditorProxyErrorKind {
213    /// Indicates Out of Memory situation
214    CreateProxyFailed,
215    /// Registering the C callbacks failed.
216    RegisterCallbacksFailed(InputMethodErrorCode),
217}
218pub struct CreateTextEditorProxyError {
219    /// Returns the ime passed to [`RawTextEditorProxy::new`].
220    pub ime: Box<dyn Ime>,
221    pub reason: CreateTextEditorProxyErrorKind,
222}
223
224impl Debug for CreateTextEditorProxyError {
225    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226        f.write_fmt(format_args!("{:?}", self.reason))
227    }
228}
229
230impl RawTextEditorProxy {
231    pub fn new(ime: Box<dyn Ime>) -> Result<Self, CreateTextEditorProxyError> {
232        let raw_proxy = unsafe { OH_TextEditorProxy_Create() };
233        let Some(raw_proxy) = NonNull::new(raw_proxy) else {
234            let err = Err(CreateTextEditorProxyError {
235                ime,
236                reason: CreateTextEditorProxyErrorKind::CreateProxyFailed,
237            });
238            return err;
239        };
240        let mut proxy = Self { raw: raw_proxy };
241        DISPATCHER.register(proxy.raw, ime);
242        proxy.register_dispatcher_callbacks().map_err(|e| {
243            // unregistering here should never fail
244            let ime = DISPATCHER.unregister(proxy.raw).expect("Unregister failed");
245            CreateTextEditorProxyError {
246                ime,
247                reason: CreateTextEditorProxyErrorKind::RegisterCallbacksFailed(e),
248            }
249        })?;
250        Ok(proxy)
251    }
252
253    fn register_dispatcher_callbacks(&mut self) -> InputMethodResult {
254        use text_editor::*;
255        unsafe {
256            OH_TextEditorProxy_SetGetTextConfigFunc(self.raw.as_ptr(), Some(get_text_config))?;
257            OH_TextEditorProxy_SetInsertTextFunc(self.raw.as_ptr(), Some(insert_text))?;
258            OH_TextEditorProxy_SetDeleteForwardFunc(self.raw.as_ptr(), Some(delete_forward))?;
259            OH_TextEditorProxy_SetDeleteBackwardFunc(self.raw.as_ptr(), Some(delete_backward))?;
260            OH_TextEditorProxy_SetSendKeyboardStatusFunc(
261                self.raw.as_ptr(),
262                Some(send_keyboard_status),
263            )?;
264            OH_TextEditorProxy_SetSendEnterKeyFunc(self.raw.as_ptr(), Some(send_enter_key))?;
265            OH_TextEditorProxy_SetMoveCursorFunc(self.raw.as_ptr(), Some(move_cursor))?;
266            OH_TextEditorProxy_SetHandleSetSelectionFunc(
267                self.raw.as_ptr(),
268                Some(handle_set_selection),
269            )?;
270            OH_TextEditorProxy_SetHandleExtendActionFunc(
271                self.raw.as_ptr(),
272                Some(handle_extend_action),
273            )?;
274            OH_TextEditorProxy_SetGetLeftTextOfCursorFunc(
275                self.raw.as_ptr(),
276                Some(get_left_text_of_cursor),
277            )?;
278            OH_TextEditorProxy_SetGetRightTextOfCursorFunc(
279                self.raw.as_ptr(),
280                Some(get_right_text_of_cursor),
281            )?;
282            OH_TextEditorProxy_SetGetTextIndexAtCursorFunc(
283                self.raw.as_ptr(),
284                Some(get_text_index_at_cursor),
285            )?;
286            OH_TextEditorProxy_SetReceivePrivateCommandFunc(
287                self.raw.as_ptr(),
288                Some(receive_private_command),
289            )?;
290            OH_TextEditorProxy_SetSetPreviewTextFunc(self.raw.as_ptr(), Some(set_preview_text))?;
291            OH_TextEditorProxy_SetFinishTextPreviewFunc(
292                self.raw.as_ptr(),
293                Some(finish_text_preview),
294            )?;
295            Ok(())
296        }
297    }
298}
299
300impl Drop for RawTextEditorProxy {
301    fn drop(&mut self) {
302        unsafe {
303            OH_TextEditorProxy_Destroy(self.raw.as_ptr());
304        }
305    }
306}
307
308#[derive(Copy, Clone, Debug)]
309pub enum KeyboardStatus {
310    None,
311    Hidden,
312    Shown,
313    Unknown(u32),
314}
315
316impl From<InputMethod_KeyboardStatus> for KeyboardStatus {
317    fn from(status: InputMethod_KeyboardStatus) -> Self {
318        match status {
319            status if status == InputMethod_KeyboardStatus::IME_KEYBOARD_STATUS_NONE => {
320                KeyboardStatus::None
321            }
322            status if status == InputMethod_KeyboardStatus::IME_KEYBOARD_STATUS_HIDE => {
323                KeyboardStatus::Hidden
324            }
325            status if status == InputMethod_KeyboardStatus::IME_KEYBOARD_STATUS_SHOW => {
326                KeyboardStatus::Shown
327            }
328            status => {
329                warn!("Unknown keyboard status enum variant: {}", status.0);
330                KeyboardStatus::Unknown(status.0)
331            }
332        }
333    }
334}