dear-imgui-bevy 0.15.0

Experimental Bevy-native backend for dear-imgui-rs on Bevy 0.19.0-rc.2
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
//! Experimental Bevy-native backend for `dear-imgui-rs`.
//!
//! This crate is the Bevy-side integration point for the Bevy Native Backend workstream. It is not
//! a wrapper around `dear-imgui-winit` or `dear-imgui-wgpu`: Bevy owns windows, input, WGPU device
//! state, render schedules, and camera targets. The backend owns only the Bevy plugin/resources that
//! adapt those systems to Dear ImGui.
//!
//! # Compatibility and gates
//!
//! The first proof target is Bevy `0.19.0-rc.2`, which declares Rust `1.95.0`. The root
//! `dear-imgui-rs` workspace currently remains on Rust `1.92`, so this crate has a dedicated
//! `rust-version = "1.95.0"` and should be validated with an explicit Bevy gate, for example:
//!
//! ```text
//! cargo +stable check -p dear-imgui-bevy --no-default-features
//! cargo +stable check -p dear-imgui-bevy --features render
//! cargo +stable check -p dear-imgui-bevy --target wasm32-unknown-unknown --no-default-features
//! cargo +stable check -p dear-imgui-bevy --target wasm32-unknown-unknown --features render
//! cargo +stable nextest run -p dear-imgui-bevy
//! ```
//!
//! Core workspace gates should not silently rely on this crate until the repository-wide MSRV is
//! intentionally raised or CI has a dedicated Rust 1.95+ Bevy lane. The crate currently compiles
//! on `wasm32-unknown-unknown` for both the core and `render` feature sets; mobile targets remain a
//! platform-specific follow-on if a future Bevy target train needs a dedicated gate.
//!
//! The crate also exposes `configure_example_context` for the shared example/editor ImGui setup
//! pattern so the backend examples do not repeat the same initialization boilerplate.
//!
//! # Multi-viewport status
//!
//! `ImguiBackendConfig::multi_viewport` records an explicit request for Dear ImGui platform
//! windows. With the `multi-viewport` and `render` features on native targets, the backend installs
//! the PlatformIO lifecycle bridge, all-window input/platform feedback, and per-window render
//! routing before advertising full multi-viewport support.

pub mod context;
pub mod helpers;
pub mod input;
pub mod schedule;
pub mod texture;
pub mod viewport;

use bevy_app::{App, Plugin};
use bevy_ecs::resource::Resource;

pub use self::context::{ImguiContexts, ImguiFrameOutput, ImguiFrameState};
pub use self::helpers::configure_example_context;
pub use self::schedule::{ImguiBeginFrame, ImguiEndFrame, ImguiPrimaryContextPass};
#[cfg(feature = "render")]
pub use self::texture::ImguiBevyTextures;
pub use self::texture::ImguiTextureFeedbackQueue;
pub use self::viewport::{
    ImguiViewportBridge, ImguiViewportCamera, ImguiViewportCommand, ImguiViewportFeedback,
    ImguiViewportId, ImguiViewportSnapshot, ImguiViewportWindow,
};

const MULTI_VIEWPORT_FEATURE_ENABLED: bool = cfg!(feature = "multi-viewport");
const NATIVE_PLATFORM_TARGET: bool = !cfg!(target_arch = "wasm32");

/// Bevy plugin that installs the minimal Dear ImGui resources.
///
/// Later workstream tasks add input collection, frame scheduling, render extraction, and renderer
/// systems. For now the plugin establishes ownership boundaries and resource locations only.
#[derive(Debug, Clone, Default)]
pub struct ImguiPlugin {
    config: ImguiBackendConfig,
}

impl ImguiPlugin {
    /// Create a plugin with explicit backend configuration.
    #[must_use]
    pub fn new(config: ImguiBackendConfig) -> Self {
        Self { config }
    }

    /// Borrow the plugin configuration.
    #[must_use]
    pub fn config(&self) -> &ImguiBackendConfig {
        &self.config
    }
}

impl Plugin for ImguiPlugin {
    fn build(&self, app: &mut App) {
        if !app.world().contains_resource::<ImguiBackendConfig>() {
            app.insert_resource(self.config.clone());
        }
        if app.world().get_non_send::<ImguiContext>().is_none() {
            app.insert_non_send(ImguiContext::new(dear_imgui_rs::Context::create()));
        }
        schedule::install_imgui_schedules(app);
        input::install_input_mapping(app);
        context::install_context_lifecycle(app);
        viewport::install_viewport_bridge(app);
        #[cfg(feature = "render")]
        let render_integration_installed = render::install_render_extraction(app);
        #[cfg(not(feature = "render"))]
        let render_integration_installed = false;
        refresh_backend_status(app, render_integration_installed);
    }

