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 azul_core::{
36    dom::DomNodeId,
37    events::{EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent},
38    selection::TextCursor,
39    task::Instant,
40};
41use 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)]
46pub 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
55impl 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)]
79pub enum OptionPendingTextEdit {
80    None,
81    Some(PendingTextEdit),
82}
83
84impl 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
93impl 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
102impl<'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)]
113pub 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.
128pub 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
136impl TextInputManager {
137    /// Create a new TextInputManager
138    pub fn new() -> Self {
139        Self {
140            pending_changeset: None,
141            input_source: None,
142        }
143    }
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    pub fn record_input(
159        &mut self,
160        node: DomNodeId,
161        inserted_text: String,
162        old_text: String,
163        source: TextInputSource,
164    ) -> DomNodeId {
165        self.pending_changeset = Some(PendingTextEdit {
166            node,
167            inserted_text: inserted_text.into(),
168            old_text: old_text.into(),
169        });
170
171        self.input_source = Some(source);
172
173        node
174    }
175
176    /// Get the pending changeset (if any)
177    pub fn get_pending_changeset(&self) -> Option<&PendingTextEdit> {
178        self.pending_changeset.as_ref()
179    }
180
181    /// Clear the pending changeset
182    ///
183    /// This is called after applying the changeset or if preventDefault was set.
184    pub fn clear_changeset(&mut self) {
185        self.pending_changeset = None;
186        self.input_source = None;
187    }
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
195impl Default for TextInputManager {
196    fn default() -> Self {
197        Self::new()
198    }
199}
200
201impl 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}