cranpose_ui/text_input_session.rs
1//! Platform text-input session: soft-keyboard visibility hooks.
2//!
3//! Platforms with an on-screen keyboard (Android, iOS, some Linux shells)
4//! install a [`PlatformTextInputHandler`] so the framework can tell them when
5//! editable text gains or loses focus. The text-field focus manager
6//! ([`crate::text_field_focus`]) fires these notifications:
7//!
8//! - a text field requests the keyboard → `notify_text_input_focus_gained` →
9//! `show_keyboard`
10//! - focus was explicitly cleared, or the focused field left the composition →
11//! `notify_text_input_focus_lost` → `hide_keyboard`
12//!
13//! Focus requests show the keyboard when the field's `show_keyboard_on_focus`
14//! option is true. A pointer tap always requests it, including on an
15//! already-focused field. The user may have dismissed the
16//! keyboard (e.g. Android back gesture) without the framework knowing, and
17//! tapping the field again must bring it back. Platform show/hide calls are
18//! expected to be idempotent. `hide_keyboard` is only forwarded when the
19//! framework previously requested the keyboard, so repeated stale-focus checks
20//! do not spam the platform.
21//!
22//! The handler is stored per [`AppContext`](crate::render_state::AppContext),
23//! like the focus state itself, so multiple app instances in one process do
24//! not observe each other's keyboards.
25
26use std::{
27 cell::{Cell, RefCell},
28 rc::Rc,
29};
30
31/// Callbacks a platform installs to control its on-screen keyboard.
32///
33/// Implementations must be idempotent: `show_keyboard` may be invoked while
34/// the keyboard is already visible (every tap on a text field re-requests it)
35/// and `hide_keyboard` may race a keyboard the user already dismissed.
36pub trait PlatformTextInputHandler {
37 /// A focused text field requests its software keyboard.
38 fn show_keyboard(&self);
39 /// No text field is focused anymore; the platform should hide its soft
40 /// keyboard.
41 fn hide_keyboard(&self);
42}
43
44pub(crate) struct PlatformTextInputState {
45 handler: RefCell<Option<Rc<dyn PlatformTextInputHandler>>>,
46 keyboard_requested: Cell<bool>,
47 // Set while a focus request must leave the keyboard closed.
48 keyboard_held: Cell<bool>,
49}
50
51impl PlatformTextInputState {
52 pub(crate) fn new() -> Self {
53 Self {
54 handler: RefCell::new(None),
55 keyboard_requested: Cell::new(false),
56 keyboard_held: Cell::new(false),
57 }
58 }
59
60 fn set_handler(&self, handler: Option<Rc<dyn PlatformTextInputHandler>>) {
61 *self.handler.borrow_mut() = handler;
62 self.keyboard_requested.set(false);
63 }
64
65 fn handler(&self) -> Option<Rc<dyn PlatformTextInputHandler>> {
66 self.handler.borrow().clone()
67 }
68
69 pub(crate) fn notify_focus_lost(&self) {
70 if !self.keyboard_requested.replace(false) {
71 return;
72 }
73 if let Some(handler) = self.handler() {
74 handler.hide_keyboard();
75 }
76 }
77}
78
79/// Installs the platform soft-keyboard handler for the current app context.
80///
81/// Replaces any previously installed handler. Must be called inside an app
82/// context (platform runtimes go through
83/// `AppShell::set_platform_text_input`).
84pub fn set_platform_text_input_handler(handler: Rc<dyn PlatformTextInputHandler>) {
85 crate::render_state::with_text_input_session(|state| state.set_handler(Some(handler)));
86}
87
88/// Removes the installed platform soft-keyboard handler, if any.
89pub fn clear_platform_text_input_handler() {
90 crate::render_state::with_text_input_session(|state| state.set_handler(None));
91}
92
93pub(crate) fn notify_text_input_focus_gained() {
94 let handler = crate::render_state::with_text_input_session(|state| {
95 if state.keyboard_held.get() {
96 return None;
97 }
98 let handler = state.handler();
99 if handler.is_some() {
100 state.keyboard_requested.set(true);
101 }
102 handler
103 });
104 if let Some(handler) = handler {
105 handler.show_keyboard();
106 }
107}
108
109pub(crate) fn notify_text_input_focus_lost() {
110 crate::render_state::require_current_app_context("platform text input session access")
111 .text_input_session
112 .notify_focus_lost();
113}
114
115// Runs `focus`, in which a text field that takes focus leaves the software
116// keyboard closed.
117pub(crate) fn without_keyboard<R>(focus: impl FnOnce() -> R) -> R {
118 let held =
119 crate::render_state::with_text_input_session(|state| state.keyboard_held.replace(true));
120 let result = focus();
121 crate::render_state::with_text_input_session(|state| state.keyboard_held.set(held));
122 result
123}
124
125/// Hides the software keyboard the framework asked for, keeping the field's
126/// focus: a tap on the field asks for it again.
127pub(crate) fn hide_software_keyboard() {
128 notify_text_input_focus_lost();
129}
130
131/// Notifies the framework that the host app was paused (backgrounded — e.g.
132/// Android `onPause`).
133///
134/// Any outstanding soft-keyboard request is withdrawn and the platform is told
135/// to hide its keyboard, clearing the "keyboard shown" state so it cannot
136/// survive into the next resume. Without this, a platform that remembers the
137/// last editor view (Android's `InputMethodManager`) re-shows the keyboard when
138/// the app returns to the foreground even though the framework no longer has a
139/// focused field. Gated on an outstanding request, so it is a no-op when the
140/// keyboard was not showing.
141pub fn notify_app_paused() {
142 notify_text_input_focus_lost();
143}
144
145/// Notifies the framework that the host app resumed (foregrounded — e.g.
146/// Android `onResume`).
147///
148/// The soft keyboard is **never** auto-shown on resume, even when a text field
149/// is still focused. A warm resume (return from HOME / task switch / back-exit
150/// then relaunch) restores the process with the field's focus and caret intact,
151/// but the framework must not resurrect the keyboard for it: the platform's
152/// `InputMethodManager` remembers the last editor and would otherwise pop the
153/// keyboard back open on its own. The user brings it back by tapping the field
154/// (which re-requests it through `notify_text_input_focus_gained`).
155///
156/// Always returns `false` so the platform runtime force-hides the OS-restored
157/// keyboard. Pruning stale focus here keeps the keyboard-request bookkeeping
158/// consistent (a focused-but-detached field is dropped and its outstanding
159/// request withdrawn) without ever calling `show`.
160pub fn notify_app_resumed() -> bool {
161 let _ = crate::text_field_focus::has_focused_field();
162 false
163}
164
165#[cfg(test)]
166#[path = "tests/text_input_session_tests.rs"]
167mod tests;