cranpose_ui/
text_input_session.rs1use std::cell::{Cell, RefCell};
26use std::rc::Rc;
27
28pub trait PlatformTextInputHandler {
34 fn show_keyboard(&self);
36 fn hide_keyboard(&self);
39}
40
41pub(crate) struct PlatformTextInputState {
43 handler: RefCell<Option<Rc<dyn PlatformTextInputHandler>>>,
44 keyboard_requested: Cell<bool>,
48}
49
50impl PlatformTextInputState {
51 pub(crate) fn new() -> Self {
52 Self {
53 handler: RefCell::new(None),
54 keyboard_requested: Cell::new(false),
55 }
56 }
57
58 fn set_handler(&self, handler: Option<Rc<dyn PlatformTextInputHandler>>) {
59 *self.handler.borrow_mut() = handler;
60 self.keyboard_requested.set(false);
61 }
62
63 fn handler(&self) -> Option<Rc<dyn PlatformTextInputHandler>> {
64 self.handler.borrow().clone()
65 }
66}
67
68pub fn set_platform_text_input_handler(handler: Rc<dyn PlatformTextInputHandler>) {
74 crate::render_state::with_text_input_session(|state| state.set_handler(Some(handler)));
75}
76
77pub fn clear_platform_text_input_handler() {
79 crate::render_state::with_text_input_session(|state| state.set_handler(None));
80}
81
82pub(crate) fn notify_text_input_focus_gained() {
87 let handler = crate::render_state::with_text_input_session(|state| {
88 let handler = state.handler();
89 if handler.is_some() {
90 state.keyboard_requested.set(true);
91 }
92 handler
93 });
94 if let Some(handler) = handler {
97 handler.show_keyboard();
98 }
99}
100
101pub(crate) fn notify_text_input_focus_lost() {
107 let handler = crate::render_state::with_text_input_session(|state| {
108 if !state.keyboard_requested.replace(false) {
109 return None;
110 }
111 state.handler()
112 });
113 if let Some(handler) = handler {
114 handler.hide_keyboard();
115 }
116}
117
118pub fn notify_app_paused() {
129 notify_text_input_focus_lost();
133}
134
135pub fn notify_app_resumed() -> bool {
142 if !crate::text_field_focus::has_focused_field() {
145 return false;
146 }
147 notify_text_input_focus_gained();
148 true
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use std::cell::RefCell as StdRefCell;
155
156 #[derive(Default)]
157 struct RecordingHandler {
158 calls: StdRefCell<Vec<&'static str>>,
159 }
160
161 impl PlatformTextInputHandler for RecordingHandler {
162 fn show_keyboard(&self) {
163 self.calls.borrow_mut().push("show");
164 }
165
166 fn hide_keyboard(&self) {
167 self.calls.borrow_mut().push("hide");
168 }
169 }
170
171 fn install_recording_handler() -> Rc<RecordingHandler> {
172 let handler = Rc::new(RecordingHandler::default());
173 set_platform_text_input_handler(handler.clone());
174 handler
175 }
176
177 #[test]
178 fn focus_gained_shows_keyboard() {
179 let _app_context = crate::render_state::app_context_test_scope();
180 let handler = install_recording_handler();
181
182 notify_text_input_focus_gained();
183
184 assert_eq!(*handler.calls.borrow(), vec!["show"]);
185 }
186
187 #[test]
188 fn focus_lost_hides_keyboard_once() {
189 let _app_context = crate::render_state::app_context_test_scope();
190 let handler = install_recording_handler();
191
192 notify_text_input_focus_gained();
193 notify_text_input_focus_lost();
194 notify_text_input_focus_lost();
197
198 assert_eq!(*handler.calls.borrow(), vec!["show", "hide"]);
199 }
200
201 #[test]
202 fn focus_lost_without_prior_show_is_not_forwarded() {
203 let _app_context = crate::render_state::app_context_test_scope();
204 let handler = install_recording_handler();
205
206 notify_text_input_focus_lost();
207
208 assert!(handler.calls.borrow().is_empty());
209 }
210
211 #[test]
212 fn repeated_focus_gain_reshows_keyboard() {
213 let _app_context = crate::render_state::app_context_test_scope();
214 let handler = install_recording_handler();
215
216 notify_text_input_focus_gained();
219 notify_text_input_focus_gained();
220
221 assert_eq!(*handler.calls.borrow(), vec!["show", "show"]);
222 }
223
224 #[test]
225 fn notifications_without_handler_are_noops() {
226 let _app_context = crate::render_state::app_context_test_scope();
227 notify_text_input_focus_gained();
228 notify_text_input_focus_lost();
229 }
230
231 #[test]
232 fn clearing_handler_stops_notifications() {
233 let _app_context = crate::render_state::app_context_test_scope();
234 let handler = install_recording_handler();
235
236 notify_text_input_focus_gained();
237 clear_platform_text_input_handler();
238 notify_text_input_focus_lost();
239
240 assert_eq!(*handler.calls.borrow(), vec!["show"]);
241 }
242
243 struct NoopFocusHandler;
244 impl crate::text_field_focus::FocusedTextFieldHandler for NoopFocusHandler {
245 fn handle_key(&self, _: &crate::key_event::KeyEvent) -> bool {
246 false
247 }
248 fn insert_text(&self, _: &str) {}
249 fn delete_surrounding(&self, _: usize, _: usize) {}
250 fn copy_selection(&self) -> Option<String> {
251 None
252 }
253 fn cut_selection(&self) -> Option<String> {
254 None
255 }
256 fn set_composition(&self, _: &str, _: Option<(usize, usize)>) {}
257 }
258
259 fn focus_a_field() -> Rc<std::cell::RefCell<bool>> {
260 let focus = Rc::new(std::cell::RefCell::new(false));
261 crate::text_field_focus::request_focus(Rc::clone(&focus), Rc::new(NoopFocusHandler));
262 focus
263 }
264
265 #[test]
266 fn resume_without_a_focused_field_does_not_show_the_keyboard() {
267 let _app_context = crate::render_state::app_context_test_scope();
268 let handler = install_recording_handler();
269
270 notify_text_input_focus_gained();
274 notify_app_paused();
275 assert_eq!(*handler.calls.borrow(), vec!["show", "hide"]);
276
277 assert!(!notify_app_resumed());
278 assert_eq!(
279 *handler.calls.borrow(),
280 vec!["show", "hide"],
281 "resume with no focused field must not re-show the keyboard"
282 );
283 }
284
285 #[test]
286 fn pause_hides_and_resume_restores_the_keyboard_for_a_focused_field() {
287 let _app_context = crate::render_state::app_context_test_scope();
288 let handler = install_recording_handler();
289
290 let _focus = focus_a_field();
292 assert_eq!(*handler.calls.borrow(), vec!["show"]);
293
294 notify_app_paused();
296 assert_eq!(*handler.calls.borrow(), vec!["show", "hide"]);
297
298 assert!(notify_app_resumed());
300 assert_eq!(*handler.calls.borrow(), vec!["show", "hide", "show"]);
301
302 crate::text_field_focus::clear_focus();
303 }
304
305 #[test]
306 fn cold_start_with_no_focus_does_not_show_the_keyboard() {
307 let _app_context = crate::render_state::app_context_test_scope();
313 let handler = install_recording_handler();
314
315 assert!(
316 !notify_app_resumed(),
317 "a launch with no focused field must not re-open the keyboard"
318 );
319 assert!(
320 handler.calls.borrow().is_empty(),
321 "no platform show/hide should be requested for an unfocused cold start"
322 );
323 }
324
325 #[test]
326 fn pause_is_a_noop_when_the_keyboard_was_not_showing() {
327 let _app_context = crate::render_state::app_context_test_scope();
328 let handler = install_recording_handler();
329
330 notify_app_paused();
331 assert!(
332 handler.calls.borrow().is_empty(),
333 "pausing without a shown keyboard must not call the platform"
334 );
335 }
336}