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