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