Skip to main content

dear_imgui_bevy/
lib.rs

1//! Experimental Bevy-native backend for `dear-imgui-rs`.
2//!
3//! This crate is the Bevy-side integration point for the Bevy Native Backend workstream. It is not
4//! a wrapper around `dear-imgui-winit` or `dear-imgui-wgpu`: Bevy owns windows, input, WGPU device
5//! state, render schedules, and camera targets. The backend owns only the Bevy plugin/resources that
6//! adapt those systems to Dear ImGui.
7//!
8//! # Compatibility and gates
9//!
10//! The first proof target is Bevy `0.19.0-rc.2`, which declares Rust `1.95.0`. The root
11//! `dear-imgui-rs` workspace currently remains on Rust `1.92`, so this crate has a dedicated
12//! `rust-version = "1.95.0"` and should be validated with an explicit Bevy gate, for example:
13//!
14//! ```text
15//! cargo +stable check -p dear-imgui-bevy --no-default-features
16//! cargo +stable check -p dear-imgui-bevy --features render
17//! cargo +stable check -p dear-imgui-bevy --target wasm32-unknown-unknown --no-default-features
18//! cargo +stable check -p dear-imgui-bevy --target wasm32-unknown-unknown --features render
19//! cargo +stable nextest run -p dear-imgui-bevy
20//! ```
21//!
22//! Core workspace gates should not silently rely on this crate until the repository-wide MSRV is
23//! intentionally raised or CI has a dedicated Rust 1.95+ Bevy lane. The crate currently compiles
24//! on `wasm32-unknown-unknown` for both the core and `render` feature sets; mobile targets remain a
25//! platform-specific follow-on if a future Bevy target train needs a dedicated gate.
26//!
27//! The crate also exposes `configure_example_context` for the shared example/editor ImGui setup
28//! pattern so the backend examples do not repeat the same initialization boilerplate.
29//!
30//! # Multi-viewport status
31//!
32//! `ImguiBackendConfig::multi_viewport` records an explicit request for Dear ImGui platform
33//! windows. With the `multi-viewport` and `render` features on native targets, the backend installs
34//! the PlatformIO lifecycle bridge, all-window input/platform feedback, and per-window render
35//! routing before advertising full multi-viewport support.
36
37pub mod context;
38pub mod helpers;
39pub mod input;
40pub mod schedule;
41pub mod texture;
42pub mod viewport;
43
44use bevy_app::{App, Plugin};
45use bevy_ecs::resource::Resource;
46
47pub use self::context::{ImguiContexts, ImguiFrameOutput, ImguiFrameState};
48pub use self::helpers::configure_example_context;
49pub use self::schedule::{ImguiBeginFrame, ImguiEndFrame, ImguiPrimaryContextPass};
50#[cfg(feature = "render")]
51pub use self::texture::ImguiBevyTextures;
52pub use self::texture::ImguiTextureFeedbackQueue;
53pub use self::viewport::{
54    ImguiViewportBridge, ImguiViewportCamera, ImguiViewportCommand, ImguiViewportFeedback,
55    ImguiViewportId, ImguiViewportSnapshot, ImguiViewportWindow,
56};
57
58const MULTI_VIEWPORT_FEATURE_ENABLED: bool = cfg!(feature = "multi-viewport");
59const NATIVE_PLATFORM_TARGET: bool = !cfg!(target_arch = "wasm32");
60
61/// Bevy plugin that installs the minimal Dear ImGui resources.
62///
63/// Later workstream tasks add input collection, frame scheduling, render extraction, and renderer
64/// systems. For now the plugin establishes ownership boundaries and resource locations only.
65#[derive(Debug, Clone, Default)]
66pub struct ImguiPlugin {
67    config: ImguiBackendConfig,
68}
69
70impl ImguiPlugin {
71    /// Create a plugin with explicit backend configuration.
72    #[must_use]
73    pub fn new(config: ImguiBackendConfig) -> Self {
74        Self { config }
75    }
76
77    /// Borrow the plugin configuration.
78    #[must_use]
79    pub fn config(&self) -> &ImguiBackendConfig {
80        &self.config
81    }
82}
83
84impl Plugin for ImguiPlugin {
85    fn build(&self, app: &mut App) {
86        if !app.world().contains_resource::<ImguiBackendConfig>() {
87            app.insert_resource(self.config.clone());
88        }
89        if app.world().get_non_send::<ImguiContext>().is_none() {
90            app.insert_non_send(ImguiContext::new(dear_imgui_rs::Context::create()));
91        }
92        schedule::install_imgui_schedules(app);
93        input::install_input_mapping(app);
94        context::install_context_lifecycle(app);
95        viewport::install_viewport_bridge(app);
96        #[cfg(feature = "render")]
97        let render_integration_installed = render::install_render_extraction(app);
98        #[cfg(not(feature = "render"))]
99        let render_integration_installed = false;
100        refresh_backend_status(app, render_integration_installed);
101    }
102
103    fn finish(&self, _app: &mut App) {
104        #[cfg(feature = "render")]
105        {
106            let render_integration_installed = render::install_render_extraction(_app);
107            refresh_backend_status(_app, render_integration_installed);
108        }
109    }
110}
111
112fn refresh_backend_status(app: &mut App, render_integration_installed: bool) {
113    let effective_config = app.world().resource::<ImguiBackendConfig>().clone();
114    sync_backend_context_config(app, &effective_config, render_integration_installed);
115    app.insert_resource(ImguiBackendStatus::from_config(
116        &effective_config,
117        render_integration_installed,
118    ));
119}
120
121fn sync_backend_context_config(
122    app: &mut App,
123    config: &ImguiBackendConfig,
124    render_integration_installed: bool,
125) {
126    let Some(mut imgui_context) = app.world_mut().get_non_send_mut::<ImguiContext>() else {
127        return;
128    };
129    let context = imgui_context.context_mut();
130    let mut config_flags = context.io().config_flags();
131    if config.docking {
132        config_flags.insert(dear_imgui_rs::ConfigFlags::DOCKING_ENABLE);
133    } else {
134        config_flags.remove(dear_imgui_rs::ConfigFlags::DOCKING_ENABLE);
135    }
136    context.io_mut().set_config_flags(config_flags);
137
138    let imgui_name = sanitized_imgui_backend_name(&config.name);
139    context
140        .set_platform_name(Some(imgui_name.clone()))
141        .expect("sanitized backend names must be valid C strings");
142    if !config.multi_viewport || !MULTI_VIEWPORT_FEATURE_ENABLED || !NATIVE_PLATFORM_TARGET {
143        context
144            .io_mut()
145            .set_backend_platform_user_data(std::ptr::null_mut());
146        clear_platform_backend_handlers(context);
147    }
148    context
149        .io_mut()
150        .set_backend_renderer_user_data(std::ptr::null_mut());
151    clear_renderer_backend_handlers(context);
152    let mut backend_flags = context.io().backend_flags();
153    if render_integration_installed {
154        #[cfg(feature = "render")]
155        render::install_standard_draw_callbacks_for_context(context);
156        backend_flags.insert(
157            dear_imgui_rs::BackendFlags::RENDERER_HAS_TEXTURES
158                | dear_imgui_rs::BackendFlags::RENDERER_HAS_VTX_OFFSET,
159        );
160        context
161            .set_renderer_name(Some(imgui_name))
162            .expect("sanitized backend names must be valid C strings");
163    } else {
164        backend_flags.remove(
165            dear_imgui_rs::BackendFlags::RENDERER_HAS_TEXTURES
166                | dear_imgui_rs::BackendFlags::RENDERER_HAS_VTX_OFFSET,
167        );
168        context
169            .set_renderer_name::<String>(None)
170            .expect("clearing BackendRendererName must not fail");
171    }
172    context.io_mut().set_backend_flags(backend_flags);
173}
174
175fn sanitized_imgui_backend_name(name: &str) -> String {
176    name.replace('\0', "?")
177}
178
179/// Static configuration for the Bevy backend.
180#[derive(Resource, Debug, Clone, Eq, PartialEq)]
181pub struct ImguiBackendConfig {
182    /// User-facing label recorded in the Dear ImGui context and diagnostics.
183    pub name: String,
184    /// Whether the backend should request docking support when lifecycle code wires IO flags.
185    pub docking: bool,
186    /// Whether the user requested Dear ImGui docking multi-viewport OS windows.
187    ///
188    /// This is recorded in [`ImguiBackendStatus::multi_viewport_requested`]. Full support is only
189    /// advertised after the native PlatformIO lifecycle bridge, all-window input feedback, and
190    /// secondary viewport render routing are all available.
191    pub multi_viewport: bool,
192}
193
194impl Default for ImguiBackendConfig {
195    fn default() -> Self {
196        Self {
197            name: "dear-imgui-bevy".to_owned(),
198            docking: true,
199            multi_viewport: false,
200        }
201    }
202}
203
204/// Observable backend state installed by [`ImguiPlugin`].
205#[derive(Resource, Debug, Clone, Eq, PartialEq)]
206pub struct ImguiBackendStatus {
207    /// First Bevy version targeted by this crate skeleton.
208    pub bevy_target: &'static str,
209    /// Rust version required by the Bevy target train.
210    pub rust_target: &'static str,
211    /// Whether render integration has been compiled in.
212    pub render_feature_enabled: bool,
213    /// Whether render-world extraction and overlay systems were installed into Bevy's `RenderApp`.
214    pub render_integration_installed: bool,
215    /// Whether the current backend configuration requested Dear ImGui platform windows.
216    pub multi_viewport_requested: bool,
217    /// Whether the Cargo feature needed to compile PlatformIO viewport callbacks is enabled.
218    pub multi_viewport_feature_enabled: bool,
219    /// Whether the current target can use native Bevy OS windows for Dear ImGui platform windows.
220    pub native_platform_target: bool,
221    /// Whether PlatformIO lifecycle callbacks can be connected to Bevy-owned window entities.
222    pub viewport_lifecycle_bridge_enabled: bool,
223    /// Whether input, focus, cursor, DPI, and IME feedback covers all Dear ImGui platform windows.
224    pub viewport_input_feedback_enabled: bool,
225    /// Whether secondary Dear ImGui viewport draw data is routed to matching Bevy window targets.
226    pub viewport_render_routing_enabled: bool,
227    /// Whether the backend currently wires the required Bevy OS-window platform callbacks.
228    ///
229    /// This remains `false` until lifecycle, input feedback, and renderer routing are all wired.
230    /// The `multi-viewport` feature may install an internal lifecycle bridge before the backend is
231    /// ready to advertise full Dear ImGui OS-level viewport support.
232    pub multi_viewport_supported: bool,
233}
234
235impl ImguiBackendStatus {
236    fn from_config(config: &ImguiBackendConfig, render_integration_installed: bool) -> Self {
237        let viewport_lifecycle_bridge_enabled =
238            config.multi_viewport && MULTI_VIEWPORT_FEATURE_ENABLED && NATIVE_PLATFORM_TARGET;
239        let viewport_input_feedback_enabled =
240            config.multi_viewport && MULTI_VIEWPORT_FEATURE_ENABLED && NATIVE_PLATFORM_TARGET;
241        let viewport_render_routing_enabled = config.multi_viewport
242            && MULTI_VIEWPORT_FEATURE_ENABLED
243            && NATIVE_PLATFORM_TARGET
244            && render_integration_installed;
245
246        Self {
247            bevy_target: BEVY_TARGET_VERSION,
248            rust_target: RUST_TARGET_VERSION,
249            render_feature_enabled: cfg!(feature = "render"),
250            render_integration_installed,
251            multi_viewport_requested: config.multi_viewport,
252            multi_viewport_feature_enabled: MULTI_VIEWPORT_FEATURE_ENABLED,
253            native_platform_target: NATIVE_PLATFORM_TARGET,
254            viewport_lifecycle_bridge_enabled,
255            viewport_input_feedback_enabled,
256            viewport_render_routing_enabled,
257            multi_viewport_supported: viewport_lifecycle_bridge_enabled
258                && viewport_input_feedback_enabled
259                && viewport_render_routing_enabled,
260        }
261    }
262}
263
264impl Default for ImguiBackendStatus {
265    fn default() -> Self {
266        Self::from_config(&ImguiBackendConfig::default(), false)
267    }
268}
269
270/// Non-send Bevy resource that owns the Dear ImGui context.
271///
272/// Dear ImGui has process-global current-context state and `dear_imgui_rs::Context` is intentionally
273/// not `Send`/`Sync`. Storing it as a Bevy non-send resource keeps UI lifecycle work on the main
274/// thread until later tasks add schedule-specific accessors.
275pub struct ImguiContext {
276    context: dear_imgui_rs::Context,
277}
278
279impl ImguiContext {
280    /// Wrap an existing Dear ImGui context for insertion into a Bevy world.
281    #[must_use]
282    pub fn new(context: dear_imgui_rs::Context) -> Self {
283        Self { context }
284    }
285
286    /// Borrow the inner Dear ImGui context.
287    #[must_use]
288    pub fn context(&self) -> &dear_imgui_rs::Context {
289        &self.context
290    }
291
292    /// Mutably borrow the inner Dear ImGui context.
293    #[must_use]
294    pub fn context_mut(&mut self) -> &mut dear_imgui_rs::Context {
295        &mut self.context
296    }
297
298    /// Consume the wrapper and return the Dear ImGui context.
299    #[must_use]
300    pub fn into_inner(mut self) -> dear_imgui_rs::Context {
301        self.clear_backend_data();
302        let this = std::mem::ManuallyDrop::new(self);
303        // SAFETY: `this` will not run `Drop`, and we return ownership of the inner context to the
304        // caller exactly once.
305        unsafe { std::ptr::read(&this.context) }
306    }
307
308    fn clear_backend_data(&mut self) {
309        self.context
310            .io_mut()
311            .set_backend_platform_user_data(std::ptr::null_mut());
312        self.context
313            .set_platform_name::<String>(None)
314            .expect("clearing BackendPlatformName must not fail");
315        self.context
316            .io_mut()
317            .set_backend_renderer_user_data(std::ptr::null_mut());
318        self.context
319            .set_renderer_name::<String>(None)
320            .expect("clearing BackendRendererName must not fail");
321        clear_renderer_backend_handlers(&mut self.context);
322
323        let mut backend_flags = self.context.io().backend_flags();
324        backend_flags.remove(
325            dear_imgui_rs::BackendFlags::RENDERER_HAS_TEXTURES
326                | dear_imgui_rs::BackendFlags::RENDERER_HAS_VTX_OFFSET
327                | dear_imgui_rs::BackendFlags::HAS_MOUSE_HOVERED_VIEWPORT,
328        );
329        #[cfg(feature = "multi-viewport")]
330        {
331            backend_flags.remove(
332                dear_imgui_rs::BackendFlags::PLATFORM_HAS_VIEWPORTS
333                    | dear_imgui_rs::BackendFlags::RENDERER_HAS_VIEWPORTS,
334            );
335            self.context.destroy_platform_windows();
336        }
337        clear_platform_backend_handlers(&mut self.context);
338        self.context.io_mut().set_backend_flags(backend_flags);
339    }
340}
341
342pub(crate) fn clear_platform_backend_handlers(context: &mut dear_imgui_rs::Context) {
343    let platform_io = context.platform_io_mut();
344    let clipboard_handlers = ClipboardPlatformHandlers::capture(platform_io.as_raw());
345    #[cfg(feature = "multi-viewport")]
346    {
347        platform_io.clear_platform_handlers();
348    }
349    #[cfg(not(feature = "multi-viewport"))]
350    unsafe {
351        dear_imgui_rs::sys::ImGuiPlatformIO_ClearPlatformHandlers(platform_io.as_raw_mut());
352    }
353    clipboard_handlers.restore(platform_io.as_raw_mut());
354}
355
356struct ClipboardPlatformHandlers {
357    get: Option<
358        unsafe extern "C" fn(ctx: *mut dear_imgui_rs::sys::ImGuiContext) -> *const std::ffi::c_char,
359    >,
360    set: Option<
361        unsafe extern "C" fn(
362            ctx: *mut dear_imgui_rs::sys::ImGuiContext,
363            text: *const std::ffi::c_char,
364        ),
365    >,
366    user_data: *mut std::ffi::c_void,
367}
368
369impl ClipboardPlatformHandlers {
370    fn capture(platform_io: *const dear_imgui_rs::sys::ImGuiPlatformIO) -> Self {
371        let platform_io = unsafe { &*platform_io };
372        Self {
373            get: platform_io.Platform_GetClipboardTextFn,
374            set: platform_io.Platform_SetClipboardTextFn,
375            user_data: platform_io.Platform_ClipboardUserData,
376        }
377    }
378
379    fn restore(self, platform_io: *mut dear_imgui_rs::sys::ImGuiPlatformIO) {
380        let platform_io = unsafe { &mut *platform_io };
381        platform_io.Platform_GetClipboardTextFn = self.get;
382        platform_io.Platform_SetClipboardTextFn = self.set;
383        platform_io.Platform_ClipboardUserData = self.user_data;
384    }
385}
386
387fn clear_renderer_backend_handlers(context: &mut dear_imgui_rs::Context) {
388    let platform_io = context.platform_io_mut();
389    #[cfg(feature = "multi-viewport")]
390    {
391        platform_io.clear_renderer_handlers();
392    }
393    #[cfg(not(feature = "multi-viewport"))]
394    unsafe {
395        dear_imgui_rs::sys::ImGuiPlatformIO_ClearRendererHandlers(platform_io.as_raw_mut());
396    }
397}
398
399impl Drop for ImguiContext {
400    fn drop(&mut self) {
401        self.clear_backend_data();
402    }
403}
404
405/// First Bevy version targeted by this crate.
406pub const BEVY_TARGET_VERSION: &str = "0.19.0-rc.2";
407/// Bevy reference commit used by the workstream.
408pub const BEVY_TARGET_COMMIT: &str = "a389b928aee5906928a16a7d4e66cb02c7362901";
409/// Rust version required by the first Bevy target train.
410pub const RUST_TARGET_VERSION: &str = "1.95.0";
411/// WGPU version used by Bevy `0.19.0-rc.2`.
412pub const WGPU_TARGET_VERSION: &str = "29.0.3";
413
414#[cfg(feature = "render")]
415pub mod render;