Skip to main content

dear_imgui_rs/context/
frame.rs

1//! Frame lifecycle APIs.
2//!
3//! Use [`Context::frame_with_result`] when a callback should also close and render the frame.
4//! The callback-only `Context::frame_with` API is intentionally unavailable because it leaves
5//! frame completion implicit:
6//!
7//! ```compile_fail
8//! # use dear_imgui_rs::Context;
9//! # let mut context = Context::create();
10//! let _ = context.frame_with(|ui| ui.text("frame"));
11//! ```
12
13use crate::sys;
14
15use super::Context;
16use super::binding::{CTX_MUTEX, with_bound_context};
17
18/// Runtime state for a Dear ImGui frame owned by an external engine schedule.
19#[derive(Copy, Clone, Debug, Eq, PartialEq)]
20pub enum FrameLifecycleState {
21    /// No Dear ImGui frame is currently open for this context.
22    Idle,
23    /// A frame was opened and can accept UI commands.
24    InFrame,
25    /// The last opened frame has been rendered and draw data is available until the next frame.
26    Rendered,
27}
28
29/// Options used by [`Context::prepare_frame`].
30#[derive(Copy, Clone, Debug)]
31pub struct FramePrepareOptions {
32    /// Main display size in pixels.
33    pub display_size: [f32; 2],
34    /// Time elapsed since the previous frame, in seconds.
35    pub delta_time: f32,
36    /// Optional framebuffer scale for HiDPI render targets.
37    pub framebuffer_scale: Option<[f32; 2]>,
38    /// Backend capability flags to OR into the context before opening the frame.
39    pub backend_flags: crate::BackendFlags,
40}
41
42impl FramePrepareOptions {
43    /// Create frame preparation options with display size and delta time.
44    pub fn new(display_size: [f32; 2], delta_time: f32) -> Self {
45        Self {
46            display_size,
47            delta_time,
48            framebuffer_scale: None,
49            backend_flags: crate::BackendFlags::empty(),
50        }
51    }
52
53    /// Set the framebuffer scale used by the frame.
54    #[must_use]
55    pub fn framebuffer_scale(mut self, scale: [f32; 2]) -> Self {
56        self.framebuffer_scale = Some(scale);
57        self
58    }
59
60    /// OR backend capability flags into the context before opening the frame.
61    #[must_use]
62    pub fn backend_flags(mut self, flags: crate::BackendFlags) -> Self {
63        self.backend_flags |= flags;
64        self
65    }
66
67    /// Convenience for modern renderers that support ImGui 1.92 texture requests.
68    #[must_use]
69    pub fn renderer_has_textures(self) -> Self {
70        self.backend_flags(crate::BackendFlags::RENDERER_HAS_TEXTURES)
71    }
72}
73
74/// A frame opened by [`Context::begin_frame`].
75///
76/// This token is intended for engine integrations that need to make the Dear ImGui frame boundary
77/// explicit: one system opens the frame, several user systems draw through [`Self::ui`], and one
78/// system consumes the token to render or snapshot the frame. The existing [`Context::frame`] and
79/// [`Context::render`] calls remain available for traditional immediate-mode loops.
80#[must_use = "dropping FrameToken ends the frame without rendering; call render() or render_snapshot() to produce draw data"]
81#[doc(alias = "EndFrame")]
82pub struct FrameToken<'ctx> {
83    ctx: &'ctx mut Context,
84    closed: bool,
85}
86
87/// Result returned by [`Context::frame_with_result`].
88pub struct FrameResult<'ctx, T> {
89    /// Value returned by the UI-building closure.
90    pub value: T,
91    /// Context-borrowed render lease produced after the closure returned.
92    pub rendered_frame: crate::render::RenderedFrame<'ctx>,
93}
94
95impl Context {
96    /// Prepare IO values commonly needed before starting a frame.
97    ///
98    /// This does not call `NewFrame()`. Engine backends can call it from their input/window update
99    /// stage and then open the actual Dear ImGui frame later in the schedule with
100    /// [`Context::begin_frame`].
101    pub fn prepare_frame(&mut self, options: FramePrepareOptions) {
102        let io = self.io_mut();
103        io.set_display_size(options.display_size);
104        io.set_delta_time(options.delta_time);
105        if let Some(scale) = options.framebuffer_scale {
106            io.set_display_framebuffer_scale(scale);
107        }
108        if !options.backend_flags.is_empty() {
109            io.set_backend_flags(io.backend_flags() | options.backend_flags);
110        }
111    }
112
113    /// Return the current frame lifecycle state for this context.
114    pub fn frame_lifecycle_state(&self) -> FrameLifecycleState {
115        let _guard = CTX_MUTEX.lock();
116        self.assert_current_context("Context::frame_lifecycle_state()");
117        self.frame_lifecycle_state_unlocked()
118    }
119
120    /// End an open frame without producing render data.
121    ///
122    /// Returns `true` when an open native frame was closed and `false` when the Context was
123    /// already idle or rendered. The operation is idempotent so engine teardown can revoke UI
124    /// access before detaching platform and renderer state.
125    #[doc(alias = "EndFrame")]
126    pub fn end_frame(&mut self) -> bool {
127        let _guard = CTX_MUTEX.lock();
128        self.assert_current_context("Context::end_frame()");
129        self.end_frame_for_teardown_unlocked()
130    }
131
132    pub(super) fn end_frame_for_teardown_unlocked(&mut self) -> bool {
133        if self.raw.is_null() || !unsafe { (*self.raw).WithinFrameScope } {
134            return false;
135        }
136        unsafe {
137            with_bound_context(self.raw, || {
138                let _ = crate::list_clipper::forget_context_clippers(self.raw);
139                sys::igEndFrame();
140            });
141        }
142        self.texture_registry
143            .borrow_mut()
144            .observe_native_texture_list_refresh();
145        true
146    }
147
148    /// Begin a Dear ImGui frame and return an explicit frame token.
149    ///
150    /// Engine integrations should prefer this when the frame is owned by a schedule rather than a
151    /// single function. Draw UI through [`FrameToken::ui`] and then consume the token with
152    /// [`FrameToken::render`] or [`FrameToken::render_snapshot`].
153    pub fn begin_frame(&mut self) -> FrameToken<'_> {
154        let _ = self.frame();
155        FrameToken {
156            ctx: self,
157            closed: false,
158        }
159    }
160
161    /// Creates a new frame and returns a Ui object for building the interface.
162    ///
163    /// Note: you must update `io.DisplaySize` (and usually `io.DeltaTime`) before calling this,
164    /// unless you are using a platform backend that does it for you (e.g. `dear-imgui-winit`).
165    #[doc(alias = "NewFrame")]
166    pub fn frame(&mut self) -> &mut crate::ui::Ui {
167        let _guard = CTX_MUTEX.lock();
168        self.assert_current_context("Context::frame()");
169        self.assert_can_begin_frame_unlocked("Context::frame()");
170        self.poll_snapshot_completions_or_panic("Context::frame()");
171        self.collect_retired_textures();
172
173        unsafe {
174            // Dear ImGui initializes DisplaySize to (-1, -1). Calling NewFrame() without a
175            // platform backend (or without setting DisplaySize manually) will trip an internal
176            // assertion and abort the process. Fail fast with a Rust panic to make the setup
177            // requirement obvious.
178            let io = sys::igGetIO_Nil();
179            if !io.is_null() && ((*io).DisplaySize.x < 0.0 || (*io).DisplaySize.y < 0.0) {
180                panic!(
181                    "Context::frame() called with invalid io.DisplaySize ({}, {}). \
182Set io.DisplaySize (and typically io.DeltaTime) before starting a frame. \
183If you are using a windowing/event-loop library, prefer a platform backend such as \
184dear-imgui-winit::WinitPlatform::prepare_frame().",
185                    (*io).DisplaySize.x,
186                    (*io).DisplaySize.y
187                );
188            }
189            self.assert_docking_config_stable_unlocked("Context::frame()");
190            #[cfg(feature = "multi-viewport")]
191            self.prepare_multi_viewport_new_frame_contract_unlocked("Context::frame()");
192            crate::fonts::assert_no_font_atlas_texture_borrows((*io).Fonts, "Context::frame()");
193            let renderer_has_textures =
194                ((*io).BackendFlags & sys::ImGuiBackendFlags_RendererHasTextures as i32) != 0;
195            crate::fonts::assert_font_atlas_renderer_mode(
196                (*io).Fonts,
197                renderer_has_textures,
198                "Context::frame()",
199            );
200            if let Some(shared_font_atlas) = &self.shared_font_atlas {
201                shared_font_atlas.prepare_frame(renderer_has_textures);
202            }
203            sys::igNewFrame();
204        }
205        &mut self.ui
206    }
207
208    fn assert_docking_config_stable_unlocked(&self, caller: &str) {
209        let frame_count = unsafe { (*self.raw).FrameCount };
210        if frame_count == 0 {
211            return;
212        }
213
214        let io = self.io_ptr(caller);
215        let requested = unsafe { (*io).ConfigFlags } & sys::ImGuiConfigFlags_DockingEnable as i32;
216        let active = unsafe { (*self.raw).ConfigFlagsCurrFrame }
217            & sys::ImGuiConfigFlags_DockingEnable as i32;
218        assert_eq!(
219            requested, active,
220            "{caller} cannot change ConfigFlags::DOCKING_ENABLE after the first frame; Dear ImGui clears live dock nodes when docking is disabled and cannot restore them safely when it is re-enabled"
221        );
222    }
223
224    /// Begin a frame, run a UI-building closure, render the frame, and return both values.
225    ///
226    /// This is a convenience for callers that want the old callback style but also want the draw
227    /// data produced by closing the frame. Use [`Context::begin_frame`] when the UI is built across
228    /// several engine systems.
229    pub fn frame_with_result<F, R>(&mut self, f: F) -> FrameResult<'_, R>
230    where
231        F: FnOnce(&crate::ui::Ui) -> R,
232    {
233        let frame = self.begin_frame();
234        let value = f(frame.ui());
235        let rendered_frame = frame.render();
236        FrameResult {
237            value,
238            rendered_frame,
239        }
240    }
241
242    /// Render the frame and return a Context-borrowed synchronous lease.
243    ///
244    /// This finalizes the Dear ImGui frame and prepares all draw data for rendering.
245    /// The returned draw data contains all the information needed to render the frame.
246    ///
247    #[doc(alias = "Render", alias = "GetDrawData")]
248    pub fn render(&mut self) -> crate::render::RenderedFrame<'_> {
249        let draw_data = self.render_raw();
250        let renderer_has_textures = unsafe {
251            let io = self.io_ptr("Context::render()");
252            ((*io).BackendFlags & sys::ImGuiBackendFlags_RendererHasTextures as i32) != 0
253        };
254        let (epoch, requests) = if renderer_has_textures {
255            let (epoch, requests) = self
256                .begin_synchronous_render(draw_data.as_ptr().cast_const())
257                .unwrap_or_else(|error| {
258                    panic!("Context::render() requires an active synchronous renderer consumer: {error}")
259                });
260            (Some(epoch), requests)
261        } else {
262            (None, Vec::new())
263        };
264        crate::render::RenderedFrame::new(self, draw_data, epoch, requests)
265    }
266
267    fn render_raw(&mut self) -> std::ptr::NonNull<crate::render::DrawData> {
268        let _guard = CTX_MUTEX.lock();
269        self.assert_current_context("Context::render()");
270        self.assert_can_render_unlocked("Context::render()");
271
272        let draw_data = unsafe {
273            let abandoned_clippers = crate::list_clipper::forget_context_clippers(self.raw);
274            if abandoned_clippers != 0 {
275                self.end_frame_for_teardown_unlocked();
276                panic!(
277                    "Context::render() rejected a frame with {abandoned_clippers} list clipper token(s) still active or forgotten"
278                );
279            }
280            sys::igRender();
281            let dd = sys::igGetDrawData();
282            if dd.is_null() {
283                panic!("Context::render() returned null draw data");
284            }
285            std::ptr::NonNull::new_unchecked(dd as *mut crate::render::DrawData)
286        };
287        self.texture_registry
288            .borrow_mut()
289            .observe_native_texture_list_refresh();
290        draw_data
291    }
292
293    /// Render the current frame and build a thread-safe main-viewport snapshot.
294    ///
295    /// This Context-level entry point supports engine schedules that open and close the native
296    /// frame in separate systems and therefore cannot retain a [`FrameToken`]. The snapshot is
297    /// still bound to the supplied renderer consumer, Context, generation, and ordered epoch.
298    pub fn render_snapshot(
299        &mut self,
300        consumer: &crate::render::snapshot::RendererConsumer,
301    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
302    {
303        let draw_data = self.render_raw();
304        self.capture_main_snapshot(consumer, draw_data.as_ptr())
305    }
306
307    /// Render the current frame and build a thread-safe snapshot for all platform viewports.
308    ///
309    /// With the `multi-viewport` feature enabled, Dear ImGui stores draw data on each viewport.
310    /// `Context::render()` and `igGetDrawData()` only expose the main viewport draw data, so
311    /// engine backends that render secondary OS windows should use this method after opening a
312    /// frame. The returned snapshot still keeps [`FrameSnapshot::draw`] as the main viewport for
313    /// compatibility and adds per-viewport draw data in
314    /// [`FrameSnapshot::viewports`](crate::render::snapshot::FrameSnapshot::viewports).
315    #[cfg(feature = "multi-viewport")]
316    pub fn render_platform_viewport_snapshot(
317        &mut self,
318        consumer: &crate::render::snapshot::RendererConsumer,
319    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
320    {
321        let _ = self.render_raw();
322        self.platform_viewport_snapshot(consumer)
323    }
324
325    /// Build a thread-safe snapshot from the current platform viewport draw data.
326    ///
327    /// This does not call `Render()`. Call it only after a frame has already been rendered and
328    /// before the next frame starts. Engine integrations can use this when they need to run
329    /// platform-window maintenance after `Render()` but before cloning draw data for another
330    /// render schedule.
331    #[cfg(feature = "multi-viewport")]
332    pub fn platform_viewport_snapshot(
333        &mut self,
334        consumer: &crate::render::snapshot::RendererConsumer,
335    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
336    {
337        let _guard = CTX_MUTEX.lock();
338        self.assert_current_context("Context::platform_viewport_snapshot()");
339        if self.frame_lifecycle_state_unlocked() != FrameLifecycleState::Rendered {
340            panic!(
341                "Context::platform_viewport_snapshot() called before rendering the current frame"
342            );
343        }
344        self.capture_platform_snapshot(consumer)
345    }
346
347    pub(super) fn frame_lifecycle_state_unlocked(&self) -> FrameLifecycleState {
348        unsafe {
349            let raw = &*self.raw;
350            if raw.WithinFrameScope {
351                FrameLifecycleState::InFrame
352            } else if raw.FrameCountRendered == raw.FrameCount && raw.FrameCount > 0 {
353                FrameLifecycleState::Rendered
354            } else {
355                FrameLifecycleState::Idle
356            }
357        }
358    }
359
360    fn assert_can_begin_frame_unlocked(&self, caller: &str) {
361        if self.frame_lifecycle_state_unlocked() == FrameLifecycleState::InFrame {
362            panic!("{caller} called while another Dear ImGui frame is already open");
363        }
364    }
365
366    fn assert_can_render_unlocked(&self, caller: &str) {
367        if self.frame_lifecycle_state_unlocked() != FrameLifecycleState::InFrame {
368            panic!("{caller} called without an open Dear ImGui frame");
369        }
370    }
371}
372
373impl<'ctx> FrameToken<'ctx> {
374    /// Borrow the UI for this frame.
375    ///
376    /// This can be called repeatedly by an engine-owned frame runner to let multiple systems draw
377    /// into the same frame, as long as those systems are scheduled sequentially.
378    pub fn ui(&self) -> &crate::ui::Ui {
379        &self.ctx.ui
380    }
381
382    /// Return the lifecycle state while this token owns the open frame.
383    pub fn lifecycle_state(&self) -> FrameLifecycleState {
384        self.ctx.frame_lifecycle_state()
385    }
386
387    /// Render this frame and return the resulting draw data.
388    pub fn render(mut self) -> crate::render::RenderedFrame<'ctx> {
389        let ctx = self.ctx as *mut Context;
390        let draw_data = unsafe { (&mut *ctx).render() };
391        self.closed = true;
392        std::mem::forget(self);
393        draw_data
394    }
395
396    /// Render this frame and build a thread-safe snapshot from the resulting draw data.
397    ///
398    /// This is the preferred handoff shape for render-world integrations such as Bevy, where raw
399    /// ImGui pointers must not cross the engine extraction boundary.
400    pub fn render_snapshot(
401        mut self,
402        consumer: &crate::render::snapshot::RendererConsumer,
403    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
404    {
405        let ctx = self.ctx as *mut Context;
406        let draw_data = unsafe { (&mut *ctx).render_raw().as_ptr() };
407        self.closed = true;
408        std::mem::forget(self);
409        unsafe { (&mut *ctx).capture_main_snapshot(consumer, draw_data) }
410    }
411
412    /// Render this frame and build a thread-safe snapshot for all platform viewports.
413    #[cfg(feature = "multi-viewport")]
414    pub fn render_platform_viewport_snapshot(
415        mut self,
416        consumer: &crate::render::snapshot::RendererConsumer,
417    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
418    {
419        let ctx = self.ctx as *mut Context;
420        let snapshot = unsafe { (&mut *ctx).render_platform_viewport_snapshot(consumer) };
421        self.closed = true;
422        std::mem::forget(self);
423        snapshot
424    }
425}
426
427impl Drop for FrameToken<'_> {
428    fn drop(&mut self) {
429        if self.closed {
430            return;
431        }
432
433        let _guard = CTX_MUTEX.lock();
434        self.ctx.end_frame_for_teardown_unlocked();
435        self.closed = true;
436    }
437}