@bornengine/engine 0.4.16 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +24 -14
  2. package/native/android/Cargo.lock +577 -28
  3. package/native/android/Cargo.toml +2 -0
  4. package/native/android/src/lib.rs +39 -3
  5. package/native/ios/Cargo.lock +573 -24
  6. package/native/ios/Cargo.toml +2 -0
  7. package/native/ios/src/lib.rs +72 -9
  8. package/native/ios/src/text_input.rs +48 -0
  9. package/native/linux/Cargo.lock +623 -35
  10. package/native/linux/Cargo.toml +3 -0
  11. package/native/macos/Cargo.lock +630 -35
  12. package/native/macos/Cargo.toml +3 -0
  13. package/native/shared/Cargo.lock +430 -27
  14. package/native/shared/Cargo.toml +5 -0
  15. package/native/shared/build.rs +41 -1
  16. package/native/shared/src/engine.rs +54 -2
  17. package/native/shared/src/ffi.rs +54 -1
  18. package/native/shared/src/ffi_core/mod.rs +2 -0
  19. package/native/shared/src/ffi_core/ui.rs +182 -0
  20. package/native/shared/src/input.rs +317 -12
  21. package/native/shared/src/lib.rs +1 -0
  22. package/native/shared/src/renderer/mod.rs +47 -201
  23. package/native/shared/src/renderer/texture_store.rs +5 -0
  24. package/native/shared/src/renderer/ui_pass.rs +742 -0
  25. package/native/shared/src/textures.rs +2 -0
  26. package/native/shared/src/ui/commands.rs +213 -0
  27. package/native/shared/src/ui/egui.rs +1282 -0
  28. package/native/shared/src/ui/imgui.rs +1056 -0
  29. package/native/shared/src/ui/input.rs +173 -0
  30. package/native/shared/src/ui/mod.rs +252 -0
  31. package/native/shared/src/ui/responses.rs +61 -0
  32. package/native/tvos/Cargo.lock +576 -27
  33. package/native/tvos/Cargo.toml +2 -0
  34. package/native/visionos/Cargo.lock +571 -22
  35. package/native/visionos/Cargo.toml +2 -0
  36. package/native/watchos/src/ffi_stubs.rs +27 -0
  37. package/native/web/Cargo.lock +346 -11
  38. package/native/web/Cargo.toml +6 -0
  39. package/native/web/bloom_glue.js +84 -11
  40. package/native/web/build.sh +150 -34
  41. package/native/web/src/lib.rs +2 -0
  42. package/native/web/src/ui_ffi.rs +148 -0
  43. package/native/windows/Cargo.lock +638 -38
  44. package/native/windows/Cargo.toml +3 -0
  45. package/native/windows/src/lib.rs +50 -1
  46. package/native/windows/src/windows_text.rs +69 -0
  47. package/package.json +91 -2
  48. package/src/debug-ui/index.ts +4 -0
  49. package/src/debug-ui/types.ts +1 -0
  50. package/src/game/adapters/audio-source-component.ts +67 -0
  51. package/src/game/adapters/rigid-body-component.ts +69 -0
  52. package/src/game/adapters/scene-node-component.ts +49 -0
  53. package/src/game/game-component.ts +89 -0
  54. package/src/game/game-object.ts +338 -0
  55. package/src/game/game-scene.ts +303 -0
  56. package/src/game/index.ts +7 -0
  57. package/src/game/transform.ts +315 -0
  58. package/src/index.ts +27 -0
  59. package/src/ui/api.ts +240 -0
  60. package/src/ui/index.ts +5 -0
  61. package/src/ui/opcodes.ts +66 -0
  62. package/src/ui/types.ts +82 -0
