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#[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#[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 pub fn rgba32(size: [u16; 2], pixels: &'data [u8]) -> Self {
53 Self::new(size, TextureFormat::RGBA32, pixels)
54 }
55
56 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 pub fn size(&self) -> [u16; 2] {
84 self.size
85 }
86
87 pub fn format(&self) -> TextureFormat {
89 self.format
90 }
91}
92
93#[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 pub fn texture(&self) -> TextureRef<'_> {
110 TextureRef::from_font_atlas_raw(self.atlas, self.texture)
111 }
112
113 pub fn pixels(&self) -> TextureRect {
115 self.pixels
116 }
117
118 pub fn uv0(&self) -> [f32; 2] {
120 self.uv0
121 }
122
123 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 #[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 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 #[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 #[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 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 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 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}