Skip to main content

ferroui_native/
ferro_native_text_input_method.rs

1use crate::frn_string::to_c_string;
2use crate::helpers::{to_frn_rect, ComResultExt};
3use crate::interop::*;
4use ferroui_base::input::text_input::{ITextInputMethodImpl, TextInputMethodClient, TextInputOptions, TextSelection};
5use ferroui_base::reactive::IDisposable;
6use ferroui_base::Rect;
7use ferroui_microcom::ComPtr;
8use std::cell::RefCell;
9use std::ffi::CStr;
10use std::rc::{Rc, Weak};
11
12/// The text input method (IME) of a top-level.
13pub struct FerroNativeTextInputMethod {
14    weak_self: Weak<FerroNativeTextInputMethod>,
15    client: RefCell<Option<Rc<dyn TextInputMethodClient>>>,
16    /// The subscriptions to the events of the current client.
17    subscriptions: RefCell<Vec<Rc<dyn IDisposable>>>,
18    native_client: RefCell<Option<ComPtr<IFrnTextInputMethodClient>>>,
19    input_method: RefCell<Option<ComPtr<IFrnTextInputMethod>>>,
20}
21
22impl FerroNativeTextInputMethod {
23    pub(crate) fn new(top_level: &IFrnTopLevel) -> Rc<FerroNativeTextInputMethod> {
24        let input_method = top_level.get_input_method().check();
25        Rc::new_cyclic(|weak_self| FerroNativeTextInputMethod {
26            weak_self: weak_self.clone(),
27            client: RefCell::new(None),
28            subscriptions: RefCell::new(Vec::new()),
29            native_client: RefCell::new(None),
30            input_method: RefCell::new(input_method),
31        })
32    }
33
34    #[track_caller]
35    fn input_method(&self) -> ComPtr<IFrnTextInputMethod> {
36        match self.input_method.borrow().clone() {
37            Some(input_method) => input_method,
38            None => panic!("Cannot access a disposed object: the native text input method"),
39        }
40    }
41
42    fn client(&self) -> Option<Rc<dyn TextInputMethodClient>> {
43        self.client.borrow().clone()
44    }
45
46    /// Releases the native input method and the native client.
47    pub fn dispose(&self) {
48        let input_method = self.input_method.borrow_mut().take();
49        drop(input_method);
50        let native_client = self.native_client.borrow_mut().take();
51        drop(native_client);
52    }
53
54    fn unsubscribe(&self) {
55        let subscriptions = std::mem::take(&mut *self.subscriptions.borrow_mut());
56        for subscription in subscriptions {
57            subscription.dispose();
58        }
59    }
60
61    fn on_cursor_rectangle_changed(&self) {
62        let Some(client) = self.client() else {
63            return;
64        };
65
66        let Some(text_view_visual) = client.text_view_visual() else {
67            return;
68        };
69
70        let Some(visual_root) = text_view_visual.visual_root() else {
71            return;
72        };
73
74        let Some(transform) = text_view_visual.transform_to_visual(&visual_root) else {
75            return;
76        };
77
78        let rect = client.cursor_rectangle().transform_to_aabb(transform);
79
80        self.input_method().set_cursor_rect(to_frn_rect(rect));
81    }
82
83    fn on_surrounding_text_changed(&self) {
84        let Some(client) = self.client() else {
85            return;
86        };
87
88        let surrounding_text = client.surrounding_text();
89        let selection = client.selection();
90
91        self.input_method().set_surrounding_text(Some(&to_c_string(&surrounding_text)), selection.start, selection.end);
92    }
93
94    fn on_selection_changed(&self) {
95        let Some(client) = self.client() else {
96            return;
97        };
98
99        let selection = client.selection();
100        self.input_method().set_selection_in_surrounding_text(selection.start, selection.end);
101    }
102
103    fn handler(&self, action: fn(&FerroNativeTextInputMethod)) -> Rc<dyn Fn()> {
104        let this = self.weak_self.clone();
105        Rc::new(move || {
106            if let Some(this) = this.upgrade() {
107                action(&this);
108            }
109        })
110    }
111}
112
113impl ITextInputMethodImpl for FerroNativeTextInputMethod {
114    fn reset(&self) {
115        self.input_method().reset();
116    }
117
118    fn set_client(&self, client: Option<Rc<dyn TextInputMethodClient>>) {
119        let old_client = self.client();
120        if old_client.is_some_and(|client| client.supports_surrounding_text()) {
121            self.unsubscribe();
122        }
123
124        let old_native_client = self.native_client.borrow_mut().take();
125        drop(old_native_client);
126        let old_client = self.client.replace(client.clone());
127        drop(old_client);
128
129        if let Some(client) = client {
130            *self.native_client.borrow_mut() =
131                Some(IFrnTextInputMethodClient::from_impl(NativeTextInputMethodClient { client: client.clone() }));
132
133            self.on_surrounding_text_changed();
134            self.on_cursor_rectangle_changed();
135            // Note: on_selection_changed isn't called, it's already up-to-date thanks to on_surrounding_text_changed
136
137            // A client that does not support surrounding text is never
138            // unsubscribed from (see above), so stale subscriptions of such
139            // a client are released here rather than accumulated.
140            self.unsubscribe();
141            let subscriptions = vec![
142                client.surrounding_text_changed(self.handler(Self::on_surrounding_text_changed)),
143                client.cursor_rectangle_changed(self.handler(Self::on_cursor_rectangle_changed)),
144                client.selection_changed(self.handler(Self::on_selection_changed)),
145            ];
146            *self.subscriptions.borrow_mut() = subscriptions;
147        }
148
149        let native_client = self.native_client.borrow().clone();
150        self.input_method().set_client(native_client.as_deref()).check();
151    }
152
153    fn set_cursor_rect(&self, rect: Rect) {
154        self.input_method().set_cursor_rect(to_frn_rect(rect));
155    }
156
157    fn set_options(&self, _options: &TextInputOptions) {}
158}
159
160/// The client handed to the native input method.
161struct NativeTextInputMethodClient {
162    client: Rc<dyn TextInputMethodClient>,
163}
164
165impl IFrnTextInputMethodClientImpl for NativeTextInputMethodClient {
166    fn set_preedit_text(&self, preedit_text: Option<&CStr>) {
167        crate::callback_base::guard((), || {
168            if self.client.supports_preedit() {
169                let preedit_text = preedit_text.map(|text| text.to_string_lossy());
170                self.client.set_preedit_text(preedit_text.as_deref());
171            }
172        })
173    }
174
175    fn select_in_surrounding_text(&self, start: i32, end: i32) {
176        crate::callback_base::guard((), || {
177            if self.client.supports_surrounding_text() {
178                self.client.set_selection(TextSelection::new(start, end));
179            }
180        })
181    }
182}