@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
@@ -0,0 +1,1056 @@
1
+ use std::cell::RefCell;
2
+ use std::collections::HashMap;
3
+ use std::path::PathBuf;
4
+ use std::time::Duration;
5
+
6
+ use imgui::{Condition, FontConfig, FontId, FontSource, Key, MouseButton, Ui};
7
+
8
+ use super::{
9
+ color_channel, UiBackend, UiCaptureState, UiCommand, UiInputEvent, UiInputSnapshot, UiOpcode,
10
+ UiResponse,
11
+ };
12
+
13
+ const MAX_FONT_BYTES: usize = 1_048_576;
14
+
15
+ // Dear ImGui's context is !Send. Keep its mutable state thread-local so
16
+ // UiSystem only carries Send commands and readbacks into native engine locks.
17
+ thread_local! {
18
+ static DEAR_IMGUI: RefCell<DearImGuiUi> = RefCell::new(DearImGuiUi::default());
19
+ }
20
+
21
+ pub(crate) fn with_dear_imgui<R>(f: impl FnOnce(&mut DearImGuiUi) -> R) -> R {
22
+ DEAR_IMGUI.with(|state| f(&mut state.borrow_mut()))
23
+ }
24
+
25
+ pub struct DearImGuiUi {
26
+ context: imgui::Context,
27
+ responses: HashMap<u32, UiResponse>,
28
+ capture: UiCaptureState,
29
+ font_ids: HashMap<u32, FontId>,
30
+ loaded_fonts: HashMap<u32, Vec<u8>>,
31
+ demo_open: bool,
32
+ metrics_open: bool,
33
+ }
34
+
35
+ impl Default for DearImGuiUi {
36
+ fn default() -> Self {
37
+ let mut context = imgui::Context::create();
38
+ context.set_ini_filename(None::<PathBuf>);
39
+ Self {
40
+ context,
41
+ responses: HashMap::new(),
42
+ capture: UiCaptureState::default(),
43
+ font_ids: HashMap::new(),
44
+ loaded_fonts: HashMap::new(),
45
+ demo_open: true,
46
+ metrics_open: true,
47
+ }
48
+ }
49
+ }
50
+
51
+ impl DearImGuiUi {
52
+ pub fn context_mut(&mut self) -> &mut imgui::Context {
53
+ &mut self.context
54
+ }
55
+
56
+ pub fn take_frame_readback(&mut self) -> (Vec<(u32, UiResponse)>, UiCaptureState) {
57
+ (
58
+ std::mem::take(&mut self.responses).into_iter().collect(),
59
+ self.capture,
60
+ )
61
+ }
62
+
63
+ pub fn register_fonts(&mut self, commands: &[UiCommand]) -> bool {
64
+ let mut changed = false;
65
+ for command in commands.iter().filter(|command| {
66
+ command.backend == UiBackend::DearImGui && command.opcode == UiOpcode::LoadFont
67
+ }) {
68
+ if command.scratch.is_empty() || command.scratch.len() > MAX_FONT_BYTES {
69
+ continue;
70
+ }
71
+ let Some(bytes) = command
72
+ .scratch
73
+ .iter()
74
+ .map(|value| {
75
+ (value.is_finite() && *value >= 0.0 && *value <= 255.0 && value.fract() == 0.0)
76
+ .then_some(*value as u8)
77
+ })
78
+ .collect::<Option<Vec<_>>>()
79
+ else {
80
+ continue;
81
+ };
82
+ if self.loaded_fonts.get(&command.id) == Some(&bytes) {
83
+ continue;
84
+ }
85
+ let name = if command.text.trim().is_empty() {
86
+ format!("bornengine-debug-font-{}", command.id)
87
+ } else {
88
+ command.text.clone()
89
+ };
90
+ let font_id = self.context.fonts().add_font(&[FontSource::TtfData {
91
+ data: &bytes,
92
+ size_pixels: 16.0,
93
+ config: Some(FontConfig {
94
+ name: Some(name),
95
+ ..FontConfig::default()
96
+ }),
97
+ }]);
98
+ self.font_ids.insert(command.id, font_id);
99
+ self.loaded_fonts.insert(command.id, bytes);
100
+ changed = true;
101
+ }
102
+ changed
103
+ }
104
+
105
+ pub fn prepare_frame<'a>(
106
+ &'a mut self,
107
+ commands: &[UiCommand],
108
+ input: &UiInputSnapshot,
109
+ display_size: [f32; 2],
110
+ framebuffer_scale: f32,
111
+ dt: f64,
112
+ texture_ids: &HashMap<u64, (imgui::TextureId, [f32; 2])>,
113
+ ) -> &'a imgui::DrawData {
114
+ let DearImGuiUi {
115
+ context,
116
+ responses,
117
+ capture,
118
+ font_ids,
119
+ demo_open,
120
+ metrics_open,
121
+ ..
122
+ } = self;
123
+
124
+ apply_theme(context, commands);
125
+ {
126
+ let io = context.io_mut();
127
+ io.display_size = [display_size[0].max(1.0), display_size[1].max(1.0)];
128
+ io.display_framebuffer_scale = [framebuffer_scale.max(0.1); 2];
129
+ io.update_delta_time(Duration::from_secs_f64(if dt.is_finite() {
130
+ dt.clamp(1.0 / 1000.0, 0.25)
131
+ } else {
132
+ 1.0 / 60.0
133
+ }));
134
+ io.key_ctrl = input.modifiers.ctrl;
135
+ io.key_shift = input.modifiers.shift;
136
+ io.key_alt = input.modifiers.alt;
137
+ io.key_super = input.modifiers.super_key;
138
+ if let Some([x, y]) = input.pointer_position {
139
+ if x.is_finite() && y.is_finite() {
140
+ io.add_mouse_pos_event([x as f32, y as f32]);
141
+ } else {
142
+ io.add_mouse_pos_event([-f32::MAX, -f32::MAX]);
143
+ }
144
+ } else {
145
+ io.add_mouse_pos_event([-f32::MAX, -f32::MAX]);
146
+ }
147
+ for event in &input.pointer_buttons {
148
+ if let Some(button) = mouse_button(event.button) {
149
+ io.add_mouse_button_event(button, event.pressed);
150
+ }
151
+ }
152
+ if input.scroll_x.is_finite() || input.scroll_y.is_finite() {
153
+ io.add_mouse_wheel_event([
154
+ if input.scroll_x.is_finite() {
155
+ input.scroll_x as f32
156
+ } else {
157
+ 0.0
158
+ },
159
+ if input.scroll_y.is_finite() {
160
+ input.scroll_y as f32
161
+ } else {
162
+ 0.0
163
+ },
164
+ ]);
165
+ }
166
+ for event in input.ordered_key_text_events() {
167
+ match event {
168
+ UiInputEvent::Key(key_event) => {
169
+ if let Some(key) = key_from_bloom(key_event.key) {
170
+ io.add_key_event(key, key_event.pressed);
171
+ }
172
+ }
173
+ UiInputEvent::Text(text) => {
174
+ for character in text.chars() {
175
+ io.add_input_character(character);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ }
181
+
182
+ let commands: Vec<_> = commands
183
+ .iter()
184
+ .filter(|command| command.backend == UiBackend::DearImGui)
185
+ .cloned()
186
+ .collect();
187
+ let pairs = container_pairs(&commands);
188
+ let mut positions = HashMap::new();
189
+ let mut sizes = HashMap::new();
190
+ for command in &commands {
191
+ match command.opcode {
192
+ UiOpcode::SetWindowPosition => {
193
+ if let (Some(x), Some(y)) =
194
+ (finite_f32(command.args[0]), finite_f32(command.args[1]))
195
+ {
196
+ positions.insert(command.id, ([x, y], command.args[2] != 0.0));
197
+ }
198
+ }
199
+ UiOpcode::SetWindowSize => {
200
+ if let (Some(width), Some(height)) =
201
+ (finite_f32(command.args[0]), finite_f32(command.args[1]))
202
+ {
203
+ sizes.insert(
204
+ command.id,
205
+ ([width.max(32.0), height.max(32.0)], command.args[2] != 0.0),
206
+ );
207
+ }
208
+ }
209
+ _ => {}
210
+ }
211
+ }
212
+
213
+ let mut eval = EvalState {
214
+ commands: &commands,
215
+ pairs,
216
+ positions,
217
+ sizes,
218
+ texture_ids,
219
+ responses: HashMap::new(),
220
+ text_edit_focused: false,
221
+ demo_open,
222
+ metrics_open,
223
+ };
224
+ let ui = context.frame();
225
+ let selected_font = commands.iter().rev().find_map(|command| {
226
+ if command.opcode == UiOpcode::SetStyle && finite_usize(command.args[0]) == Some(2) {
227
+ finite_u32(command.args[1]).and_then(|id| font_ids.get(&id).copied())
228
+ } else {
229
+ None
230
+ }
231
+ });
232
+ if let Some(font_id) = selected_font {
233
+ let _font = ui.push_font(font_id);
234
+ eval.render_range(&ui, 0, eval.commands.len(), false);
235
+ } else {
236
+ eval.render_range(&ui, 0, eval.commands.len(), false);
237
+ }
238
+ let text_edit_focused = eval.text_edit_focused;
239
+ *responses = eval.responses;
240
+
241
+ *capture = UiCaptureState {
242
+ pointer: context.io().want_capture_mouse,
243
+ keyboard: context.io().want_capture_keyboard,
244
+ };
245
+ let _ = text_edit_focused;
246
+ context.render()
247
+ }
248
+ }
249
+
250
+ struct EvalState<'a> {
251
+ commands: &'a [UiCommand],
252
+ pairs: HashMap<usize, usize>,
253
+ positions: HashMap<u32, ([f32; 2], bool)>,
254
+ sizes: HashMap<u32, ([f32; 2], bool)>,
255
+ texture_ids: &'a HashMap<u64, (imgui::TextureId, [f32; 2])>,
256
+ responses: HashMap<u32, UiResponse>,
257
+ text_edit_focused: bool,
258
+ demo_open: &'a mut bool,
259
+ metrics_open: &'a mut bool,
260
+ }
261
+
262
+ impl EvalState<'_> {
263
+ fn render_range(&mut self, ui: &Ui, start: usize, end: usize, horizontal: bool) {
264
+ let mut index = start;
265
+ let mut first_item = true;
266
+ while index < end {
267
+ let command = &self.commands[index];
268
+ let pair = self
269
+ .pairs
270
+ .get(&index)
271
+ .copied()
272
+ .filter(|pair_end| *pair_end < end);
273
+ if horizontal && is_visual(command.opcode) {
274
+ if first_item {
275
+ first_item = false;
276
+ } else {
277
+ ui.same_line();
278
+ }
279
+ }
280
+ match command.opcode {
281
+ UiOpcode::BeginWindow if pair.is_some() => {
282
+ let close = pair.unwrap();
283
+ let (position, position_always) = self
284
+ .positions
285
+ .get(&command.id)
286
+ .copied()
287
+ .unwrap_or(([16.0, 16.0], false));
288
+ let (size, size_always) = self
289
+ .sizes
290
+ .get(&command.id)
291
+ .copied()
292
+ .unwrap_or(([320.0, 240.0], false));
293
+ let name = format!("{}##bornengine-ui-{}", command.text, command.id);
294
+ ui.window(name)
295
+ .position(position, condition(position_always))
296
+ .size(size, condition(size_always))
297
+ .build(|| self.render_range(ui, index + 1, close, false));
298
+ index = close + 1;
299
+ }
300
+ UiOpcode::BeginPanel | UiOpcode::BeginScrollArea if pair.is_some() => {
301
+ let close = pair.unwrap();
302
+ let child = ui
303
+ .child_window(format!("##bornengine-ui-child-{}", command.id))
304
+ .size([0.0, 0.0])
305
+ .border(command.opcode == UiOpcode::BeginPanel)
306
+ .begin();
307
+ if child.is_some() {
308
+ self.render_range(ui, index + 1, close, false);
309
+ }
310
+ drop(child);
311
+ index = close + 1;
312
+ }
313
+ UiOpcode::BeginHorizontal if pair.is_some() => {
314
+ let close = pair.unwrap();
315
+ self.render_range(ui, index + 1, close, true);
316
+ index = close + 1;
317
+ }
318
+ UiOpcode::BeginVertical if pair.is_some() => {
319
+ let close = pair.unwrap();
320
+ self.render_range(ui, index + 1, close, false);
321
+ index = close + 1;
322
+ }
323
+ UiOpcode::BeginTabBar if pair.is_some() => {
324
+ let close = pair.unwrap();
325
+ let token = ui.tab_bar(format!("bornengine-ui-tabs-{}", command.id));
326
+ if token.is_some() {
327
+ self.render_range(ui, index + 1, close, false);
328
+ }
329
+ drop(token);
330
+ index = close + 1;
331
+ }
332
+ UiOpcode::BeginTabItem if pair.is_some() => {
333
+ let close = pair.unwrap();
334
+ let token = ui.tab_item(&command.text);
335
+ let opened = token.is_some();
336
+ if opened {
337
+ self.render_range(ui, index + 1, close, false);
338
+ }
339
+ self.responses.insert(
340
+ command.id,
341
+ UiResponse {
342
+ value: if opened { 1.0 } else { 0.0 },
343
+ ..UiResponse::default()
344
+ },
345
+ );
346
+ drop(token);
347
+ index = close + 1;
348
+ }
349
+ UiOpcode::Combo if pair.is_some() => {
350
+ let close = pair.unwrap();
351
+ let token = ui.begin_combo(&command.id.to_string(), &command.text);
352
+ let opened = token.is_some();
353
+ if opened {
354
+ self.render_range(ui, index + 1, close, false);
355
+ }
356
+ self.responses.insert(
357
+ command.id,
358
+ UiResponse {
359
+ value: if opened { 1.0 } else { 0.0 },
360
+ ..UiResponse::default()
361
+ },
362
+ );
363
+ drop(token);
364
+ index = close + 1;
365
+ }
366
+ UiOpcode::BeginMenuBar if pair.is_some() => {
367
+ let close = pair.unwrap();
368
+ let token = ui.begin_menu_bar();
369
+ if token.is_some() {
370
+ self.render_range(ui, index + 1, close, false);
371
+ }
372
+ drop(token);
373
+ index = close + 1;
374
+ }
375
+ UiOpcode::BeginMenu if pair.is_some() => {
376
+ let close = pair.unwrap();
377
+ let token = ui.begin_menu(&command.text);
378
+ let opened = token.is_some();
379
+ if opened {
380
+ self.render_range(ui, index + 1, close, false);
381
+ }
382
+ self.responses.insert(
383
+ command.id,
384
+ UiResponse {
385
+ value: if opened { 1.0 } else { 0.0 },
386
+ ..UiResponse::default()
387
+ },
388
+ );
389
+ drop(token);
390
+ index = close + 1;
391
+ }
392
+ UiOpcode::BeginTable if pair.is_some() => {
393
+ let close = pair.unwrap();
394
+ let columns = finite_usize(command.args[0]).unwrap_or(1).clamp(1, 64);
395
+ let token =
396
+ ui.begin_table(format!("bornengine-ui-table-{}", command.id), columns);
397
+ if token.is_some() {
398
+ self.render_range(ui, index + 1, close, false);
399
+ }
400
+ drop(token);
401
+ index = close + 1;
402
+ }
403
+ UiOpcode::CollapsingHeader if pair.is_some() => {
404
+ let close = pair.unwrap();
405
+ let opened = ui.collapsing_header(&command.text, imgui::TreeNodeFlags::empty());
406
+ self.responses.insert(
407
+ command.id,
408
+ UiResponse {
409
+ value: if opened { 1.0 } else { 0.0 },
410
+ ..UiResponse::default()
411
+ },
412
+ );
413
+ if opened {
414
+ self.render_range(ui, index + 1, close, false);
415
+ }
416
+ index = close + 1;
417
+ }
418
+ UiOpcode::TreeNode if pair.is_some() => {
419
+ let close = pair.unwrap();
420
+ let id_scope = ui.push_id(format!("bornengine-ui-tree-{}", command.id));
421
+ let token = ui.tree_node(&command.text);
422
+ let opened = token.is_some();
423
+ self.responses.insert(
424
+ command.id,
425
+ UiResponse {
426
+ value: if opened { 1.0 } else { 0.0 },
427
+ ..UiResponse::default()
428
+ },
429
+ );
430
+ if opened {
431
+ self.render_range(ui, index + 1, close, false);
432
+ }
433
+ drop(token);
434
+ drop(id_scope);
435
+ index = close + 1;
436
+ }
437
+ UiOpcode::Spacing => {
438
+ let amount = finite_f32(command.args[0]).unwrap_or(8.0).max(0.0);
439
+ ui.dummy([1.0, amount]);
440
+ index += 1;
441
+ }
442
+ UiOpcode::Separator => {
443
+ ui.separator();
444
+ index += 1;
445
+ }
446
+ UiOpcode::Label => {
447
+ ui.text(&command.text);
448
+ index += 1;
449
+ }
450
+ UiOpcode::Link | UiOpcode::Button => {
451
+ let id_scope = ui.push_id(format!("bornengine-ui-link-{}", command.id));
452
+ let clicked = ui.button(&command.text);
453
+ self.store_item(ui, command.id, clicked, false, 0.0, String::new());
454
+ drop(id_scope);
455
+ index += 1;
456
+ }
457
+ UiOpcode::Checkbox => {
458
+ let mut value = command.args[0] != 0.0;
459
+ let id_scope = ui.push_id(format!("bornengine-ui-checkbox-{}", command.id));
460
+ let changed = ui.checkbox(&command.text, &mut value);
461
+ self.store_item(
462
+ ui,
463
+ command.id,
464
+ changed,
465
+ changed,
466
+ if value { 1.0 } else { 0.0 },
467
+ String::new(),
468
+ );
469
+ drop(id_scope);
470
+ index += 1;
471
+ }
472
+ UiOpcode::RadioButton => {
473
+ let id_scope = ui.push_id(format!("bornengine-ui-radio-{}", command.id));
474
+ let clicked = ui.radio_button_bool(&command.text, command.args[0] != 0.0);
475
+ self.store_item(
476
+ ui,
477
+ command.id,
478
+ clicked,
479
+ clicked,
480
+ if clicked { 1.0 } else { command.args[0] },
481
+ String::new(),
482
+ );
483
+ drop(id_scope);
484
+ index += 1;
485
+ }
486
+ UiOpcode::SliderFloat => {
487
+ let mut value = finite_f32(command.args[0]).unwrap_or(0.0);
488
+ let min = finite_f32(command.args[1]).unwrap_or(0.0);
489
+ let max = finite_f32(command.args[2]).unwrap_or(1.0);
490
+ let id_scope = ui.push_id(format!("bornengine-ui-slider-float-{}", command.id));
491
+ let changed = ui.slider(&command.text, min.min(max), min.max(max), &mut value);
492
+ self.store_item(
493
+ ui,
494
+ command.id,
495
+ false,
496
+ changed,
497
+ f64::from(value),
498
+ String::new(),
499
+ );
500
+ drop(id_scope);
501
+ index += 1;
502
+ }
503
+ UiOpcode::SliderInt => {
504
+ let mut value = command.args[0].round() as i32;
505
+ let min = command.args[1].round() as i32;
506
+ let max = command.args[2].round() as i32;
507
+ let id_scope = ui.push_id(format!("bornengine-ui-slider-int-{}", command.id));
508
+ let changed = ui.slider(&command.text, min.min(max), min.max(max), &mut value);
509
+ self.store_item(
510
+ ui,
511
+ command.id,
512
+ false,
513
+ changed,
514
+ f64::from(value),
515
+ String::new(),
516
+ );
517
+ drop(id_scope);
518
+ index += 1;
519
+ }
520
+ UiOpcode::DragFloat => {
521
+ let mut value = finite_f32(command.args[0]).unwrap_or(0.0);
522
+ let speed = finite_f32(command.args[1])
523
+ .unwrap_or(0.1)
524
+ .abs()
525
+ .max(0.000_001);
526
+ let mut drag = imgui::Drag::<f32, _>::new(&command.text).speed(speed);
527
+ if let (Some(min), Some(max)) =
528
+ (finite_f32(command.args[2]), finite_f32(command.args[3]))
529
+ {
530
+ drag = drag.range(min.min(max), min.max(max));
531
+ }
532
+ let id_scope = ui.push_id(format!("bornengine-ui-drag-{}", command.id));
533
+ let changed = drag.build(ui, &mut value);
534
+ self.store_item(
535
+ ui,
536
+ command.id,
537
+ false,
538
+ changed,
539
+ f64::from(value),
540
+ String::new(),
541
+ );
542
+ drop(id_scope);
543
+ index += 1;
544
+ }
545
+ UiOpcode::TextEdit => {
546
+ let mut value = command.text.clone();
547
+ let id_scope = ui.push_id(format!("bornengine-ui-text-{}", command.id));
548
+ let changed = ui
549
+ .input_text(&format!("##bornengine-ui-text-{}", command.id), &mut value)
550
+ .build();
551
+ self.text_edit_focused |= ui.is_item_focused();
552
+ self.store_item(ui, command.id, false, changed, 0.0, value);
553
+ drop(id_scope);
554
+ index += 1;
555
+ }
556
+ UiOpcode::Selectable => {
557
+ let id_scope = ui.push_id(format!("bornengine-ui-selectable-{}", command.id));
558
+ let clicked = ui
559
+ .selectable_config(&command.text)
560
+ .selected(command.args[0] != 0.0)
561
+ .build();
562
+ self.store_item(
563
+ ui,
564
+ command.id,
565
+ clicked,
566
+ clicked,
567
+ if clicked { 1.0 } else { 0.0 },
568
+ String::new(),
569
+ );
570
+ drop(id_scope);
571
+ index += 1;
572
+ }
573
+ UiOpcode::ProgressBar => {
574
+ let fraction = finite_f32(command.args[0]).unwrap_or(0.0).clamp(0.0, 1.0);
575
+ imgui::ProgressBar::new(fraction)
576
+ .overlay_text(&command.text)
577
+ .build(ui);
578
+ self.responses.insert(
579
+ command.id,
580
+ UiResponse {
581
+ value: f64::from(fraction),
582
+ ..UiResponse::default()
583
+ },
584
+ );
585
+ index += 1;
586
+ }
587
+ UiOpcode::Image => {
588
+ let handle = finite_u64(command.args[0]);
589
+ if let Some((texture_id, texture_size)) =
590
+ handle.and_then(|handle| self.texture_ids.get(&handle).copied())
591
+ {
592
+ let size = [
593
+ finite_f32(command.args[1]).unwrap_or(0.0).max(0.0),
594
+ finite_f32(command.args[2]).unwrap_or(0.0).max(0.0),
595
+ ];
596
+ let size = [
597
+ if size[0] > 0.0 {
598
+ size[0]
599
+ } else {
600
+ texture_size[0]
601
+ },
602
+ if size[1] > 0.0 {
603
+ size[1]
604
+ } else {
605
+ texture_size[1]
606
+ },
607
+ ];
608
+ imgui::Image::new(texture_id, size).build(ui);
609
+ self.responses.insert(
610
+ command.id,
611
+ UiResponse {
612
+ value: handle.unwrap_or_default() as f64,
613
+ ..UiResponse::default()
614
+ },
615
+ );
616
+ } else {
617
+ ui.text("[texture unavailable]");
618
+ self.responses.insert(command.id, UiResponse::default());
619
+ }
620
+ index += 1;
621
+ }
622
+ UiOpcode::PaintLine
623
+ | UiOpcode::PaintRect
624
+ | UiOpcode::PaintCircle
625
+ | UiOpcode::PaintText
626
+ | UiOpcode::PaintPolyline
627
+ | UiOpcode::PaintPolygon => {
628
+ self.paint(ui, command);
629
+ self.responses.insert(command.id, UiResponse::default());
630
+ index += 1;
631
+ }
632
+ UiOpcode::MenuItem => {
633
+ let id_scope = ui.push_id(format!("bornengine-ui-menu-item-{}", command.id));
634
+ let clicked = ui.menu_item(&command.text);
635
+ self.store_item(
636
+ ui,
637
+ command.id,
638
+ clicked,
639
+ false,
640
+ if clicked { 1.0 } else { 0.0 },
641
+ String::new(),
642
+ );
643
+ drop(id_scope);
644
+ index += 1;
645
+ }
646
+ UiOpcode::DemoWindow => {
647
+ ui.show_demo_window(self.demo_open);
648
+ index += 1;
649
+ }
650
+ UiOpcode::MetricsWindow => {
651
+ ui.show_metrics_window(self.metrics_open);
652
+ index += 1;
653
+ }
654
+ UiOpcode::SetStyle | UiOpcode::LoadFont | UiOpcode::RegisterTexture => {
655
+ index += 1;
656
+ }
657
+ UiOpcode::BeginPanel
658
+ | UiOpcode::BeginWindow
659
+ | UiOpcode::Combo
660
+ | UiOpcode::CollapsingHeader
661
+ | UiOpcode::BeginScrollArea
662
+ | UiOpcode::BeginTabBar
663
+ | UiOpcode::BeginTabItem
664
+ | UiOpcode::BeginMenuBar
665
+ | UiOpcode::BeginMenu
666
+ | UiOpcode::BeginTable
667
+ | UiOpcode::TreeNode
668
+ | UiOpcode::EndWindow
669
+ | UiOpcode::EndPanel
670
+ | UiOpcode::BeginHorizontal
671
+ | UiOpcode::EndHorizontal
672
+ | UiOpcode::BeginVertical
673
+ | UiOpcode::EndVertical
674
+ | UiOpcode::SetWindowPosition
675
+ | UiOpcode::SetWindowSize
676
+ | UiOpcode::EndScrollArea
677
+ | UiOpcode::EndTabBar
678
+ | UiOpcode::EndTabItem
679
+ | UiOpcode::EndCombo
680
+ | UiOpcode::EndCollapsingHeader
681
+ | UiOpcode::EndMenuBar
682
+ | UiOpcode::EndMenu
683
+ | UiOpcode::TreePop
684
+ | UiOpcode::EndTable
685
+ | UiOpcode::TableNextRow
686
+ | UiOpcode::TableNextColumn
687
+ | UiOpcode::PlotLines => {
688
+ match command.opcode {
689
+ UiOpcode::TableNextRow => ui.table_next_row(),
690
+ UiOpcode::TableNextColumn => {
691
+ ui.table_next_column();
692
+ }
693
+ _ => {}
694
+ }
695
+ index += 1;
696
+ }
697
+ }
698
+ }
699
+ }
700
+
701
+ fn store_item(
702
+ &mut self,
703
+ ui: &Ui,
704
+ id: u32,
705
+ clicked: bool,
706
+ changed: bool,
707
+ value: f64,
708
+ text: String,
709
+ ) {
710
+ self.responses.insert(
711
+ id,
712
+ UiResponse {
713
+ clicked,
714
+ changed,
715
+ hovered: ui.is_item_hovered(),
716
+ focused: ui.is_item_focused(),
717
+ dragged: ui.is_item_active(),
718
+ value,
719
+ text,
720
+ },
721
+ );
722
+ }
723
+
724
+ fn paint(&mut self, ui: &Ui, command: &UiCommand) {
725
+ let values = &command.scratch;
726
+ let Some(origin) = values_origin(ui) else {
727
+ return;
728
+ };
729
+ let coordinate = |index: usize| values.get(index).copied().and_then(finite_f32);
730
+ let color = |start: usize| rgba(values, start);
731
+ let draw = ui.get_window_draw_list();
732
+ match command.opcode {
733
+ UiOpcode::PaintLine if values.len() >= 9 => {
734
+ if let (Some(x1), Some(y1), Some(x2), Some(y2)) =
735
+ (coordinate(0), coordinate(1), coordinate(2), coordinate(3))
736
+ {
737
+ draw.add_line(
738
+ [origin[0] + x1, origin[1] + y1],
739
+ [origin[0] + x2, origin[1] + y2],
740
+ color(4),
741
+ )
742
+ .thickness(coordinate(8).unwrap_or(1.0).max(0.1))
743
+ .build();
744
+ }
745
+ }
746
+ UiOpcode::PaintRect if values.len() >= 8 => {
747
+ if let (Some(x), Some(y), Some(width), Some(height)) =
748
+ (coordinate(0), coordinate(1), coordinate(2), coordinate(3))
749
+ {
750
+ draw.add_rect(
751
+ [origin[0] + x, origin[1] + y],
752
+ [
753
+ origin[0] + x + width.max(0.0),
754
+ origin[1] + y + height.max(0.0),
755
+ ],
756
+ color(4),
757
+ )
758
+ .filled(true)
759
+ .build();
760
+ }
761
+ }
762
+ UiOpcode::PaintCircle if values.len() >= 8 => {
763
+ if let (Some(x), Some(y), Some(radius)) =
764
+ (coordinate(0), coordinate(1), coordinate(2))
765
+ {
766
+ draw.add_circle([origin[0] + x, origin[1] + y], radius.max(0.0), color(3))
767
+ .thickness(coordinate(7).unwrap_or(1.0).max(0.1))
768
+ .build();
769
+ }
770
+ }
771
+ UiOpcode::PaintText if values.len() >= 7 => {
772
+ if let (Some(x), Some(y)) = (coordinate(0), coordinate(1)) {
773
+ draw.add_text([origin[0] + x, origin[1] + y], color(3), &command.text);
774
+ }
775
+ }
776
+ UiOpcode::PaintPolyline | UiOpcode::PaintPolygon => {
777
+ let coordinate_count = values.len().saturating_sub(5) & !1;
778
+ let mut points: Vec<[f32; 2]> = values[..coordinate_count]
779
+ .chunks_exact(2)
780
+ .filter_map(|point| {
781
+ Some([
782
+ origin[0] + finite_f32(point[0])?,
783
+ origin[1] + finite_f32(point[1])?,
784
+ ])
785
+ })
786
+ .collect();
787
+ if command.opcode == UiOpcode::PaintPolygon && points.len() > 2 {
788
+ points.push(points[0]);
789
+ }
790
+ if points.len() >= 2 {
791
+ draw.add_polyline(points, color(coordinate_count))
792
+ .thickness(coordinate(coordinate_count + 4).unwrap_or(1.0).max(0.1))
793
+ .build();
794
+ }
795
+ }
796
+ _ => {}
797
+ }
798
+ }
799
+ }
800
+
801
+ fn apply_theme(context: &mut imgui::Context, commands: &[UiCommand]) {
802
+ let theme = commands.iter().rev().find_map(|command| {
803
+ if command.opcode == UiOpcode::SetStyle {
804
+ match finite_usize(command.args[0]) {
805
+ Some(0) => Some(false),
806
+ Some(1) => Some(true),
807
+ _ => None,
808
+ }
809
+ } else {
810
+ None
811
+ }
812
+ });
813
+ if let Some(light) = theme {
814
+ if light {
815
+ context.style_mut().use_light_colors();
816
+ } else {
817
+ context.style_mut().use_dark_colors();
818
+ }
819
+ }
820
+ }
821
+
822
+ fn condition(always: bool) -> Condition {
823
+ if always {
824
+ Condition::Always
825
+ } else {
826
+ Condition::FirstUseEver
827
+ }
828
+ }
829
+
830
+ fn is_visual(opcode: UiOpcode) -> bool {
831
+ matches!(
832
+ opcode,
833
+ UiOpcode::BeginWindow
834
+ | UiOpcode::BeginPanel
835
+ | UiOpcode::BeginHorizontal
836
+ | UiOpcode::BeginVertical
837
+ | UiOpcode::BeginScrollArea
838
+ | UiOpcode::BeginTabBar
839
+ | UiOpcode::BeginTabItem
840
+ | UiOpcode::Combo
841
+ | UiOpcode::BeginMenu
842
+ | UiOpcode::BeginTable
843
+ | UiOpcode::CollapsingHeader
844
+ | UiOpcode::TreeNode
845
+ | UiOpcode::Label
846
+ | UiOpcode::Link
847
+ | UiOpcode::Button
848
+ | UiOpcode::Checkbox
849
+ | UiOpcode::RadioButton
850
+ | UiOpcode::SliderFloat
851
+ | UiOpcode::SliderInt
852
+ | UiOpcode::DragFloat
853
+ | UiOpcode::TextEdit
854
+ | UiOpcode::Selectable
855
+ | UiOpcode::ProgressBar
856
+ | UiOpcode::Image
857
+ | UiOpcode::PaintLine
858
+ | UiOpcode::PaintRect
859
+ | UiOpcode::PaintCircle
860
+ | UiOpcode::PaintText
861
+ | UiOpcode::PaintPolyline
862
+ | UiOpcode::PaintPolygon
863
+ | UiOpcode::MenuItem
864
+ | UiOpcode::DemoWindow
865
+ | UiOpcode::MetricsWindow
866
+ )
867
+ }
868
+
869
+ fn container_pairs(commands: &[UiCommand]) -> HashMap<usize, usize> {
870
+ let mut stack: Vec<(usize, UiOpcode)> = Vec::new();
871
+ let mut pairs = HashMap::new();
872
+ for (index, command) in commands.iter().enumerate() {
873
+ if let Some(end) = matching_end(command.opcode) {
874
+ stack.push((index, end));
875
+ } else if let Some((start, expected)) = stack.last().copied() {
876
+ if expected == command.opcode {
877
+ stack.pop();
878
+ pairs.insert(start, index);
879
+ }
880
+ }
881
+ }
882
+ pairs
883
+ }
884
+
885
+ fn matching_end(opcode: UiOpcode) -> Option<UiOpcode> {
886
+ Some(match opcode {
887
+ UiOpcode::BeginWindow => UiOpcode::EndWindow,
888
+ UiOpcode::BeginPanel => UiOpcode::EndPanel,
889
+ UiOpcode::BeginHorizontal => UiOpcode::EndHorizontal,
890
+ UiOpcode::BeginVertical => UiOpcode::EndVertical,
891
+ UiOpcode::BeginScrollArea => UiOpcode::EndScrollArea,
892
+ UiOpcode::BeginTabBar => UiOpcode::EndTabBar,
893
+ UiOpcode::BeginTabItem => UiOpcode::EndTabItem,
894
+ UiOpcode::Combo => UiOpcode::EndCombo,
895
+ UiOpcode::BeginMenuBar => UiOpcode::EndMenuBar,
896
+ UiOpcode::BeginMenu => UiOpcode::EndMenu,
897
+ UiOpcode::BeginTable => UiOpcode::EndTable,
898
+ UiOpcode::CollapsingHeader => UiOpcode::EndCollapsingHeader,
899
+ UiOpcode::TreeNode => UiOpcode::TreePop,
900
+ _ => return None,
901
+ })
902
+ }
903
+
904
+ fn mouse_button(button: u8) -> Option<MouseButton> {
905
+ Some(match button {
906
+ 0 => MouseButton::Left,
907
+ 1 => MouseButton::Right,
908
+ 2 => MouseButton::Middle,
909
+ 3 => MouseButton::Extra1,
910
+ 4 => MouseButton::Extra2,
911
+ _ => return None,
912
+ })
913
+ }
914
+
915
+ fn key_from_bloom(code: u32) -> Option<Key> {
916
+ Some(match code {
917
+ 32 => Key::Space,
918
+ 8 => Key::Backspace,
919
+ 9 => Key::Tab,
920
+ 13 | 265 => Key::Enter,
921
+ 27 => Key::Escape,
922
+ 127 => Key::Delete,
923
+ 256 => Key::UpArrow,
924
+ 257 => Key::DownArrow,
925
+ 258 => Key::LeftArrow,
926
+ 259 => Key::RightArrow,
927
+ 260 => Key::Insert,
928
+ 261 => Key::Home,
929
+ 262 => Key::End,
930
+ 263 => Key::PageUp,
931
+ 264 => Key::PageDown,
932
+ 280 => Key::LeftShift,
933
+ 281 => Key::RightShift,
934
+ 282 => Key::LeftCtrl,
935
+ 283 => Key::RightCtrl,
936
+ 284 => Key::LeftAlt,
937
+ 285 => Key::RightAlt,
938
+ 286 => Key::LeftSuper,
939
+ 287 => Key::RightSuper,
940
+ 48..=57 => [
941
+ Key::Alpha0,
942
+ Key::Alpha1,
943
+ Key::Alpha2,
944
+ Key::Alpha3,
945
+ Key::Alpha4,
946
+ Key::Alpha5,
947
+ Key::Alpha6,
948
+ Key::Alpha7,
949
+ Key::Alpha8,
950
+ Key::Alpha9,
951
+ ][(code - 48) as usize],
952
+ 65..=90 => [
953
+ Key::A,
954
+ Key::B,
955
+ Key::C,
956
+ Key::D,
957
+ Key::E,
958
+ Key::F,
959
+ Key::G,
960
+ Key::H,
961
+ Key::I,
962
+ Key::J,
963
+ Key::K,
964
+ Key::L,
965
+ Key::M,
966
+ Key::N,
967
+ Key::O,
968
+ Key::P,
969
+ Key::Q,
970
+ Key::R,
971
+ Key::S,
972
+ Key::T,
973
+ Key::U,
974
+ Key::V,
975
+ Key::W,
976
+ Key::X,
977
+ Key::Y,
978
+ Key::Z,
979
+ ][(code - 65) as usize],
980
+ 112..=123 => [
981
+ Key::F1,
982
+ Key::F2,
983
+ Key::F3,
984
+ Key::F4,
985
+ Key::F5,
986
+ Key::F6,
987
+ Key::F7,
988
+ Key::F8,
989
+ Key::F9,
990
+ Key::F10,
991
+ Key::F11,
992
+ Key::F12,
993
+ ][(code - 112) as usize],
994
+ 39 => Key::Apostrophe,
995
+ 44 => Key::Comma,
996
+ 45 => Key::Minus,
997
+ 46 => Key::Period,
998
+ 47 => Key::Slash,
999
+ 59 => Key::Semicolon,
1000
+ 61 => Key::Equal,
1001
+ 91 => Key::LeftBracket,
1002
+ 92 => Key::Backslash,
1003
+ 93 => Key::RightBracket,
1004
+ 96 => Key::GraveAccent,
1005
+ _ => return None,
1006
+ })
1007
+ }
1008
+
1009
+ fn values_origin(ui: &Ui) -> Option<[f32; 2]> {
1010
+ let window = ui.window_pos();
1011
+ let cursor = ui.cursor_start_pos();
1012
+ let position = [window[0] + cursor[0], window[1] + cursor[1]];
1013
+ (position[0].is_finite() && position[1].is_finite()).then_some(position)
1014
+ }
1015
+
1016
+ fn rgba(values: &[f64], start: usize) -> imgui::ImColor32 {
1017
+ let component = |index: usize| values.get(index).copied().map(color_channel).unwrap_or(255);
1018
+ imgui::ImColor32::from_rgba(
1019
+ component(start),
1020
+ component(start + 1),
1021
+ component(start + 2),
1022
+ component(start + 3),
1023
+ )
1024
+ }
1025
+
1026
+ fn finite_f32(value: f64) -> Option<f32> {
1027
+ (value.is_finite() && value.abs() <= f32::MAX as f64).then_some(value as f32)
1028
+ }
1029
+
1030
+ fn finite_usize(value: f64) -> Option<usize> {
1031
+ (value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= usize::MAX as f64)
1032
+ .then_some(value as usize)
1033
+ }
1034
+
1035
+ fn finite_u32(value: f64) -> Option<u32> {
1036
+ (value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= u32::MAX as f64)
1037
+ .then_some(value as u32)
1038
+ }
1039
+
1040
+ fn finite_u64(value: f64) -> Option<u64> {
1041
+ (value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= u64::MAX as f64)
1042
+ .then_some(value as u64)
1043
+ }
1044
+
1045
+ #[cfg(test)]
1046
+ mod tests {
1047
+ use super::rgba;
1048
+
1049
+ #[test]
1050
+ fn paint_color_channels_use_engine_byte_range() {
1051
+ assert_eq!(
1052
+ rgba(&[128.0, 64.0, 255.0, 200.0], 0).to_rgba(),
1053
+ [128, 64, 255, 200]
1054
+ );
1055
+ }
1056
+ }