Skip to main content

dear_imgui_rs/context/
platform.rs

1use crate::sys;
2
3use super::Context;
4#[cfg(feature = "multi-viewport")]
5use super::attachment::{ContextPlatformWindowTeardown, ContextPlatformWindowTeardownError};
6use super::binding::{CTX_MUTEX, with_bound_context};
7
8#[cfg(feature = "multi-viewport")]
9struct PlatformDrawDataTextureMask {
10    entries: Vec<(
11        std::ptr::NonNull<sys::ImDrawData>,
12        *mut sys::ImVector_ImTextureDataPtr,
13    )>,
14}
15
16#[cfg(feature = "multi-viewport")]
17impl PlatformDrawDataTextureMask {
18    unsafe fn install(platform_io: *mut sys::ImGuiPlatformIO) -> Self {
19        let mut entries = Vec::new();
20        let viewports = unsafe { &(*platform_io).Viewports };
21        if viewports.Size <= 0 {
22            return Self { entries };
23        }
24        assert!(
25            viewports.Capacity >= viewports.Size && !viewports.Data.is_null(),
26            "ImGuiPlatformIO.Viewports has invalid native storage"
27        );
28        for index in 0..viewports.Size as usize {
29            let viewport = unsafe { *viewports.Data.add(index) };
30            let Some(viewport) = std::ptr::NonNull::new(viewport) else {
31                continue;
32            };
33            let Some(draw_data) = std::ptr::NonNull::new(unsafe { viewport.as_ref().DrawData })
34            else {
35                continue;
36            };
37            let textures = unsafe { draw_data.as_ref().Textures };
38            if textures.is_null() {
39                continue;
40            }
41            unsafe { (*draw_data.as_ptr()).Textures = std::ptr::null_mut() };
42            entries.push((draw_data, textures));
43        }
44        Self { entries }
45    }
46}
47
48#[cfg(feature = "multi-viewport")]
49impl Drop for PlatformDrawDataTextureMask {
50    fn drop(&mut self) {
51        for (draw_data, textures) in self.entries.drain(..) {
52            unsafe { (*draw_data.as_ptr()).Textures = textures };
53        }
54    }
55}
56
57impl Context {
58    /// Get shared access to the platform IO.
59    ///
60    /// Note: `ImGuiPlatformIO` exists even when multi-viewport is disabled. We expose it
61    /// unconditionally so callers can use ImGui 1.92+ texture management via `PlatformIO.Textures[]`.
62    #[doc(alias = "GetPlatformIO")]
63    pub fn platform_io(&self) -> &crate::platform_io::PlatformIo {
64        let _guard = CTX_MUTEX.lock();
65        unsafe {
66            let pio = self.platform_io_ptr("Context::platform_io()");
67            crate::platform_io::PlatformIo::from_raw(pio)
68        }
69    }
70
71    /// Get mutable access to the platform IO.
72    ///
73    /// Note: `ImGuiPlatformIO` exists even when multi-viewport is disabled. We expose it
74    /// unconditionally so callers can use ImGui 1.92+ texture management via `PlatformIO.Textures[]`.
75    pub fn platform_io_mut(&mut self) -> &mut crate::platform_io::PlatformIo {
76        let _guard = CTX_MUTEX.lock();
77        unsafe {
78            let pio = self.platform_io_ptr("Context::platform_io_mut()");
79            crate::platform_io::PlatformIo::from_raw_mut(pio)
80        }
81    }
82
83    /// Returns a reference to the main Dear ImGui viewport.
84    ///
85    /// The returned reference is owned by this ImGui context and
86    /// must not be used after the context is destroyed.
87    #[doc(alias = "GetMainViewport")]
88    pub fn main_viewport(&mut self) -> &mut crate::platform_io::Viewport {
89        let _guard = CTX_MUTEX.lock();
90        unsafe {
91            with_bound_context(self.raw, || {
92                let ptr = sys::igGetMainViewport();
93                if ptr.is_null() {
94                    panic!("Context::main_viewport() requires a valid ImGui context");
95                }
96                crate::platform_io::Viewport::from_raw_mut(ptr)
97            })
98        }
99    }
100
101    /// Enable Dear ImGui's multi-viewport capability.
102    ///
103    /// Docking is an independent capability and must be enabled explicitly by the caller.
104    /// A platform and renderer backend that advertise multi-viewport support must be installed
105    /// before the next frame begins.
106    ///
107    /// Prefer enabling this before the first frame so Dear ImGui can load settings in the correct
108    /// coordinate space. Enabling it between the first and second frames is rejected by Dear
109    /// ImGui. When it is enabled after a later completed frame, [`Context::frame`] automatically
110    /// advances the platform-window lifecycle for the preceding disabled frame.
111    #[cfg(feature = "multi-viewport")]
112    pub fn enable_multi_viewport(&mut self) {
113        let io = self.io_mut();
114        let mut flags = io.config_flags();
115        flags.insert(crate::ConfigFlags::VIEWPORTS_ENABLE);
116        io.set_config_flags(flags);
117    }
118
119    /// Update platform windows
120    ///
121    /// This function should be called every frame when multi-viewport is enabled.
122    /// It updates all platform windows and handles viewport management.
123    ///
124    /// # Panics
125    ///
126    /// Panics if the current frame has not ended, this frame was already updated, or the active
127    /// multi-viewport backend contract is incomplete.
128    #[cfg(feature = "multi-viewport")]
129    #[doc(alias = "UpdatePlatformWindows")]
130    pub fn update_platform_windows(&mut self) {
131        let _guard = CTX_MUTEX.lock();
132        unsafe {
133            with_bound_context(self.raw, || {
134                self.assert_can_update_platform_windows_unlocked(
135                    "Context::update_platform_windows()",
136                );
137                sys::igUpdatePlatformWindows();
138            });
139        }
140    }
141
142    /// Render platform windows with default implementation
143    ///
144    /// This function renders all platform windows using the default implementation.
145    /// It calls the platform and renderer backends to render each viewport.
146    ///
147    /// # Panics
148    ///
149    /// Panics unless [`Self::update_platform_windows`] completed for the current frame after
150    /// [`Self::render`], or unless the installed backends provide `Platform_RenderWindow` or
151    /// `Renderer_RenderWindow`. Snapshot-driven renderers should render their detached viewport
152    /// data directly instead of calling this default callback pump.
153    #[cfg(feature = "multi-viewport")]
154    #[doc(alias = "RenderPlatformWindowsDefault")]
155    pub fn render_platform_windows_default(&mut self) {
156        let _guard = CTX_MUTEX.lock();
157        unsafe {
158            with_bound_context(self.raw, || {
159                let raw = &*self.raw;
160                assert!(
161                    raw.FrameCount > 0
162                        && raw.FrameCountRendered == raw.FrameCount
163                        && raw.FrameCountPlatformEnded == raw.FrameCount,
164                    "Context::render_platform_windows_default() requires a rendered frame followed by Context::update_platform_windows()"
165                );
166                let platform_io = sys::igGetPlatformIO_Nil();
167                assert!(
168                    !platform_io.is_null()
169                        && ((*platform_io).Platform_RenderWindow.is_some()
170                            || (*platform_io).Renderer_RenderWindow.is_some()),
171                    "Context::render_platform_windows_default() requires Platform_RenderWindow or Renderer_RenderWindow; render snapshots directly when the renderer does not use default callbacks"
172                );
173                let renderer_has_textures =
174                    (*self.io_ptr("Context::render_platform_windows_default()")).BackendFlags
175                        & sys::ImGuiBackendFlags_RendererHasTextures as i32
176                        != 0;
177                if renderer_has_textures {
178                    assert!(
179                        self.snapshot_hub
180                            .is_synchronous_frame_reconciled(raw.FrameCount),
181                        "Context::render_platform_windows_default() requires managed-texture reconciliation for the current rendered frame"
182                    );
183                }
184                let _texture_mask = renderer_has_textures
185                    .then(|| PlatformDrawDataTextureMask::install(platform_io));
186                sys::igRenderPlatformWindowsDefault(std::ptr::null_mut(), std::ptr::null_mut());
187            });
188        }
189    }
190
191    /// Destroy all platform windows
192    ///
193    /// This function should be called during shutdown to properly clean up
194    /// all platform windows and their associated resources.
195    ///
196    /// Any open frame is ended first through the same idempotent lifecycle path used by Context
197    /// destruction. This lets backends revoke UI access before destroying secondary windows. A
198    /// rejected observer preflight still leaves that frame ended.
199    ///
200    /// Platform backends receive a bounded observer scope around the native call so they can
201    /// validate callback ownership and keep their teardown state coherent. If that preflight
202    /// fails, native teardown does not begin.
203    ///
204    /// This is a platform-backend shutdown primitive. A registered backend may transition its
205    /// own multi-viewport runtime to a released state; prefer its explicit shutdown API when one
206    /// is available.
207    ///
208    /// # Errors
209    ///
210    /// Returns an error when the active platform attachment rejects the transaction, cannot
211    /// complete post-teardown cleanup, or when the operation is re-entered. Post-teardown errors
212    /// report that native platform windows have already been destroyed.
213    #[cfg(feature = "multi-viewport")]
214    #[doc(alias = "DestroyPlatformWindows")]
215    pub fn destroy_platform_windows(&mut self) -> Result<(), ContextPlatformWindowTeardownError> {
216        let _guard = CTX_MUTEX.lock();
217        self.end_frame_for_teardown_unlocked();
218        let teardown = ContextPlatformWindowTeardown::new(&self.state);
219        let invocation = self.attachments.begin_platform_window_teardown(&teardown)?;
220        unsafe {
221            with_bound_context(self.raw, || {
222                sys::igDestroyPlatformWindows();
223            });
224        }
225        invocation.finish(&teardown)
226    }
227
228    #[cfg(feature = "multi-viewport")]
229    pub(super) fn prepare_multi_viewport_new_frame_contract_unlocked(&self, caller: &str) {
230        unsafe {
231            let config_flags = (*self.io_ptr(caller)).ConfigFlags;
232            let viewports_enabled = config_flags & sys::ImGuiConfigFlags_ViewportsEnable != 0;
233            if !viewports_enabled {
234                return;
235            }
236
237            let frame_count = (*self.raw).FrameCount;
238            let frame_count_ended = (*self.raw).FrameCountEnded;
239            let frame_count_platform_ended = (*self.raw).FrameCountPlatformEnded;
240            let config_flags_current_frame = (*self.raw).ConfigFlagsCurrFrame;
241
242            if frame_count == 1
243                && config_flags_current_frame & sys::ImGuiConfigFlags_ViewportsEnable == 0
244            {
245                panic!(
246                    "{caller} cannot enable multi-viewport on the second frame; enable it before the first frame or after the second frame so Dear ImGui preserves its settings contract"
247                );
248            }
249            if !self.multi_viewport_backends_advertised_unlocked() {
250                // Dear ImGui intentionally clears ViewportsEnable when either backend declines
251                // support. Preserve that graceful fallback instead of turning it into an error.
252                return;
253            }
254            if frame_count > 0 && frame_count_platform_ended != frame_count {
255                if config_flags_current_frame & sys::ImGuiConfigFlags_ViewportsEnable == 0 {
256                    assert_eq!(
257                        frame_count_ended, frame_count,
258                        "{caller} cannot enable multi-viewport while the previous frame is still open"
259                    );
260                    // The preceding frame had viewports disabled, so native UpdatePlatformWindows
261                    // only advances its frame watermark and cannot invoke backend callbacks.
262                    sys::igUpdatePlatformWindows();
263                } else {
264                    panic!(
265                        "{caller} cannot begin a new multi-viewport frame before Context::update_platform_windows() completes the previous frame"
266                    );
267                }
268            }
269            self.assert_multi_viewport_backend_contract_unlocked(
270                caller,
271                config_flags | config_flags_current_frame,
272            );
273        }
274    }
275
276    #[cfg(feature = "multi-viewport")]
277    fn assert_can_update_platform_windows_unlocked(&self, caller: &str) {
278        unsafe {
279            let frame_count = (*self.raw).FrameCount;
280            let frame_count_ended = (*self.raw).FrameCountEnded;
281            let frame_count_platform_ended = (*self.raw).FrameCountPlatformEnded;
282            let config_flags_current_frame = (*self.raw).ConfigFlagsCurrFrame;
283            assert!(
284                frame_count_ended == frame_count,
285                "{caller} requires Context::render() or an ended frame first"
286            );
287            assert!(
288                frame_count_platform_ended < frame_count,
289                "{caller} was already called for frame {}",
290                frame_count
291            );
292            self.assert_multi_viewport_backend_contract_unlocked(
293                caller,
294                config_flags_current_frame,
295            );
296        }
297    }
298
299    #[cfg(feature = "multi-viewport")]
300    fn assert_multi_viewport_backend_contract_unlocked(&self, caller: &str, config_flags: i32) {
301        if config_flags & sys::ImGuiConfigFlags_ViewportsEnable == 0 {
302            return;
303        }
304
305        unsafe {
306            let io = &*self.io_ptr(caller);
307            assert!(
308                self.multi_viewport_backends_advertised_unlocked(),
309                "{caller} requires platform and renderer backends that advertise multi-viewport support"
310            );
311
312            let platform_io = &*self.platform_io_ptr(caller);
313            for (name, installed) in [
314                (
315                    "Platform_CreateWindow",
316                    platform_io.Platform_CreateWindow.is_some(),
317                ),
318                (
319                    "Platform_DestroyWindow",
320                    platform_io.Platform_DestroyWindow.is_some(),
321                ),
322                (
323                    "Platform_ShowWindow",
324                    platform_io.Platform_ShowWindow.is_some(),
325                ),
326                (
327                    "Platform_GetWindowPos",
328                    platform_io.Platform_GetWindowPos.is_some(),
329                ),
330                (
331                    "Platform_SetWindowPos",
332                    platform_io.Platform_SetWindowPos.is_some(),
333                ),
334                (
335                    "Platform_GetWindowSize",
336                    platform_io.Platform_GetWindowSize.is_some(),
337                ),
338                (
339                    "Platform_SetWindowSize",
340                    platform_io.Platform_SetWindowSize.is_some(),
341                ),
342                (
343                    "Platform_SetWindowTitle",
344                    platform_io.Platform_SetWindowTitle.is_some(),
345                ),
346            ] {
347                assert!(
348                    installed,
349                    "{caller} requires the {name} callback before multi-viewport can run"
350                );
351            }
352
353            let monitor_count = platform_io.Monitors.Size;
354            assert!(
355                monitor_count > 0 && !platform_io.Monitors.Data.is_null(),
356                "{caller} requires at least one valid PlatformIO monitor"
357            );
358            assert!(
359                platform_io.Monitors.Capacity >= monitor_count,
360                "{caller} rejected a corrupt PlatformIO monitor vector"
361            );
362            let monitors = std::slice::from_raw_parts(
363                platform_io.Monitors.Data,
364                usize::try_from(monitor_count)
365                    .expect("positive PlatformIO monitor count must fit usize"),
366            );
367            crate::platform_io::assert_monitor_contract(monitors, caller);
368
369            let main_viewport = sys::igGetMainViewport();
370            assert!(
371                !main_viewport.is_null(),
372                "{caller} requires a valid main viewport"
373            );
374            assert!(
375                !(*main_viewport).PlatformUserData.is_null()
376                    || !(*main_viewport).PlatformHandle.is_null(),
377                "{caller} requires the platform backend to initialize the main viewport"
378            );
379
380            let transparent_docking = io.ConfigDockingTransparentPayload
381                && config_flags & sys::ImGuiConfigFlags_DockingEnable != 0;
382            assert!(
383                !transparent_docking || platform_io.Platform_SetWindowAlpha.is_some(),
384                "{caller} requires Platform_SetWindowAlpha when transparent docking payloads are enabled"
385            );
386        }
387    }
388
389    #[cfg(feature = "multi-viewport")]
390    fn multi_viewport_backends_advertised_unlocked(&self) -> bool {
391        unsafe {
392            let backend_flags = (*self.io_ptr("multi-viewport backend validation")).BackendFlags;
393            backend_flags & sys::ImGuiBackendFlags_PlatformHasViewports != 0
394                && backend_flags & sys::ImGuiBackendFlags_RendererHasViewports != 0
395        }
396    }
397}
398
399#[cfg(all(test, feature = "multi-viewport"))]
400mod tests {
401    use super::*;
402
403    #[test]
404    fn managed_platform_draw_texture_mask_restores_every_pointer() {
405        let mut textures = sys::ImVector_ImTextureDataPtr::default();
406        let textures_ptr = &mut textures as *mut sys::ImVector_ImTextureDataPtr;
407        let mut draw_data = sys::ImDrawData {
408            Textures: textures_ptr,
409            ..Default::default()
410        };
411        let mut viewport = sys::ImGuiViewport {
412            DrawData: &mut draw_data,
413            ..Default::default()
414        };
415        let viewport = &mut viewport as *mut sys::ImGuiViewport;
416        let mut viewports = [viewport, viewport];
417        let mut platform_io = sys::ImGuiPlatformIO {
418            Viewports: sys::ImVector_ImGuiViewportPtr {
419                Size: viewports.len() as i32,
420                Capacity: viewports.len() as i32,
421                Data: viewports.as_mut_ptr(),
422            },
423            ..Default::default()
424        };
425
426        let mask = unsafe { PlatformDrawDataTextureMask::install(&mut platform_io) };
427        assert!(draw_data.Textures.is_null());
428        drop(mask);
429        assert_eq!(draw_data.Textures, textures_ptr);
430    }
431}