Skip to main content

dear_imgui_rs/fonts/atlas/
custom_rect.rs

1use std::marker::PhantomData;
2use std::rc::Rc;
3
4use crate::texture::{TextureFormat, TextureRect, TextureRef, get_format_bytes_per_pixel};
5use crate::{Ui, sys};
6
7use super::state::{
8    current_context_font_atlas, custom_rect_nonce_is_active, font_atlas_state,
9    register_custom_rect_nonce, unregister_custom_rect_nonce,
10};
11use super::{FontAtlas, FontAtlasTexture};
12
13/// Persistent, atlas-validated identity for a custom font-atlas rectangle.
14///
15/// The ID survives texture growth and repacking. Rectangle coordinates, UVs, and texture
16/// references do not; query a fresh [`CustomRectSnapshot`] whenever they are needed.
17#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
18pub struct CustomRectId {
19    raw: sys::ImFontAtlasRectId,
20    atlas: *mut sys::ImFontAtlas,
21    atlas_stamp: u64,
22    generation: u64,
23    nonce: u64,
24    _not_send_sync: PhantomData<Rc<()>>,
25}
26
27impl CustomRectId {
28    fn from_raw_parts(raw: sys::ImFontAtlasRectId, atlas: *mut sys::ImFontAtlas) -> Self {
29        assert!(raw >= 0, "CustomRectId requires a valid native ID");
30        let (state, nonce) = register_custom_rect_nonce(atlas, raw);
31        Self {
32            raw,
33            atlas,
34            atlas_stamp: state.stamp,
35            generation: state.custom_rect_generation,
36            nonce,
37            _not_send_sync: PhantomData,
38        }
39    }
40}
41
42/// Pixel data used to create or fully replace a custom atlas rectangle.
43#[derive(Copy, Clone, Debug)]
44pub struct CustomRectData<'data> {
45    size: [u16; 2],
46    format: TextureFormat,
47    pixels: &'data [u8],
48}
49
50impl<'data> CustomRectData<'data> {
51    /// Create RGBA8 custom-rectangle data.
52    pub fn rgba32(size: [u16; 2], pixels: &'data [u8]) -> Self {
53        Self::new(size, TextureFormat::RGBA32, pixels)
54    }
55
56    /// Create alpha-only custom-rectangle data.
57    pub fn alpha8(size: [u16; 2], pixels: &'data [u8]) -> Self {
58        Self::new(size, TextureFormat::Alpha8, pixels)
59    }
60
61    fn new(size: [u16; 2], format: TextureFormat, pixels: &'data [u8]) -> Self {
62        assert!(
63            size[0] > 0 && size[1] > 0,
64            "CustomRectData dimensions must be positive"
65        );
66        let expected = usize::from(size[0])
67            .checked_mul(usize::from(size[1]))
68            .and_then(|pixels| pixels.checked_mul(get_format_bytes_per_pixel(format)))
69            .expect("CustomRectData byte length overflowed usize");
70        assert_eq!(
71            pixels.len(),
72            expected,
73            "CustomRectData pixel byte count does not match its size and format"
74        );
75        Self {
76            size,
77            format,
78            pixels,
79        }
80    }
81
82    /// Rectangle dimensions in pixels.
83    pub fn size(&self) -> [u16; 2] {
84        self.size
85    }
86
87    /// Source pixel format.
88    pub fn format(&self) -> TextureFormat {
89        self.format
90    }
91}
92
93/// Copy-out snapshot of a custom rectangle's current texture placement.
94///
95/// Texture growth may make these values stale after another ImGui call. Use the snapshot
96/// immediately, or query the ID again before submitting a later draw command.
97#[derive(Debug)]
98pub struct CustomRectSnapshot<'scope> {
99    atlas: *mut sys::ImFontAtlas,
100    texture: sys::ImTextureRef,
101    _texture_lease: Option<FontAtlasTexture<'scope>>,
102    pixels: TextureRect,
103    uv0: [f32; 2],
104    uv1: [f32; 2],
105}
106
107impl<'scope> CustomRectSnapshot<'scope> {
108    /// Current managed texture reference.
109    pub fn texture(&self) -> TextureRef<'_> {
110        TextureRef::from_font_atlas_raw(self.atlas, self.texture)
111    }
112
113    /// Current pixel-space rectangle.
114    pub fn pixels(&self) -> TextureRect {
115        self.pixels
116    }
117
118    /// Current upper-left UV coordinate.
119    pub fn uv0(&self) -> [f32; 2] {
120        self.uv0
121    }
122
123    /// Current lower-right UV coordinate.
124    pub fn uv1(&self) -> [f32; 2] {
125        self.uv1
126    }
127}
128
129fn validate_for_atlas(
130    id: CustomRectId,
131    atlas: *mut sys::ImFontAtlas,
132    caller: &str,
133) -> Option<sys::ImFontAtlasRectId> {
134    assert!(
135        std::ptr::addr_eq(id.atlas.cast_const(), atlas.cast_const()),
136        "{caller} received a CustomRectId from a different font atlas"
137    );
138    let state = font_atlas_state(atlas);
139    assert_eq!(
140        id.atlas_stamp, state.stamp,
141        "{caller} received a CustomRectId from a destroyed or reused font atlas"
142    );
143    assert_eq!(
144        id.generation, state.custom_rect_generation,
145        "{caller} received a stale CustomRectId invalidated by font atlas mutation"
146    );
147    custom_rect_nonce_is_active(atlas, id.raw, id.nonce).then_some(id.raw)
148}
149
150fn get_native_rect(
151    atlas: *mut sys::ImFontAtlas,
152    id: CustomRectId,
153    caller: &str,
154) -> Option<sys::ImFontAtlasRect> {
155    let raw_id = validate_for_atlas(id, atlas, caller)?;
156    let mut rect = sys::ImFontAtlasRect::default();
157    if unsafe { sys::ImFontAtlas_GetCustomRect(atlas, raw_id, &mut rect) } {
158        Some(rect)
159    } else {
160        None
161    }
162}
163
164unsafe fn snapshot_from_native<'scope>(
165    atlas: *mut sys::ImFontAtlas,
166    rect: sys::ImFontAtlasRect,
167    texture_lease: Option<FontAtlasTexture<'scope>>,
168) -> CustomRectSnapshot<'scope> {
169    CustomRectSnapshot {
170        atlas,
171        texture: unsafe { (*atlas).TexRef },
172        _texture_lease: texture_lease,
173        pixels: TextureRect {
174            x: rect.x,
175            y: rect.y,
176            w: rect.w,
177            h: rect.h,
178        },
179        uv0: [rect.uv0.x, rect.uv0.y],
180        uv1: [rect.uv1.x, rect.uv1.y],
181    }
182}
183
184impl FontAtlas {
185    /// Add and initialize a custom atlas rectangle.
186    ///
187    /// Returns `None` when the atlas cannot allocate the requested rectangle.
188    #[doc(alias = "AddCustomRect")]
189    pub fn add_custom_rect(&self, data: CustomRectData<'_>) -> Option<CustomRectId> {
190        self.assert_mutation_allowed("FontAtlas::add_custom_rect()");
191        self.assert_custom_rect_write_supported(data, "FontAtlas::add_custom_rect()");
192        let atlas = self.raw();
193        let mut rect = sys::ImFontAtlasRect::default();
194        let raw_id = unsafe {
195            sys::ImFontAtlas_AddCustomRect(
196                atlas,
197                i32::from(data.size[0]),
198                i32::from(data.size[1]),
199                &mut rect,
200            )
201        };
202        if raw_id < 0 {
203            return None;
204        }
205
206        self.write_native_rect(rect, data);
207        Some(CustomRectId::from_raw_parts(raw_id, atlas))
208    }
209
210    /// Fully replace a custom rectangle's pixels and queue the exact region for upload.
211    ///
212    /// Returns `false` if the native rectangle has been removed.
213    pub fn write_custom_rect(&self, id: CustomRectId, data: CustomRectData<'_>) -> bool {
214        self.assert_mutation_allowed("FontAtlas::write_custom_rect()");
215        self.assert_custom_rect_write_supported(data, "FontAtlas::write_custom_rect()");
216        let Some(rect) = get_native_rect(self.raw(), id, "FontAtlas::write_custom_rect()") else {
217            return false;
218        };
219        assert_eq!(
220            data.size,
221            [rect.w, rect.h],
222            "FontAtlas::write_custom_rect() data size must match the allocated rectangle"
223        );
224        self.write_native_rect(rect, data);
225        true
226    }
227
228    /// Remove a custom rectangle.
229    ///
230    /// Returns `false` if the rectangle was already removed.
231    #[doc(alias = "RemoveCustomRect")]
232    pub fn remove_custom_rect(&self, id: CustomRectId) -> bool {
233        self.assert_mutation_allowed("FontAtlas::remove_custom_rect()");
234        let atlas = self.raw();
235        let Some(raw_id) = validate_for_atlas(id, atlas, "FontAtlas::remove_custom_rect()") else {
236            return false;
237        };
238        let exists = unsafe { sys::ImFontAtlas_GetCustomRect(atlas, raw_id, std::ptr::null_mut()) };
239        if exists {
240            unsafe { sys::ImFontAtlas_RemoveCustomRect(atlas, raw_id) };
241        }
242        unregister_custom_rect_nonce(atlas, raw_id, id.nonce);
243        exists
244    }
245
246    /// Query the rectangle's current placement.
247    #[doc(alias = "GetCustomRect")]
248    pub fn custom_rect(&self, id: CustomRectId) -> Option<CustomRectSnapshot<'_>> {
249        let atlas = self.raw();
250        let rect = get_native_rect(atlas, id, "FontAtlas::custom_rect()")?;
251        let texture_lease = self.tex_data_internal();
252        Some(unsafe { snapshot_from_native(atlas, rect, texture_lease) })
253    }
254
255    fn assert_custom_rect_write_supported(&self, data: CustomRectData<'_>, caller: &str) {
256        let atlas = self.raw();
257        unsafe {
258            let texture = (*atlas).TexData;
259            let destination_format = if texture.is_null() {
260                TextureFormat::from((*atlas).TexDesiredFormat)
261            } else {
262                TextureFormat::from((*texture).Format)
263            };
264            assert!(
265                data.format != TextureFormat::RGBA32 || destination_format == TextureFormat::RGBA32,
266                "{caller} cannot store RGBA32 pixels in an Alpha8 font atlas; use an RGBA32 atlas or alpha8 custom-rectangle data"
267            );
268            assert!(
269                (*atlas).RendererHasTextures
270                    || texture.is_null()
271                    || (*texture).Status != sys::ImTextureStatus_OK,
272                "{caller} cannot update an already-uploaded legacy font atlas; enable a renderer with RENDERER_HAS_TEXTURES or rebuild and fully re-upload the atlas"
273            );
274        }
275    }
276
277    fn write_native_rect(&self, rect: sys::ImFontAtlasRect, data: CustomRectData<'_>) {
278        let atlas = self.raw();
279        unsafe {
280            let texture = (*atlas).TexData;
281            assert!(
282                !texture.is_null() && !(*texture).Pixels.is_null(),
283                "custom rectangle requires allocated atlas texture pixels"
284            );
285            assert!(
286                (*texture).Status != sys::ImTextureStatus_WantDestroy
287                    && (*texture).Status != sys::ImTextureStatus_Destroyed,
288                "custom rectangle texture is not writable in its current status"
289            );
290
291            let texture_width = usize::try_from((*texture).Width)
292                .expect("font atlas texture width must be non-negative");
293            let texture_height = usize::try_from((*texture).Height)
294                .expect("font atlas texture height must be non-negative");
295            let destination_bpp = usize::try_from((*texture).BytesPerPixel)
296                .expect("font atlas texture bytes per pixel must be non-negative");
297            assert_eq!(
298                destination_bpp,
299                get_format_bytes_per_pixel(TextureFormat::from((*texture).Format)),
300                "font atlas texture format and bytes-per-pixel metadata disagree"
301            );
302            let x = usize::from(rect.x);
303            let y = usize::from(rect.y);
304            let width = usize::from(rect.w);
305            let height = usize::from(rect.h);
306            assert!(
307                x.checked_add(width).is_some_and(|end| end <= texture_width)
308                    && y.checked_add(height)
309                        .is_some_and(|end| end <= texture_height),
310                "custom rectangle exceeds the current atlas texture"
311            );
312
313            let source_bpp = get_format_bytes_per_pixel(data.format);
314            let source_pitch = width
315                .checked_mul(source_bpp)
316                .expect("custom rectangle source pitch overflowed usize");
317            let destination_pitch = texture_width
318                .checked_mul(destination_bpp)
319                .expect("font atlas texture pitch overflowed usize");
320            let destination_offset = y
321                .checked_mul(texture_width)
322                .and_then(|offset| offset.checked_add(x))
323                .and_then(|offset| offset.checked_mul(destination_bpp))
324                .expect("custom rectangle destination offset overflowed usize");
325            let destination = (*texture).Pixels.add(destination_offset);
326            let destination_format = TextureFormat::from((*texture).Format);
327            for row in 0..height {
328                let source_row = data.pixels.as_ptr().add(row * source_pitch);
329                let destination_row = destination.add(row * destination_pitch);
330                match (data.format, destination_format) {
331                    (TextureFormat::RGBA32, TextureFormat::RGBA32)
332                    | (TextureFormat::Alpha8, TextureFormat::Alpha8) => {
333                        std::ptr::copy_nonoverlapping(source_row, destination_row, source_pitch);
334                    }
335                    (TextureFormat::Alpha8, TextureFormat::RGBA32) => {
336                        for column in 0..width {
337                            let alpha = *source_row.add(column);
338                            let pixel = destination_row.add(column * 4);
339                            std::ptr::copy_nonoverlapping(
340                                [255, 255, 255, alpha].as_ptr(),
341                                pixel,
342                                4,
343                            );
344                        }
345                    }
346                    (TextureFormat::RGBA32, TextureFormat::Alpha8) => unreachable!(
347                        "RGBA32 to Alpha8 custom-rectangle writes are rejected before mutation"
348                    ),
349                }
350            }
351            if data.format == TextureFormat::RGBA32 {
352                (*atlas).TexPixelsUseColors = true;
353                (*texture).UseColors = true;
354            }
355            if (*atlas).RendererHasTextures {
356                sys::igImFontAtlasTextureBlockQueueUpload(
357                    atlas,
358                    texture,
359                    i32::from(rect.x),
360                    i32::from(rect.y),
361                    i32::from(rect.w),
362                    i32::from(rect.h),
363                );
364            }
365        }
366    }
367}
368
369impl Ui {
370    /// Query a custom rectangle for immediate use in the current frame.
371    pub fn custom_rect(&self, id: CustomRectId) -> Option<CustomRectSnapshot<'_>> {
372        self.run_with_bound_context(|| {
373            let atlas = current_context_font_atlas("Ui::custom_rect()");
374            let rect = get_native_rect(atlas, id, "Ui::custom_rect()")?;
375            Some(unsafe { snapshot_from_native(atlas, rect, None) })
376        })
377    }
378
379    /// Draw a custom rectangle using its latest texture reference and UVs.
380    ///
381    /// Returns `false` if the rectangle has been removed.
382    pub fn image_custom_rect(&self, id: CustomRectId, size: [f32; 2]) -> bool {
383        let Some(rect) = self.custom_rect(id) else {
384            return false;
385        };
386        self.image_config(rect.texture(), size)
387            .uv0(rect.uv0())
388            .uv1(rect.uv1())
389            .build();
390        true
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397
398    #[test]
399    fn custom_rect_writes_pixels_and_queues_exact_updates() {
400        let ctx = crate::Context::create();
401        let pixels = [
402            255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255,
403        ];
404        let id = ctx
405            .font_atlas()
406            .add_custom_rect(CustomRectData::rgba32([2, 2], &pixels))
407            .expect("the custom rectangle should fit");
408
409        let snapshot = ctx
410            .font_atlas()
411            .custom_rect(id)
412            .expect("the custom rectangle should resolve");
413        assert_eq!(snapshot.pixels().w, 2);
414        assert_eq!(snapshot.pixels().h, 2);
415
416        let rect = snapshot.pixels();
417        drop(snapshot);
418        let atlas = ctx.font_atlas();
419        unsafe {
420            (*atlas.raw()).RendererHasTextures = true;
421            (*(*atlas.raw()).TexData).Status = sys::ImTextureStatus_OK;
422        }
423
424        let replacement = [7u8; 16];
425        assert!(atlas.write_custom_rect(id, CustomRectData::rgba32([2, 2], &replacement)));
426        let updates: Vec<_> = atlas
427            .tex_data_internal()
428            .expect("atlas texture should remain available")
429            .updates()
430            .collect();
431        assert_eq!(updates.last().copied(), Some(rect));
432    }
433
434    #[test]
435    fn custom_rect_snapshot_leases_its_texture_until_drop() {
436        let ctx = crate::Context::create();
437        let atlas = ctx.font_atlas();
438        let id = atlas
439            .add_custom_rect(CustomRectData::alpha8([1, 1], &[0x7f]))
440            .expect("the custom rectangle should fit");
441        let snapshot = atlas
442            .custom_rect(id)
443            .expect("the custom rectangle should resolve");
444
445        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
446            let _ = atlas.write_custom_rect(id, CustomRectData::alpha8([1, 1], &[0xff]));
447        }));
448        assert!(result.is_err());
449
450        drop(snapshot);
451        assert!(atlas.write_custom_rect(id, CustomRectData::alpha8([1, 1], &[0xff])));
452    }
453
454    #[test]
455    fn custom_rect_rejects_updates_after_a_legacy_upload() {
456        let ctx = crate::Context::create();
457        let id = ctx
458            .font_atlas()
459            .add_custom_rect(CustomRectData::alpha8([1, 1], &[0x7f]))
460            .expect("the custom rectangle should fit");
461        let legacy = ctx
462            .font_atlas()
463            .try_claim_legacy_renderer()
464            .expect("the test models a legacy renderer");
465        unsafe {
466            // The test models a completed legacy upload for this unshared atlas.
467            legacy.set_texture_id(crate::TextureId::new(17));
468        }
469
470        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
471            let _ = ctx
472                .font_atlas()
473                .write_custom_rect(id, CustomRectData::alpha8([1, 1], &[0xff]));
474        }));
475        assert!(result.is_err());
476    }
477
478    #[test]
479    fn custom_rect_rejects_rgba_data_for_an_alpha_atlas_before_reading_alignment() {
480        let ctx = crate::Context::create();
481        let atlas = ctx.font_atlas();
482        unsafe { (*atlas.raw()).TexDesiredFormat = sys::ImTextureFormat_Alpha8 };
483        let legacy = atlas
484            .try_claim_legacy_renderer()
485            .expect("the test requires a legacy font atlas");
486        legacy.build();
487        assert_eq!(
488            legacy
489                .tex_data()
490                .expect("atlas texture should exist")
491                .format(),
492            TextureFormat::Alpha8
493        );
494
495        let storage = [0u8; 8];
496        let alignment = std::mem::align_of::<u32>();
497        let offset = (0..alignment)
498            .find(|offset| (storage.as_ptr() as usize + offset) % alignment != 0)
499            .expect("one of the first u32-alignment offsets must be unaligned");
500        let unaligned_rgba = &storage[offset..offset + 4];
501        assert_ne!((unaligned_rgba.as_ptr() as usize) % alignment, 0);
502        let builder = unsafe { (*atlas.raw()).Builder };
503        assert!(!builder.is_null());
504        let rect_count_before = unsafe { (*builder).RectsIndex.Size };
505        let texture_status_before = unsafe { (*(*atlas.raw()).TexData).Status };
506        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
507            let _ = atlas.add_custom_rect(CustomRectData::rgba32([1, 1], unaligned_rgba));
508        }));
509        let panic = result.expect_err("RGBA32 data must be rejected before FFI");
510        let message = panic
511            .downcast_ref::<String>()
512            .map(String::as_str)
513            .or_else(|| panic.downcast_ref::<&str>().copied())
514            .unwrap_or_default();
515        assert!(message.contains("cannot store RGBA32 pixels in an Alpha8 font atlas"));
516        assert_eq!(unsafe { (*builder).RectsIndex.Size }, rect_count_before);
517        assert_eq!(
518            unsafe { (*(*atlas.raw()).TexData).Status },
519            texture_status_before
520        );
521    }
522
523    #[test]
524    fn custom_rect_converts_alpha_pixels_to_rgba() {
525        let ctx = crate::Context::create();
526        let id = ctx
527            .font_atlas()
528            .add_custom_rect(CustomRectData::alpha8([1, 1], &[0x7f]))
529            .expect("the custom rectangle should fit");
530        let rect = ctx
531            .font_atlas()
532            .custom_rect(id)
533            .expect("the custom rectangle should resolve")
534            .pixels();
535        let legacy = ctx
536            .font_atlas()
537            .try_claim_legacy_renderer()
538            .expect("the test requires a legacy font atlas");
539        let texture = legacy.tex_data().expect("atlas texture should exist");
540        let pixel = texture
541            .pixels_at(u32::from(rect.x), u32::from(rect.y))
542            .expect("custom rectangle pixel should be addressable");
543        assert_eq!(&pixel[..4], &[255, 255, 255, 0x7f]);
544    }
545
546    #[test]
547    fn removed_and_cross_atlas_custom_rect_ids_are_rejected() {
548        let ctx_a = crate::Context::create();
549        let id = ctx_a
550            .font_atlas()
551            .add_custom_rect(CustomRectData::alpha8([1, 1], &[255]))
552            .expect("the custom rectangle should fit");
553        assert!(ctx_a.font_atlas().remove_custom_rect(id));
554        assert!(!ctx_a.font_atlas().remove_custom_rect(id));
555        let suspended_a = ctx_a.suspend_or_panic();
556
557        let ctx_b = crate::Context::create();
558        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
559            let _ = ctx_b.font_atlas().custom_rect(id);
560        }));
561        assert!(result.is_err());
562
563        drop(ctx_b);
564        drop(suspended_a);
565    }
566
567    #[test]
568    fn removed_custom_rect_id_does_not_revive_after_native_generation_wrap() {
569        let ctx = crate::Context::create();
570        let atlas = ctx.font_atlas();
571        let data = CustomRectData::alpha8([1, 1], &[255]);
572
573        let generation_zero = atlas
574            .add_custom_rect(data)
575            .expect("the initial custom rectangle should fit");
576        assert!(atlas.remove_custom_rect(generation_zero));
577
578        let stale = atlas
579            .add_custom_rect(data)
580            .expect("the reusable custom rectangle should fit");
581        assert!(atlas.remove_custom_rect(stale));
582
583        for _ in 0..1022 {
584            let current = atlas
585                .add_custom_rect(data)
586                .expect("the recycled custom rectangle should fit");
587            assert!(atlas.remove_custom_rect(current));
588        }
589
590        let replacement = atlas
591            .add_custom_rect(data)
592            .expect("the wrapped custom rectangle should fit");
593        assert_eq!(
594            stale.raw, replacement.raw,
595            "the test must exercise native ID reuse after its 10-bit generation wraps"
596        );
597        assert!(atlas.custom_rect(stale).is_none());
598        assert!(!atlas.write_custom_rect(stale, data));
599        assert!(!atlas.remove_custom_rect(stale));
600        assert!(atlas.custom_rect(replacement).is_some());
601    }
602
603    #[test]
604    fn custom_rect_data_requires_an_exact_pixel_count() {
605        assert!(std::panic::catch_unwind(|| CustomRectData::rgba32([2, 2], &[0; 15])).is_err());
606    }
607
608    #[test]
609    fn custom_rect_id_survives_repacking_but_not_builder_clear() {
610        let ctx = crate::Context::create();
611        let discarded = ctx
612            .font_atlas()
613            .add_custom_rect(CustomRectData::alpha8([32, 32], &[255; 32 * 32]))
614            .expect("the first custom rectangle should fit");
615        let retained = ctx
616            .font_atlas()
617            .add_custom_rect(CustomRectData::alpha8([16, 16], &[127; 16 * 16]))
618            .expect("the second custom rectangle should fit");
619        assert!(ctx.font_atlas().remove_custom_rect(discarded));
620        ctx.font_atlas().compact_cache();
621        assert!(ctx.font_atlas().custom_rect(retained).is_some());
622
623        ctx.font_atlas().clear_fonts();
624        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
625            let _ = ctx.font_atlas().custom_rect(retained);
626        }));
627        assert!(result.is_err());
628    }
629
630    #[test]
631    fn ui_draws_custom_rect_from_a_fresh_frame_snapshot() {
632        let mut ctx = crate::Context::create();
633        let id = ctx
634            .font_atlas()
635            .add_custom_rect(CustomRectData::rgba32([1, 1], &[255, 0, 0, 255]))
636            .expect("the custom rectangle should fit");
637        ctx.font_atlas()
638            .try_claim_legacy_renderer()
639            .expect("legacy renderer font atlas should be available")
640            .build();
641        ctx.io_mut().set_display_size([128.0, 128.0]);
642        ctx.io_mut().set_delta_time(1.0 / 60.0);
643
644        assert!(ctx.frame().image_custom_rect(id, [8.0, 8.0]));
645        let _ = ctx.render_legacy();
646    }
647}