1
//! Text Input Manager
2
//!
3
//! Centralizes all text editing logic for contenteditable nodes.
4
//!
5
//! This manager handles text input from multiple sources:
6
//!
7
//! - Keyboard input (character insertion, backspace, etc.)
8
//! - IME composition (multi-character input for Asian languages)
9
//! - Accessibility actions (screen readers, voice control)
10
//! - Programmatic edits (from callbacks)
11
//!
12
//! ## Architecture
13
//!
14
//! The text input system uses a two-phase approach:
15
//!
16
//! 1. **Record Phase**: When text input occurs, record what changed (old_text + inserted_text)
17
//!
18
//!    - Store in `pending_changeset`
19
//!    - Do NOT modify any caches yet
20
//!    - Return affected nodes so callbacks can be invoked
21
//!
22
//! 2. **Apply Phase**: After callbacks, if preventDefault was not set:
23
//!
24
//!    - Compute new text using text3::edit
25
//!    - Update cursor position
26
//!    - Update text cache
27
//!    - Mark nodes dirty for re-layout
28
//!
29
//! This separation allows:
30
//!
31
//! - User callbacks to inspect the changeset before it's applied
32
//! - preventDefault to cancel the edit
33
//! - Consistent behavior across keyboard/IME/A11y sources
34

            
35
use azul_core::{
36
    dom::DomNodeId,
37
    events::{EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent},
38
    selection::TextCursor,
39
    task::Instant,
40
};
41
use azul_css::corety::AzString;
42

            
43
/// Information about a pending text edit that hasn't been applied yet
44
#[derive(Debug, Clone)]
45
#[repr(C)]
46
pub struct PendingTextEdit {
47
    /// The node that was edited
48
    pub node: DomNodeId,
49
    /// The text that was inserted
50
    pub inserted_text: AzString,
51
    /// The old text before the edit (plain text extracted from InlineContent)
52
    pub old_text: AzString,
53
}
54

            
55
impl PendingTextEdit {
56
    /// Compute the resulting text after applying the edit
57
    ///
58
    /// This is a pure function that applies the inserted_text to old_text
59
    /// using the current cursor position.
60
    ///
61
    /// NOTE: Actual text application is handled by apply_text_changeset() in window.rs
62
    /// which uses text3::edit::insert_text() for proper cursor-based insertion.
63
    /// This method is for preview/inspection purposes only.
64
    pub fn resulting_text(&self, cursor: Option<&TextCursor>) -> AzString {
65
        // For preview: append the inserted text
66
        // Actual insertion at cursor is done by text3::edit::insert_text()
67
        let mut result = self.old_text.as_str().to_string();
68
        result.push_str(self.inserted_text.as_str());
69

            
70
        let _ = cursor; // Preview doesn't need cursor - actual insert does
71

            
72
        result.into()
73
    }
74
}
75

            
76
/// C-compatible Option type for PendingTextEdit
77
#[derive(Debug, Clone)]
78
#[repr(C, u8)]
79
pub enum OptionPendingTextEdit {
80
    None,
81
    Some(PendingTextEdit),
82
}
83

            
84
impl OptionPendingTextEdit {
85
    pub fn into_option(self) -> Option<PendingTextEdit> {
86
        match self {
87
            OptionPendingTextEdit::None => None,
88
            OptionPendingTextEdit::Some(t) => Some(t),
89
        }
90
    }
91
}
92

            
93
impl From<Option<PendingTextEdit>> for OptionPendingTextEdit {
94
    fn from(o: Option<PendingTextEdit>) -> Self {
95
        match o {
96
            Some(v) => OptionPendingTextEdit::Some(v),
97
            None => OptionPendingTextEdit::None,
98
        }
99
    }
100
}
101

            
102
impl<'a> From<Option<&'a PendingTextEdit>> for OptionPendingTextEdit {
103
    fn from(o: Option<&'a PendingTextEdit>) -> Self {
104
        match o {
105
            Some(v) => OptionPendingTextEdit::Some(v.clone()),
106
            None => OptionPendingTextEdit::None,
107
        }
108
    }
109
}
110

            
111
/// Source of a text input event
112
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113
pub enum TextInputSource {
114
    /// Regular keyboard input
115
    Keyboard,
116
    /// IME composition (multi-character input)
117
    Ime,
118
    /// Accessibility action from assistive technology
119
    Accessibility,
120
    /// Programmatic edit from user callback
121
    Programmatic,
122
}
123

            
124
/// Text Input Manager
125
///
126
/// Centralizes all text editing logic. This is the single source of truth
127
/// for text input state.
128
pub struct TextInputManager {
129
    /// The pending text changeset that hasn't been applied yet.
130
    /// This is set during the "record" phase and cleared after the "apply" phase.
131
    pub pending_changeset: Option<PendingTextEdit>,
132
    /// Source of the current text input
133
    pub input_source: Option<TextInputSource>,
134
}
135

            
136
impl TextInputManager {
137
    /// Create a new TextInputManager
138
2321
    pub fn new() -> Self {
139
2321
        Self {
140
2321
            pending_changeset: None,
141
2321
            input_source: None,
142
2321
        }
143
2321
    }
144

            
145
    /// Record a text input event (Phase 1)
146
    ///
147
    /// This ONLY records what text was inserted. It does NOT apply the changes yet.
148
    /// The changes are applied later in `apply_changeset()` if preventDefault is not set.
149
    ///
150
    /// # Arguments
151
    ///
152
    /// - `node` - The DOM node being edited
153
    /// - `inserted_text` - The text being inserted
154
    /// - `old_text` - The current text before the edit
155
    /// - `source` - Where the input came from (keyboard, IME, A11y, etc.)
156
    ///
157
    /// Returns the affected node for event generation.
158
455
    pub fn record_input(
159
455
        &mut self,
160
455
        node: DomNodeId,
161
455
        inserted_text: String,
162
455
        old_text: String,
163
455
        source: TextInputSource,
164
455
    ) -> DomNodeId {
165
455
        self.pending_changeset = Some(PendingTextEdit {
166
455
            node,
167
455
            inserted_text: inserted_text.into(),
168
455
            old_text: old_text.into(),
169
455
        });
170

            
171
455
        self.input_source = Some(source);
172

            
173
455
        node
174
455
    }
175

            
176
    /// Get the pending changeset (if any)
177
910
    pub fn get_pending_changeset(&self) -> Option<&PendingTextEdit> {
178
910
        self.pending_changeset.as_ref()
179
910
    }
180

            
181
    /// Clear the pending changeset
182
    ///
183
    /// This is called after applying the changeset or if preventDefault was set.
184
455
    pub fn clear_changeset(&mut self) {
185
455
        self.pending_changeset = None;
186
455
        self.input_source = None;
187
455
    }
188

            
189
    /// Check if there's a pending changeset that needs to be applied
190
    pub fn has_pending_changeset(&self) -> bool {
191
        self.pending_changeset.is_some()
192
    }
193
}
194

            
195
impl Default for TextInputManager {
196
    fn default() -> Self {
197
        Self::new()
198
    }
199
}
200

            
201
impl EventProvider for TextInputManager {
202
    /// Get pending text input events.
203
    ///
204
    /// If there's a pending changeset, returns an Input event for the affected node.
205
    /// The event data includes the old text and inserted text so callbacks can
206
    /// query the changeset.
207
    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
208
        let mut events = Vec::new();
209

            
210
        if let Some(changeset) = &self.pending_changeset {
211
            let event_source = match self.input_source {
212
                Some(TextInputSource::Keyboard) | Some(TextInputSource::Ime) => {
213
                    CoreEventSource::User
214
                }
215
                Some(TextInputSource::Accessibility) => CoreEventSource::User, /* A11y is still */
216
                // user input
217
                Some(TextInputSource::Programmatic) => CoreEventSource::Programmatic,
218
                None => CoreEventSource::User,
219
            };
220

            
221
            // Generate Input event (fires on every keystroke)
222
            events.push(SyntheticEvent::new(
223
                EventType::Input,
224
                event_source,
225
                changeset.node,
226
                timestamp,
227
                // Callbacks can query changeset via
228
                // text_input_manager.get_pending_changeset()
229
                EventData::None,
230
            ));
231

            
232
            // Note: We don't generate Change events here - those are generated
233
            // when focus is lost or Enter is pressed (handled elsewhere)
234
        }
235

            
236
        events
237
    }
238
}