agg_gui/widgets/on_screen_keyboard/state.rs
1//! Module-level state for the on-screen keyboard.
2//!
3//! Lives in a thread-local because the keyboard is a singleton — a
4//! browser tab has exactly one on-screen keyboard at any time, and the
5//! WASM target is single-threaded anyway. Native targets that want a
6//! distinct keyboard per window would have to wrap this state in a
7//! struct owned by the App; we don't ship that yet.
8
9use std::cell::RefCell;
10use std::time::Duration;
11use web_time::Instant;
12
13use crate::animation::Tween;
14
15use super::key::PaintedKey;
16use super::layouts::Layer;
17
18/// Slide animation duration (seconds). Tuned to feel like an OS keyboard
19/// raise — fast enough to not feel laggy, slow enough to register as a
20/// transition rather than a snap.
21pub const SLIDE_DURATION_SECS: f64 = 0.22;
22
23/// All mutable state owned by the keyboard module.
24///
25/// Kept private to the module so callers can't accidentally diverge from
26/// the controlled-mutation rules (e.g. retargeting the tween *also*
27/// requires `request_draw` to wake the event loop).
28pub struct KeyboardState {
29 /// Host opted in via [`super::set_enabled`]. Defaults to `false` so
30 /// desktop apps that never call it see no keyboard.
31 pub enabled: bool,
32 /// Set by [`super::set_text_input_focused`] when the focused widget
33 /// reports `accepts_text_input`.
34 pub text_input_focused: bool,
35 /// Slide animation. Value in `[0.0, 1.0]` interprets as
36 /// "fraction of the keyboard panel visible from the bottom".
37 pub slide: Tween,
38 /// Active layer (lowercase letters / shifted letters / numbers /
39 /// symbols).
40 pub current_layer: Layer,
41 /// Painted keys from the most recent paint pass. Used for tap
42 /// hit-testing. Coordinates are in the same Y-up viewport space the
43 /// paint pass uses.
44 pub last_painted_keys: Vec<PaintedKey>,
45 /// Height of the most recently painted panel in logical pixels.
46 /// `None` until first paint. Used by [`super::occluded_height`] to
47 /// report how much screen real estate the keyboard is consuming.
48 pub last_panel_height: Option<f64>,
49 /// Index into `last_painted_keys` of the key currently held down,
50 /// if any. Cleared when the pointer is released or moves off the
51 /// panel.
52 pub pressed_key_index: Option<usize>,
53 /// `true` between MouseDown and MouseUp on the panel — used by
54 /// the move/up handlers to know they should keep consuming events.
55 pub captured_pointer: bool,
56 /// `true` if the user has toggled caps lock on (via double-tap
57 /// shift). Holds the keyboard in the `Shifted` layer until shift
58 /// is tapped again.
59 pub caps_lock: bool,
60 /// Most recent shift-key tap, used to detect double-tap → caps lock.
61 /// Cleared after a non-shift key press or after the double-tap
62 /// window expires.
63 pub last_shift_tap: Option<Instant>,
64 /// State machine for the held key (currently only Backspace). When
65 /// set we keep firing the key every `repeat_period` after an
66 /// initial delay, until the pointer releases / leaves.
67 pub key_repeat: Option<KeyRepeatState>,
68 /// Set by [`super::dismiss`] when the user taps the keyboard's
69 /// close key. Drained once per event-loop iteration by the App
70 /// (see `App::drain_keyboard_events`), which calls
71 /// `set_focus(None)` so the previously-focused text field gets a
72 /// `FocusLost` and the keyboard-aware lift retargets back to 0 —
73 /// otherwise the keyboard panel slides down but the tree stays
74 /// lifted, leaving an empty band where the keyboard used to be.
75 pub dismiss_requested: bool,
76}
77
78/// Hold-to-repeat state captured the moment the user presses a
79/// repeat-eligible key (currently Backspace only). Polled from
80/// [`super::paint_software_keyboard`] every frame so the firing cadence
81/// happens in lockstep with the animation loop — no separate timer
82/// thread, fully WASM-friendly.
83#[derive(Debug, Clone, Copy)]
84pub struct KeyRepeatState {
85 /// Index into `last_painted_keys`. We re-check the key still exists
86 /// and is still under the pointer each tick.
87 pub key_index: usize,
88 /// When the user pressed the key down. `held_for` = now - pressed_at.
89 pub pressed_at: Instant,
90 /// When we last fired a synthetic key for this hold. `None` = never
91 /// fired; the first fire happens after `initial_delay` elapses.
92 pub last_fired_at: Option<Instant>,
93}
94
95impl KeyRepeatState {
96 /// How long the user must hold before the first repeat fires.
97 pub const INITIAL_DELAY: Duration = Duration::from_millis(450);
98 /// Period between subsequent repeats. Constant for now; we could
99 /// ramp it down for an accelerating delete-line feel later.
100 pub const REPEAT_PERIOD: Duration = Duration::from_millis(70);
101}
102
103/// Maximum gap between two Shift taps to count as a double-tap →
104/// caps lock toggle.
105pub const SHIFT_DOUBLE_TAP_WINDOW: Duration = Duration::from_millis(350);
106
107impl Default for KeyboardState {
108 fn default() -> Self {
109 Self {
110 enabled: false,
111 text_input_focused: false,
112 slide: Tween::new(0.0, SLIDE_DURATION_SECS),
113 current_layer: Layer::Letters,
114 last_painted_keys: Vec::new(),
115 last_panel_height: None,
116 pressed_key_index: None,
117 captured_pointer: false,
118 caps_lock: false,
119 last_shift_tap: None,
120 key_repeat: None,
121 dismiss_requested: false,
122 }
123 }
124}
125
126impl KeyboardState {
127 /// Current eased visible fraction. Wraps [`Tween::value`] so callers
128 /// outside the module don't need a mutable borrow just to peek.
129 pub fn visible_fraction(&self) -> f64 {
130 self.slide.value()
131 }
132}
133
134thread_local! {
135 static STATE: RefCell<KeyboardState> = RefCell::new(KeyboardState::default());
136}
137
138pub fn with_state_ref<R>(f: impl FnOnce(&KeyboardState) -> R) -> R {
139 STATE.with(|cell| f(&cell.borrow()))
140}
141
142pub fn with_state_mut<R>(f: impl FnOnce(&mut KeyboardState) -> R) -> R {
143 STATE.with(|cell| f(&mut cell.borrow_mut()))
144}