@@ -17,6 +17,11 @@ pub struct InputState {
17
17
  keys_down: [bool; MAX_KEYS],
18
18
  keys_released: [bool; MAX_KEYS],
19
19
  prev_keys_down: [bool; MAX_KEYS],
20
+ // Preserve complete edge transitions for UI input when press/release both
21
+ // arrive before begin_frame computes the gameplay-facing key state.
22
+ pending_key_pressed: [bool; MAX_KEYS],
23
+ pending_key_released: [bool; MAX_KEYS],
24
+ pending_ui_events: Vec<crate::ui::UiInputEvent>,
20
25
 
21
26
  // Injected keys (bloom_inject_key_down/up) are staged here and applied at the
22
27
  // top of begin_frame, rather than written straight into keys_down.
@@ -55,6 +60,11 @@ pub struct InputState {
55
60
  char_queue: [u32; 32],
56
61
  char_queue_head: usize,
57
62
  char_queue_tail: usize,
63
+ /// Independent consumer queue for the UI input snapshot. Games and UI
64
+ /// widgets each receive text once without advancing the other's cursor.
65
+ ui_char_queue: [u32; 32],
66
+ ui_char_queue_head: usize,
67
+ ui_char_queue_tail: usize,
58
68
  /// Cursor shape requested by the editor. The platform event loop polls
59
69
  /// this and applies the appropriate native cursor.
60
70
  /// 0=default, 1=hand, 2=move, 3=text, 4=resize_h, 5=resize_v, 6=crosshair
@@ -111,6 +121,9 @@ impl InputState {
111
121
  keys_down: [false; MAX_KEYS],
112
122
  keys_released: [false; MAX_KEYS],
113
123
  prev_keys_down: [false; MAX_KEYS],
124
+ pending_key_pressed: [false; MAX_KEYS],
125
+ pending_key_released: [false; MAX_KEYS],
126
+ pending_ui_events: Vec::new(),
114
127
  pending_key_down: [false; MAX_KEYS],
115
128
  pending_key_up: [false; MAX_KEYS],
116
129
  mouse_x: 0.0,
@@ -126,6 +139,9 @@ impl InputState {
126
139
  char_queue: [0; 32],
127
140
  char_queue_head: 0,
128
141
  char_queue_tail: 0,
142
+ ui_char_queue: [0; 32],
143
+ ui_char_queue_head: 0,
144
+ ui_char_queue_tail: 0,
129
145
  cursor_shape: 0,
130
146
  cursor_disabled: false,
131
147
  mouse_pressed: [false; MAX_MOUSE_BUTTONS],
@@ -154,6 +170,15 @@ impl InputState {
154
170
  /// one-frame isKeyRepeated edge by the next begin_frame.
155
171
  pub fn queue_key_repeat(&mut self, key: usize) {
156
172
  if key < MAX_KEYS {
173
+ if !self.repeat_pending[key] {
174
+ self.pending_ui_events.push(crate::ui::UiInputEvent::Key(
175
+ crate::ui::UiKeyEvent {
176
+ key: key as u32,
177
+ pressed: true,
178
+ repeated: true,
179
+ },
180
+ ));
181
+ }
157
182
  self.repeat_pending[key] = true;
158
183
  }
159
184
  }
@@ -208,6 +233,8 @@ impl InputState {
208
233
 
209
234
  pub fn end_frame(&mut self) {
210
235
  self.prev_keys_down = self.keys_down;
236
+ // Pending UI key edges are consumed in ui_snapshot so late events can
237
+ // survive this frame boundary.
211
238
  self.prev_mouse_down = self.mouse_down;
212
239
  self.prev_gamepad_buttons = self.gamepad_buttons_down;
213
240
  self.prev_mouse_x = self.mouse_x;
@@ -228,16 +255,67 @@ impl InputState {
228
255
  // Keyboard
229
256
  /// Real key events, delivered from the platform's message pump. These run
230
257
  /// before begin_frame(), so writing keys_down directly is correct here.
231
- pub fn set_key_down(&mut self, key: usize) { if key < MAX_KEYS { self.keys_down[key] = true; } }
232
- pub fn set_key_up(&mut self, key: usize) { if key < MAX_KEYS { self.keys_down[key] = false; } }
258
+ pub fn set_key_down(&mut self, key: usize) {
259
+ if key < MAX_KEYS {
260
+ if !self.keys_down[key] {
261
+ self.pending_key_pressed[key] = true;
262
+ self.pending_ui_events.push(crate::ui::UiInputEvent::Key(
263
+ crate::ui::UiKeyEvent {
264
+ key: key as u32,
265
+ pressed: true,
266
+ repeated: false,
267
+ },
268
+ ));
269
+ }
270
+ self.keys_down[key] = true;
271
+ }
272
+ }
273
+ pub fn set_key_up(&mut self, key: usize) {
274
+ if key < MAX_KEYS {
275
+ if self.keys_down[key] {
276
+ self.pending_key_released[key] = true;
277
+ self.pending_ui_events.push(crate::ui::UiInputEvent::Key(
278
+ crate::ui::UiKeyEvent {
279
+ key: key as u32,
280
+ pressed: false,
281
+ repeated: false,
282
+ },
283
+ ));
284
+ }
285
+ self.keys_down[key] = false;
286
+ }
287
+ }
233
288
 
234
289
  /// Synthetic key events (bloom_inject_key_down/up), which callers raise from
235
- /// inside the game loop. Staged, not written — see pending_key_down.
290
+ /// inside the game loop. Their UI edges are kept even if a down/up pair is
291
+ /// staged before the next frame.
236
292
  pub fn inject_key_down(&mut self, key: usize) {
237
- if key < MAX_KEYS { self.pending_key_down[key] = true; self.pending_key_up[key] = false; }
293
+ if key < MAX_KEYS {
294
+ self.pending_key_down[key] = true;
295
+ self.pending_key_up[key] = false;
296
+ self.pending_key_pressed[key] = true;
297
+ self.pending_ui_events.push(crate::ui::UiInputEvent::Key(
298
+ crate::ui::UiKeyEvent {
299
+ key: key as u32,
300
+ pressed: true,
301
+ repeated: false,
302
+ },
303
+ ));
304
+ }
238
305
  }
239
306
  pub fn inject_key_up(&mut self, key: usize) {
240
- if key < MAX_KEYS { self.pending_key_up[key] = true; self.pending_key_down[key] = false; }
307
+ if key < MAX_KEYS {
308
+ self.pending_key_up[key] = true;
309
+ self.pending_key_down[key] = false;
310
+ self.pending_key_released[key] = true;
311
+ self.pending_ui_events.push(crate::ui::UiInputEvent::Key(
312
+ crate::ui::UiKeyEvent {
313
+ key: key as u32,
314
+ pressed: false,
315
+ repeated: false,
316
+ },
317
+ ));
318
+ }
241
319
  }
242
320
 
243
321
  pub fn is_key_pressed(&self, key: usize) -> bool { key < MAX_KEYS && self.keys_pressed[key] }
@@ -256,15 +334,43 @@ impl InputState {
256
334
  self.mouse_wheel_y = 0.0;
257
335
  v
258
336
  }
259
- /// Push a Unicode codepoint into the character input queue. Called by
260
- /// platform event loops when a key-down event produces a printable char.
337
+ /// Push a Unicode codepoint into independent gameplay and UI text queues.
338
+ /// Platform event loops call this for committed text input.
261
339
  pub fn push_char(&mut self, c: u32) {
262
- let next = (self.char_queue_head + 1) % self.char_queue.len();
263
- if next != self.char_queue_tail {
264
- self.char_queue[self.char_queue_head] = c;
265
- self.char_queue_head = next;
340
+ push_char_to_queue(
341
+ &mut self.char_queue,
342
+ &mut self.char_queue_head,
343
+ self.char_queue_tail,
344
+ c,
345
+ );
346
+ push_char_to_queue(
347
+ &mut self.ui_char_queue,
348
+ &mut self.ui_char_queue_head,
349
+ self.ui_char_queue_tail,
350
+ c,
351
+ );
352
+ if let Some(character) = char::from_u32(c) {
353
+ self.pending_ui_events
354
+ .push(crate::ui::UiInputEvent::Text(character.to_string()));
355
+ }
356
+ }
357
+
358
+ /// Push committed text directly to the UI without exposing it through the
359
+ /// gameplay character queue.
360
+ pub fn push_ui_text(&mut self, text: String) {
361
+ if text.is_empty() {
362
+ return;
363
+ }
364
+ self.pending_ui_events
365
+ .push(crate::ui::UiInputEvent::Text(text.clone()));
366
+ for character in text.chars() {
367
+ push_char_to_queue(
368
+ &mut self.ui_char_queue,
369
+ &mut self.ui_char_queue_head,
370
+ self.ui_char_queue_tail,
371
+ character as u32,
372
+ );
266
373
  }
267
- // If the queue is full, silently drop the character.
268
374
  }
269
375
 
270
376
  /// Pop the next queued character. Returns 0 when the queue is empty.
@@ -392,6 +498,90 @@ impl InputState {
392
498
  || self.gamepad_buttons_pressed.iter().any(|&g| g)
393
499
  || self.touch_count > 0
394
500
  }
501
+
502
+ /// Snapshot input for UI evaluation. Text is consumed from its dedicated UI
503
+ /// queue, leaving the game's character queue and other getters untouched.
504
+ pub fn ui_snapshot(&mut self) -> crate::ui::UiInputSnapshot {
505
+ use crate::ui::{
506
+ UiInputSnapshot, UiKeyEvent, UiModifiers, UiPointerButtonEvent, UiTouchEvent,
507
+ };
508
+
509
+ let pointer_buttons = (0..MAX_MOUSE_BUTTONS).flat_map(|button| {
510
+ let mut events = Vec::with_capacity(2);
511
+ if self.mouse_pressed[button] {
512
+ events.push(UiPointerButtonEvent { button: button as u8, pressed: true });
513
+ }
514
+ if self.mouse_released[button] {
515
+ events.push(UiPointerButtonEvent { button: button as u8, pressed: false });
516
+ }
517
+ events
518
+ }).collect();
519
+
520
+ let ordered_events = std::mem::take(&mut self.pending_ui_events);
521
+ let mut keys = Vec::new();
522
+ for key in 0..MAX_KEYS {
523
+ if self.keys_pressed[key] || self.pending_key_pressed[key] {
524
+ keys.push(UiKeyEvent { key: key as u32, pressed: true, repeated: false });
525
+ }
526
+ if self.keys_released[key] || self.pending_key_released[key] {
527
+ keys.push(UiKeyEvent { key: key as u32, pressed: false, repeated: false });
528
+ }
529
+ // Clear an edge only after this snapshot observes it. UI events
530
+ // arriving after this point belong to the next snapshot.
531
+ self.pending_key_pressed[key] = false;
532
+ self.pending_key_released[key] = false;
533
+ if self.keys_repeated[key] {
534
+ keys.push(UiKeyEvent { key: key as u32, pressed: true, repeated: true });
535
+ }
536
+ }
537
+
538
+ let mut text = Vec::new();
539
+ while self.ui_char_queue_tail != self.ui_char_queue_head {
540
+ let character = self.ui_char_queue[self.ui_char_queue_tail];
541
+ self.ui_char_queue_tail = (self.ui_char_queue_tail + 1) % self.ui_char_queue.len();
542
+ if let Some(ch) = char::from_u32(character) {
543
+ text.push(ch.to_string());
544
+ }
545
+ }
546
+
547
+ let touches = (0..MAX_TOUCH_POINTS).filter_map(|slot| {
548
+ let point = &self.touch_points[slot];
549
+ (point.active || self.touch_pending_release[slot]).then_some(UiTouchEvent {
550
+ slot,
551
+ x: point.x,
552
+ y: point.y,
553
+ active: point.active,
554
+ released: self.touch_pending_release[slot],
555
+ })
556
+ }).collect();
557
+
558
+ UiInputSnapshot {
559
+ pointer_position: Some([self.mouse_x, self.mouse_y]),
560
+ pointer_delta: [self.mouse_delta_x, self.mouse_delta_y],
561
+ pointer_buttons,
562
+ scroll_x: 0.0,
563
+ scroll_y: self.mouse_wheel_y,
564
+ ordered_events,
565
+ keys,
566
+ modifiers: UiModifiers {
567
+ shift: self.keys_down[280] || self.keys_down[281],
568
+ ctrl: self.keys_down[282] || self.keys_down[283],
569
+ alt: self.keys_down[284] || self.keys_down[285],
570
+ super_key: self.keys_down[286] || self.keys_down[287],
571
+ },
572
+ text,
573
+ touches,
574
+ }
575
+ }
576
+ }
577
+
578
+ fn push_char_to_queue(queue: &mut [u32; 32], head: &mut usize, tail: usize, character: u32) {
579
+ let next = (*head + 1) % queue.len();
580
+ if next != tail {
581
+ queue[*head] = character;
582
+ *head = next;
583
+ }
584
+ // If the queue is full, silently drop the character.
395
585
  }
396
586
 
397
587
  #[cfg(test)]
@@ -445,4 +635,119 @@ mod tests {
445
635
  assert!(!input.is_mouse_button_down(0));
446
636
  assert!(!input.is_mouse_button_released(0));
447
637
  }
638
+
639
+ #[test]
640
+ fn ui_snapshot_keeps_scroll_and_text_after_game_consumes_them() {
641
+ let mut input = InputState::new();
642
+ input.accumulate_mouse_wheel(1.0);
643
+ input.push_char('é' as u32);
644
+ input.begin_frame();
645
+
646
+ let snapshot = input.ui_snapshot();
647
+ assert_eq!(snapshot.scroll_y, 1.0);
648
+ assert_eq!(input.consume_mouse_wheel(), 1.0);
649
+ assert_eq!(snapshot.text, vec!["é"]);
650
+ assert_eq!(input.pop_char(), 'é' as u32);
651
+ }
652
+
653
+ #[test]
654
+ fn ui_snapshot_delivers_each_character_only_once() {
655
+ let mut input = InputState::new();
656
+ input.push_char('x' as u32);
657
+
658
+ assert_eq!(input.ui_snapshot().text, vec!["x"]);
659
+ assert!(input.ui_snapshot().text.is_empty());
660
+ assert_eq!(input.pop_char(), 'x' as u32);
661
+ }
662
+
663
+ #[test]
664
+ fn game_character_reads_do_not_consume_ui_text() {
665
+ let mut input = InputState::new();
666
+ input.push_char('y' as u32);
667
+
668
+ assert_eq!(input.pop_char(), 'y' as u32);
669
+ assert_eq!(input.ui_snapshot().text, vec!["y"]);
670
+ }
671
+
672
+ #[test]
673
+ fn ui_snapshot_keeps_touch_release_for_a_fast_tap() {
674
+ let mut input = InputState::new();
675
+ input.set_touch(0, 12.0, 34.0, true);
676
+ input.release_touch(0, 12.0, 34.0);
677
+ input.begin_frame();
678
+
679
+ let snapshot = input.ui_snapshot();
680
+ let touch = snapshot.touches.iter().find(|touch| touch.slot == 0).unwrap();
681
+ assert!(touch.active);
682
+ assert!(touch.released);
683
+ }
684
+
685
+ #[test]
686
+ fn ui_snapshot_keeps_fast_key_click_edges() {
687
+ let mut input = InputState::new();
688
+ input.set_key_down(65);
689
+ input.set_key_up(65);
690
+ input.begin_frame();
691
+
692
+ let snapshot = input.ui_snapshot();
693
+ assert!(snapshot.keys.iter().any(|event| event.key == 65 && event.pressed));
694
+ assert!(snapshot.keys.iter().any(|event| event.key == 65 && !event.pressed));
695
+ assert!(!input.is_key_pressed(65));
696
+ assert!(!input.is_key_released(65));
697
+ }
698
+
699
+ #[test]
700
+ fn ui_snapshot_keeps_injected_key_tap_edges() {
701
+ let mut input = InputState::new();
702
+ input.inject_key_down(8);
703
+ input.inject_key_up(8);
704
+ input.begin_frame();
705
+
706
+ let snapshot = input.ui_snapshot();
707
+ assert!(snapshot.keys.iter().any(|event| event.key == 8 && event.pressed));
708
+ assert!(snapshot.keys.iter().any(|event| event.key == 8 && !event.pressed));
709
+ }
710
+
711
+ #[test]
712
+ fn ui_snapshot_keeps_injected_key_tap_until_next_snapshot() {
713
+ let mut input = InputState::new();
714
+ input.begin_frame();
715
+ let _ = input.ui_snapshot();
716
+
717
+ input.inject_key_down(8);
718
+ input.inject_key_up(8);
719
+ input.end_frame();
720
+ input.begin_frame();
721
+
722
+ let snapshot = input.ui_snapshot();
723
+ assert!(snapshot.keys.iter().any(|event| event.key == 8 && event.pressed));
724
+ assert!(snapshot.keys.iter().any(|event| event.key == 8 && !event.pressed));
725
+ }
726
+
727
+ #[test]
728
+ fn ui_snapshot_preserves_text_and_key_event_order() {
729
+ let mut input = InputState::new();
730
+ input.push_ui_text("x".to_owned());
731
+ input.inject_key_down(8);
732
+ input.inject_key_up(8);
733
+ input.begin_frame();
734
+
735
+ assert_eq!(input.pop_char(), 0);
736
+ assert_eq!(
737
+ input.ui_snapshot().ordered_events,
738
+ [
739
+ crate::ui::UiInputEvent::Text("x".to_owned()),
740
+ crate::ui::UiInputEvent::Key(crate::ui::UiKeyEvent {
741
+ key: 8,
742
+ pressed: true,
743
+ repeated: false,
744
+ }),
745
+ crate::ui::UiInputEvent::Key(crate::ui::UiKeyEvent {
746
+ key: 8,
747
+ pressed: false,
748
+ repeated: false,
749
+ }),
750
+ ]
751
+ );
752
+ }
448
753
  }
@@ -36,6 +36,7 @@ pub mod jolt_sys;
36
36
  pub mod physics_jolt;
37
37
  pub mod engine;
38
38
  pub mod drs;
39
+ pub mod ui;
39
40
  // Host-surface attach path (PerryTS/perry#5519). Pulls in wgpu's
40
41
  // raw-surface API; web builds its surface from a canvas id instead, so
41
42
  // this is native-only.