@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.
- package/README.md +24 -14
- package/native/android/Cargo.lock +577 -28
- package/native/android/Cargo.toml +2 -0
- package/native/android/src/lib.rs +39 -3
- package/native/ios/Cargo.lock +573 -24
- package/native/ios/Cargo.toml +2 -0
- package/native/ios/src/lib.rs +72 -9
- package/native/ios/src/text_input.rs +48 -0
- package/native/linux/Cargo.lock +623 -35
- package/native/linux/Cargo.toml +3 -0
- package/native/macos/Cargo.lock +630 -35
- package/native/macos/Cargo.toml +3 -0
- package/native/shared/Cargo.lock +430 -27
- package/native/shared/Cargo.toml +5 -0
- package/native/shared/build.rs +41 -1
- package/native/shared/src/engine.rs +54 -2
- package/native/shared/src/ffi.rs +54 -1
- package/native/shared/src/ffi_core/mod.rs +2 -0
- package/native/shared/src/ffi_core/ui.rs +182 -0
- package/native/shared/src/input.rs +317 -12
- package/native/shared/src/lib.rs +1 -0
- package/native/shared/src/renderer/mod.rs +47 -201
- package/native/shared/src/renderer/texture_store.rs +5 -0
- package/native/shared/src/renderer/ui_pass.rs +742 -0
- package/native/shared/src/textures.rs +2 -0
- package/native/shared/src/ui/commands.rs +213 -0
- package/native/shared/src/ui/egui.rs +1282 -0
- package/native/shared/src/ui/imgui.rs +1056 -0
- package/native/shared/src/ui/input.rs +173 -0
- package/native/shared/src/ui/mod.rs +252 -0
- package/native/shared/src/ui/responses.rs +61 -0
- package/native/tvos/Cargo.lock +576 -27
- package/native/tvos/Cargo.toml +2 -0
- package/native/visionos/Cargo.lock +571 -22
- package/native/visionos/Cargo.toml +2 -0
- package/native/watchos/src/ffi_stubs.rs +27 -0
- package/native/web/Cargo.lock +346 -11
- package/native/web/Cargo.toml +6 -0
- package/native/web/bloom_glue.js +84 -11
- package/native/web/build.sh +150 -34
- package/native/web/src/lib.rs +2 -0
- package/native/web/src/ui_ffi.rs +148 -0
- package/native/windows/Cargo.lock +638 -38
- package/native/windows/Cargo.toml +3 -0
- package/native/windows/src/lib.rs +50 -1
- package/native/windows/src/windows_text.rs +69 -0
- package/package.json +91 -2
- package/src/debug-ui/index.ts +4 -0
- package/src/debug-ui/types.ts +1 -0
- package/src/game/adapters/audio-source-component.ts +67 -0
- package/src/game/adapters/rigid-body-component.ts +69 -0
- package/src/game/adapters/scene-node-component.ts +49 -0
- package/src/game/game-component.ts +89 -0
- package/src/game/game-object.ts +338 -0
- package/src/game/game-scene.ts +303 -0
- package/src/game/index.ts +7 -0
- package/src/game/transform.ts +315 -0
- package/src/index.ts +27 -0
- package/src/ui/api.ts +240 -0
- package/src/ui/index.ts +5 -0
- package/src/ui/opcodes.ts +66 -0
- package/src/ui/types.ts +82 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
use std::collections::VecDeque;
|
|
2
|
+
|
|
3
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
4
|
+
pub struct UiPointerButtonEvent {
|
|
5
|
+
pub button: u8,
|
|
6
|
+
pub pressed: bool,
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
10
|
+
pub struct UiKeyEvent {
|
|
11
|
+
pub key: u32,
|
|
12
|
+
pub pressed: bool,
|
|
13
|
+
pub repeated: bool,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
17
|
+
pub enum UiInputEvent {
|
|
18
|
+
Key(UiKeyEvent),
|
|
19
|
+
Text(String),
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
|
23
|
+
pub struct UiModifiers {
|
|
24
|
+
pub shift: bool,
|
|
25
|
+
pub ctrl: bool,
|
|
26
|
+
pub alt: bool,
|
|
27
|
+
pub super_key: bool,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
31
|
+
pub struct UiTouchEvent {
|
|
32
|
+
pub slot: usize,
|
|
33
|
+
pub x: f64,
|
|
34
|
+
pub y: f64,
|
|
35
|
+
pub active: bool,
|
|
36
|
+
pub released: bool,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#[derive(Clone, Debug, Default, PartialEq)]
|
|
40
|
+
pub struct UiInputSnapshot {
|
|
41
|
+
pub pointer_position: Option<[f64; 2]>,
|
|
42
|
+
pub pointer_delta: [f64; 2],
|
|
43
|
+
pub pointer_buttons: Vec<UiPointerButtonEvent>,
|
|
44
|
+
pub scroll_x: f64,
|
|
45
|
+
pub scroll_y: f64,
|
|
46
|
+
/// Text and keyboard events in their original platform delivery order.
|
|
47
|
+
pub ordered_events: Vec<UiInputEvent>,
|
|
48
|
+
pub keys: Vec<UiKeyEvent>,
|
|
49
|
+
pub modifiers: UiModifiers,
|
|
50
|
+
pub text: Vec<String>,
|
|
51
|
+
pub touches: Vec<UiTouchEvent>,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl UiInputSnapshot {
|
|
55
|
+
pub fn ordered_key_text_events(&self) -> Vec<UiInputEvent> {
|
|
56
|
+
if !self.ordered_events.is_empty() {
|
|
57
|
+
return self.ordered_events.clone();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
self.keys
|
|
61
|
+
.iter()
|
|
62
|
+
.copied()
|
|
63
|
+
.map(UiInputEvent::Key)
|
|
64
|
+
.chain(self.text.iter().cloned().map(UiInputEvent::Text))
|
|
65
|
+
.collect()
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
#[derive(Default)]
|
|
70
|
+
pub struct UiInputBridge {
|
|
71
|
+
pending_text: Vec<String>,
|
|
72
|
+
text_focused: bool,
|
|
73
|
+
keyboard_requests: VecDeque<bool>,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
impl UiInputBridge {
|
|
77
|
+
pub fn inject_text(&mut self, text: String) {
|
|
78
|
+
if !text.is_empty() {
|
|
79
|
+
self.pending_text.push(text);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
pub fn merge_snapshot(&mut self, mut snapshot: UiInputSnapshot) -> UiInputSnapshot {
|
|
84
|
+
if snapshot.ordered_events.is_empty() {
|
|
85
|
+
snapshot.ordered_events.extend(
|
|
86
|
+
snapshot
|
|
87
|
+
.keys
|
|
88
|
+
.iter()
|
|
89
|
+
.copied()
|
|
90
|
+
.map(UiInputEvent::Key)
|
|
91
|
+
.chain(snapshot.text.iter().cloned().map(UiInputEvent::Text)),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
for text in self.pending_text.drain(..) {
|
|
95
|
+
snapshot
|
|
96
|
+
.ordered_events
|
|
97
|
+
.push(UiInputEvent::Text(text.clone()));
|
|
98
|
+
snapshot.text.push(text);
|
|
99
|
+
}
|
|
100
|
+
snapshot
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pub fn set_text_focus(&mut self, focused: bool) {
|
|
104
|
+
if focused != self.text_focused {
|
|
105
|
+
self.text_focused = focused;
|
|
106
|
+
self.keyboard_requests.push_back(focused);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
pub fn take_keyboard_request(&mut self) -> Option<bool> {
|
|
111
|
+
self.keyboard_requests.pop_front()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[cfg(test)]
|
|
115
|
+
pub fn take_keyboard_requests(&mut self) -> Vec<bool> {
|
|
116
|
+
self.keyboard_requests.drain(..).collect()
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
#[cfg(test)]
|
|
121
|
+
mod tests {
|
|
122
|
+
use super::{UiInputBridge, UiInputEvent, UiInputSnapshot, UiKeyEvent};
|
|
123
|
+
|
|
124
|
+
#[test]
|
|
125
|
+
fn ui_text_injection_preserves_a_complete_unicode_event() {
|
|
126
|
+
let mut bridge = UiInputBridge::default();
|
|
127
|
+
bridge.inject_text("東京".to_owned());
|
|
128
|
+
let snapshot = bridge.merge_snapshot(UiInputSnapshot::default());
|
|
129
|
+
|
|
130
|
+
assert_eq!(snapshot.text, ["東京"]);
|
|
131
|
+
assert_eq!(
|
|
132
|
+
snapshot.ordered_events,
|
|
133
|
+
[UiInputEvent::Text("東京".to_owned())]
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
#[test]
|
|
138
|
+
fn bridge_appends_injected_text_after_existing_key_events() {
|
|
139
|
+
let mut bridge = UiInputBridge::default();
|
|
140
|
+
bridge.inject_text("x".to_owned());
|
|
141
|
+
|
|
142
|
+
let snapshot = bridge.merge_snapshot(UiInputSnapshot {
|
|
143
|
+
keys: vec![UiKeyEvent {
|
|
144
|
+
key: 8,
|
|
145
|
+
pressed: true,
|
|
146
|
+
repeated: false,
|
|
147
|
+
}],
|
|
148
|
+
..Default::default()
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
assert_eq!(
|
|
152
|
+
snapshot.ordered_events,
|
|
153
|
+
[
|
|
154
|
+
UiInputEvent::Key(UiKeyEvent {
|
|
155
|
+
key: 8,
|
|
156
|
+
pressed: true,
|
|
157
|
+
repeated: false,
|
|
158
|
+
}),
|
|
159
|
+
UiInputEvent::Text("x".to_owned()),
|
|
160
|
+
]
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
#[test]
|
|
165
|
+
fn keyboard_focus_requests_only_on_transitions() {
|
|
166
|
+
let mut bridge = UiInputBridge::default();
|
|
167
|
+
bridge.set_text_focus(true);
|
|
168
|
+
bridge.set_text_focus(true);
|
|
169
|
+
bridge.set_text_focus(false);
|
|
170
|
+
|
|
171
|
+
assert_eq!(bridge.take_keyboard_requests(), [true, false]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
mod commands;
|
|
2
|
+
mod egui;
|
|
3
|
+
#[cfg(feature = "debug-ui")]
|
|
4
|
+
mod imgui;
|
|
5
|
+
mod input;
|
|
6
|
+
mod responses;
|
|
7
|
+
|
|
8
|
+
pub use commands::{UiCommand, UiOpcode};
|
|
9
|
+
pub use egui::{EguiFrameOutput, EguiUi};
|
|
10
|
+
#[cfg(feature = "debug-ui")]
|
|
11
|
+
pub(crate) use imgui::with_dear_imgui;
|
|
12
|
+
#[cfg(feature = "debug-ui")]
|
|
13
|
+
pub use imgui::DearImGuiUi;
|
|
14
|
+
pub use input::{
|
|
15
|
+
UiInputBridge, UiInputEvent, UiInputSnapshot, UiKeyEvent, UiModifiers, UiPointerButtonEvent,
|
|
16
|
+
UiTouchEvent,
|
|
17
|
+
};
|
|
18
|
+
pub use responses::UiResponse;
|
|
19
|
+
|
|
20
|
+
use std::collections::HashMap;
|
|
21
|
+
|
|
22
|
+
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
23
|
+
#[repr(u32)]
|
|
24
|
+
pub enum UiBackend {
|
|
25
|
+
Egui = 0,
|
|
26
|
+
DearImGui = 1,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
impl UiBackend {
|
|
30
|
+
pub fn from_abi(value: f64) -> Option<Self> {
|
|
31
|
+
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value > u32::MAX as f64 {
|
|
32
|
+
return None;
|
|
33
|
+
}
|
|
34
|
+
match value as u32 {
|
|
35
|
+
0 => Some(Self::Egui),
|
|
36
|
+
1 => Some(Self::DearImGui),
|
|
37
|
+
_ => None,
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
pub fn id_from_abi(value: f64) -> Option<u32> {
|
|
43
|
+
if !value.is_finite() || value < 0.0 || value.fract() != 0.0 || value > u32::MAX as f64 {
|
|
44
|
+
return None;
|
|
45
|
+
}
|
|
46
|
+
Some(value as u32)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
|
50
|
+
pub struct UiCaptureState {
|
|
51
|
+
pub pointer: bool,
|
|
52
|
+
pub keyboard: bool,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const MAX_QUEUED_COMMANDS: usize = 16_384;
|
|
56
|
+
const MAX_SCRATCH_VALUES: usize = 1_048_576;
|
|
57
|
+
|
|
58
|
+
fn color_channel(value: f64) -> u8 {
|
|
59
|
+
if value.is_finite() {
|
|
60
|
+
value.clamp(0.0, 255.0).round() as u8
|
|
61
|
+
} else {
|
|
62
|
+
255
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
#[derive(Default)]
|
|
67
|
+
pub struct UiSystem {
|
|
68
|
+
commands: Vec<UiCommand>,
|
|
69
|
+
completed_responses: HashMap<UiBackend, HashMap<u32, UiResponse>>,
|
|
70
|
+
capture: HashMap<UiBackend, UiCaptureState>,
|
|
71
|
+
scratch: HashMap<UiBackend, Vec<f64>>,
|
|
72
|
+
input_bridge: UiInputBridge,
|
|
73
|
+
input_snapshot: UiInputSnapshot,
|
|
74
|
+
egui: EguiUi,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl UiSystem {
|
|
78
|
+
/// Start a new TypeScript command frame without clearing readback values
|
|
79
|
+
/// from the most recently completed UI frame.
|
|
80
|
+
pub fn begin_frame(&mut self) {
|
|
81
|
+
self.commands.clear();
|
|
82
|
+
self.scratch.clear();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
pub fn queue_command(&mut self, command: UiCommand) -> bool {
|
|
86
|
+
if self.commands.len() >= MAX_QUEUED_COMMANDS {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
self.commands.push(command);
|
|
90
|
+
true
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
pub fn take_commands(&mut self, backend: UiBackend) -> Vec<UiCommand> {
|
|
94
|
+
let mut selected = Vec::new();
|
|
95
|
+
self.commands.retain(|command| {
|
|
96
|
+
if command.backend == backend {
|
|
97
|
+
selected.push(command.clone());
|
|
98
|
+
false
|
|
99
|
+
} else {
|
|
100
|
+
true
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
selected
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
pub fn response(&self, backend: UiBackend, id: u32) -> UiResponse {
|
|
107
|
+
self.completed_responses
|
|
108
|
+
.get(&backend)
|
|
109
|
+
.and_then(|responses| responses.get(&id))
|
|
110
|
+
.cloned()
|
|
111
|
+
.unwrap_or_default()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
pub fn has_response(&self, backend: UiBackend, id: u32) -> bool {
|
|
115
|
+
self.completed_responses
|
|
116
|
+
.get(&backend)
|
|
117
|
+
.is_some_and(|responses| responses.contains_key(&id))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
pub fn response_text(&self, backend: UiBackend, id: u32) -> String {
|
|
121
|
+
self.response(backend, id).text
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pub fn set_input_snapshot(&mut self, snapshot: UiInputSnapshot) {
|
|
125
|
+
self.input_snapshot = self.input_bridge.merge_snapshot(snapshot);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
pub fn input_snapshot(&self) -> &UiInputSnapshot {
|
|
129
|
+
&self.input_snapshot
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
pub fn evaluate_egui(
|
|
133
|
+
&mut self,
|
|
134
|
+
screen_rect: [f32; 4],
|
|
135
|
+
pixels_per_point: f32,
|
|
136
|
+
dt: f64,
|
|
137
|
+
) -> EguiFrameOutput {
|
|
138
|
+
self.egui.set_native_pixels_per_point(pixels_per_point);
|
|
139
|
+
let commands = self.take_commands(UiBackend::Egui);
|
|
140
|
+
let output = self
|
|
141
|
+
.egui
|
|
142
|
+
.run_frame(&commands, self.input_snapshot.clone(), screen_rect, dt);
|
|
143
|
+
self.publish_completed_responses(
|
|
144
|
+
UiBackend::Egui,
|
|
145
|
+
output
|
|
146
|
+
.responses
|
|
147
|
+
.iter()
|
|
148
|
+
.map(|(&id, response)| (id, response.clone()))
|
|
149
|
+
.collect(),
|
|
150
|
+
);
|
|
151
|
+
self.set_capture(
|
|
152
|
+
UiBackend::Egui,
|
|
153
|
+
UiCaptureState {
|
|
154
|
+
pointer: output.wants_pointer_input,
|
|
155
|
+
keyboard: output.wants_keyboard_input,
|
|
156
|
+
},
|
|
157
|
+
);
|
|
158
|
+
self.set_text_focus(output.text_edit_focused);
|
|
159
|
+
output
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
pub fn publish_completed_responses(
|
|
163
|
+
&mut self,
|
|
164
|
+
backend: UiBackend,
|
|
165
|
+
responses: Vec<(u32, UiResponse)>,
|
|
166
|
+
) {
|
|
167
|
+
self.completed_responses
|
|
168
|
+
.insert(backend, responses.into_iter().collect());
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
pub fn set_capture(&mut self, backend: UiBackend, capture: UiCaptureState) {
|
|
172
|
+
self.capture.insert(backend, capture);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
pub fn wants_input(&self, backend: UiBackend, kind: u32) -> bool {
|
|
176
|
+
let capture = self.capture.get(&backend).copied().unwrap_or_default();
|
|
177
|
+
match kind {
|
|
178
|
+
0 => capture.pointer,
|
|
179
|
+
1 => capture.keyboard,
|
|
180
|
+
_ => false,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
pub fn is_available(&self, backend: UiBackend) -> bool {
|
|
185
|
+
match backend {
|
|
186
|
+
UiBackend::Egui => true,
|
|
187
|
+
UiBackend::DearImGui => cfg!(feature = "debug-ui"),
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
pub(crate) fn registered_texture_handles(&self, backend: UiBackend) -> Vec<u64> {
|
|
192
|
+
self.commands
|
|
193
|
+
.iter()
|
|
194
|
+
.filter(|command| {
|
|
195
|
+
command.backend == backend && command.opcode == UiOpcode::RegisterTexture
|
|
196
|
+
})
|
|
197
|
+
.filter_map(|command| {
|
|
198
|
+
let handle = command.args[0];
|
|
199
|
+
(handle.is_finite()
|
|
200
|
+
&& handle >= 0.0
|
|
201
|
+
&& handle.fract() == 0.0
|
|
202
|
+
&& handle <= u64::MAX as f64)
|
|
203
|
+
.then_some(handle as u64)
|
|
204
|
+
})
|
|
205
|
+
.collect()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
pub fn inject_text(&mut self, text: String) {
|
|
209
|
+
self.input_bridge.inject_text(text);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
pub fn set_text_focus(&mut self, focused: bool) {
|
|
213
|
+
self.input_bridge.set_text_focus(focused);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pub fn take_keyboard_request(&mut self) -> Option<bool> {
|
|
217
|
+
self.input_bridge.take_keyboard_request()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
pub fn reset_scratch(&mut self, backend: UiBackend) {
|
|
221
|
+
self.scratch.insert(backend, Vec::new());
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
pub fn push_scratch(&mut self, backend: UiBackend, value: f64) -> bool {
|
|
225
|
+
let values = self.scratch.entry(backend).or_default();
|
|
226
|
+
if values.len() >= MAX_SCRATCH_VALUES || !value.is_finite() {
|
|
227
|
+
return false;
|
|
228
|
+
}
|
|
229
|
+
values.push(value);
|
|
230
|
+
true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
pub fn queue_scratch_command(
|
|
234
|
+
&mut self,
|
|
235
|
+
backend: UiBackend,
|
|
236
|
+
opcode: UiOpcode,
|
|
237
|
+
id: u32,
|
|
238
|
+
count: usize,
|
|
239
|
+
text: impl Into<String>,
|
|
240
|
+
) -> bool {
|
|
241
|
+
let Some(values) = self.scratch.get_mut(&backend) else {
|
|
242
|
+
return false;
|
|
243
|
+
};
|
|
244
|
+
if count > values.len() || self.commands.len() >= MAX_QUEUED_COMMANDS {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
let scratch = values.drain(..count).collect();
|
|
248
|
+
self.commands
|
|
249
|
+
.push(UiCommand::new(backend, opcode, id, [0.0; 4], text).with_scratch(scratch));
|
|
250
|
+
true
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#[derive(Clone, Debug, Default, PartialEq)]
|
|
2
|
+
pub struct UiResponse {
|
|
3
|
+
pub clicked: bool,
|
|
4
|
+
pub changed: bool,
|
|
5
|
+
pub hovered: bool,
|
|
6
|
+
pub focused: bool,
|
|
7
|
+
pub dragged: bool,
|
|
8
|
+
pub value: f64,
|
|
9
|
+
pub text: String,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#[cfg(test)]
|
|
13
|
+
mod tests {
|
|
14
|
+
use crate::ui::{UiBackend, UiResponse, UiSystem};
|
|
15
|
+
|
|
16
|
+
#[test]
|
|
17
|
+
fn completed_responses_survive_the_next_game_callback_then_replace() {
|
|
18
|
+
let mut ui = UiSystem::default();
|
|
19
|
+
ui.begin_frame();
|
|
20
|
+
ui.publish_completed_responses(
|
|
21
|
+
UiBackend::Egui,
|
|
22
|
+
vec![(
|
|
23
|
+
42,
|
|
24
|
+
UiResponse {
|
|
25
|
+
clicked: true,
|
|
26
|
+
..Default::default()
|
|
27
|
+
},
|
|
28
|
+
)],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
assert!(ui.response(UiBackend::Egui, 42).clicked);
|
|
32
|
+
assert!(!ui.response(UiBackend::DearImGui, 42).clicked);
|
|
33
|
+
|
|
34
|
+
// begin_frame runs before the TypeScript callback; preserve the last
|
|
35
|
+
// completed response so that callback can read it.
|
|
36
|
+
ui.begin_frame();
|
|
37
|
+
assert!(ui.response(UiBackend::Egui, 42).clicked);
|
|
38
|
+
|
|
39
|
+
// The response set is replaced only after the next UI evaluation.
|
|
40
|
+
ui.publish_completed_responses(UiBackend::Egui, vec![]);
|
|
41
|
+
assert!(!ui.response(UiBackend::Egui, 42).clicked);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#[test]
|
|
45
|
+
fn missing_response_returns_default_for_backend_and_id() {
|
|
46
|
+
let mut ui = UiSystem::default();
|
|
47
|
+
ui.publish_completed_responses(
|
|
48
|
+
UiBackend::Egui,
|
|
49
|
+
vec![(
|
|
50
|
+
42,
|
|
51
|
+
UiResponse {
|
|
52
|
+
clicked: true,
|
|
53
|
+
..Default::default()
|
|
54
|
+
},
|
|
55
|
+
)],
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
assert_eq!(ui.response(UiBackend::Egui, 404), UiResponse::default());
|
|
59
|
+
assert_eq!(ui.response(UiBackend::DearImGui, 42), UiResponse::default());
|
|
60
|
+
}
|
|
61
|
+
}
|