    fn finish(&self, _app: &mut App) {
        #[cfg(feature = "render")]
        {
            let render_integration_installed = render::install_render_extraction(_app);
            refresh_backend_status(_app, render_integration_installed);
        }
    }
}

fn refresh_backend_status(app: &mut App, render_integration_installed: bool) {
    let effective_config = app.world().resource::<ImguiBackendConfig>().clone();
    sync_backend_context_config(app, &effective_config, render_integration_installed);
    app.insert_resource(ImguiBackendStatus::from_config(
        &effective_config,
        render_integration_installed,
    ));
}

fn sync_backend_context_config(
    app: &mut App,
    config: &ImguiBackendConfig,
    render_integration_installed: bool,
) {
    let Some(mut imgui_context) = app.world_mut().get_non_send_mut::<ImguiContext>() else {
        return;
    };
    let context = imgui_context.context_mut();
    let mut config_flags = context.io().config_flags();
    if config.docking {
        config_flags.insert(dear_imgui_rs::ConfigFlags::DOCKING_ENABLE);
    } else {
        config_flags.remove(dear_imgui_rs::ConfigFlags::DOCKING_ENABLE);
    }
    context.io_mut().set_config_flags(config_flags);

    let imgui_name = sanitized_imgui_backend_name(&config.name);
    context
        .set_platform_name(Some(imgui_name.clone()))
        .expect("sanitized backend names must be valid C strings");
    if !config.multi_viewport || !MULTI_VIEWPORT_FEATURE_ENABLED || !NATIVE_PLATFORM_TARGET {
        context
            .io_mut()
            .set_backend_platform_user_data(std::ptr::null_mut());
        clear_platform_backend_handlers(context);
    }
    context
        .io_mut()
        .set_backend_renderer_user_data(std::ptr::null_mut());
    clear_renderer_backend_handlers(context);
    let mut backend_flags = context.io().backend_flags();
    if render_integration_installed {
        #[cfg(feature = "render")]
        render::install_standard_draw_callbacks_for_context(context);
        backend_flags.insert(
            dear_imgui_rs::BackendFlags::RENDERER_HAS_TEXTURES
                | dear_imgui_rs::BackendFlags::RENDERER_HAS_VTX_OFFSET,
        );
        context
            .set_renderer_name(Some(imgui_name))
            .expect("sanitized backend names must be valid C strings");
    } else {
        backend_flags.remove(
            dear_imgui_rs::BackendFlags::RENDERER_HAS_TEXTURES
                | dear_imgui_rs::BackendFlags::RENDERER_HAS_VTX_OFFSET,
        );
        context
            .set_renderer_name::<String>(None)
            .expect("clearing BackendRendererName must not fail");
    }
    context.io_mut().set_backend_flags(backend_flags);
}

fn sanitized_imgui_backend_name(name: &str) -> String {
    name.replace('\0', "?")
}

/// Static configuration for the Bevy backend.
#[derive(Resource, Debug, Clone, Eq, PartialEq)]
pub struct ImguiBackendConfig {
    /// User-facing label recorded in the Dear ImGui context and diagnostics.
    pub name: String,
    /// Whether the backend should request docking support when lifecycle code wires IO flags.
    pub docking: bool,
    /// Whether the user requested Dear ImGui docking multi-viewport OS windows.
    ///
    /// This is recorded in [`ImguiBackendStatus::multi_viewport_requested`]. Full support is only
    /// advertised after the native PlatformIO lifecycle bridge, all-window input feedback, and
    /// secondary viewport render routing are all available.
    pub multi_viewport: bool,
}

impl Default for ImguiBackendConfig {
    fn default() -> Self {
        Self {
            name: "dear-imgui-bevy".to_owned(),
            docking: true,
            multi_viewport: false,
        }
    }
}

