1use azul_core::{
36 dom::DomNodeId,
37 events::{
38 EventData, EventProvider, EventSource as CoreEventSource, EventType, SyntheticEvent,
39 TextInputEventData,
40 },
41 task::Instant,
42};
43use azul_css::corety::AzString;
44
45#[derive(Debug, Clone)]
47#[repr(C)]
48pub struct PendingTextEdit {
49 pub node: DomNodeId,
51 pub inserted_text: AzString,
53 pub old_text: AzString,
55}
56
57impl PendingTextEdit {
58 #[must_use] pub fn resulting_text(&self) -> AzString {
63 let mut result = self.old_text.as_str().to_string();
64 result.push_str(self.inserted_text.as_str());
65 result.into()
66 }
67}
68#[allow(variant_size_differences)] #[derive(Debug, Clone)]
71#[repr(C, u8)]
72pub enum OptionPendingTextEdit {
73 None,
74 Some(PendingTextEdit),
75}
76
77impl OptionPendingTextEdit {
78 #[must_use] pub fn into_option(self) -> Option<PendingTextEdit> {
79 match self {
80 Self::None => None,
81 Self::Some(t) => Some(t),
82 }
83 }
84}
85
86impl From<Option<PendingTextEdit>> for OptionPendingTextEdit {
87 fn from(o: Option<PendingTextEdit>) -> Self {
88 o.map_or_else(|| Self::None, Self::Some)
89 }
90}
91
92impl<'a> From<Option<&'a PendingTextEdit>> for OptionPendingTextEdit {
93 fn from(o: Option<&'a PendingTextEdit>) -> Self {
94 o.map_or_else(|| Self::None, |v| Self::Some(v.clone()))
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum TextInputSource {
101 Keyboard,
103 Ime,
105 Accessibility,
107 Programmatic,
109}
110
111#[derive(Debug)]
116pub struct TextInputManager {
117 pub pending_changeset: Option<PendingTextEdit>,
120 pub input_source: Option<TextInputSource>,
122}
123
124impl TextInputManager {
125 #[must_use] pub const fn new() -> Self {
127 Self {
128 pending_changeset: None,
129 input_source: None,
130 }
131 }
132
133 pub fn record_input(
147 &mut self,
148 node: DomNodeId,
149 inserted_text: String,
150 old_text: String,
151 source: TextInputSource,
152 ) -> DomNodeId {
153 self.pending_changeset = Some(PendingTextEdit {
154 node,
155 inserted_text: inserted_text.into(),
156 old_text: old_text.into(),
157 });
158
159 self.input_source = Some(source);
160
161 node
162 }
163
164 #[must_use] pub const fn get_pending_changeset(&self) -> Option<&PendingTextEdit> {
166 self.pending_changeset.as_ref()
167 }
168
169 pub fn clear_changeset(&mut self) {
173 self.pending_changeset = None;
174 self.input_source = None;
175 }
176}
177
178impl Default for TextInputManager {
179 fn default() -> Self {
180 Self::new()
181 }
182}
183
184impl EventProvider for TextInputManager {
185 #[allow(clippy::match_same_arms)] fn get_pending_events(&self, timestamp: Instant) -> Vec<SyntheticEvent> {
192 let mut events = Vec::new();
193
194 if let Some(changeset) = &self.pending_changeset {
195 let event_source = match self.input_source {
196 Some(TextInputSource::Keyboard | TextInputSource::Ime) => {
197 CoreEventSource::User
198 }
199 Some(TextInputSource::Accessibility) => CoreEventSource::User, Some(TextInputSource::Programmatic) => CoreEventSource::Programmatic,
202 None => CoreEventSource::User,
203 };
204
205 events.push(SyntheticEvent::new(
211 EventType::Input,
212 event_source,
213 changeset.node,
214 timestamp,
215 EventData::TextInput(TextInputEventData {
216 inserted_text: changeset.inserted_text.as_str().to_string(),
217 old_text: changeset.old_text.as_str().to_string(),
218 }),
219 ));
220
221 }
224
225 events
226 }
227}
228
229impl crate::managers::NodeIdRemap for TextInputManager {
230 fn remap_node_ids(&mut self, dom: azul_core::dom::DomId, map: &crate::managers::NodeIdMap) {
236 let Some(ref mut pending) = self.pending_changeset else {
237 return;
238 };
239 if let Some(new_id) = map.resolve_dom_node_id(dom, pending.node) { pending.node = new_id } else {
240 self.pending_changeset = None;
241 self.input_source = None;
242 }
243 }
244}
245
246#[cfg(test)]
247mod autotest_generated {
248 use azul_core::{
249 dom::{DomId, DomNodeId, NodeId},
250 styled_dom::NodeHierarchyItemId,
251 task::SystemTick,
252 };
253
254 use super::*;
255 use crate::managers::{NodeIdMap, NodeIdRemap};
256
257 fn dom_node(dom: usize, index: usize) -> DomNodeId {
259 DomNodeId {
260 dom: DomId { inner: dom },
261 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(index))),
262 }
263 }
264
265 fn ts() -> Instant {
266 Instant::Tick(SystemTick::new(0))
267 }
268
269 fn edit(old: &str, inserted: &str) -> PendingTextEdit {
270 PendingTextEdit {
271 node: dom_node(0, 0),
272 inserted_text: inserted.to_string().into(),
273 old_text: old.to_string().into(),
274 }
275 }
276
277 fn adversarial_strings() -> Vec<String> {
281 vec![
282 String::new(),
283 "a".to_string(),
284 "hรฉllo".to_string(),
285 "e\u{0301}\u{0300}\u{0327}".to_string(),
286 "๐จโ๐ฉโ๐งโ๐ฆ".to_string(),
287 "๐ฉ๐ช๐ซ๐ท".to_string(),
288 "ู
ุฑุญุจุง ุจุงูุนุงูู
".to_string(),
289 "\u{202E}override\u{202C}".to_string(),
290 "\0nul\0inside\0".to_string(),
291 "\r\n\t\u{0b}\u{0c}".to_string(),
292 "\u{FFFD}\u{FEFF}".to_string(),
293 "๐ฅ๐๐ฉ๐ฅ".to_string(),
294 "a".repeat(1024),
295 ]
296 }
297
298 fn text_input_data(ev: &SyntheticEvent) -> &TextInputEventData {
299 match &ev.data {
300 EventData::TextInput(d) => d,
301 other => panic!("expected EventData::TextInput, got {other:?}"),
302 }
303 }
304
305 #[test]
310 fn resulting_text_basic_append() {
311 assert_eq!(edit("Hello", " World").resulting_text().as_str(), "Hello World");
312 }
313
314 #[test]
315 fn resulting_text_empty_instance_is_empty() {
316 assert_eq!(edit("", "").resulting_text().as_str(), "");
317 }
318
319 #[test]
320 fn resulting_text_pure_deletion_keeps_old_text() {
321 assert_eq!(edit("abc", "").resulting_text().as_str(), "abc");
324 }
325
326 #[test]
327 fn resulting_text_is_exact_byte_concatenation_for_unicode() {
328 for old in adversarial_strings() {
329 for inserted in adversarial_strings() {
330 let result = edit(&old, &inserted).resulting_text();
331 let expected = format!("{old}{inserted}");
332 assert_eq!(
333 result.as_str(),
334 expected.as_str(),
335 "concat mismatch for old={old:?} inserted={inserted:?}"
336 );
337 assert_eq!(
338 result.as_str().len(),
339 old.len() + inserted.len(),
340 "byte length must be additive (no normalization / no truncation)"
341 );
342 }
343 }
344 }
345
346 #[test]
347 fn resulting_text_preserves_interior_nul_bytes() {
348 let result = edit("a\0b", "c\0d").resulting_text();
351 assert_eq!(result.as_str(), "a\0bc\0d");
352 assert_eq!(result.as_str().len(), 6);
353 assert_eq!(result.as_str().matches('\0').count(), 2);
354 }
355
356 #[test]
357 fn resulting_text_round_trips_every_adversarial_string() {
358 for s in adversarial_strings() {
360 assert_eq!(edit(&s, "").resulting_text().as_str(), s.as_str());
361 assert_eq!(edit("", &s).resulting_text().as_str(), s.as_str());
362 }
363 }
364
365 #[test]
366 fn resulting_text_huge_strings_do_not_panic_or_truncate() {
367 let old = "a".repeat(300_000);
368 let inserted = "b".repeat(200_000);
369 let result = edit(&old, &inserted).resulting_text();
370 assert_eq!(result.as_str().len(), 500_000);
371 assert!(result.as_str().starts_with("aaaa"));
372 assert!(result.as_str().ends_with("bbbb"));
373 }
374
375 #[test]
376 fn resulting_text_is_pure_and_repeatable() {
377 let e = edit("old", "new");
378 let first = e.resulting_text();
379 let second = e.resulting_text();
380 assert_eq!(first.as_str(), second.as_str());
381 assert_eq!(e.old_text.as_str(), "old");
383 assert_eq!(e.inserted_text.as_str(), "new");
384 }
385
386 #[test]
391 fn option_pending_text_edit_none_round_trip() {
392 assert!(OptionPendingTextEdit::None.into_option().is_none());
393 assert!(
395 OptionPendingTextEdit::from(None::<PendingTextEdit>)
396 .into_option()
397 .is_none()
398 );
399 assert!(
400 OptionPendingTextEdit::from(None::<&PendingTextEdit>)
401 .into_option()
402 .is_none()
403 );
404 }
405
406 #[test]
407 fn option_pending_text_edit_some_round_trip_preserves_fields() {
408 for s in adversarial_strings() {
409 let original = PendingTextEdit {
410 node: dom_node(7, 13),
411 inserted_text: s.clone().into(),
412 old_text: s.clone().into(),
413 };
414 let recovered = OptionPendingTextEdit::from(Some(original.clone()))
415 .into_option()
416 .expect("Some must round-trip to Some");
417 assert_eq!(recovered.node, original.node);
418 assert_eq!(recovered.inserted_text.as_str(), s.as_str());
419 assert_eq!(recovered.old_text.as_str(), s.as_str());
420 }
421 }
422
423 #[test]
424 fn option_pending_text_edit_from_ref_deep_clones() {
425 let original = edit("old", "ins");
426 let cloned = OptionPendingTextEdit::from(Some(&original))
427 .into_option()
428 .expect("Some(&T) must map to Some");
429 assert_eq!(cloned.old_text.as_str(), "old");
431 assert_eq!(cloned.inserted_text.as_str(), "ins");
432 assert_eq!(original.old_text.as_str(), "old");
433 }
434
435 #[test]
440 fn new_starts_with_no_pending_state() {
441 let m = TextInputManager::new();
442 assert!(m.pending_changeset.is_none());
443 assert!(m.input_source.is_none());
444 assert!(m.get_pending_changeset().is_none());
445 assert!(m.get_pending_events(ts()).is_empty());
446 }
447
448 #[test]
449 fn default_matches_new() {
450 let d = TextInputManager::default();
451 assert!(d.pending_changeset.is_none());
452 assert!(d.input_source.is_none());
453 }
454
455 #[test]
460 fn record_input_returns_the_node_it_was_given_and_stores_it() {
461 let mut m = TextInputManager::new();
462 let node = dom_node(3, 42);
463 let returned = m.record_input(
464 node,
465 "abc".to_string(),
466 "xyz".to_string(),
467 TextInputSource::Keyboard,
468 );
469 assert_eq!(returned, node);
470
471 let pending = m.get_pending_changeset().expect("changeset must be recorded");
472 assert_eq!(pending.node, node);
473 assert_eq!(pending.inserted_text.as_str(), "abc");
474 assert_eq!(pending.old_text.as_str(), "xyz");
475 assert_eq!(m.input_source, Some(TextInputSource::Keyboard));
476 }
477
478 #[test]
479 fn record_input_survives_empty_unicode_and_huge_payloads() {
480 let mut m = TextInputManager::new();
481 for s in adversarial_strings() {
482 let returned = m.record_input(
483 dom_node(0, 0),
484 s.clone(),
485 s.clone(),
486 TextInputSource::Ime,
487 );
488 assert_eq!(returned, dom_node(0, 0));
489 let pending = m.get_pending_changeset().expect("recorded");
490 assert_eq!(pending.inserted_text.as_str(), s.as_str());
491 assert_eq!(pending.old_text.as_str(), s.as_str());
492 }
493
494 let huge = "z".repeat(1_000_000);
495 m.record_input(
496 dom_node(0, 0),
497 huge.clone(),
498 String::new(),
499 TextInputSource::Programmatic,
500 );
501 assert_eq!(
502 m.get_pending_changeset().expect("recorded").inserted_text.as_str().len(),
503 1_000_000
504 );
505 }
506
507 #[test]
508 fn record_input_accepts_extreme_node_ids() {
509 let mut m = TextInputManager::new();
510
511 let none_node = DomNodeId {
513 dom: DomId { inner: usize::MAX },
514 node: NodeHierarchyItemId::NONE,
515 };
516 assert_eq!(m.record_input(none_node, "a".into(), "b".into(), TextInputSource::Keyboard), none_node);
517 assert_eq!(m.get_pending_changeset().expect("recorded").node, none_node);
518
519 let max_node = DomNodeId {
521 dom: DomId::ROOT_ID,
522 node: NodeHierarchyItemId::from_raw(usize::MAX),
523 };
524 assert_eq!(m.record_input(max_node, "a".into(), "b".into(), TextInputSource::Keyboard), max_node);
525 assert_eq!(m.get_pending_changeset().expect("recorded").node, max_node);
526
527 assert_eq!(DomNodeId::ROOT.node.into_crate_internal(), None);
528 }
529
530 #[test]
531 fn record_input_is_last_write_wins() {
532 let mut m = TextInputManager::new();
533 m.record_input(dom_node(0, 1), "first".into(), "old1".into(), TextInputSource::Keyboard);
534 m.record_input(dom_node(1, 2), "second".into(), "old2".into(), TextInputSource::Accessibility);
535
536 let pending = m.get_pending_changeset().expect("recorded");
537 assert_eq!(pending.node, dom_node(1, 2));
538 assert_eq!(pending.inserted_text.as_str(), "second");
539 assert_eq!(pending.old_text.as_str(), "old2");
540 assert_eq!(m.input_source, Some(TextInputSource::Accessibility));
541
542 let events = m.get_pending_events(ts());
544 assert_eq!(events.len(), 1);
545 assert_eq!(events[0].target, dom_node(1, 2));
546 }
547
548 #[test]
549 fn clear_changeset_is_idempotent_on_a_fresh_manager() {
550 let mut m = TextInputManager::new();
551 m.clear_changeset();
552 m.clear_changeset();
553 m.clear_changeset();
554 assert!(m.get_pending_changeset().is_none());
555 assert!(m.input_source.is_none());
556 }
557
558 #[test]
559 fn clear_changeset_clears_both_changeset_and_source() {
560 let mut m = TextInputManager::new();
561 m.record_input(dom_node(0, 5), "x".into(), "y".into(), TextInputSource::Programmatic);
562 assert!(m.get_pending_changeset().is_some());
563
564 m.clear_changeset();
565 assert!(m.get_pending_changeset().is_none());
566 assert!(m.input_source.is_none());
568 assert!(m.get_pending_events(ts()).is_empty());
569
570 m.clear_changeset();
572 assert!(m.get_pending_changeset().is_none());
573 }
574
575 #[test]
580 fn no_events_without_a_pending_changeset() {
581 assert!(TextInputManager::new().get_pending_events(ts()).is_empty());
582 }
583
584 #[test]
585 fn pending_event_carries_text_verbatim() {
586 for s in adversarial_strings() {
587 let mut m = TextInputManager::new();
588 m.record_input(
589 dom_node(2, 9),
590 s.clone(),
591 format!("old-{s}"),
592 TextInputSource::Keyboard,
593 );
594
595 let events = m.get_pending_events(ts());
596 assert_eq!(events.len(), 1, "exactly one Input event per changeset");
597 assert_eq!(events[0].event_type, EventType::Input);
598 assert_eq!(events[0].target, dom_node(2, 9));
599
600 let data = text_input_data(&events[0]);
601 assert_eq!(data.inserted_text, s);
602 assert_eq!(data.old_text, format!("old-{s}"));
603 }
604 }
605
606 #[test]
607 fn event_source_mapping_is_stable_for_every_input_source() {
608 let cases = [
609 (TextInputSource::Keyboard, CoreEventSource::User),
610 (TextInputSource::Ime, CoreEventSource::User),
611 (TextInputSource::Accessibility, CoreEventSource::User),
612 (TextInputSource::Programmatic, CoreEventSource::Programmatic),
613 ];
614 for (input_source, expected) in cases {
615 let mut m = TextInputManager::new();
616 m.record_input(dom_node(0, 0), "a".into(), String::new(), input_source);
617 let events = m.get_pending_events(ts());
618 assert_eq!(events.len(), 1);
619 assert_eq!(
620 events[0].source, expected,
621 "{input_source:?} must map to {expected:?}"
622 );
623 }
624 }
625
626 #[test]
627 fn pending_changeset_without_a_source_defaults_to_user() {
628 let m = TextInputManager {
631 pending_changeset: Some(edit("old", "ins")),
632 input_source: None,
633 };
634 let events = m.get_pending_events(ts());
635 assert_eq!(events.len(), 1);
636 assert_eq!(events[0].source, CoreEventSource::User);
637 assert_eq!(text_input_data(&events[0]).inserted_text, "ins");
638 }
639
640 #[test]
641 fn get_pending_events_does_not_consume_the_changeset() {
642 let mut m = TextInputManager::new();
643 m.record_input(dom_node(0, 4), "a".into(), "b".into(), TextInputSource::Keyboard);
644 assert_eq!(m.get_pending_events(ts()).len(), 1);
645 assert_eq!(m.get_pending_events(ts()).len(), 1);
647 assert!(m.get_pending_changeset().is_some());
648 }
649
650 #[test]
655 fn remap_rewrites_a_surviving_node() {
656 let mut m = TextInputManager::new();
657 m.record_input(dom_node(0, 3), "a".into(), "b".into(), TextInputSource::Keyboard);
658
659 let map = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(1))]);
660 m.remap_node_ids(DomId::ROOT_ID, &map);
661
662 let pending = m.get_pending_changeset().expect("mapped node must survive");
663 assert_eq!(pending.node, dom_node(0, 1));
664 assert_eq!(pending.inserted_text.as_str(), "a");
665 assert_eq!(m.input_source, Some(TextInputSource::Keyboard));
666 }
667
668 #[test]
669 fn remap_drops_changeset_and_source_when_the_node_is_unmounted() {
670 let mut m = TextInputManager::new();
671 m.record_input(dom_node(0, 3), "a".into(), "b".into(), TextInputSource::Ime);
672
673 let map = NodeIdMap::from_pairs([(NodeId::new(4), NodeId::new(3))]);
676 m.remap_node_ids(DomId::ROOT_ID, &map);
677
678 assert!(m.get_pending_changeset().is_none());
679 assert!(m.input_source.is_none(), "source must be cleared with the changeset");
680 assert!(m.get_pending_events(ts()).is_empty());
681 }
682
683 #[test]
684 fn remap_with_an_empty_map_drops_everything() {
685 let mut m = TextInputManager::new();
686 m.record_input(dom_node(0, 0), "a".into(), "b".into(), TextInputSource::Keyboard);
687
688 let map = NodeIdMap::from_pairs(Vec::<(NodeId, NodeId)>::new());
689 assert!(map.is_empty());
690 m.remap_node_ids(DomId::ROOT_ID, &map);
691
692 assert!(m.get_pending_changeset().is_none());
693 assert!(m.input_source.is_none());
694 }
695
696 #[test]
697 fn remap_leaves_other_doms_untouched() {
698 let mut m = TextInputManager::new();
699 m.record_input(dom_node(1, 3), "a".into(), "b".into(), TextInputSource::Keyboard);
700
701 let map = NodeIdMap::from_pairs([(NodeId::new(3), NodeId::new(9))]);
703 m.remap_node_ids(DomId::ROOT_ID, &map);
704
705 let pending = m.get_pending_changeset().expect("other-DOM state must survive");
706 assert_eq!(pending.node, dom_node(1, 3), "node id must NOT be rewritten");
707 }
708
709 #[test]
710 fn remap_drops_a_changeset_recorded_on_the_none_node_sentinel() {
711 let mut m = TextInputManager::new();
715 m.record_input(DomNodeId::ROOT, "a".into(), "b".into(), TextInputSource::Keyboard);
716
717 let map = NodeIdMap::from_pairs([(NodeId::new(0), NodeId::new(0))]);
718 m.remap_node_ids(DomId::ROOT_ID, &map);
719
720 assert!(m.get_pending_changeset().is_none());
721 assert!(m.input_source.is_none());
722 }
723
724 #[test]
725 fn remap_handles_extreme_node_indices() {
726 let mut m = TextInputManager::new();
727 let huge = DomNodeId {
728 dom: DomId::ROOT_ID,
729 node: NodeHierarchyItemId::from_raw(usize::MAX),
730 };
731 m.record_input(huge, "a".into(), "b".into(), TextInputSource::Keyboard);
732
733 let map = NodeIdMap::from_pairs([(NodeId::new(usize::MAX - 1), NodeId::new(2))]);
736 m.remap_node_ids(DomId::ROOT_ID, &map);
737
738 assert_eq!(m.get_pending_changeset().expect("mapped").node, dom_node(0, 2));
739 }
740
741 #[test]
742 fn remap_on_an_empty_manager_is_a_noop() {
743 let mut m = TextInputManager::new();
744 let map = NodeIdMap::from_pairs([(NodeId::new(0), NodeId::new(1))]);
745 m.remap_node_ids(DomId::ROOT_ID, &map);
746 assert!(m.get_pending_changeset().is_none());
747 assert!(m.input_source.is_none());
748 }
749
750 #[test]
751 fn remap_is_idempotent_when_ids_are_stable() {
752 let mut m = TextInputManager::new();
753 m.record_input(dom_node(0, 5), "a".into(), "b".into(), TextInputSource::Keyboard);
754
755 let map = NodeIdMap::from_pairs([(NodeId::new(5), NodeId::new(5))]);
756 m.remap_node_ids(DomId::ROOT_ID, &map);
757 m.remap_node_ids(DomId::ROOT_ID, &map);
758
759 assert_eq!(m.get_pending_changeset().expect("stable").node, dom_node(0, 5));
760 }
761}