Skip to main content

dear_imgui_bevy/
context.rs

1//! Main-world Dear ImGui context lifecycle for Bevy.
2//!
3//! This module bridges Bevy's schedule model to Dear ImGui's immediate-mode frame model. User
4//! systems should be added to [`crate::ImguiPrimaryContextPass`] and request [`ImguiContexts`]
5//! instead of calling `Context::frame()` / `Context::render()` directly.
6
7#[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/// Output produced by the last completed Dear ImGui frame.
75#[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    /// Monotonic primary-context frame index for the latest completed frame.
84    #[must_use]
85    pub fn frame_index(&self) -> u64 {
86        self.frame_index
87    }
88
89    /// Thread-safe snapshot produced by the latest completed frame.
90    #[must_use]
91    pub fn snapshot(&self) -> Option<&imgui::render::snapshot::FrameSnapshot> {
92        self.snapshot.as_ref()
93    }
94
95    /// Snapshot error produced by the latest completed frame, if snapshotting failed.
96    #[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/// Non-send state for the currently open Bevy-managed Dear ImGui frame.
130///
131/// The raw pointer stored here is valid only between `ImguiBeginFrame` and `ImguiEndFrame`. It
132/// points to the `Ui` owned by the non-send [`ImguiContext`] resource and is never sent to another
133/// thread.
134#[derive(Default)]
135pub struct ImguiFrameState {
136    frame_index: u64,
137    ui: Option<NonNull<imgui::Ui>>,
138}
139
140impl ImguiFrameState {
141    /// Current or most recently opened frame index.
142    #[must_use]
143    pub fn frame_index(&self) -> u64 {
144        self.frame_index
145    }
146
147    /// Whether the Bevy-managed Dear ImGui frame is currently open.
148    #[must_use]
149    pub fn is_frame_open(&self) -> bool {
150        self.ui.is_some()
151    }
152
153    /// Borrow the currently open `Ui`, if called inside `ImguiPrimaryContextPass`.
154    #[must_use]
155    pub fn ui(&self) -> Option<&imgui::Ui> {
156        let ui = self.ui?;
157        // SAFETY: `ui` is set from the live `ImguiContext` in `begin_primary_frame_system` and
158        // cleared by `end_primary_frame_system` before the context renders/closes the frame.
159        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/// Bevy system param used by user UI systems in [`crate::ImguiPrimaryContextPass`].
174#[derive(SystemParam)]
175pub struct ImguiContexts<'w> {
176    frame_state: NonSend<'w, ImguiFrameState>,
177    _main_thread: NonSendMarker,
178}
179
180impl<'w> ImguiContexts<'w> {
181    /// Borrow the primary Dear ImGui `Ui` for the current frame.
182    #[must_use]
183    pub fn primary_ui_mut(&mut self) -> Option<&imgui::Ui> {
184        self.frame_state.ui()
185    }
186
187    /// Current primary-context frame index, if a frame is open.
188    #[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    // SAFETY: `ImguiContext` owns this live Dear ImGui context for the lifetime of the Bevy
362    // resource, and the frame is still open while platform feedback is synchronized.
363    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}