/// Observable backend state installed by [`ImguiPlugin`].
#[derive(Resource, Debug, Clone, Eq, PartialEq)]
pub struct ImguiBackendStatus {
    /// First Bevy version targeted by this crate skeleton.
    pub bevy_target: &'static str,
    /// Rust version required by the Bevy target train.
    pub rust_target: &'static str,
    /// Whether render integration has been compiled in.
    pub render_feature_enabled: bool,
    /// Whether render-world extraction and overlay systems were installed into Bevy's `RenderApp`.
    pub render_integration_installed: bool,
    /// Whether the current backend configuration requested Dear ImGui platform windows.
    pub multi_viewport_requested: bool,
    /// Whether the Cargo feature needed to compile PlatformIO viewport callbacks is enabled.
    pub multi_viewport_feature_enabled: bool,
    /// Whether the current target can use native Bevy OS windows for Dear ImGui platform windows.
    pub native_platform_target: bool,
    /// Whether PlatformIO lifecycle callbacks can be connected to Bevy-owned window entities.
    pub viewport_lifecycle_bridge_enabled: bool,
    /// Whether input, focus, cursor, DPI, and IME feedback covers all Dear ImGui platform windows.
    pub viewport_input_feedback_enabled: bool,
    /// Whether secondary Dear ImGui viewport draw data is routed to matching Bevy window targets.
    pub viewport_render_routing_enabled: bool,
    /// Whether the backend currently wires the required Bevy OS-window platform callbacks.
    ///
    /// This remains `false` until lifecycle, input feedback, and renderer routing are all wired.
    /// The `multi-viewport` feature may install an internal lifecycle bridge before the backend is
    /// ready to advertise full Dear ImGui OS-level viewport support.
    pub multi_viewport_supported: bool,
}

impl ImguiBackendStatus {
    fn from_config(config: &ImguiBackendConfig, render_integration_installed: bool) -> Self {
        let viewport_lifecycle_bridge_enabled =
            config.multi_viewport && MULTI_VIEWPORT_FEATURE_ENABLED && NATIVE_PLATFORM_TARGET;
        let viewport_input_feedback_enabled =
            config.multi_viewport && MULTI_VIEWPORT_FEATURE_ENABLED && NATIVE_PLATFORM_TARGET;
        let viewport_render_routing_enabled = config.multi_viewport
            && MULTI_VIEWPORT_FEATURE_ENABLED
            && NATIVE_PLATFORM_TARGET
            && render_integration_installed;

        Self {
            bevy_target: BEVY_TARGET_VERSION,
            rust_target: RUST_TARGET_VERSION,
            render_feature_enabled: cfg!(feature = "render"),
            render_integration_installed,
            multi_viewport_requested: config.multi_viewport,
            multi_viewport_feature_enabled: MULTI_VIEWPORT_FEATURE_ENABLED,
            native_platform_target: NATIVE_PLATFORM_TARGET,
            viewport_lifecycle_bridge_enabled,
            viewport_input_feedback_enabled,
            viewport_render_routing_enabled,
            multi_viewport_supported: viewport_lifecycle_bridge_enabled
                && viewport_input_feedback_enabled
                && viewport_render_routing_enabled,
        }
    }
}

impl Default for ImguiBackendStatus {
    fn default() -> Self {
        Self::from_config(&ImguiBackendConfig::default(), false)
    }
}

/// Non-send Bevy resource that owns the Dear ImGui context.
///
/// Dear ImGui has process-global current-context state and `dear_imgui_rs::Context` is intentionally
/// not `Send`/`Sync`. Storing it as a Bevy non-send resource keeps UI lifecycle work on the main
/// thread until later tasks add schedule-specific accessors.
pub struct ImguiContext {
    context: dear_imgui_rs::Context,
}

impl ImguiContext {
    /// Wrap an existing Dear ImGui context for insertion into a Bevy world.
    #[must_use]
    pub fn new(context: dear_imgui_rs::Context) -> Self {
        Self { context }
    }

    /// Borrow the inner Dear ImGui context.
    #[must_use]
    pub fn context(&self) -> &dear_imgui_rs::Context {
        &self.context
    }

    /// Mutably borrow the inner Dear ImGui context.
    #[must_use]
    pub fn context_mut(&mut self) -> &mut dear_imgui_rs::Context {
        &mut self.context
    }

    /// Consume the wrapper and return the Dear ImGui context.
    #[must_use]
    pub fn into_inner(mut self) -> dear_imgui_rs::Context {
        self.clear_backend_data();
        let this = std::mem::ManuallyDrop::new(self);
        // SAFETY: `this` will not run `Drop`, and we return ownership of the inner context to the
        // caller exactly once.
        unsafe { std::ptr::read(&this.context) }
    }

