Skip to main content

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}