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}
85
86/// Result returned by [`Context::frame_with_result`].
87#[must_use = "use the closure result and reconcile the pending frame before drawing"]
88pub struct FrameResult<'ctx, T> {
89    /// Value returned by the UI-building closure.
90    pub value: T,
91    /// Context-borrowed pending frame produced after the closure returned.
92    pub pending_frame: crate::render::PendingFrame<'ctx>,
93}
94
95impl<'ctx, T> FrameResult<'ctx, T> {
96    /// Split the closure result from the pending renderer capability.
97    pub fn into_parts(self) -> (T, crate::render::PendingFrame<'ctx>) {
98        (self.value, self.pending_frame)
99    }
100}
101
102impl Context {
103    /// Prepare IO values commonly needed before starting a frame.
104    ///
105    /// This does not call `NewFrame()`. Engine backends can call it from their input/window update
106    /// stage and then open the actual Dear ImGui frame later in the schedule with
107    /// [`Context::begin_frame`].
108    pub fn prepare_frame(&mut self, options: FramePrepareOptions) {
109        let io = self.io_mut();
110        io.set_display_size(options.display_size);
111        io.set_delta_time(options.delta_time);
112        if let Some(scale) = options.framebuffer_scale {
113            io.set_display_framebuffer_scale(scale);
114        }
115        if !options.backend_flags.is_empty() {
116            io.set_backend_flags(io.backend_flags() | options.backend_flags);
117        }
118    }
119
120    /// Return the current frame lifecycle state for this context.
121    pub fn frame_lifecycle_state(&self) -> FrameLifecycleState {
122        let _guard = CTX_MUTEX.lock();
123        self.assert_current_context("Context::frame_lifecycle_state()");
124        self.frame_lifecycle_state_unlocked()
125    }
126
127    /// End an open frame without producing render data.
128    ///
129    /// Returns `true` when an open native frame was closed and `false` when the Context was
130    /// already idle or rendered. The operation is idempotent so engine teardown can revoke UI
131    /// access before detaching platform and renderer state.
132    #[doc(alias = "EndFrame")]
133    pub fn end_frame(&mut self) -> bool {
134        let _guard = CTX_MUTEX.lock();
135        self.assert_current_context("Context::end_frame()");
136        self.end_frame_for_teardown_unlocked()
137    }
138
139    pub(super) fn end_frame_for_teardown_unlocked(&mut self) -> bool {
140        if self.raw.is_null() || !unsafe { (*self.raw).WithinFrameScope } {
141            return false;
142        }
143        unsafe {
144            with_bound_context(self.raw, || {
145                let _ = crate::list_clipper::forget_context_clippers(self.raw);
146                sys::igEndFrame();
147            });
148        }
149        self.texture_registry
150            .borrow_mut()
151            .observe_native_texture_list_refresh();
152        true
153    }
154
155    /// Begin a Dear ImGui frame and return an explicit frame token.
156    ///
157    /// Engine integrations should prefer this when the frame is owned by a schedule rather than a
158    /// single function. Draw UI through [`FrameToken::ui`] and then consume the token with
159    /// [`FrameToken::render`] or [`FrameToken::render_snapshot`].
160    pub fn begin_frame(&mut self) -> FrameToken<'_> {
161        self.try_begin_frame()
162            .unwrap_or_else(|error| panic!("Context::begin_frame() rejected completion: {error}"))
163    }
164
165    /// Begin a frame while returning detached-completion failures to the caller.
166    pub fn try_begin_frame(
167        &mut self,
168    ) -> Result<FrameToken<'_>, crate::render::RendererConsumerError> {
169        let _ = self.try_frame()?;
170        Ok(FrameToken { ctx: self })
171    }
172
173    /// Creates a new frame and returns a Ui object for building the interface.
174    ///
175    /// Note: you must update `io.DisplaySize` (and usually `io.DeltaTime`) before calling this,
176    /// unless you are using a platform backend that does it for you (e.g. `dear-imgui-winit`).
177    #[doc(alias = "NewFrame")]
178    pub fn frame(&mut self) -> &mut crate::ui::Ui {
179        self.try_frame()
180            .unwrap_or_else(|error| panic!("Context::frame() rejected completion: {error}"))
181    }
182
183    /// Create a frame while returning detached-completion failures to the caller.
184    ///
185    /// Native frame-order, display-size, docking, and font-atlas programmer errors retain their
186    /// documented panic behavior. This method makes the asynchronous renderer completion boundary
187    /// fallible so event-loop and engine integrations can recover without a later panic.
188    pub fn try_frame(
189        &mut self,
190    ) -> Result<&mut crate::ui::Ui, crate::render::RendererConsumerError> {
191        let _guard = CTX_MUTEX.lock();
192        self.assert_current_context("Context::try_frame()");
193        self.assert_can_begin_frame_unlocked("Context::try_frame()");
194        self.poll_snapshot_completions_unlocked()?;
195        self.collect_retired_textures();
196
197        unsafe {
198            // Dear ImGui initializes DisplaySize to (-1, -1). Calling NewFrame() without a
199            // platform backend (or without setting DisplaySize manually) will trip an internal
200            // assertion and abort the process. Fail fast with a Rust panic to make the setup
201            // requirement obvious.
202            let io = sys::igGetIO_Nil();
203            if !io.is_null() && ((*io).DisplaySize.x < 0.0 || (*io).DisplaySize.y < 0.0) {
204                panic!(
205                    "Context::try_frame() called with invalid io.DisplaySize ({}, {}). \
206Set io.DisplaySize (and typically io.DeltaTime) before starting a frame. \
207If you are using a windowing/event-loop library, prefer a platform backend such as \
208dear-imgui-winit::WinitPlatform::prepare_frame().",
209                    (*io).DisplaySize.x,
210                    (*io).DisplaySize.y
211                );
212            }
213            self.assert_docking_config_stable_unlocked("Context::try_frame()");
214            #[cfg(feature = "multi-viewport")]
215            self.prepare_multi_viewport_new_frame_contract_unlocked("Context::try_frame()");
216            crate::fonts::assert_no_font_atlas_texture_borrows((*io).Fonts, "Context::try_frame()");
217            let renderer_has_textures =
218                ((*io).BackendFlags & sys::ImGuiBackendFlags_RendererHasTextures as i32) != 0;
219            crate::fonts::assert_font_atlas_renderer_mode(
220                (*io).Fonts,
221                renderer_has_textures,
222                "Context::try_frame()",
223            );
224            if let Some(shared_font_atlas) = &self.shared_font_atlas {
225                shared_font_atlas.prepare_frame(renderer_has_textures);
226            }
227            sys::igNewFrame();
228        }
229        Ok(&mut self.ui)
230    }
231
232    fn assert_docking_config_stable_unlocked(&self, caller: &str) {
233        let frame_count = unsafe { (*self.raw).FrameCount };
234        if frame_count == 0 {
235            return;
236        }
237
238        let io = self.io_ptr(caller);
239        let requested = unsafe { (*io).ConfigFlags } & sys::ImGuiConfigFlags_DockingEnable as i32;
240        let active = unsafe { (*self.raw).ConfigFlagsCurrFrame }
241            & sys::ImGuiConfigFlags_DockingEnable as i32;
242        assert_eq!(
243            requested, active,
244            "{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"
245        );
246    }
247
248    /// Begin a frame, run a UI-building closure, render the frame, and return both values.
249    ///
250    /// This is a convenience for callers that want the old callback style but also want the draw
251    /// data produced by closing the frame. Use [`Context::begin_frame`] when the UI is built across
252    /// several engine systems.
253    pub fn frame_with_result<F, R>(
254        &mut self,
255        consumer: &crate::render::SynchronousRendererConsumer,
256        f: F,
257    ) -> FrameResult<'_, R>
258    where
259        F: FnOnce(&crate::ui::Ui) -> R,
260    {
261        let frame = self.begin_frame();
262        let value = f(frame.ui());
263        let pending_frame = frame.render(consumer);
264        FrameResult {
265            value,
266            pending_frame,
267        }
268    }
269
270    /// Render a managed-texture frame and return its non-drawable pending capability.
271    #[doc(alias = "Render", alias = "GetDrawData")]
272    pub fn render(
273        &mut self,
274        consumer: &crate::render::SynchronousRendererConsumer,
275    ) -> crate::render::PendingFrame<'_> {
276        self.try_render(consumer)
277            .unwrap_or_else(|error| panic!("Context::render() failed: {error}"))
278    }
279
280    /// Render a managed-texture frame while returning capture or consumer failures.
281    pub fn try_render(
282        &mut self,
283        consumer: &crate::render::SynchronousRendererConsumer,
284    ) -> Result<crate::render::PendingFrame<'_>, crate::render::SnapshotError> {
285        self.require_managed_renderer("Context::try_render()")?;
286        let draw_data = self.render_raw();
287        let (epoch, requests) =
288            self.begin_synchronous_render(consumer, draw_data.as_ptr().cast_const())?;
289        Ok(crate::render::PendingFrame::new(
290            self, draw_data, epoch, requests,
291        ))
292    }
293
294    /// Render a frame for a legacy renderer that does not implement managed texture requests.
295    ///
296    /// # Panics
297    ///
298    /// Panics when the active renderer advertises `RENDERER_HAS_TEXTURES`; managed renderers must
299    /// use [`Self::render`] with their synchronous consumer and reconcile every request.
300    pub fn render_legacy(&mut self) -> crate::render::ReconciledFrame<'_> {
301        let renderer_has_textures = self
302            .io()
303            .backend_flags()
304            .contains(crate::BackendFlags::RENDERER_HAS_TEXTURES);
305        assert!(
306            !renderer_has_textures,
307            "Context::render_legacy() cannot bypass a managed renderer consumer"
308        );
309        let draw_data = self.render_raw();
310        crate::render::ReconciledFrame::new_legacy(self, draw_data)
311    }
312
313    fn require_managed_renderer(
314        &self,
315        caller: &'static str,
316    ) -> Result<(), crate::render::RendererConsumerError> {
317        if self
318            .io()
319            .backend_flags()
320            .contains(crate::BackendFlags::RENDERER_HAS_TEXTURES)
321        {
322            Ok(())
323        } else {
324            Err(crate::render::RendererConsumerError::RendererTexturesUnavailable { caller })
325        }
326    }
327
328    fn render_raw(&mut self) -> std::ptr::NonNull<crate::render::DrawData> {
329        let _guard = CTX_MUTEX.lock();
330        self.assert_current_context("Context::render()");
331        self.assert_can_render_unlocked("Context::render()");
332
333        let draw_data = unsafe {
334            let abandoned_clippers = crate::list_clipper::forget_context_clippers(self.raw);
335            if abandoned_clippers != 0 {
336                self.end_frame_for_teardown_unlocked();
337                panic!(
338                    "Context::render() rejected a frame with {abandoned_clippers} list clipper token(s) still active or forgotten"
339                );
340            }
341            sys::igRender();
342            let dd = sys::igGetDrawData();
343            if dd.is_null() {
344                panic!("Context::render() returned null draw data");
345            }
346            std::ptr::NonNull::new_unchecked(dd as *mut crate::render::DrawData)
347        };
348        self.texture_registry
349            .borrow_mut()
350            .observe_native_texture_list_refresh();
351        draw_data
352    }
353
354    /// Render the current frame and build a thread-safe main-viewport snapshot.
355    ///
356    /// This Context-level entry point supports engine schedules that open and close the native
357    /// frame in separate systems and therefore cannot retain a [`FrameToken`]. The snapshot is
358    /// still bound to the supplied renderer consumer, Context, generation, and ordered epoch.
359    pub fn render_snapshot(
360        &mut self,
361        consumer: &crate::render::snapshot::DetachedRendererConsumer,
362    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
363    {
364        let draw_data = self.render_raw();
365        self.capture_main_snapshot(consumer, draw_data.as_ptr())
366    }
367
368    /// Render the current frame and build a thread-safe snapshot for all platform viewports.
369    ///
370    /// With the `multi-viewport` feature enabled, Dear ImGui stores draw data on each viewport.
371    /// `Context::render()` and `igGetDrawData()` only expose the main viewport draw data, so
372    /// engine backends that render secondary OS windows should use this method after opening a
373    /// frame. The returned snapshot still keeps [`FrameSnapshot::draw`] as the main viewport for
374    /// compatibility and adds per-viewport draw data in
375    /// [`FrameSnapshot::viewports`](crate::render::snapshot::FrameSnapshot::viewports).
376    #[cfg(feature = "multi-viewport")]
377    pub fn render_platform_viewport_snapshot(
378        &mut self,
379        consumer: &crate::render::snapshot::DetachedRendererConsumer,
380    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
381    {
382        let _ = self.render_raw();
383        self.platform_viewport_snapshot(consumer)
384    }
385
386    /// Build a thread-safe snapshot from the current platform viewport draw data.
387    ///
388    /// This does not call `Render()`. Call it only after a frame has already been rendered and
389    /// before the next frame starts. Engine integrations can use this when they need to run
390    /// platform-window maintenance after `Render()` but before cloning draw data for another
391    /// render schedule.
392    #[cfg(feature = "multi-viewport")]
393    pub fn platform_viewport_snapshot(
394        &mut self,
395        consumer: &crate::render::snapshot::DetachedRendererConsumer,
396    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
397    {
398        let _guard = CTX_MUTEX.lock();
399        self.assert_current_context("Context::platform_viewport_snapshot()");
400        if self.frame_lifecycle_state_unlocked() != FrameLifecycleState::Rendered {
401            panic!(
402                "Context::platform_viewport_snapshot() called before rendering the current frame"
403            );
404        }
405        self.capture_platform_snapshot(consumer)
406    }
407
408    pub(super) fn frame_lifecycle_state_unlocked(&self) -> FrameLifecycleState {
409        unsafe {
410            let raw = &*self.raw;
411            if raw.WithinFrameScope {
412                FrameLifecycleState::InFrame
413            } else if raw.FrameCountRendered == raw.FrameCount && raw.FrameCount > 0 {
414                FrameLifecycleState::Rendered
415            } else {
416                FrameLifecycleState::Idle
417            }
418        }
419    }
420
421    fn assert_can_begin_frame_unlocked(&self, caller: &str) {
422        if self.frame_lifecycle_state_unlocked() == FrameLifecycleState::InFrame {
423            panic!("{caller} called while another Dear ImGui frame is already open");
424        }
425    }
426
427    fn assert_can_render_unlocked(&self, caller: &str) {
428        if self.frame_lifecycle_state_unlocked() != FrameLifecycleState::InFrame {
429            panic!("{caller} called without an open Dear ImGui frame");
430        }
431    }
432}
433
434impl<'ctx> FrameToken<'ctx> {
435    /// Borrow the UI for this frame.
436    ///
437    /// This can be called repeatedly by an engine-owned frame runner to let multiple systems draw
438    /// into the same frame, as long as those systems are scheduled sequentially.
439    pub fn ui(&self) -> &crate::ui::Ui {
440        &self.ctx.ui
441    }
442
443    /// Return the lifecycle state while this token owns the open frame.
444    pub fn lifecycle_state(&self) -> FrameLifecycleState {
445        self.ctx.frame_lifecycle_state()
446    }
447
448    /// Render this managed-texture frame and return its pending capability.
449    pub fn render(
450        self,
451        consumer: &crate::render::SynchronousRendererConsumer,
452    ) -> crate::render::PendingFrame<'ctx> {
453        self.try_render(consumer)
454            .unwrap_or_else(|error| panic!("FrameToken::render() failed: {error}"))
455    }
456
457    /// Render this managed-texture frame while returning capture or consumer failures.
458    pub fn try_render(
459        self,
460        consumer: &crate::render::SynchronousRendererConsumer,
461    ) -> Result<crate::render::PendingFrame<'ctx>, crate::render::SnapshotError> {
462        let ctx = self.ctx as *mut Context;
463        match unsafe { (&mut *ctx).try_render(consumer) } {
464            Ok(frame) => {
465                std::mem::forget(self);
466                Ok(frame)
467            }
468            Err(error) => Err(error),
469        }
470    }
471
472    /// Render this frame through the explicit legacy renderer path.
473    pub fn render_legacy(self) -> crate::render::ReconciledFrame<'ctx> {
474        let ctx = self.ctx as *mut Context;
475        let frame = unsafe { (&mut *ctx).render_legacy() };
476        std::mem::forget(self);
477        frame
478    }
479
480    /// Render this frame and build a thread-safe snapshot from the resulting draw data.
481    ///
482    /// This is the preferred handoff shape for render-world integrations such as Bevy, where raw
483    /// ImGui pointers must not cross the engine extraction boundary.
484    pub fn render_snapshot(
485        self,
486        consumer: &crate::render::snapshot::DetachedRendererConsumer,
487    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
488    {
489        let ctx = self.ctx as *mut Context;
490        let draw_data = unsafe { (&mut *ctx).render_raw().as_ptr() };
491        std::mem::forget(self);
492        unsafe { (&mut *ctx).capture_main_snapshot(consumer, draw_data) }
493    }
494
495    /// Render this frame and build a thread-safe snapshot for all platform viewports.
496    #[cfg(feature = "multi-viewport")]
497    pub fn render_platform_viewport_snapshot(
498        self,
499        consumer: &crate::render::snapshot::DetachedRendererConsumer,
500    ) -> Result<crate::render::snapshot::FrameSnapshot, crate::render::snapshot::SnapshotError>
501    {
502        let ctx = self.ctx as *mut Context;
503        let snapshot = unsafe { (&mut *ctx).render_platform_viewport_snapshot(consumer) };
504        std::mem::forget(self);
505        snapshot
506    }
507}
508
509impl Drop for FrameToken<'_> {
510    fn drop(&mut self) {
511        let _guard = CTX_MUTEX.lock();
512        self.ctx.end_frame_for_teardown_unlocked();
513    }
514}