1#[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
8use crate::ImguiViewportBridge;
9use crate::{
10 ImguiBackendStatus, ImguiContext, ImguiTextureFeedbackQueue, ImguiViewportWindow,
11 input::{
12 ImguiInputState, map_imgui_mouse_cursor, sanitized_window_display_size,
13 sanitized_window_framebuffer_scale,
14 },
15};
16use bevy_app::App;
17use bevy_ecs::prelude::*;
18use bevy_ecs::system::{NonSendMarker, SystemParam};
19use bevy_math::Vec2;
20use bevy_time::{Real, Time};
21use bevy_window::{CursorIcon, CursorOptions, PrimaryWindow, Window, WindowPosition};
22#[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
23use bevy_window::{Monitor, PrimaryMonitor};
24use dear_imgui_rs as imgui;
25use std::ptr::NonNull;
26
27type PrimaryInputWindowQuery<'w, 's> =
28 Query<'w, 's, (Entity, &'static Window), With<PrimaryWindow>>;
29#[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
30type ViewportInputWindowQuery<'w, 's> =
31 Query<'w, 's, (Entity, &'static Window, &'static ImguiViewportWindow), Without<PrimaryWindow>>;
32type PrimaryFeedbackWindowQuery<'w, 's> = Query<
33 'w,
34 's,
35 (
36 Entity,
37 &'static mut Window,
38 &'static mut CursorOptions,
39 Option<&'static mut CursorIcon>,
40 ),
41 With<PrimaryWindow>,
42>;
43type ViewportFeedbackWindowQuery<'w, 's> = Query<
44 'w,
45 's,
46 (
47 Entity,
48 &'static mut Window,
49 &'static mut CursorOptions,
50 Option<&'static mut CursorIcon>,
51 &'static ImguiViewportWindow,
52 ),
53 Without<PrimaryWindow>,
54>;
55
56#[derive(SystemParam)]
57struct BeginFrameParams<'w, 's> {
58 primary_window: PrimaryInputWindowQuery<'w, 's>,
59 #[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
60 viewport_windows: ViewportInputWindowQuery<'w, 's>,
61 #[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
62 monitors: Query<'w, 's, (&'static Monitor, Option<&'static PrimaryMonitor>)>,
63 imgui_context: NonSendMut<'w, ImguiContext>,
64 #[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
65 viewport_bridge: Option<NonSendMut<'w, ImguiViewportBridge>>,
66 frame_state: NonSendMut<'w, ImguiFrameState>,
67 output: ResMut<'w, ImguiFrameOutput>,
68 texture_feedback: ResMut<'w, ImguiTextureFeedbackQueue>,
69 #[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
70 backend_status: Res<'w, ImguiBackendStatus>,
71 real_time: Option<Res<'w, Time<Real>>>,
72}
73
74#[derive(Resource, Debug, Default)]
76pub struct ImguiFrameOutput {
77 frame_index: u64,
78 snapshot: Option<imgui::render::snapshot::FrameSnapshot>,
79 snapshot_error: Option<String>,
80}
81
82impl ImguiFrameOutput {
83 #[must_use]
85 pub fn frame_index(&self) -> u64 {
86 self.frame_index
87 }
88
89 #[must_use]
91 pub fn snapshot(&self) -> Option<&imgui::render::snapshot::FrameSnapshot> {
92 self.snapshot.as_ref()
93 }
94
95 #[must_use]
97 pub fn snapshot_error(&self) -> Option<&str> {
98 self.snapshot_error.as_deref()
99 }
100
101 fn set_snapshot(
102 &mut self,
103 frame_index: u64,
104 snapshot: Result<
105 imgui::render::snapshot::FrameSnapshot,
106 imgui::render::snapshot::SnapshotError,
107 >,
108 ) {
109 self.frame_index = frame_index;
110 match snapshot {
111 Ok(snapshot) => {
112 self.snapshot = Some(snapshot);
113 self.snapshot_error = None;
114 }
115 Err(err) => {
116 self.snapshot = None;
117 self.snapshot_error = Some(err.to_string());
118 }
119 }
120 }
121
122 fn clear_snapshot(&mut self, frame_index: u64) {
123 self.frame_index = frame_index;
124 self.snapshot = None;
125 self.snapshot_error = None;
126 }
127}
128
129#[derive(Default)]
135pub struct ImguiFrameState {
136 frame_index: u64,
137 ui: Option<NonNull<imgui::Ui>>,
138}
139
140impl ImguiFrameState {
141 #[must_use]
143 pub fn frame_index(&self) -> u64 {
144 self.frame_index
145 }
146
147 #[must_use]
149 pub fn is_frame_open(&self) -> bool {
150 self.ui.is_some()
151 }
152
153 #[must_use]
155 pub fn ui(&self) -> Option<&imgui::Ui> {
156 let ui = self.ui?;
157 Some(unsafe { ui.as_ref() })
160 }
161
162 fn begin(&mut self, ui: &imgui::Ui) {
163 self.frame_index = self.frame_index.saturating_add(1);
164 self.ui = Some(NonNull::from(ui));
165 }
166
167 fn end(&mut self) -> u64 {
168 self.ui = None;
169 self.frame_index
170 }
171}
172
173#[derive(SystemParam)]
175pub struct ImguiContexts<'w> {
176 frame_state: NonSend<'w, ImguiFrameState>,
177 _main_thread: NonSendMarker,
178}
179
180impl<'w> ImguiContexts<'w> {
181 #[must_use]
183 pub fn primary_ui_mut(&mut self) -> Option<&imgui::Ui> {
184 self.frame_state.ui()
185 }
186
187 #[must_use]
189 pub fn frame_index(&self) -> Option<u64> {
190 self.frame_state
191 .is_frame_open()
192 .then_some(self.frame_state.frame_index())
193 }
194}
195
196pub(crate) fn install_context_lifecycle(app: &mut App) {
197 app.init_non_send::<ImguiFrameState>()
198 .init_resource::<ImguiFrameOutput>()
199 .init_resource::<ImguiTextureFeedbackQueue>()
200 .add_systems(crate::ImguiBeginFrame, begin_primary_frame_system)
201 .add_systems(crate::ImguiEndFrame, end_primary_frame_system);
202}
203
204#[cfg_attr(
205 not(all(feature = "multi-viewport", not(target_arch = "wasm32"))),
206 allow(unused_variables)
207)]
208fn begin_primary_frame_system(mut params: BeginFrameParams) {
209 if params.frame_state.is_frame_open() {
210 return;
211 }
212
213 let Ok((primary_window_entity, window)) = params.primary_window.single() else {
214 let feedback = params.texture_feedback.drain();
215 let applied = params
216 .imgui_context
217 .context_mut()
218 .platform_io_mut()
219 .apply_texture_feedback(&feedback);
220 params.texture_feedback.set_last_applied(applied);
221 params
222 .output
223 .clear_snapshot(params.frame_state.frame_index());
224 return;
225 };
226
227 let context = params.imgui_context.context_mut();
228 let feedback = params.texture_feedback.drain();
229 let applied = context.platform_io_mut().apply_texture_feedback(&feedback);
230 params.texture_feedback.set_last_applied(applied);
231
232 #[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
233 if let Some(viewport_bridge) = params.viewport_bridge.as_deref_mut() {
234 let viewport_feedback = params
235 .viewport_windows
236 .iter()
237 .map(|(entity, window, viewport_window)| {
238 (
239 entity,
240 viewport_window.viewport_id,
241 crate::viewport::viewport_feedback_from_window(
242 entity,
243 window,
244 viewport_bridge.viewport_feedback(viewport_window.viewport_id),
245 ),
246 )
247 })
248 .collect::<Vec<_>>();
249 let monitors = crate::viewport::platform_monitors_from_bevy_monitors(
250 params
251 .monitors
252 .iter()
253 .map(|(monitor, primary)| (monitor.clone(), primary.is_some())),
254 );
255 crate::viewport::prepare_platform_viewports_for_frame(
256 context,
257 viewport_bridge,
258 primary_window_entity,
259 window,
260 &monitors,
261 viewport_feedback.into_iter(),
262 params.backend_status.multi_viewport_supported,
263 );
264 }
265
266 context.prepare_frame(
267 imgui::FramePrepareOptions::new(
268 sanitized_window_display_size(window),
269 imgui_delta_time(context, params.real_time.as_deref()),
270 )
271 .framebuffer_scale(sanitized_window_framebuffer_scale(window)),
272 );
273
274 let ui = context.frame();
275 params.frame_state.begin(ui);
276}
277
278fn imgui_delta_time(context: &imgui::Context, real_time: Option<&Time<Real>>) -> f32 {
279 real_time
280 .map(Time::delta_secs)
281 .unwrap_or_else(|| context.io().delta_time())
282 .max(f32::EPSILON)
283}
284
285#[allow(clippy::too_many_arguments)]
286pub(crate) fn end_primary_frame_system(
287 mut commands: Commands,
288 mut imgui_context: NonSendMut<ImguiContext>,
289 mut frame_state: NonSendMut<ImguiFrameState>,
290 backend_status: Res<ImguiBackendStatus>,
291 input_state: Res<ImguiInputState>,
292 mut primary_window: PrimaryFeedbackWindowQuery,
293 mut viewport_windows: ViewportFeedbackWindowQuery,
294 mut output: ResMut<ImguiFrameOutput>,
295) {
296 if !frame_state.is_frame_open() {
297 return;
298 }
299
300 if let Some(ui) = frame_state.ui() {
301 sync_primary_window_platform_feedback(
302 ui,
303 &imgui_context,
304 &input_state,
305 &mut commands,
306 &mut primary_window,
307 &mut viewport_windows,
308 );
309 }
310
311 let frame_index = frame_state.end();
312 let snapshot = render_frame_snapshot(
313 imgui_context.context_mut(),
314 backend_status.multi_viewport_supported,
315 );
316 output.set_snapshot(frame_index, snapshot);
317}
318
319fn render_frame_snapshot(
320 context: &mut imgui::Context,
321 _multi_viewport_supported: bool,
322) -> Result<imgui::render::snapshot::FrameSnapshot, imgui::render::snapshot::SnapshotError> {
323 #[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
324 {
325 if _multi_viewport_supported {
326 let _ = context.render();
327 context.update_platform_windows();
328 return context
329 .platform_viewport_snapshot(imgui::render::snapshot::SnapshotOptions::default());
330 }
331 }
332
333 {
334 let draw_data = context.render();
335 imgui::render::snapshot::FrameSnapshot::from_draw_data(
336 draw_data,
337 imgui::render::snapshot::SnapshotOptions::default(),
338 )
339 }
340}
341
342fn sync_primary_window_platform_feedback(
343 ui: &imgui::Ui,
344 imgui_context: &ImguiContext,
345 input_state: &ImguiInputState,
346 commands: &mut Commands,
347 primary_window: &mut PrimaryFeedbackWindowQuery,
348 viewport_windows: &mut ViewportFeedbackWindowQuery,
349) {
350 let Ok((
351 primary_entity,
352 mut primary_window,
353 mut primary_cursor_options,
354 mut primary_cursor_icon,
355 )) = primary_window.single_mut()
356 else {
357 return;
358 };
359
360 let raw_context = imgui_context.context().as_raw();
361 let ime_data = unsafe { &(*raw_context).PlatformImeData };
364 let ime_target_viewport = (ime_data.ViewportId != 0).then_some(ime_data.ViewportId);
365 let ime_position = [ime_data.InputPos.x, ime_data.InputPos.y];
366 let hovered_window = input_state.mouse_hovered_window();
367 let cursor_target = hovered_window.unwrap_or(primary_entity);
368
369 let mut cursor_applied = false;
370 if cursor_target == primary_entity {
371 apply_window_cursor_feedback(
372 ui,
373 commands,
374 primary_entity,
375 &mut primary_cursor_options,
376 primary_cursor_icon.take(),
377 );
378 cursor_applied = true;
379 } else {
380 clear_window_cursor_feedback(
381 commands,
382 primary_entity,
383 &mut primary_cursor_options,
384 primary_cursor_icon.take(),
385 );
386 }
387
388 let mut ime_applied = false;
389 if ime_target_viewport.is_none() {
390 apply_window_ime_feedback(
391 primary_entity,
392 &mut primary_window,
393 ime_data.WantTextInput,
394 ime_position,
395 true,
396 );
397 ime_applied = true;
398 } else {
399 primary_window.ime_enabled = false;
400 }
401
402 for (window_entity, mut window, mut cursor_options, cursor_icon, viewport_window) in
403 viewport_windows.iter_mut()
404 {
405 if cursor_target == window_entity {
406 apply_window_cursor_feedback(
407 ui,
408 commands,
409 window_entity,
410 &mut cursor_options,
411 cursor_icon,
412 );
413 cursor_applied = true;
414 } else {
415 clear_window_cursor_feedback(commands, window_entity, &mut cursor_options, cursor_icon);
416 }
417
418 if ime_target_viewport == Some(viewport_window.viewport_id.raw()) {
419 apply_window_ime_feedback(
420 window_entity,
421 &mut window,
422 ime_data.WantTextInput,
423 ime_position,
424 false,
425 );
426 ime_applied = true;
427 } else {
428 window.ime_enabled = false;
429 }
430 }
431
432 if !cursor_applied {
433 apply_window_cursor_feedback(
434 ui,
435 commands,
436 primary_entity,
437 &mut primary_cursor_options,
438 primary_cursor_icon.take(),
439 );
440 }
441
442 if !ime_applied {
443 apply_window_ime_feedback(
444 primary_entity,
445 &mut primary_window,
446 ime_data.WantTextInput,
447 ime_position,
448 true,
449 );
450 }
451}
452
453fn apply_window_ime_feedback(
454 entity: Entity,
455 window: &mut Window,
456 want_text_input: bool,
457 ime_position: [f32; 2],
458 is_primary: bool,
459) {
460 window.ime_enabled = want_text_input;
461 window.ime_position = ime_position_for_window(entity, window, ime_position, is_primary);
462}
463
464fn ime_position_for_window(
465 _entity: Entity,
466 window: &Window,
467 ime_position: [f32; 2],
468 is_primary: bool,
469) -> Vec2 {
470 let mut position = Vec2::new(ime_position[0], ime_position[1]);
471 #[cfg(all(feature = "multi-viewport", not(target_arch = "wasm32")))]
472 {
473 if let Some(origin) = crate::viewport::window_client_origin_logical(
474 _entity,
475 &window.position,
476 window.scale_factor(),
477 ) {
478 position.x -= origin[0];
479 position.y -= origin[1];
480 return position;
481 }
482 }
483
484 if !is_primary && let WindowPosition::At(window_position) = window.position {
485 let scale_factor = sanitized_window_framebuffer_scale(window)[0];
486 position.x -= window_position.x as f32 / scale_factor;
487 position.y -= window_position.y as f32 / scale_factor;
488 }
489 position
490}
491
492fn apply_window_cursor_feedback(
493 ui: &imgui::Ui,
494 commands: &mut Commands,
495 window_entity: Entity,
496 cursor_options: &mut CursorOptions,
497 cursor_icon: Option<Mut<CursorIcon>>,
498) {
499 let mouse_cursor = ui.mouse_cursor();
500 let draw_cursor = ui.io().mouse_draw_cursor();
501 let hide_os_cursor = draw_cursor || mouse_cursor.is_none();
502 cursor_options.visible = !hide_os_cursor;
503
504 let has_cursor_icon = cursor_icon.is_some();
505 if hide_os_cursor {
506 if has_cursor_icon {
507 commands.entity(window_entity).remove::<CursorIcon>();
508 }
509 } else if let Some(mouse_cursor) = mouse_cursor
510 && let Some(cursor_icon_value) = map_imgui_mouse_cursor(mouse_cursor)
511 {
512 match cursor_icon {
513 Some(mut current_cursor_icon) => {
514 *current_cursor_icon = cursor_icon_value;
515 }
516 None => {
517 commands.entity(window_entity).insert(cursor_icon_value);
518 }
519 }
520 }
521}
522
523fn clear_window_cursor_feedback(
524 commands: &mut Commands,
525 window_entity: Entity,
526 cursor_options: &mut CursorOptions,
527 cursor_icon: Option<Mut<CursorIcon>>,
528) {
529 cursor_options.visible = true;
530 if cursor_icon.is_some() {
531 commands.entity(window_entity).remove::<CursorIcon>();
532 }
533}