Skip to main content

azul_layout/managers/
text_input.rs

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
35use std::collections::BTreeMap;
36
37use azul_core::{
38    dom::{DomId, DomNodeId, NodeId},
39    events::{EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent},
40    selection::TextCursor,
41    task::Instant,
42};
43use azul_css::corety::AzString;
44
45/// Information about a pending text edit that hasn't been applied yet
46#[derive(Debug, Clone)]
47#[repr(C)]
48pub struct PendingTextEdit {
49    /// The node that was edited
50    pub node: DomNodeId,
51    /// The text that was inserted
52    pub inserted_text: AzString,
53    /// The old text before the edit (plain text extracted from InlineContent)
54    pub old_text: AzString,
55}
56
57impl PendingTextEdit {
58    /// Compute the resulting text after applying the edit
59    ///
60    /// This is a pure function that applies the inserted_text to old_text
61    /// using the current cursor position.
62    ///
63    /// NOTE: Actual text application is handled by apply_text_changeset() in window.rs
64    /// which uses text3::edit::insert_text() for proper cursor-based insertion.
65    /// This method is for preview/inspection purposes only.
66    pub fn resulting_text(&self, cursor: Option<&TextCursor>) -> AzString {
67        // For preview: append the inserted text
68        // Actual insertion at cursor is done by text3::edit::insert_text()
69        let mut result = self.old_text.as_str().to_string();
70        result.push_str(self.inserted_text.as_str());
71
72        let _ = cursor; // Preview doesn't need cursor - actual insert does
73
74        result.into()
75    }
76}
77
78/// C-compatible Option type for PendingTextEdit
79#[derive(Debug, Clone)]
80#[repr(C, u8)]
81pub enum OptionPendingTextEdit {
82    None,
83    Some(PendingTextEdit),
84}
85
86impl OptionPendingTextEdit {
87    pub fn into_option(self) -> Option<PendingTextEdit> {
88        match self {
89            OptionPendingTextEdit::None => None,
90            OptionPendingTextEdit::Some(t) => Some(t),
91        }
92    }
93}
94
95impl From<Option<PendingTextEdit>> for OptionPendingTextEdit {
96    fn from(o: Option<PendingTextEdit>) -> Self {
97        match o {
98            Some(v) => OptionPendingTextEdit::Some(v),
99            None => OptionPendingTextEdit::None,
100        }
101    }
102}
103
104impl<'a> From<Option<&'a PendingTextEdit>> for OptionPendingTextEdit {
105    fn from(o: Option<&'a PendingTextEdit>) -> Self {
106        match o {
107            Some(v) => OptionPendingTextEdit::Some(v.clone()),
108            None => OptionPendingTextEdit::None,
109        }
110    }
111}
112
113/// Source of a text input event
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub enum TextInputSource {
116    /// Regular keyboard input
117    Keyboard,
118    /// IME composition (multi-character input)
119    Ime,
120    /// Accessibility action from assistive technology
121    Accessibility,
122    /// Programmatic edit from user callback
123    Programmatic,
124}
125
126/// Text Input Manager
127///
128/// Centralizes all text editing logic. This is the single source of truth
129/// for text input state.
130pub struct TextInputManager {
131    /// The pending text changeset that hasn't been applied yet.
132    /// This is set during the "record" phase and cleared after the "apply" phase.
133    pub pending_changeset: Option<PendingTextEdit>,
134    /// Source of the current text input
135    pub input_source: Option<TextInputSource>,
136}
137
138impl TextInputManager {
139    /// Create a new TextInputManager
140    pub fn new() -> Self {
141        Self {
142            pending_changeset: None,
143            input_source: None,
144        }
145    }
146
147    /// Record a text input event (Phase 1)
148    ///
149    /// This ONLY records what text was inserted. It does NOT apply the changes yet.
150    /// The changes are applied later in `apply_changeset()` if preventDefault is not set.
151    ///
152    /// # Arguments
153    ///
154    /// - `node` - The DOM node being edited
155    /// - `inserted_text` - The text being inserted
156    /// - `old_text` - The current text before the edit
157    /// - `source` - Where the input came from (keyboard, IME, A11y, etc.)
158    ///
159    /// Returns the affected node for event generation.
160    pub fn record_input(
161        &mut self,
162        node: DomNodeId,
163        inserted_text: String,
164        old_text: String,
165        source: TextInputSource,
166    ) -> DomNodeId {
167        println!("[TextInputManager::record_input] Recording input for node {:?}", node);
168        println!("[TextInputManager::record_input] Inserted text: '{}', old_text len: {}", inserted_text, old_text.len());
169        println!("[TextInputManager::record_input] Source: {:?}", source);
170
171        // Clear any previous changeset
172        self.pending_changeset = None;
173
174        // Store the new changeset
175        self.pending_changeset = Some(PendingTextEdit {
176            node,
177            inserted_text: inserted_text.into(),
178            old_text: old_text.into(),
179        });
180
181        self.input_source = Some(source);
182        println!("[TextInputManager::record_input] Changeset stored successfully");
183
184        node
185    }
186
187    /// Get the pending changeset (if any)
188    pub fn get_pending_changeset(&self) -> Option<&PendingTextEdit> {
189        let result = self.pending_changeset.as_ref();
190        if result.is_some() {
191            println!("[TextInputManager::get_pending_changeset] Returning pending changeset");
192        } else {
193            println!("[TextInputManager::get_pending_changeset] No pending changeset!");
194        }
195        result
196    }
197
198    /// Clear the pending changeset
199    ///
200    /// This is called after applying the changeset or if preventDefault was set.
201    pub fn clear_changeset(&mut self) {
202        println!("[TextInputManager::clear_changeset] Clearing changeset");
203        self.pending_changeset = None;
204        self.input_source = None;
205    }
206
207    /// Check if there's a pending changeset that needs to be applied
208    pub fn has_pending_changeset(&self) -> bool {
209        self.pending_changeset.is_some()
210    }
211}
212
213impl Default for TextInputManager {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219impl EventProvider for TextInputManager {
220    /// Get pending text input events.
221    ///
222    /// If there's a pending changeset, returns an Input event for the affected node.
223    /// The event data includes the old text and inserted text so callbacks can
224    /// query the changeset.
225    fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
226        let mut events = Vec::new();
227
228        if let Some(changeset) = &self.pending_changeset {
229            let event_source = match self.input_source {
230                Some(TextInputSource::Keyboard) | Some(TextInputSource::Ime) => {
231                    CoreEventSource::User
232                }
233                Some(TextInputSource::Accessibility) => CoreEventSource::User, /* A11y is still */
234                // user input
235                Some(TextInputSource::Programmatic) => CoreEventSource::Programmatic,
236                None => CoreEventSource::User,
237            };
238
239            // Generate Input event (fires on every keystroke)
240            events.push(SyntheticEvent::new(
241                EventType::Input,
242                event_source,
243                changeset.node,
244                timestamp,
245                // Callbacks can query changeset via
246                // text_input_manager.get_pending_changeset()
247                EventData::None,
248            ));
249
250            // Note: We don't generate Change events here - those are generated
251            // when focus is lost or Enter is pressed (handled elsewhere)
252        }
253
254        events
255    }
256}