@bornengine/engine 0.4.16 → 0.5.0

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 +216 -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,1282 @@
1
+ use std::collections::{BTreeMap, HashMap, HashSet};
2
+ use std::sync::Arc;
3
+
4
+ use egui::{
5
+ Align2, Color32, Context, Event, Id, Key, Modifiers, MouseWheelUnit, PointerButton, Pos2,
6
+ RawInput, Rect, Shape, Stroke, TouchDeviceId, TouchId, TouchPhase, Ui, Vec2,
7
+ };
8
+
9
+ use super::{
10
+ color_channel, UiBackend, UiCommand, UiInputEvent, UiInputSnapshot, UiOpcode, UiResponse,
11
+ };
12
+
13
+ #[derive(Default)]
14
+ pub struct EguiUi {
15
+ context: Context,
16
+ elapsed_seconds: f64,
17
+ native_pixels_per_point: Option<f32>,
18
+ active_touches: HashSet<usize>,
19
+ active_tabs: HashMap<u32, u32>,
20
+ custom_fonts: BTreeMap<u32, (String, Arc<egui::FontData>)>,
21
+ }
22
+
23
+ pub struct EguiFrameOutput {
24
+ pub paint_jobs: Vec<egui::epaint::ClippedPrimitive>,
25
+ pub textures_delta: egui::TexturesDelta,
26
+ pub platform_output: egui::PlatformOutput,
27
+ pub pixels_per_point: f32,
28
+ pub responses: HashMap<u32, UiResponse>,
29
+ pub registered_textures: HashMap<u32, u64>,
30
+ pub wants_pointer_input: bool,
31
+ pub wants_keyboard_input: bool,
32
+ pub text_edit_focused: bool,
33
+ }
34
+
35
+ impl EguiFrameOutput {
36
+ pub fn response(&self, id: u32) -> UiResponse {
37
+ self.responses.get(&id).cloned().unwrap_or_default()
38
+ }
39
+ }
40
+
41
+ impl EguiUi {
42
+ pub fn set_native_pixels_per_point(&mut self, pixels_per_point: f32) {
43
+ if pixels_per_point.is_finite() && pixels_per_point > 0.0 {
44
+ self.native_pixels_per_point = Some(pixels_per_point);
45
+ }
46
+ }
47
+
48
+ pub fn run_frame(
49
+ &mut self,
50
+ commands: &[UiCommand],
51
+ input: UiInputSnapshot,
52
+ screen_rect: [f32; 4],
53
+ dt: f64,
54
+ ) -> EguiFrameOutput {
55
+ self.elapsed_seconds += if dt.is_finite() {
56
+ dt.clamp(0.0, 0.25)
57
+ } else {
58
+ 1.0 / 60.0
59
+ };
60
+
61
+ let commands: Vec<_> = commands
62
+ .iter()
63
+ .filter(|command| command.backend == UiBackend::Egui)
64
+ .cloned()
65
+ .collect();
66
+ self.register_fonts(&commands);
67
+ let pairs = container_pairs(&commands);
68
+ let mut positions = HashMap::new();
69
+ let mut sizes = HashMap::new();
70
+ for command in &commands {
71
+ match command.opcode {
72
+ UiOpcode::SetWindowPosition => {
73
+ if let (Some(x), Some(y)) =
74
+ (finite_f32(command.args[0]), finite_f32(command.args[1]))
75
+ {
76
+ positions.insert(command.id, Pos2::new(x, y));
77
+ }
78
+ }
79
+ UiOpcode::SetWindowSize => {
80
+ if let (Some(width), Some(height)) =
81
+ (finite_f32(command.args[0]), finite_f32(command.args[1]))
82
+ {
83
+ sizes.insert(command.id, Vec2::new(width.max(32.0), height.max(32.0)));
84
+ }
85
+ }
86
+ _ => {}
87
+ }
88
+ }
89
+
90
+ let modifiers = to_egui_modifiers(input.modifiers);
91
+ let mut events = Vec::new();
92
+ if let Some([x, y]) = input.pointer_position {
93
+ if x.is_finite() && y.is_finite() {
94
+ let position = Pos2::new(x as f32, y as f32);
95
+ events.push(Event::PointerMoved(position));
96
+ if input.pointer_delta[0].is_finite() && input.pointer_delta[1].is_finite() {
97
+ let delta =
98
+ Vec2::new(input.pointer_delta[0] as f32, input.pointer_delta[1] as f32);
99
+ if delta != Vec2::ZERO {
100
+ events.push(Event::MouseMoved(delta));
101
+ }
102
+ }
103
+ for button_event in &input.pointer_buttons {
104
+ if let Some(button) = pointer_button(button_event.button) {
105
+ events.push(Event::PointerButton {
106
+ pos: position,
107
+ button,
108
+ pressed: button_event.pressed,
109
+ modifiers,
110
+ });
111
+ }
112
+ }
113
+ }
114
+ }
115
+ if input.scroll_x.is_finite() || input.scroll_y.is_finite() {
116
+ let delta = Vec2::new(
117
+ if input.scroll_x.is_finite() {
118
+ input.scroll_x as f32
119
+ } else {
120
+ 0.0
121
+ },
122
+ if input.scroll_y.is_finite() {
123
+ input.scroll_y as f32
124
+ } else {
125
+ 0.0
126
+ },
127
+ );
128
+ if delta != Vec2::ZERO {
129
+ events.push(Event::MouseWheel {
130
+ unit: MouseWheelUnit::Line,
131
+ delta,
132
+ phase: TouchPhase::Move,
133
+ modifiers,
134
+ });
135
+ }
136
+ }
137
+ for event in input.ordered_key_text_events() {
138
+ match event {
139
+ UiInputEvent::Key(key_event) => {
140
+ if let Some(key) = key_from_bloom(key_event.key) {
141
+ events.push(Event::Key {
142
+ key,
143
+ physical_key: None,
144
+ pressed: key_event.pressed,
145
+ repeat: key_event.repeated,
146
+ modifiers,
147
+ });
148
+ }
149
+ }
150
+ UiInputEvent::Text(text) if !text.is_empty() => events.push(Event::Text(text)),
151
+ UiInputEvent::Text(_) => {}
152
+ }
153
+ }
154
+
155
+ let mut next_active_touches = HashSet::new();
156
+ for touch in input.touches {
157
+ if !touch.x.is_finite() || !touch.y.is_finite() {
158
+ continue;
159
+ }
160
+ let position = Pos2::new(touch.x as f32, touch.y as f32);
161
+ let was_active = self.active_touches.contains(&touch.slot);
162
+ let phase = if touch.released || !touch.active {
163
+ TouchPhase::End
164
+ } else if was_active {
165
+ TouchPhase::Move
166
+ } else {
167
+ TouchPhase::Start
168
+ };
169
+ events.push(Event::Touch {
170
+ device_id: TouchDeviceId(0),
171
+ id: TouchId(touch.slot as u64),
172
+ phase,
173
+ pos: position,
174
+ force: None,
175
+ });
176
+ if phase == TouchPhase::Start {
177
+ events.push(Event::PointerButton {
178
+ pos: position,
179
+ button: PointerButton::Primary,
180
+ pressed: true,
181
+ modifiers,
182
+ });
183
+ } else if phase == TouchPhase::End {
184
+ events.push(Event::PointerButton {
185
+ pos: position,
186
+ button: PointerButton::Primary,
187
+ pressed: false,
188
+ modifiers,
189
+ });
190
+ } else {
191
+ events.push(Event::PointerMoved(position));
192
+ }
193
+ if phase != TouchPhase::End && phase != TouchPhase::Cancel {
194
+ next_active_touches.insert(touch.slot);
195
+ }
196
+ }
197
+ self.active_touches = next_active_touches;
198
+
199
+ let [x, y, width, height] = screen_rect;
200
+ let mut raw_input = RawInput {
201
+ screen_rect: Some(Rect::from_min_size(
202
+ Pos2::new(x, y),
203
+ Vec2::new(width.max(0.0), height.max(0.0)),
204
+ )),
205
+ time: Some(self.elapsed_seconds),
206
+ predicted_dt: dt as f32,
207
+ modifiers,
208
+ events,
209
+ ..Default::default()
210
+ };
211
+ if let Some(root_viewport) = raw_input.viewports.get_mut(&egui::ViewportId::ROOT) {
212
+ root_viewport.native_pixels_per_point = self.native_pixels_per_point;
213
+ }
214
+
215
+ let mut state = EvalState {
216
+ commands: &commands,
217
+ pairs,
218
+ positions,
219
+ sizes,
220
+ default_window_size: Vec2::new(
221
+ (screen_rect[2].min(372.0).max(128.0) - 32.0).max(96.0),
222
+ (screen_rect[3].min(452.0).max(128.0) - 32.0).max(96.0),
223
+ ),
224
+ responses: HashMap::new(),
225
+ registered_textures: HashMap::new(),
226
+ active_tabs: &mut self.active_tabs,
227
+ custom_font_names: self
228
+ .custom_fonts
229
+ .iter()
230
+ .map(|(&id, (name, _))| (id, name.clone()))
231
+ .collect(),
232
+ text_edit_focused: false,
233
+ };
234
+ let context = self.context.clone();
235
+ let full_output = context.run_ui(raw_input, |ui| {
236
+ state.render_range(ui, 0, state.commands.len());
237
+ });
238
+ let paint_jobs = context.tessellate(full_output.shapes, full_output.pixels_per_point);
239
+
240
+ EguiFrameOutput {
241
+ paint_jobs,
242
+ textures_delta: full_output.textures_delta,
243
+ platform_output: full_output.platform_output,
244
+ pixels_per_point: full_output.pixels_per_point,
245
+ responses: state.responses,
246
+ registered_textures: state.registered_textures,
247
+ wants_pointer_input: context.egui_wants_pointer_input(),
248
+ wants_keyboard_input: context.egui_wants_keyboard_input(),
249
+ text_edit_focused: state.text_edit_focused,
250
+ }
251
+ }
252
+
253
+ fn register_fonts(&mut self, commands: &[UiCommand]) {
254
+ let mut changed = false;
255
+ for command in commands
256
+ .iter()
257
+ .filter(|command| command.opcode == UiOpcode::LoadFont)
258
+ {
259
+ if command.scratch.is_empty() || command.scratch.len() > 1_048_576 {
260
+ continue;
261
+ }
262
+ let Some(bytes) = command
263
+ .scratch
264
+ .iter()
265
+ .map(|value| {
266
+ if value.is_finite() && *value >= 0.0 && *value <= 255.0 && value.fract() == 0.0
267
+ {
268
+ Some(*value as u8)
269
+ } else {
270
+ None
271
+ }
272
+ })
273
+ .collect::<Option<Vec<_>>>()
274
+ else {
275
+ continue;
276
+ };
277
+ let name = if command.text.trim().is_empty() {
278
+ format!("bornengine-ui-font-{}", command.id)
279
+ } else {
280
+ command.text.clone()
281
+ };
282
+ self.custom_fonts.insert(
283
+ command.id,
284
+ (name, Arc::new(egui::FontData::from_owned(bytes))),
285
+ );
286
+ changed = true;
287
+ }
288
+
289
+ if changed {
290
+ let mut definitions = egui::FontDefinitions::default();
291
+ for (name, font_data) in self.custom_fonts.values() {
292
+ definitions
293
+ .font_data
294
+ .insert(name.clone(), font_data.clone());
295
+ let family = egui::FontFamily::Name(Arc::<str>::from(name.as_str()));
296
+ definitions.families.insert(family, vec![name.clone()]);
297
+ }
298
+ self.context.set_fonts(definitions);
299
+ }
300
+ }
301
+ }
302
+
303
+ struct EvalState<'a> {
304
+ commands: &'a [UiCommand],
305
+ pairs: HashMap<usize, usize>,
306
+ positions: HashMap<u32, Pos2>,
307
+ sizes: HashMap<u32, Vec2>,
308
+ default_window_size: Vec2,
309
+ responses: HashMap<u32, UiResponse>,
310
+ registered_textures: HashMap<u32, u64>,
311
+ active_tabs: &'a mut HashMap<u32, u32>,
312
+ custom_font_names: HashMap<u32, String>,
313
+ text_edit_focused: bool,
314
+ }
315
+
316
+ impl EvalState<'_> {
317
+ fn render_range(&mut self, ui: &mut Ui, start: usize, end: usize) {
318
+ let mut index = start;
319
+ while index < end {
320
+ let command = &self.commands[index];
321
+ let pair = self
322
+ .pairs
323
+ .get(&index)
324
+ .copied()
325
+ .filter(|pair_end| *pair_end < end);
326
+ match command.opcode {
327
+ UiOpcode::BeginWindow if pair.is_some() => {
328
+ let close = pair.unwrap();
329
+ let window_id = command.id;
330
+ let position = self
331
+ .positions
332
+ .get(&command.id)
333
+ .copied()
334
+ .unwrap_or(Pos2::new(16.0, 16.0));
335
+ let size = self
336
+ .sizes
337
+ .get(&command.id)
338
+ .copied()
339
+ .unwrap_or(self.default_window_size);
340
+ let title = command.text.clone();
341
+ let bounds = Rect::from_min_size(position, size);
342
+ let layout = egui::Layout::top_down(egui::Align::Min);
343
+ ui.scope_builder(
344
+ egui::UiBuilder::new().max_rect(bounds).layout(layout),
345
+ |window_ui| {
346
+ window_ui.push_id(("bornengine-ui-window", window_id), |window_ui| {
347
+ egui::Frame::window(window_ui.style()).show(window_ui, |content| {
348
+ content
349
+ .set_min_size((size - Vec2::splat(16.0)).max(Vec2::ZERO));
350
+ content.label(title);
351
+ content.separator();
352
+ self.render_range(content, index + 1, close);
353
+ });
354
+ });
355
+ },
356
+ );
357
+ index = close + 1;
358
+ }
359
+ UiOpcode::BeginPanel if pair.is_some() => {
360
+ let close = pair.unwrap();
361
+ let id = command.id;
362
+ ui.push_id(("bornengine-ui-panel", id), |inner| {
363
+ inner.group(|inner| self.render_range(inner, index + 1, close));
364
+ });
365
+ index = close + 1;
366
+ }
367
+ UiOpcode::BeginHorizontal if pair.is_some() => {
368
+ let close = pair.unwrap();
369
+ let id = command.id;
370
+ ui.push_id(("bornengine-ui-horizontal", id), |inner| {
371
+ inner.horizontal(|inner| self.render_range(inner, index + 1, close));
372
+ });
373
+ index = close + 1;
374
+ }
375
+ UiOpcode::BeginVertical if pair.is_some() => {
376
+ let close = pair.unwrap();
377
+ let id = command.id;
378
+ ui.push_id(("bornengine-ui-vertical", id), |inner| {
379
+ inner.vertical(|inner| self.render_range(inner, index + 1, close));
380
+ });
381
+ index = close + 1;
382
+ }
383
+ UiOpcode::BeginScrollArea if pair.is_some() => {
384
+ let close = pair.unwrap();
385
+ let area =
386
+ egui::ScrollArea::vertical().id_salt(("bornengine-ui-scroll", command.id));
387
+ area.show(ui, |inner| self.render_range(inner, index + 1, close));
388
+ index = close + 1;
389
+ }
390
+ UiOpcode::BeginTabBar if pair.is_some() => {
391
+ let close = pair.unwrap();
392
+ self.render_tab_bar(ui, index, close, command.id);
393
+ index = close + 1;
394
+ }
395
+ UiOpcode::Combo if pair.is_some() => {
396
+ let close = pair.unwrap();
397
+ let selected = command.text.clone();
398
+ let combo_id = command.id;
399
+ let result = egui::ComboBox::from_id_salt(("bornengine-ui-combo", combo_id))
400
+ .selected_text(selected)
401
+ .show_ui(ui, |inner| self.render_range(inner, index + 1, close));
402
+ let response = result.response;
403
+ self.store_response(combo_id, &response, 0.0, String::new());
404
+ index = close + 1;
405
+ }
406
+ UiOpcode::BeginMenuBar if pair.is_some() => {
407
+ let close = pair.unwrap();
408
+ let id = command.id;
409
+ ui.push_id(("bornengine-ui-menubar", id), |inner| {
410
+ inner.horizontal(|inner| self.render_range(inner, index + 1, close));
411
+ });
412
+ index = close + 1;
413
+ }
414
+ UiOpcode::BeginMenu if pair.is_some() => {
415
+ let close = pair.unwrap();
416
+ let text = command.text.clone();
417
+ let id = command.id;
418
+ let response = ui
419
+ .push_id(("bornengine-ui-menu", id), |ui| {
420
+ ui.menu_button(text, |inner| self.render_range(inner, index + 1, close))
421
+ })
422
+ .inner;
423
+ self.store_response(id, &response.response, 0.0, String::new());
424
+ index = close + 1;
425
+ }
426
+ UiOpcode::BeginTable if pair.is_some() => {
427
+ let close = pair.unwrap();
428
+ let id = command.id;
429
+ let columns = finite_usize(command.args[0]).unwrap_or(1).clamp(1, 64);
430
+ egui::Grid::new(("bornengine-ui-table", id))
431
+ .num_columns(columns)
432
+ .striped(true)
433
+ .show(ui, |inner| self.render_range(inner, index + 1, close));
434
+ index = close + 1;
435
+ }
436
+ UiOpcode::CollapsingHeader if pair.is_some() => {
437
+ let close = pair.unwrap();
438
+ let id = command.id;
439
+ let text = command.text.clone();
440
+ let result = egui::CollapsingHeader::new(text)
441
+ .id_salt(("bornengine-ui-collapse", id))
442
+ .show(ui, |inner| self.render_range(inner, index + 1, close));
443
+ self.store_response(
444
+ id,
445
+ &result.header_response,
446
+ if result.fully_open() { 1.0 } else { 0.0 },
447
+ String::new(),
448
+ );
449
+ index = close + 1;
450
+ }
451
+ UiOpcode::TreeNode if pair.is_some() => {
452
+ let close = pair.unwrap();
453
+ let id = command.id;
454
+ let text = command.text.clone();
455
+ let result = egui::CollapsingHeader::new(text)
456
+ .id_salt(("bornengine-ui-tree", id))
457
+ .show(ui, |inner| self.render_range(inner, index + 1, close));
458
+ self.store_response(
459
+ id,
460
+ &result.header_response,
461
+ if result.fully_open() { 1.0 } else { 0.0 },
462
+ String::new(),
463
+ );
464
+ index = close + 1;
465
+ }
466
+ UiOpcode::Spacing => {
467
+ let amount = finite_f32(command.args[0]).unwrap_or(8.0).max(0.0);
468
+ ui.add_space(amount);
469
+ index += 1;
470
+ }
471
+ UiOpcode::Separator => {
472
+ ui.separator();
473
+ index += 1;
474
+ }
475
+ UiOpcode::Label => {
476
+ ui.label(command.text.as_str());
477
+ index += 1;
478
+ }
479
+ UiOpcode::Link => {
480
+ let response =
481
+ widget_response(ui, command.id, |ui| ui.link(command.text.as_str()));
482
+ self.store_response(command.id, &response, 0.0, String::new());
483
+ index += 1;
484
+ }
485
+ UiOpcode::Button => {
486
+ let response =
487
+ widget_response(ui, command.id, |ui| ui.button(command.text.as_str()));
488
+ self.store_response(command.id, &response, 0.0, String::new());
489
+ index += 1;
490
+ }
491
+ UiOpcode::Checkbox => {
492
+ let mut value = command.args[0] != 0.0;
493
+ let response = widget_response(ui, command.id, |ui| {
494
+ ui.checkbox(&mut value, command.text.as_str())
495
+ });
496
+ self.store_response(
497
+ command.id,
498
+ &response,
499
+ if value { 1.0 } else { 0.0 },
500
+ String::new(),
501
+ );
502
+ index += 1;
503
+ }
504
+ UiOpcode::RadioButton => {
505
+ let response = widget_response(ui, command.id, |ui| {
506
+ ui.radio(command.args[0] != 0.0, command.text.as_str())
507
+ });
508
+ self.store_response(
509
+ command.id,
510
+ &response,
511
+ if response.clicked() { 1.0 } else { 0.0 },
512
+ String::new(),
513
+ );
514
+ index += 1;
515
+ }
516
+ UiOpcode::SliderFloat => {
517
+ let mut value = finite_f64(command.args[0]).unwrap_or(0.0);
518
+ let min = finite_f64(command.args[1]).unwrap_or(0.0);
519
+ let max = finite_f64(command.args[2]).unwrap_or(1.0);
520
+ let range = ordered_range(min, max);
521
+ let response = widget_response(ui, command.id, |ui| {
522
+ ui.add(egui::Slider::new(&mut value, range).text(command.text.as_str()))
523
+ });
524
+ self.store_response(command.id, &response, value, String::new());
525
+ index += 1;
526
+ }
527
+ UiOpcode::SliderInt => {
528
+ let mut value = finite_f64(command.args[0]).unwrap_or(0.0).round() as i32;
529
+ let min = finite_f64(command.args[1]).unwrap_or(0.0).round() as i32;
530
+ let max = finite_f64(command.args[2]).unwrap_or(100.0).round() as i32;
531
+ let response = widget_response(ui, command.id, |ui| {
532
+ ui.add(
533
+ egui::Slider::new(&mut value, min.min(max)..=min.max(max))
534
+ .text(command.text.as_str()),
535
+ )
536
+ });
537
+ self.store_response(command.id, &response, f64::from(value), String::new());
538
+ index += 1;
539
+ }
540
+ UiOpcode::DragFloat => {
541
+ let mut value = finite_f64(command.args[0]).unwrap_or(0.0);
542
+ let speed = finite_f64(command.args[1])
543
+ .unwrap_or(0.1)
544
+ .abs()
545
+ .max(0.000_001);
546
+ let mut drag = egui::DragValue::new(&mut value)
547
+ .speed(speed)
548
+ .prefix(command.text.as_str());
549
+ if command.args[2].is_finite() && command.args[3].is_finite() {
550
+ drag = drag.range(ordered_range(command.args[2], command.args[3]));
551
+ }
552
+ let response = widget_response(ui, command.id, |ui| ui.add(drag));
553
+ self.store_response(command.id, &response, value, String::new());
554
+ index += 1;
555
+ }
556
+ UiOpcode::TextEdit => {
557
+ let mut text = command.text.clone();
558
+ let response = widget_response(ui, command.id, |ui| {
559
+ ui.add(
560
+ egui::TextEdit::singleline(&mut text)
561
+ .id_salt(("bornengine-ui-text", command.id)),
562
+ )
563
+ });
564
+ self.text_edit_focused |= response.has_focus();
565
+ self.store_response(command.id, &response, 0.0, text);
566
+ index += 1;
567
+ }
568
+ UiOpcode::Selectable => {
569
+ let selected = command.args[0] != 0.0;
570
+ let response = widget_response(ui, command.id, |ui| {
571
+ ui.selectable_label(selected, command.text.as_str())
572
+ });
573
+ self.store_response(
574
+ command.id,
575
+ &response,
576
+ if response.clicked() { 1.0 } else { 0.0 },
577
+ String::new(),
578
+ );
579
+ index += 1;
580
+ }
581
+ UiOpcode::ProgressBar => {
582
+ let fraction = finite_f32(command.args[0]).unwrap_or(0.0).clamp(0.0, 1.0);
583
+ let mut progress = egui::ProgressBar::new(fraction);
584
+ if !command.text.is_empty() {
585
+ progress = progress.text(command.text.as_str());
586
+ }
587
+ let response = widget_response(ui, command.id, |ui| ui.add(progress));
588
+ self.store_response(command.id, &response, f64::from(fraction), String::new());
589
+ index += 1;
590
+ }
591
+ UiOpcode::Image => {
592
+ if let (Some(handle), Some(width), Some(height)) = (
593
+ finite_u64(command.args[0]),
594
+ finite_f32(command.args[1]),
595
+ finite_f32(command.args[2]),
596
+ ) {
597
+ let response = widget_response(ui, command.id, |ui| {
598
+ ui.add(egui::Image::new((
599
+ egui::TextureId::User(handle),
600
+ Vec2::new(width.max(0.0), height.max(0.0)),
601
+ )))
602
+ });
603
+ self.store_response(command.id, &response, handle as f64, String::new());
604
+ } else {
605
+ self.responses.insert(command.id, UiResponse::default());
606
+ }
607
+ index += 1;
608
+ }
609
+ UiOpcode::PaintLine
610
+ | UiOpcode::PaintRect
611
+ | UiOpcode::PaintCircle
612
+ | UiOpcode::PaintText
613
+ | UiOpcode::PaintPolyline
614
+ | UiOpcode::PaintPolygon => {
615
+ self.paint(ui, command);
616
+ index += 1;
617
+ }
618
+ UiOpcode::SetStyle => {
619
+ let theme = finite_usize(command.args[0]).unwrap_or(0);
620
+ match theme {
621
+ 1 => ui.ctx().set_visuals(egui::Visuals::light()),
622
+ 0 => ui.ctx().set_visuals(egui::Visuals::dark()),
623
+ _ => {}
624
+ }
625
+ if let Some(font_id) = finite_usize(command.args[1])
626
+ .and_then(|font_id| self.custom_font_names.get(&(font_id as u32)))
627
+ {
628
+ let family = egui::FontFamily::Name(Arc::<str>::from(font_id.as_str()));
629
+ ui.ctx().all_styles_mut(|style| {
630
+ for font in style.text_styles.values_mut() {
631
+ font.family = family.clone();
632
+ }
633
+ });
634
+ }
635
+ index += 1;
636
+ }
637
+ UiOpcode::LoadFont => {
638
+ index += 1;
639
+ }
640
+ UiOpcode::RegisterTexture => {
641
+ if let Some(handle) = finite_u64(command.args[0]) {
642
+ self.registered_textures.insert(command.id, handle);
643
+ } else {
644
+ self.registered_textures.remove(&command.id);
645
+ }
646
+ index += 1;
647
+ }
648
+ UiOpcode::MenuItem => {
649
+ let response =
650
+ widget_response(ui, command.id, |ui| ui.button(command.text.as_str()));
651
+ self.store_response(
652
+ command.id,
653
+ &response,
654
+ if response.clicked() { 1.0 } else { 0.0 },
655
+ String::new(),
656
+ );
657
+ index += 1;
658
+ }
659
+ UiOpcode::BeginTabItem
660
+ | UiOpcode::EndWindow
661
+ | UiOpcode::EndPanel
662
+ | UiOpcode::EndHorizontal
663
+ | UiOpcode::EndVertical
664
+ | UiOpcode::EndScrollArea
665
+ | UiOpcode::EndTabBar
666
+ | UiOpcode::EndTabItem
667
+ | UiOpcode::EndCombo
668
+ | UiOpcode::EndCollapsingHeader
669
+ | UiOpcode::EndMenuBar
670
+ | UiOpcode::EndMenu
671
+ | UiOpcode::TreePop
672
+ | UiOpcode::EndTable
673
+ | UiOpcode::TableNextColumn
674
+ | UiOpcode::DemoWindow
675
+ | UiOpcode::MetricsWindow
676
+ | UiOpcode::SetWindowPosition
677
+ | UiOpcode::SetWindowSize
678
+ | UiOpcode::TableNextRow => {
679
+ if command.opcode == UiOpcode::TableNextRow {
680
+ ui.end_row();
681
+ } else if command.opcode == UiOpcode::DemoWindow {
682
+ egui::Window::new("egui demo")
683
+ .id(Id::new(("bornengine-ui-demo", command.id)))
684
+ .show(ui.ctx(), |inner| {
685
+ inner.label("BornEngine egui controls");
686
+ inner.label("The demo window is available in every egui build.");
687
+ });
688
+ } else if command.opcode == UiOpcode::MetricsWindow {
689
+ egui::Window::new("egui metrics")
690
+ .id(Id::new(("bornengine-ui-metrics", command.id)))
691
+ .show(ui.ctx(), |inner| {
692
+ inner.label(format!("widgets: {}", self.responses.len()));
693
+ inner.label(format!(
694
+ "shapes: {}",
695
+ inner.ctx().cumulative_frame_nr()
696
+ ));
697
+ });
698
+ }
699
+ index += 1;
700
+ }
701
+ _ => {
702
+ index += 1;
703
+ }
704
+ }
705
+ }
706
+ }
707
+
708
+ fn render_tab_bar(&mut self, ui: &mut Ui, start: usize, end: usize, id: u32) {
709
+ let mut tab_items = Vec::new();
710
+ let mut cursor = start + 1;
711
+ while cursor < end {
712
+ if self.commands[cursor].opcode == UiOpcode::BeginTabItem {
713
+ if let Some(close) = self
714
+ .pairs
715
+ .get(&cursor)
716
+ .copied()
717
+ .filter(|close| *close < end)
718
+ {
719
+ tab_items.push((cursor, close));
720
+ cursor = close + 1;
721
+ continue;
722
+ }
723
+ }
724
+ if let Some(close) = self
725
+ .pairs
726
+ .get(&cursor)
727
+ .copied()
728
+ .filter(|close| *close < end)
729
+ {
730
+ cursor = close + 1;
731
+ } else {
732
+ cursor += 1;
733
+ }
734
+ }
735
+ if tab_items.is_empty() {
736
+ return;
737
+ }
738
+
739
+ let current = self
740
+ .active_tabs
741
+ .entry(id)
742
+ .or_insert(self.commands[tab_items[0].0].id);
743
+ let mut selected = *current;
744
+ ui.horizontal(|ui| {
745
+ for (item_start, _) in &tab_items {
746
+ let command = &self.commands[*item_start];
747
+ let response = widget_response(ui, command.id, |ui| {
748
+ ui.selectable_label(selected == command.id, command.text.as_str())
749
+ });
750
+ self.store_response(
751
+ command.id,
752
+ &response,
753
+ if selected == command.id { 1.0 } else { 0.0 },
754
+ String::new(),
755
+ );
756
+ if response.clicked() {
757
+ selected = command.id;
758
+ }
759
+ }
760
+ });
761
+ self.active_tabs.insert(id, selected);
762
+ if let Some((item_start, item_end)) = tab_items
763
+ .iter()
764
+ .copied()
765
+ .find(|(item_start, _)| self.commands[*item_start].id == selected)
766
+ {
767
+ ui.separator();
768
+ self.render_range(ui, item_start + 1, item_end);
769
+ }
770
+ }
771
+
772
+ fn paint(&mut self, ui: &mut Ui, command: &UiCommand) {
773
+ let origin = ui.min_rect().min;
774
+ let values = &command.scratch;
775
+ let default_color = Color32::WHITE;
776
+ let coordinate = |index: usize| finite_f32(values.get(index).copied().unwrap_or(f64::NAN));
777
+ let color = |start: usize| {
778
+ if values.len() >= start + 4 {
779
+ color_from_f64(
780
+ values[start],
781
+ values[start + 1],
782
+ values[start + 2],
783
+ values[start + 3],
784
+ )
785
+ } else {
786
+ default_color
787
+ }
788
+ };
789
+ match command.opcode {
790
+ UiOpcode::PaintLine if values.len() >= 4 => {
791
+ if let (Some(x1), Some(y1), Some(x2), Some(y2)) =
792
+ (coordinate(0), coordinate(1), coordinate(2), coordinate(3))
793
+ {
794
+ let stroke = Stroke::new(
795
+ finite_f32(values.get(8).copied().unwrap_or(1.0))
796
+ .unwrap_or(1.0)
797
+ .max(0.1),
798
+ color(4),
799
+ );
800
+ ui.painter().line_segment(
801
+ [origin + Vec2::new(x1, y1), origin + Vec2::new(x2, y2)],
802
+ stroke,
803
+ );
804
+ }
805
+ }
806
+ UiOpcode::PaintRect if values.len() >= 4 => {
807
+ if let (Some(x), Some(y), Some(width), Some(height)) =
808
+ (coordinate(0), coordinate(1), coordinate(2), coordinate(3))
809
+ {
810
+ let rect = Rect::from_min_size(
811
+ origin + Vec2::new(x, y),
812
+ Vec2::new(width.max(0.0), height.max(0.0)),
813
+ );
814
+ ui.painter()
815
+ .rect_filled(rect, egui::CornerRadius::ZERO, color(4));
816
+ }
817
+ }
818
+ UiOpcode::PaintCircle if values.len() >= 3 => {
819
+ if let (Some(x), Some(y), Some(radius)) =
820
+ (coordinate(0), coordinate(1), coordinate(2))
821
+ {
822
+ let stroke = Stroke::new(
823
+ finite_f32(values.get(7).copied().unwrap_or(1.0))
824
+ .unwrap_or(1.0)
825
+ .max(0.1),
826
+ color(3),
827
+ );
828
+ ui.painter()
829
+ .circle_stroke(origin + Vec2::new(x, y), radius.max(0.0), stroke);
830
+ }
831
+ }
832
+ UiOpcode::PaintText => {
833
+ if let (Some(x), Some(y)) = (coordinate(0), coordinate(1)) {
834
+ let size = finite_f32(values.get(2).copied().unwrap_or(16.0))
835
+ .unwrap_or(16.0)
836
+ .max(1.0);
837
+ ui.painter().text(
838
+ origin + Vec2::new(x, y),
839
+ Align2::LEFT_TOP,
840
+ command.text.as_str(),
841
+ egui::FontId::proportional(size),
842
+ color(3),
843
+ );
844
+ }
845
+ }
846
+ UiOpcode::PaintPolyline | UiOpcode::PaintPolygon => {
847
+ let coordinate_count = values.len().saturating_sub(5) & !1;
848
+ let points: Vec<_> = values[..coordinate_count]
849
+ .chunks_exact(2)
850
+ .filter_map(|point| {
851
+ Some(origin + Vec2::new(finite_f32(point[0])?, finite_f32(point[1])?))
852
+ })
853
+ .collect();
854
+ if points.len() >= 2 {
855
+ let stroke = Stroke::new(
856
+ finite_f32(values.get(coordinate_count + 4).copied().unwrap_or(1.0))
857
+ .unwrap_or(1.0)
858
+ .max(0.1),
859
+ color(coordinate_count),
860
+ );
861
+ if command.opcode == UiOpcode::PaintPolygon {
862
+ ui.painter().add(Shape::convex_polygon(
863
+ points,
864
+ Color32::TRANSPARENT,
865
+ stroke,
866
+ ));
867
+ } else {
868
+ ui.painter().line(points, stroke);
869
+ }
870
+ }
871
+ }
872
+ _ => {}
873
+ }
874
+ }
875
+
876
+ fn store_response(&mut self, id: u32, response: &egui::Response, value: f64, text: String) {
877
+ self.responses.insert(
878
+ id,
879
+ UiResponse {
880
+ clicked: response.clicked(),
881
+ changed: response.changed(),
882
+ hovered: response.hovered(),
883
+ focused: response.has_focus(),
884
+ dragged: response.dragged(),
885
+ value,
886
+ text,
887
+ },
888
+ );
889
+ }
890
+ }
891
+
892
+ fn container_pairs(commands: &[UiCommand]) -> HashMap<usize, usize> {
893
+ let mut stack: Vec<(usize, UiOpcode)> = Vec::new();
894
+ let mut pairs = HashMap::new();
895
+ for (index, command) in commands.iter().enumerate() {
896
+ if let Some(end) = matching_end_opcode(command.opcode) {
897
+ stack.push((index, end));
898
+ } else if let Some((start, expected)) = stack.last().copied() {
899
+ if expected == command.opcode {
900
+ stack.pop();
901
+ pairs.insert(start, index);
902
+ }
903
+ }
904
+ }
905
+ pairs
906
+ }
907
+
908
+ fn matching_end_opcode(opcode: UiOpcode) -> Option<UiOpcode> {
909
+ Some(match opcode {
910
+ UiOpcode::BeginWindow => UiOpcode::EndWindow,
911
+ UiOpcode::BeginPanel => UiOpcode::EndPanel,
912
+ UiOpcode::BeginHorizontal => UiOpcode::EndHorizontal,
913
+ UiOpcode::BeginVertical => UiOpcode::EndVertical,
914
+ UiOpcode::BeginScrollArea => UiOpcode::EndScrollArea,
915
+ UiOpcode::BeginTabBar => UiOpcode::EndTabBar,
916
+ UiOpcode::BeginTabItem => UiOpcode::EndTabItem,
917
+ UiOpcode::Combo => UiOpcode::EndCombo,
918
+ UiOpcode::BeginMenuBar => UiOpcode::EndMenuBar,
919
+ UiOpcode::BeginMenu => UiOpcode::EndMenu,
920
+ UiOpcode::BeginTable => UiOpcode::EndTable,
921
+ UiOpcode::CollapsingHeader => UiOpcode::EndCollapsingHeader,
922
+ UiOpcode::TreeNode => UiOpcode::TreePop,
923
+ _ => return None,
924
+ })
925
+ }
926
+
927
+ fn pointer_button(button: u8) -> Option<PointerButton> {
928
+ match button {
929
+ 0 => Some(PointerButton::Primary),
930
+ 1 => Some(PointerButton::Secondary),
931
+ 2 => Some(PointerButton::Middle),
932
+ 3 => Some(PointerButton::Extra1),
933
+ 4 => Some(PointerButton::Extra2),
934
+ _ => None,
935
+ }
936
+ }
937
+
938
+ fn widget_response<R>(ui: &mut Ui, id: u32, add: impl FnOnce(&mut Ui) -> R) -> R {
939
+ ui.push_id(("bornengine-ui-widget", id), add).inner
940
+ }
941
+
942
+ fn key_from_bloom(code: u32) -> Option<Key> {
943
+ Some(match code {
944
+ 32 => Key::Space,
945
+ 8 => Key::Backspace,
946
+ 9 => Key::Tab,
947
+ 27 => Key::Escape,
948
+ 127 => Key::Delete,
949
+ 256 => Key::ArrowUp,
950
+ 257 => Key::ArrowDown,
951
+ 258 => Key::ArrowLeft,
952
+ 259 => Key::ArrowRight,
953
+ 260 => Key::Insert,
954
+ 261 => Key::Home,
955
+ 262 => Key::End,
956
+ 263 => Key::PageUp,
957
+ 264 => Key::PageDown,
958
+ 265 => Key::Enter,
959
+ 280 => Key::ShiftLeft,
960
+ 281 => Key::ShiftRight,
961
+ 282 => Key::ControlLeft,
962
+ 283 => Key::ControlRight,
963
+ 284 => Key::AltLeft,
964
+ 285 => Key::AltRight,
965
+ 286 => Key::SuperLeft,
966
+ 287 => Key::SuperRight,
967
+ 48..=57 => match code {
968
+ 48 => Key::Num0,
969
+ 49 => Key::Num1,
970
+ 50 => Key::Num2,
971
+ 51 => Key::Num3,
972
+ 52 => Key::Num4,
973
+ 53 => Key::Num5,
974
+ 54 => Key::Num6,
975
+ 55 => Key::Num7,
976
+ 56 => Key::Num8,
977
+ _ => Key::Num9,
978
+ },
979
+ 65..=90 => match code {
980
+ 65 => Key::A,
981
+ 66 => Key::B,
982
+ 67 => Key::C,
983
+ 68 => Key::D,
984
+ 69 => Key::E,
985
+ 70 => Key::F,
986
+ 71 => Key::G,
987
+ 72 => Key::H,
988
+ 73 => Key::I,
989
+ 74 => Key::J,
990
+ 75 => Key::K,
991
+ 76 => Key::L,
992
+ 77 => Key::M,
993
+ 78 => Key::N,
994
+ 79 => Key::O,
995
+ 80 => Key::P,
996
+ 81 => Key::Q,
997
+ 82 => Key::R,
998
+ 83 => Key::S,
999
+ 84 => Key::T,
1000
+ 85 => Key::U,
1001
+ 86 => Key::V,
1002
+ 87 => Key::W,
1003
+ 88 => Key::X,
1004
+ 89 => Key::Y,
1005
+ _ => Key::Z,
1006
+ },
1007
+ 112..=123 => match code {
1008
+ 112 => Key::F1,
1009
+ 113 => Key::F2,
1010
+ 114 => Key::F3,
1011
+ 115 => Key::F4,
1012
+ 116 => Key::F5,
1013
+ 117 => Key::F6,
1014
+ 118 => Key::F7,
1015
+ 119 => Key::F8,
1016
+ 120 => Key::F9,
1017
+ 121 => Key::F10,
1018
+ 122 => Key::F11,
1019
+ _ => Key::F12,
1020
+ },
1021
+ _ => return None,
1022
+ })
1023
+ }
1024
+
1025
+ fn to_egui_modifiers(modifiers: super::UiModifiers) -> Modifiers {
1026
+ Modifiers {
1027
+ alt: modifiers.alt,
1028
+ ctrl: modifiers.ctrl,
1029
+ shift: modifiers.shift,
1030
+ mac_cmd: modifiers.super_key,
1031
+ command: modifiers.super_key || modifiers.ctrl,
1032
+ }
1033
+ }
1034
+
1035
+ fn finite_f32(value: f64) -> Option<f32> {
1036
+ if value.is_finite() && value.abs() <= f32::MAX as f64 {
1037
+ Some(value as f32)
1038
+ } else {
1039
+ None
1040
+ }
1041
+ }
1042
+
1043
+ fn finite_f64(value: f64) -> Option<f64> {
1044
+ value.is_finite().then_some(value)
1045
+ }
1046
+ fn finite_u64(value: f64) -> Option<u64> {
1047
+ (value.is_finite() && value >= 0.0 && value.fract() == 0.0 && value <= u64::MAX as f64)
1048
+ .then_some(value as u64)
1049
+ }
1050
+ fn finite_usize(value: f64) -> Option<usize> {
1051
+ finite_u64(value).and_then(|value| usize::try_from(value).ok())
1052
+ }
1053
+ fn ordered_range(a: f64, b: f64) -> std::ops::RangeInclusive<f64> {
1054
+ a.min(b)..=a.max(b)
1055
+ }
1056
+ fn color_from_f64(r: f64, g: f64, b: f64, a: f64) -> Color32 {
1057
+ Color32::from_rgba_unmultiplied(
1058
+ color_channel(r),
1059
+ color_channel(g),
1060
+ color_channel(b),
1061
+ color_channel(a),
1062
+ )
1063
+ }
1064
+ #[cfg(test)]
1065
+ mod tests {
1066
+ use super::color_channel;
1067
+ use crate::ui::{
1068
+ EguiUi, UiBackend, UiCommand, UiInputEvent, UiInputSnapshot, UiKeyEvent, UiOpcode,
1069
+ UiPointerButtonEvent,
1070
+ };
1071
+
1072
+ #[test]
1073
+ fn paint_color_channels_use_engine_byte_range() {
1074
+ assert_eq!(color_channel(128.0), 128);
1075
+ assert_eq!(color_channel(255.0), 255);
1076
+ }
1077
+
1078
+ fn window(widget: UiCommand) -> Vec<UiCommand> {
1079
+ vec![
1080
+ UiCommand::new(
1081
+ UiBackend::Egui,
1082
+ UiOpcode::SetWindowPosition,
1083
+ 100,
1084
+ [16.0, 16.0, 0.0, 0.0],
1085
+ "",
1086
+ ),
1087
+ UiCommand::new(
1088
+ UiBackend::Egui,
1089
+ UiOpcode::BeginWindow,
1090
+ 100,
1091
+ [0.0; 4],
1092
+ "Settings",
1093
+ ),
1094
+ widget,
1095
+ UiCommand::new(UiBackend::Egui, UiOpcode::EndWindow, 100, [0.0; 4], ""),
1096
+ ]
1097
+ }
1098
+
1099
+ fn click_at(pos: [f64; 2]) -> UiInputSnapshot {
1100
+ UiInputSnapshot {
1101
+ pointer_position: Some(pos),
1102
+ pointer_buttons: vec![
1103
+ UiPointerButtonEvent {
1104
+ button: 0,
1105
+ pressed: true,
1106
+ },
1107
+ UiPointerButtonEvent {
1108
+ button: 0,
1109
+ pressed: false,
1110
+ },
1111
+ ],
1112
+ ..Default::default()
1113
+ }
1114
+ }
1115
+
1116
+ #[test]
1117
+ fn button_click_is_published_for_the_next_game_callback() {
1118
+ let mut ui = EguiUi::default();
1119
+ let commands = window(UiCommand::new(
1120
+ UiBackend::Egui,
1121
+ UiOpcode::Button,
1122
+ 7,
1123
+ [0.0; 4],
1124
+ "Continue",
1125
+ ));
1126
+
1127
+ ui.run_frame(
1128
+ &commands,
1129
+ UiInputSnapshot::default(),
1130
+ [0.0, 0.0, 320.0, 240.0],
1131
+ 1.0 / 60.0,
1132
+ );
1133
+ let clicked = ui.run_frame(
1134
+ &commands,
1135
+ click_at([55.0, 62.0]),
1136
+ [0.0, 0.0, 320.0, 240.0],
1137
+ 1.0 / 60.0,
1138
+ );
1139
+ assert!(clicked.response(7).clicked);
1140
+ }
1141
+
1142
+ #[test]
1143
+ fn slider_response_carries_the_value_and_stale_ids_are_removed() {
1144
+ let mut ui = EguiUi::default();
1145
+ let slider = UiCommand::new(
1146
+ UiBackend::Egui,
1147
+ UiOpcode::SliderFloat,
1148
+ 11,
1149
+ [0.75, 0.0, 1.0, 0.0],
1150
+ "Gain",
1151
+ );
1152
+ let first = ui.run_frame(
1153
+ &window(slider.clone()),
1154
+ UiInputSnapshot::default(),
1155
+ [0.0, 0.0, 320.0, 240.0],
1156
+ 1.0 / 60.0,
1157
+ );
1158
+ assert_eq!(first.response(11).value, 0.75);
1159
+
1160
+ let second = ui.run_frame(
1161
+ &window(slider),
1162
+ UiInputSnapshot::default(),
1163
+ [0.0, 0.0, 320.0, 240.0],
1164
+ 1.0 / 60.0,
1165
+ );
1166
+ assert_eq!(second.response(11).value, 0.75);
1167
+
1168
+ let missing = ui.run_frame(
1169
+ &[],
1170
+ UiInputSnapshot::default(),
1171
+ [0.0, 0.0, 320.0, 240.0],
1172
+ 1.0 / 60.0,
1173
+ );
1174
+ assert_eq!(missing.response(11), Default::default());
1175
+ }
1176
+
1177
+ #[test]
1178
+ fn text_edit_accepts_a_whole_unicode_input_event() {
1179
+ let mut ui = EguiUi::default();
1180
+ let edit = UiCommand::new(UiBackend::Egui, UiOpcode::TextEdit, 12, [0.0; 4], "");
1181
+ let commands = window(edit);
1182
+ ui.run_frame(
1183
+ &commands,
1184
+ UiInputSnapshot::default(),
1185
+ [0.0, 0.0, 320.0, 240.0],
1186
+ 1.0 / 60.0,
1187
+ );
1188
+ ui.run_frame(
1189
+ &commands,
1190
+ click_at([56.0, 62.0]),
1191
+ [0.0, 0.0, 320.0, 240.0],
1192
+ 1.0 / 60.0,
1193
+ );
1194
+
1195
+ let input = UiInputSnapshot {
1196
+ text: vec!["東京".to_owned()],
1197
+ ..Default::default()
1198
+ };
1199
+ let output = ui.run_frame(&commands, input, [0.0, 0.0, 320.0, 240.0], 1.0 / 60.0);
1200
+ assert_eq!(output.response(12).text, "東京");
1201
+ assert!(output.text_edit_focused);
1202
+ }
1203
+
1204
+ #[test]
1205
+ fn text_edit_applies_mixed_text_and_backspace_events_in_order() {
1206
+ let mut ui = EguiUi::default();
1207
+ let edit = UiCommand::new(UiBackend::Egui, UiOpcode::TextEdit, 12, [0.0; 4], "");
1208
+ let commands = window(edit);
1209
+ ui.run_frame(
1210
+ &commands,
1211
+ UiInputSnapshot::default(),
1212
+ [0.0, 0.0, 320.0, 240.0],
1213
+ 1.0 / 60.0,
1214
+ );
1215
+ ui.run_frame(
1216
+ &commands,
1217
+ click_at([56.0, 62.0]),
1218
+ [0.0, 0.0, 320.0, 240.0],
1219
+ 1.0 / 60.0,
1220
+ );
1221
+
1222
+ let output = ui.run_frame(
1223
+ &commands,
1224
+ UiInputSnapshot {
1225
+ ordered_events: vec![
1226
+ UiInputEvent::Text("x".to_owned()),
1227
+ UiInputEvent::Key(UiKeyEvent {
1228
+ key: 8,
1229
+ pressed: true,
1230
+ repeated: false,
1231
+ }),
1232
+ UiInputEvent::Key(UiKeyEvent {
1233
+ key: 8,
1234
+ pressed: false,
1235
+ repeated: false,
1236
+ }),
1237
+ ],
1238
+ ..Default::default()
1239
+ },
1240
+ [0.0, 0.0, 320.0, 240.0],
1241
+ 1.0 / 60.0,
1242
+ );
1243
+
1244
+ assert_eq!(output.response(12).text, "");
1245
+ }
1246
+
1247
+ #[test]
1248
+ fn egui_reports_pointer_capture_for_a_widget_under_the_cursor() {
1249
+ let mut ui = EguiUi::default();
1250
+ let output = ui.run_frame(
1251
+ &window(UiCommand::new(
1252
+ UiBackend::Egui,
1253
+ UiOpcode::Button,
1254
+ 13,
1255
+ [0.0; 4],
1256
+ "Apply",
1257
+ )),
1258
+ UiInputSnapshot {
1259
+ pointer_position: Some([56.0, 62.0]),
1260
+ ..Default::default()
1261
+ },
1262
+ [0.0, 0.0, 320.0, 240.0],
1263
+ 1.0 / 60.0,
1264
+ );
1265
+ assert!(output.wants_pointer_input);
1266
+ assert!(!output.wants_keyboard_input);
1267
+ }
1268
+
1269
+ #[test]
1270
+ fn egui_uses_native_pixels_per_point_for_high_dpi_surfaces() {
1271
+ let mut ui = EguiUi::default();
1272
+ ui.set_native_pixels_per_point(2.0);
1273
+ let output = ui.run_frame(
1274
+ &[],
1275
+ UiInputSnapshot::default(),
1276
+ [0.0, 0.0, 320.0, 240.0],
1277
+ 1.0 / 60.0,
1278
+ );
1279
+
1280
+ assert_eq!(output.pixels_per_point, 2.0);
1281
+ }
1282
+ }