Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/platform/wasm/soft_keyboard.rs
StratoSDK / crates / strato-ui-renderer / src / platform / wasm / soft_keyboard.rs
1//! Soft keyboard support for mobile WASM.
2//!
3//! On mobile browsers, the soft keyboard only appears when a native HTML input element
4//! is focused. This module provides utilities to manage a hidden input element to
5//! trigger the soft keyboard when needed.
6//!
7//! ## Architecture
8//!
9//! - `SoftKeyboardManager`: Coordinates the hidden input element and keyboard state
10//! - Mobile detection utilities are in the `mobile_detection` submodule
11 
12use std::cell::RefCell;
13use std::rc::Rc;
14 
15use wasm_bindgen::JsValue;
16 
17use super::hidden_input::{HiddenInput, HiddenInputEvent};
18 
19// ============================================================================
20// Soft Keyboard State
21// ============================================================================
22 
23/// Represents the visibility state of the soft keyboard.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
25pub enum SoftKeyboardState {
26 /// The soft keyboard is hidden.
27 #[default]
28 Hidden,
29 /// The soft keyboard is visible (or should be shown).
30 Visible,
31}
32 
33impl SoftKeyboardState {
34 /// Returns true if the keyboard should be visible.
35 pub fn is_visible(&self) -> bool {
36 matches!(self, Self::Visible)
37 }
38}
39 
40/// Maps a HiddenInputEvent to a SoftKeyboardInput.
41fn map_hidden_input_event(event: HiddenInputEvent) -> Option<SoftKeyboardInput> {
42 match event {
43 HiddenInputEvent::InsertText { text } => Some(SoftKeyboardInput::TextInserted(text)),
44 HiddenInputEvent::Backspace | HiddenInputEvent::Delete => {
45 Some(SoftKeyboardInput::Backspace)
46 }
47 HiddenInputEvent::Blur => Some(SoftKeyboardInput::KeyboardDismissed),
48 HiddenInputEvent::KeyDown { key } => Some(SoftKeyboardInput::KeyDown(key)),
49 }
50}
51 
52// ============================================================================
53// Soft Keyboard Manager
54// ============================================================================
55 
56/// Callback type for soft keyboard input events.
57/// The callback receives the processed input event.
58pub type SoftKeyboardInputCallback = Box<dyn FnMut(SoftKeyboardInput)>;
59 
60/// Processed input from the soft keyboard.
61#[derive(Debug, Clone)]
62pub enum SoftKeyboardInput {
63 /// Text was inserted.
64 TextInserted(String),
65 /// Backspace was pressed.
66 Backspace,
67 /// The keyboard was dismissed externally (e.g., iOS "Done" button).
68 KeyboardDismissed,
69 /// A special key was pressed (e.g., Enter).
70 KeyDown(String),
71}
72 
73/// Manages the soft keyboard for mobile WASM.
74///
75/// This struct coordinates:
76/// - The hidden input element that triggers the keyboard
77/// - The current keyboard state (visible/hidden)
78/// - Processing input events and forwarding them to the app
79///
80/// # Usage
81///
82/// ```ignore
83/// // Create the manager (only on mobile)
84/// if mobile_detection::is_mobile_device() {
85/// let manager = SoftKeyboardManager::new(|input| {
86/// // Handle input from soft keyboard
87/// })?;
88///
89/// // Show keyboard when text input is focused
90/// manager.show_keyboard();
91///
92/// // Hide keyboard when text input is blurred
93/// manager.hide_keyboard();
94/// }
95/// ```
96pub struct SoftKeyboardManager {
97 hidden_input: HiddenInput,
98 state: RefCell<SoftKeyboardState>,
99}
100 
101impl SoftKeyboardManager {
102 /// Creates a new soft keyboard manager.
103 ///
104 /// This creates the hidden input element and sets up event forwarding.
105 /// Should only be called on mobile devices (check `is_mobile_device()` first).
106 ///
107 /// # Arguments
108 /// * `on_input` - Callback invoked when the user types on the soft keyboard.
109 ///
110 /// # Errors
111 /// Returns an error if the hidden input element cannot be created.
112 pub fn new(on_input: SoftKeyboardInputCallback) -> Result<Rc<Self>, JsValue> {
113 let on_input = RefCell::new(on_input);
114 
115 // Create a callback that processes hidden input events and forwards them
116 let callback: super::hidden_input::InputCallback =
117 Rc::new(RefCell::new(move |event: HiddenInputEvent| {
118 if let Some(input) = map_hidden_input_event(event) {
119 on_input.borrow_mut()(input);
120 }
121 }));
122 
123 let hidden_input = HiddenInput::new(callback)?;
124 
125 Ok(Rc::new(Self {
126 hidden_input,
127 state: RefCell::new(SoftKeyboardState::Hidden),
128 }))
129 }
130 
131 /// Shows the soft keyboard by focusing the hidden input.
132 ///
133 /// This should be called when a text input in the app gains focus.
134 pub fn show_keyboard(&self) {
135 // Always call focus() - the browser handles redundant calls gracefully.
136 // We don't rely on our internal state because the user can dismiss the keyboard
137 // via browser controls (e.g., "Done" button), which doesn't update our state.
138 
139 if let Err(e) = self.hidden_input.focus() {
140 log::warn!("Failed to focus hidden input for soft keyboard: {:?}", e);
141 }
142 *self.state.borrow_mut() = SoftKeyboardState::Visible;
143 }
144 
145 /// Hides the soft keyboard by blurring the hidden input.
146 ///
147 /// For canvas-based apps, this must be called explicitly when the user taps
148 /// outside a text input area, since the browser can't detect "outside" taps
149 /// when everything renders to a single canvas element.
150 pub fn hide_keyboard(&self) {
151 if let Err(e) = self.hidden_input.blur() {
152 log::warn!("Failed to blur hidden input for soft keyboard: {:?}", e);
153 }
154 *self.state.borrow_mut() = SoftKeyboardState::Hidden;
155 }
156 
157 /// Returns the current keyboard state.
158 pub fn state(&self) -> SoftKeyboardState {
159 *self.state.borrow()
160 }
161 
162 /// Returns whether the soft keyboard is currently visible.
163 pub fn is_visible(&self) -> bool {
164 self.state.borrow().is_visible()
165 }
166 
167 /// Returns whether the hidden input element currently has focus.
168 ///
169 /// This is used to detect when browser focus events are due to the soft keyboard
170 /// rather than the user actually switching away from the window.
171 pub fn has_focus(&self) -> bool {
172 self.hidden_input.has_focus()
173 }
174 
175 /// Resets the hidden input to its sentinel state.
176 ///
177 /// Sets the value to a single space and positions the cursor after it.
178 /// This is automatically called on focus and after every input event,
179 /// but can be called manually if needed.
180 pub fn reset_input(&self) {
181 self.hidden_input.reset_input();
182 }
183}
184 
185#[cfg(test)]
186#[path = "soft_keyboard_tests.rs"]
187mod tests;
188