1use bevy_ecs::component::Mutable;
38use bevy_ecs::message::{Message, MessageReader, MessageRegistry, Messages};
39use bevy_ecs::prelude::*;
40use glam::Vec2;
41use lumen_core::prelude::*;
42
43use crate::ScriptHost;
44use crate::event::{self, EventData};
45use crate::runtime::ScriptCommandEvent;
46
47fn ancestors_root_first(entity: Entity) -> Vec<u64> {
51 let idx = lumen_core::node::dom_index_snapshot();
52 let mut chain: Vec<u64> = idx
53 .ancestors(entity)
54 .into_iter()
55 .map(|e| lumen_core::node::NodeHandle::new(e).pack())
56 .collect();
57 chain.reverse();
58 chain
59}
60
61fn button_code(button: PointerButton) -> i64 {
64 match button {
65 PointerButton::Primary => 0,
66 PointerButton::Middle => 1,
67 PointerButton::Secondary => 2,
68 PointerButton::Other(code) => code as i64,
69 }
70}
71
72fn key_string(key: &Key) -> String {
74 match key {
75 Key::Character(s) => s.clone(),
76 Key::Named(n) => match n {
77 NamedKey::Tab => "Tab",
78 NamedKey::Enter => "Enter",
79 NamedKey::Escape => "Escape",
80 NamedKey::Backspace => "Backspace",
81 NamedKey::Space => "Space",
82 NamedKey::ArrowUp => "ArrowUp",
83 NamedKey::ArrowDown => "ArrowDown",
84 NamedKey::ArrowLeft => "ArrowLeft",
85 NamedKey::ArrowRight => "ArrowRight",
86 NamedKey::Home => "Home",
87 NamedKey::End => "End",
88 NamedKey::Delete => "Delete",
89 }
90 .to_string(),
91 }
92}
93
94#[derive(Resource, Default)]
102pub struct PendingDomEvents {
103 input: Vec<(EventData, Entity)>,
104 state: Vec<(EventData, Entity)>,
105}
106
107#[derive(Clone, Copy)]
110pub struct EventHost {
111 dispatch: fn(&mut World, u64) -> bool,
112 drain: fn(&mut World),
113}
114
115#[derive(Resource, Default, Clone)]
118pub struct EventHosts(Vec<EventHost>);
119
120pub fn register_event_host<H: ScriptHost + Resource<Mutability = Mutable>>(world: &mut World) {
123 world
124 .get_resource_or_insert_with(EventHosts::default)
125 .0
126 .push(EventHost {
127 dispatch: dispatch_into::<H>,
128 drain: drain_into::<H>,
129 });
130}
131
132fn dispatch_into<H: ScriptHost + Resource<Mutability = Mutable>>(
135 world: &mut World,
136 token: u64,
137) -> bool {
138 world
139 .get_resource_mut::<H>()
140 .is_some_and(|mut host| !matches!(host.dispatch_event_handler(token), Ok(false)))
141}
142
143fn drain_into<H: ScriptHost + Resource<Mutability = Mutable>>(world: &mut World) {
145 let commands = match world.get_resource_mut::<H>() {
146 Some(mut host) => host.drain_commands(),
147 None => return,
148 };
149 for c in commands {
150 world.write_message(ScriptCommandEvent(c));
151 }
152}
153
154pub fn deliver_input_events(world: &mut World) {
156 event::clear_prevented_clicks();
158 deliver_pending(world, |pending| &mut pending.input);
159}
160
161pub fn deliver_state_events(world: &mut World) {
163 deliver_pending(world, |pending| &mut pending.state);
164}
165
166fn deliver_pending(
167 world: &mut World,
168 queue: fn(&mut PendingDomEvents) -> &mut Vec<(EventData, Entity)>,
169) {
170 let Some(mut pending) = world.get_resource_mut::<PendingDomEvents>() else {
171 return;
172 };
173 let events = std::mem::take(queue(&mut pending));
174 if events.is_empty() {
175 return;
176 }
177 let hosts = world
178 .get_resource::<EventHosts>()
179 .cloned()
180 .unwrap_or_default();
181 for (data, target) in events {
182 deliver(world, &hosts, data, target);
183 }
184}
185
186fn deliver(world: &mut World, hosts: &EventHosts, data: EventData, target_entity: Entity) {
191 if !event::has_bindings_for(&data.event_type) {
192 return;
193 }
194 let ancestors = ancestors_root_first(target_entity);
195 let bubbles = event::event_bubbles(&data.event_type);
196 let etype = data.event_type.clone();
197 let target_handle = data.target;
198 let result = event::dispatch(data, &ancestors, bubbles, |token| {
199 for host in &hosts.0 {
200 if (host.dispatch)(world, token) {
201 break;
202 }
203 }
204 });
205 for host in &hosts.0 {
206 (host.drain)(world);
207 }
208 if etype == "click" && result.default_prevented {
209 event::mark_prevented_click(target_handle);
210 }
211}
212
213pub(crate) type FieldText = (
215 Option<&'static lumen_core::text_model::TextBuffer>,
216 Option<&'static TextContent>,
217);
218
219pub(crate) fn field_text(
223 (buffer, text): (
224 Option<&lumen_core::text_model::TextBuffer>,
225 Option<&TextContent>,
226 ),
227) -> Option<String> {
228 buffer
229 .map(ToString::to_string)
230 .or_else(|| text.map(|t| t.0.clone()))
231}
232
233fn base(entity: Entity, event_type: &str) -> EventData {
236 EventData {
237 event_type: event_type.to_string(),
238 target: lumen_core::node::NodeHandle::new(entity).pack(),
239 ..Default::default()
240 }
241}
242
243fn with_position(
248 mut data: EventData,
249 transforms: &Query<&Transform>,
250 entity: Entity,
251 position: Vec2,
252 local: Option<Vec2>,
253) -> EventData {
254 let local = local.unwrap_or_else(|| {
255 let origin = transforms
256 .get(entity)
257 .map(|t| t.absolute)
258 .unwrap_or(Vec2::ZERO);
259 position - origin
260 });
261 data.local = (local.x as f64, local.y as f64);
262 data.client = (position.x as f64, position.y as f64);
263 data
264}
265
266fn set_mods(data: &mut EventData, mods: &Modifiers) {
267 data.shift = mods.shift;
268 data.ctrl = mods.ctrl;
269 data.alt = mods.alt;
270 data.super_ = mods.super_;
271}
272
273pub fn register_dom_event_messages(world: &mut World) {
280 fn register<M: Message>(world: &mut World) {
281 if !world.contains_resource::<Messages<M>>() {
282 MessageRegistry::register_message::<M>(world);
283 }
284 }
285 register::<ClickEvent>(world);
286 register::<DoubleClickEvent>(world);
287 register::<PointerPressed>(world);
288 register::<PointerReleased>(world);
289 register::<PointerMoved>(world);
290 register::<MouseWheel>(world);
291 register::<KeyPressed>(world);
292 register::<KeyReleased>(world);
293 register::<FocusedKey>(world);
294 register::<lumen_core::text_events::TextEditApplied>(world);
295 register::<TextInputCommitted>(world);
296}
297
298#[allow(clippy::too_many_arguments)]
303pub fn queue_pointer_and_key_events(
304 mut pending: ResMut<PendingDomEvents>,
305 mut clicks: MessageReader<ClickEvent>,
306 mut doubles: MessageReader<DoubleClickEvent>,
307 mut presses: MessageReader<PointerPressed>,
308 mut releases: MessageReader<PointerReleased>,
309 mut moves: MessageReader<PointerMoved>,
310 mut wheels: MessageReader<MouseWheel>,
311 mut keyups: MessageReader<KeyReleased>,
312 mut keydowns: MessageReader<FocusedKey>,
313 mut key_presses: MessageReader<KeyPressed>,
314 focus: Option<Res<FocusTracker>>,
315 transforms: Query<&Transform>,
316 hovered: Query<Entity, With<Hovered>>,
317 focused: Query<Entity, With<Focused>>,
318) {
319 let queue = &mut pending.input;
320 let hovered_entity = hovered.iter().next();
321 let focused_entity = focused.iter().next();
322
323 for c in clicks.read() {
325 let mut data = with_position(
326 base(c.entity, "click"),
327 &transforms,
328 c.entity,
329 c.position,
330 c.local,
331 );
332 data.button = button_code(c.button);
333 queue.push((data, c.entity));
334 }
335 for d in doubles.read() {
337 let data = with_position(
338 base(d.entity, "dblclick"),
339 &transforms,
340 d.entity,
341 d.position,
342 None,
343 );
344 queue.push((data, d.entity));
345 }
346 for p in presses.read() {
348 let Some(e) = hovered_entity else { continue };
349 let mut data = with_position(base(e, "pointerdown"), &transforms, e, p.position, p.local);
350 data.button = button_code(p.button);
351 queue.push((data, e));
352 }
353 for p in releases.read() {
354 let Some(e) = hovered_entity else { continue };
355 let mut data = with_position(base(e, "pointerup"), &transforms, e, p.position, p.local);
356 data.button = button_code(p.button);
357 queue.push((data, e));
358 }
359 for p in moves.read() {
360 let Some(e) = hovered_entity else { continue };
361 let data = with_position(base(e, "pointermove"), &transforms, e, p.position, p.local);
362 queue.push((data, e));
363 }
364 for w in wheels.read() {
365 let Some(e) = hovered_entity else { continue };
366 let mut data = with_position(base(e, "wheel"), &transforms, e, w.position, w.local);
367 data.delta = (w.delta.x as f64, w.delta.y as f64);
368 queue.push((data, e));
369 }
370 for k in keydowns.read() {
372 let mut data = base(k.entity, "keydown");
373 data.key = key_string(&k.key);
374 set_mods(&mut data, &k.modifiers);
375 queue.push((data, k.entity));
376 }
377 let nothing_focused = focus.is_none_or(|f| f.0.is_none());
381 let document = || lumen_core::node::dom_index_snapshot().document();
382 for k in key_presses.read() {
383 if !nothing_focused || matches!(k.key, Key::Named(NamedKey::Tab)) {
384 continue;
385 }
386 let Some(doc) = document() else { continue };
387 let mut data = base(doc, "keydown");
388 data.key = key_string(&k.key);
389 set_mods(&mut data, &k.modifiers);
390 queue.push((data, doc));
391 }
392 for k in keyups.read() {
394 let Some(e) = focused_entity.or_else(document) else {
395 continue;
396 };
397 let mut data = base(e, "keyup");
398 data.key = key_string(&k.key);
399 set_mods(&mut data, &k.modifiers);
400 queue.push((data, e));
401 }
402}
403
404#[allow(clippy::too_many_arguments)]
407pub fn queue_state_events(
408 mut pending: ResMut<PendingDomEvents>,
409 mut edits: MessageReader<lumen_core::text_events::TextEditApplied>,
410 mut commits: MessageReader<TextInputCommitted>,
411 gained_focus: Query<Entity, Added<Focused>>,
412 mut lost_focus: RemovedComponents<Focused>,
413 gained_hover: Query<Entity, Added<Hovered>>,
414 mut lost_hover: RemovedComponents<Hovered>,
415 scrolled: Query<Entity, Changed<ScrollOffset>>,
416 fields: Query<FieldText>,
417) {
418 let queue = &mut pending.state;
423
424 for e in gained_focus.iter() {
426 queue.push((base(e, "focus"), e));
427 }
428 for e in lost_focus.read() {
429 queue.push((base(e, "blur"), e));
430 }
431 for e in gained_hover.iter() {
433 queue.push((base(e, "pointerenter"), e));
434 }
435 for e in lost_hover.read() {
436 queue.push((base(e, "pointerleave"), e));
437 }
438 let mut fired: Vec<Entity> = Vec::new();
444 for ev in edits.read() {
445 if matches!(ev.kind, lumen_core::text_events::AppliedKind::CursorMove)
446 || fired.contains(&ev.entity)
447 {
448 continue;
449 }
450 let Some(value) = fields.get(ev.entity).ok().and_then(field_text) else {
451 continue;
452 };
453 fired.push(ev.entity);
454 let mut data = base(ev.entity, "input");
455 data.value = value;
456 queue.push((data, ev.entity));
457 }
458 for c in commits.read() {
461 for etype in ["change", "submit"] {
462 let mut data = base(c.entity, etype);
463 data.value = c.text.clone();
464 queue.push((data, c.entity));
465 }
466 }
467 for e in scrolled.iter() {
469 queue.push((base(e, "scroll"), e));
470 }
471}
472
473#[cfg(test)]
474pub(crate) mod text_event_tests {
475 use super::*;
476 use bevy_ecs::message::Messages;
477 use bevy_ecs::system::RunSystemOnce;
478 use lumen_core::text_events::{AppliedKind, TextEditApplied};
479 use lumen_core::text_model::TextBuffer;
480 use std::sync::{Arc, Mutex, MutexGuard};
481
482 fn serial() -> MutexGuard<'static, ()> {
485 event::TEST_GUARD.lock().unwrap_or_else(|e| e.into_inner())
486 }
487
488 #[derive(Resource)]
492 pub(crate) struct NoHost;
493
494 impl ScriptHost for NoHost {
495 type Closure = ();
496 fn compile_check(&self, _source: &str, _uri: &str) -> Result<(), crate::ScriptError> {
497 unimplemented!("no host in these tests")
498 }
499 fn load(&mut self, _source: &str, _uri: &str) -> Result<(), crate::ScriptError> {
500 unimplemented!("no host in these tests")
501 }
502 fn replace(&mut self, _source: &str, _uri: &str) -> Result<(), crate::ScriptError> {
503 unimplemented!("no host in these tests")
504 }
505 fn reset(&mut self) {
506 unimplemented!("no host in these tests")
507 }
508 fn call(
509 &mut self,
510 _fn_name: &str,
511 _args: &[crate::ScriptValue],
512 ) -> Result<crate::CallOutcome, crate::CallFailure> {
513 unimplemented!("no host in these tests")
514 }
515 fn call_closure(
516 &mut self,
517 _closure: &Self::Closure,
518 _args: &[crate::ScriptValue],
519 ) -> Result<crate::ScriptValue, crate::ScriptError> {
520 unimplemented!("no host in these tests")
521 }
522 fn register_script_fn(&mut self, _f: &crate::ScriptFn) -> Result<(), crate::ScriptError> {
523 unimplemented!("no host in these tests")
524 }
525 fn lang(&self) -> &'static str {
526 "test"
527 }
528 }
529
530 fn watch(node: u64, types: &[&str]) -> Arc<Mutex<Vec<(String, String)>>> {
532 let seen: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(Vec::new()));
533 for t in types {
534 let sink = Arc::clone(&seen);
535 event::register_native_binding(
536 node,
537 (*t).to_string(),
538 false,
539 Arc::new(move || {
540 if let Ok(mut v) = sink.lock() {
541 v.push((event::event_type(), event::event_value()));
542 }
543 }),
544 );
545 }
546 seen
547 }
548
549 fn drive(world: &mut World) {
554 world.init_resource::<PendingDomEvents>();
555 world
556 .run_system_once(queue_state_events)
557 .expect("system ran");
558 deliver_state_events(world);
559 world.resource_mut::<Messages<TextEditApplied>>().clear();
560 world.resource_mut::<Messages<TextInputCommitted>>().clear();
561 }
562
563 #[test]
564 fn input_fires_per_edit_and_change_only_on_commit() {
565 let _guard = serial();
566 event::clear_all_bindings();
567 let mut world = World::new();
568 world.init_resource::<Messages<ScriptCommandEvent>>();
569 world.init_resource::<Messages<TextEditApplied>>();
570 world.init_resource::<Messages<TextInputCommitted>>();
571 let field = world.spawn(TextBuffer::single_line("ab")).id();
572 let node = lumen_core::node::NodeHandle::new(field).pack();
573 let seen = watch(node, &["input", "change", "submit"]);
574
575 world
577 .resource_mut::<Messages<TextEditApplied>>()
578 .write(TextEditApplied {
579 entity: field,
580 version: 1,
581 kind: AppliedKind::Insert,
582 before_byte: 1,
583 after_byte: 2,
584 });
585 drive(&mut world);
586 assert_eq!(
587 *seen.lock().unwrap(),
588 vec![("input".to_string(), "ab".to_string())],
589 );
590
591 seen.lock().unwrap().clear();
593 world
594 .resource_mut::<Messages<TextEditApplied>>()
595 .write(TextEditApplied {
596 entity: field,
597 version: 2,
598 kind: AppliedKind::CursorMove,
599 before_byte: 2,
600 after_byte: 0,
601 });
602 drive(&mut world);
603 assert!(seen.lock().unwrap().is_empty(), "caret moves fire nothing");
604
605 world
607 .resource_mut::<Messages<TextInputCommitted>>()
608 .write(TextInputCommitted {
609 entity: field,
610 text: "ab".to_string(),
611 });
612 drive(&mut world);
613 assert_eq!(
614 *seen.lock().unwrap(),
615 vec![
616 ("change".to_string(), "ab".to_string()),
617 ("submit".to_string(), "ab".to_string()),
618 ],
619 );
620 event::clear_all_bindings();
621 }
622
623 #[test]
624 fn repeated_edits_in_one_tick_fire_input_once() {
625 let _guard = serial();
626 event::clear_all_bindings();
627 let mut world = World::new();
628 world.init_resource::<Messages<ScriptCommandEvent>>();
629 world.init_resource::<Messages<TextEditApplied>>();
630 world.init_resource::<Messages<TextInputCommitted>>();
631 let field = world.spawn(TextBuffer::single_line("hi")).id();
632 let node = lumen_core::node::NodeHandle::new(field).pack();
633 let seen = watch(node, &["input"]);
634
635 for version in 1..=3 {
638 world
639 .resource_mut::<Messages<TextEditApplied>>()
640 .write(TextEditApplied {
641 entity: field,
642 version,
643 kind: AppliedKind::Insert,
644 before_byte: 0,
645 after_byte: 1,
646 });
647 }
648 drive(&mut world);
649 assert_eq!(
650 seen.lock().unwrap().len(),
651 1,
652 "one `input` per entity per tick"
653 );
654 event::clear_all_bindings();
655 }
656
657 #[test]
661 fn keys_with_nothing_focused_reach_the_document() {
662 use bevy_ecs::system::RunSystemOnce;
663 use lumen_core::node::{DomIndex, DomRecord, publish_dom_index};
664
665 let _guard = serial();
666 event::clear_all_bindings();
667 let mut world = World::new();
668 world.init_resource::<Messages<ScriptCommandEvent>>();
669 register_dom_event_messages(&mut world);
670 world.init_resource::<FocusTracker>();
671 world.init_resource::<PendingDomEvents>();
672 let root = world.spawn_empty().id();
673 let button = world.spawn_empty().id();
674 let record = |entity: Entity, parent: Option<Entity>, children: Vec<Entity>| DomRecord {
675 entity,
676 generation: entity.generation().to_bits(),
677 tag: String::new(),
678 id: None,
679 classes: Vec::new(),
680 parent,
681 children,
682 child_index: 0,
683 sibling_count: 0,
684 doc_order: 0,
685 };
686 publish_dom_index(DomIndex::build(vec![
687 record(root, None, vec![button]),
688 record(button, Some(root), Vec::new()),
689 ]));
690 let doc = lumen_core::node::NodeHandle::new(root).pack();
691 let seen = Arc::new(Mutex::new(Vec::<String>::new()));
692 for t in ["keydown", "keyup"] {
693 let sink = Arc::clone(&seen);
694 event::register_native_binding(
695 doc,
696 t.to_string(),
697 false,
698 Arc::new(move || {
699 let target = lumen_core::node::NodeHandle::unpack(event::event_target())
700 .map(|h| h.entity);
701 sink.lock()
702 .unwrap()
703 .push(format!("{} {:?}", event::event_type(), target));
704 }),
705 );
706 }
707 let press = |world: &mut World, key: Key| {
708 world.write_message(KeyPressed {
709 key: key.clone(),
710 modifiers: Modifiers::default(),
711 repeat: false,
712 });
713 world.write_message(KeyReleased {
714 key,
715 modifiers: Modifiers::default(),
716 });
717 world
718 .run_system_once(queue_pointer_and_key_events)
719 .expect("system ran");
720 deliver_input_events(world);
721 world.resource_mut::<Messages<KeyPressed>>().clear();
722 world.resource_mut::<Messages<KeyReleased>>().clear();
723 };
724
725 press(&mut world, Key::Named(NamedKey::Escape));
726 assert_eq!(
727 *seen.lock().unwrap(),
728 vec![
729 format!("keydown {:?}", Some(root)),
730 format!("keyup {:?}", Some(root)),
731 ],
732 "an unfocused key reaches the document"
733 );
734
735 seen.lock().unwrap().clear();
738 world.resource_mut::<FocusTracker>().0 = Some(button);
739 world.entity_mut(button).insert(Focused);
740 world.write_message(FocusedKey {
741 entity: button,
742 key: Key::Named(NamedKey::Escape),
743 modifiers: Modifiers::default(),
744 repeat: false,
745 });
746 press(&mut world, Key::Named(NamedKey::Escape));
747 assert_eq!(
748 *seen.lock().unwrap(),
749 vec![
750 format!("keydown {:?}", Some(button)),
751 format!("keyup {:?}", Some(button)),
752 ],
753 "a focused key bubbles up from the element"
754 );
755 event::clear_all_bindings();
756 publish_dom_index(DomIndex::default());
757 }
758
759 #[test]
760 fn edit_without_a_buffer_fires_nothing() {
761 let _guard = serial();
762 event::clear_all_bindings();
763 let mut world = World::new();
764 world.init_resource::<Messages<ScriptCommandEvent>>();
765 world.init_resource::<Messages<TextEditApplied>>();
766 world.init_resource::<Messages<TextInputCommitted>>();
767 let ghost = world.spawn_empty().id();
768 let node = lumen_core::node::NodeHandle::new(ghost).pack();
769 let seen = watch(node, &["input"]);
770
771 world
772 .resource_mut::<Messages<TextEditApplied>>()
773 .write(TextEditApplied {
774 entity: ghost,
775 version: 1,
776 kind: AppliedKind::Insert,
777 before_byte: 0,
778 after_byte: 1,
779 });
780 drive(&mut world);
781 assert!(seen.lock().unwrap().is_empty());
782 event::clear_all_bindings();
783 }
784}