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