    fn clear_backend_data(&mut self) {
        self.context
            .io_mut()
            .set_backend_platform_user_data(std::ptr::null_mut());
        self.context
            .set_platform_name::<String>(None)
            .expect("clearing BackendPlatformName must not fail");
        self.context
            .io_mut()
            .set_backend_renderer_user_data(std::ptr::null_mut());
        self.context
            .set_renderer_name::<String>(None)
            .expect("clearing BackendRendererName must not fail");
        clear_renderer_backend_handlers(&mut self.context);

        let mut backend_flags = self.context.io().backend_flags();
        backend_flags.remove(
            dear_imgui_rs::BackendFlags::RENDERER_HAS_TEXTURES
                | dear_imgui_rs::BackendFlags::RENDERER_HAS_VTX_OFFSET
                | dear_imgui_rs::BackendFlags::HAS_MOUSE_HOVERED_VIEWPORT,
        );
        #[cfg(feature = "multi-viewport")]
        {
            backend_flags.remove(
                dear_imgui_rs::BackendFlags::PLATFORM_HAS_VIEWPORTS
                    | dear_imgui_rs::BackendFlags::RENDERER_HAS_VIEWPORTS,
            );
            self.context.destroy_platform_windows();
        }
        clear_platform_backend_handlers(&mut self.context);
        self.context.io_mut().set_backend_flags(backend_flags);
    }
}

pub(crate) fn clear_platform_backend_handlers(context: &mut dear_imgui_rs::Context) {
    let platform_io = context.platform_io_mut();
    let clipboard_handlers = ClipboardPlatformHandlers::capture(platform_io.as_raw());
    #[cfg(feature = "multi-viewport")]
    {
        platform_io.clear_platform_handlers();
    }
    #[cfg(not(feature = "multi-viewport"))]
    unsafe {
        dear_imgui_rs::sys::ImGuiPlatformIO_ClearPlatformHandlers(platform_io.as_raw_mut());
    }
    clipboard_handlers.restore(platform_io.as_raw_mut());
}

struct ClipboardPlatformHandlers {
    get: Option<
        unsafe extern "C" fn(ctx: *mut dear_imgui_rs::sys::ImGuiContext) -> *const std::ffi::c_char,
    >,
    set: Option<
        unsafe extern "C" fn(
            ctx: *mut dear_imgui_rs::sys::ImGuiContext,
            text: *const std::ffi::c_char,
        ),
    >,
    user_data: *mut std::ffi::c_void,
}

impl ClipboardPlatformHandlers {
    fn capture(platform_io: *const dear_imgui_rs::sys::ImGuiPlatformIO) -> Self {
        let platform_io = unsafe { &*platform_io };
        Self {
            get: platform_io.Platform_GetClipboardTextFn,
            set: platform_io.Platform_SetClipboardTextFn,
            user_data: platform_io.Platform_ClipboardUserData,
        }
    }

    fn restore(self, platform_io: *mut dear_imgui_rs::sys::ImGuiPlatformIO) {
        let platform_io = unsafe { &mut *platform_io };
        platform_io.Platform_GetClipboardTextFn = self.get;
        platform_io.Platform_SetClipboardTextFn = self.set;
        platform_io.Platform_ClipboardUserData = self.user_data;
    }
}

fn clear_renderer_backend_handlers(context: &mut dear_imgui_rs::Context) {
    let platform_io = context.platform_io_mut();
    #[cfg(feature = "multi-viewport")]
    {
        platform_io.clear_renderer_handlers();
    }
    #[cfg(not(feature = "multi-viewport"))]
    unsafe {
        dear_imgui_rs::sys::ImGuiPlatformIO_ClearRendererHandlers(platform_io.as_raw_mut());
    }
}

impl Drop for ImguiContext {
    fn drop(&mut self) {
        self.clear_backend_data();
    }
}

/// First Bevy version targeted by this crate.
pub const BEVY_TARGET_VERSION: &str = "0.19.0-rc.2";
/// Bevy reference commit used by the workstream.
pub const BEVY_TARGET_COMMIT: &str = "a389b928aee5906928a16a7d4e66cb02c7362901";
/// Rust version required by the first Bevy target train.
pub const RUST_TARGET_VERSION: &str = "1.95.0";
/// WGPU version used by Bevy `0.19.0-rc.2`.
pub const WGPU_TARGET_VERSION: &str = "29.0.3";

#[cfg(feature = "render")]
pub mod render;