Skip to main content

dear_imgui_rs/fonts/
baked.rs

1//! Frame-bound baked font data.
2
3use std::marker::PhantomData;
4use std::ptr::NonNull;
5use std::rc::Rc;
6
7use crate::fonts::{FontId, Glyph, validate_font_id_for_current_context};
8use crate::{Ui, sys};
9
10fn validate_positive_finite(caller: &str, name: &str, value: f32) {
11    assert!(value.is_finite(), "{caller} {name} must be finite");
12    assert!(value > 0.0, "{caller} {name} must be positive");
13}
14
15fn validate_font_size(caller: &str, size: f32) {
16    validate_positive_finite(caller, "size", size);
17    assert!(
18        size <= 512.0,
19        "{caller} size must not exceed Dear ImGui's 512px font-size limit"
20    );
21}
22
23fn wchar(c: char) -> Option<sys::ImWchar> {
24    let codepoint = c as u32;
25    if std::mem::size_of::<sys::ImWchar>() == 2 && codepoint > u16::MAX as u32 {
26        None
27    } else {
28        Some(codepoint as sys::ImWchar)
29    }
30}
31
32/// Runtime font data baked for one size and rasterizer density in the current frame.
33///
34/// Dear ImGui may compact and move baked-font storage at the next frame boundary. This view is
35/// therefore tied to the [`Ui`] borrow that created it and cannot be retained across rendering or
36/// a subsequent frame. Glyph queries return owned [`Glyph`] metric copies because lazy loading may
37/// reallocate the native glyph vector; atlas-relative UVs are deliberately omitted because a
38/// later glyph load may repack them within the same frame.
39#[derive(Debug)]
40pub struct BakedFont<'ui> {
41    ui: &'ui Ui,
42    font: FontId,
43    size: f32,
44    rasterizer_density: f32,
45    _not_send_sync: PhantomData<Rc<()>>,
46}
47
48impl<'ui> BakedFont<'ui> {
49    unsafe fn from_raw(ui: &'ui Ui, font: FontId, raw: *mut sys::ImFontBaked) -> Option<Self> {
50        if raw.is_null() {
51            return None;
52        }
53        Some(Self {
54            ui,
55            font,
56            size: unsafe { (*raw).Size },
57            rasterizer_density: unsafe { (*raw).RasterizerDensity },
58            _not_send_sync: PhantomData,
59        })
60    }
61
62    fn raw(&self) -> *mut sys::ImFontBaked {
63        self.ui.run_with_bound_context(|| {
64            let font = validate_font_id_for_current_context(self.font, "BakedFont");
65            let raw = unsafe { sys::ImFont_GetFontBaked(font, self.size, self.rasterizer_density) };
66            assert!(
67                !raw.is_null(),
68                "BakedFont could not resolve its validated font, size, and rasterizer density"
69            );
70            raw
71        })
72    }
73
74    /// Baked character height in logical pixels.
75    pub fn size(&self) -> f32 {
76        self.size
77    }
78
79    /// Rasterizer density used for this baked data.
80    pub fn rasterizer_density(&self) -> f32 {
81        self.rasterizer_density
82    }
83
84    /// Font ascent for this baked size.
85    pub fn ascent(&self) -> f32 {
86        unsafe { (*self.raw()).Ascent }
87    }
88
89    /// Font descent for this baked size.
90    pub fn descent(&self) -> f32 {
91        unsafe { (*self.raw()).Descent }
92    }
93
94    /// Approximate texture surface occupied by the loaded glyphs.
95    pub fn metrics_total_surface(&self) -> u32 {
96        unsafe { (*self.raw()).MetricsTotalSurface() }
97    }
98
99    /// Persistent ID of the font that owns this frame-local baked data.
100    pub fn font_id(&self) -> FontId {
101        self.font
102    }
103
104    /// Returns whether a glyph is already loaded without requesting a new glyph.
105    #[doc(alias = "IsGlyphLoaded")]
106    pub fn is_glyph_loaded(&self, c: char) -> bool {
107        let Some(c) = wchar(c) else {
108            return false;
109        };
110        unsafe { sys::ImFontBaked_IsGlyphLoaded(self.raw(), c) }
111    }
112
113    /// Find a glyph, falling back to the font's replacement glyph when necessary.
114    ///
115    /// This may lazily load glyph data and update the managed atlas texture.
116    #[doc(alias = "FindGlyph")]
117    pub fn glyph_or_fallback(&mut self, c: char) -> Option<Glyph> {
118        let c = wchar(c)?;
119        let glyph = unsafe { sys::ImFontBaked_FindGlyph(self.raw(), c) };
120        NonNull::new(glyph).map(|glyph| Glyph::from_raw(unsafe { *glyph.as_ptr() }))
121    }
122
123    /// Find a glyph without using the replacement glyph.
124    ///
125    /// This may lazily load glyph data and update the managed atlas texture.
126    #[doc(alias = "FindGlyphNoFallback")]
127    pub fn glyph(&mut self, c: char) -> Option<Glyph> {
128        let c = wchar(c)?;
129        let glyph = unsafe { sys::ImFontBaked_FindGlyphNoFallback(self.raw(), c) };
130        NonNull::new(glyph).map(|glyph| Glyph::from_raw(unsafe { *glyph.as_ptr() }))
131    }
132
133    /// Return the horizontal advance for a character.
134    ///
135    /// This may lazily load glyph metrics.
136    #[doc(alias = "GetCharAdvance")]
137    pub fn char_advance(&mut self, c: char) -> Option<f32> {
138        let c = wchar(c)?;
139        Some(unsafe { sys::ImFontBaked_GetCharAdvance(self.raw(), c) })
140    }
141}
142
143impl Ui {
144    /// Return baked data for the currently bound font, size, and rasterizer density.
145    ///
146    /// ```compile_fail
147    /// # use dear_imgui_rs::Context;
148    /// let baked = {
149    ///     let mut ctx = Context::create();
150    ///     let ui = ctx.frame();
151    ///     ui.current_baked_font()
152    /// };
153    /// baked.size();
154    /// ```
155    #[doc(alias = "GetFontBaked")]
156    pub fn current_baked_font(&self) -> BakedFont<'_> {
157        self.run_with_bound_context(|| {
158            let font = unsafe { FontId::from_font(sys::igGetFont(), "Ui::current_baked_font()") };
159            let raw = unsafe { sys::igGetFontBaked() };
160            unsafe { BakedFont::from_raw(self, font, raw) }
161                .expect("Ui::current_baked_font() requires an open frame with a current font")
162        })
163    }
164
165    /// Return baked data for a font at the requested size and its current rasterizer density.
166    ///
167    /// Returns `None` for a legacy renderer while the atlas is locked, because creating an
168    /// arbitrary baked size during that frame is unsupported by Dear ImGui.
169    #[doc(alias = "ImFont::GetFontBaked")]
170    pub fn baked_font(&self, font: FontId, size: f32) -> Option<BakedFont<'_>> {
171        validate_font_size("Ui::baked_font()", size);
172        self.baked_font_impl(font, size, -1.0)
173    }
174
175    /// Return baked data for a font at an explicit size and rasterizer density.
176    ///
177    /// Returns `None` for a legacy renderer while the atlas is locked.
178    #[doc(alias = "ImFont::GetFontBaked")]
179    pub fn baked_font_with_density(
180        &self,
181        font: FontId,
182        size: f32,
183        density: f32,
184    ) -> Option<BakedFont<'_>> {
185        validate_font_size("Ui::baked_font_with_density()", size);
186        validate_positive_finite("Ui::baked_font_with_density()", "density", density);
187        self.baked_font_impl(font, size, density)
188    }
189
190    fn baked_font_impl(&self, font: FontId, size: f32, density: f32) -> Option<BakedFont<'_>> {
191        self.run_with_bound_context(|| {
192            let raw_font = validate_font_id_for_current_context(font, "Ui::baked_font()");
193            let atlas = unsafe { (*raw_font).OwnerAtlas };
194            if atlas.is_null() || unsafe { (*atlas).Locked } {
195                return None;
196            }
197            let raw = unsafe { sys::ImFont_GetFontBaked(raw_font, size, density) };
198            unsafe { BakedFont::from_raw(self, font, raw) }
199        })
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    fn setup_context() -> (crate::Context, crate::FontId) {
206        let mut ctx = crate::Context::create();
207        let font = ctx
208            .font_atlas()
209            .add_font(&[crate::FontSource::default_font()]);
210        ctx.font_atlas()
211            .try_claim_legacy_renderer()
212            .expect("legacy renderer font atlas should be available")
213            .build();
214        ctx.io_mut().set_display_size([128.0, 128.0]);
215        ctx.io_mut().set_delta_time(1.0 / 60.0);
216        (ctx, font)
217    }
218
219    #[test]
220    fn current_baked_font_copies_glyphs_and_exposes_metrics() {
221        let (mut ctx, font_id) = setup_context();
222        let ui = ctx.frame();
223        let mut baked = ui.current_baked_font();
224
225        assert_eq!(baked.font_id(), font_id);
226        assert!(baked.size() > 0.0);
227        assert!(baked.rasterizer_density() > 0.0);
228        assert!(baked.ascent() > baked.descent());
229        assert!(baked.char_advance('A').is_some_and(|advance| advance > 0.0));
230        let glyph = baked
231            .glyph_or_fallback('A')
232            .expect("the default font should contain A");
233        assert_eq!(glyph.codepoint(), 'A' as u32);
234        assert!(glyph.advance_x() > 0.0);
235
236        let debug = format!("{glyph:?}");
237        for unstable_field in ["PackId", "U0", "V0", "U1", "V1"] {
238            assert!(
239                !debug.contains(unstable_field),
240                "Glyph::Debug exposed unstable atlas field {unstable_field}: {debug}"
241            );
242        }
243    }
244
245    #[test]
246    fn arbitrary_baked_font_is_rejected_while_legacy_atlas_is_locked() {
247        let (mut ctx, font_id) = setup_context();
248        let ui = ctx.frame();
249
250        assert!(ui.baked_font(font_id, 18.0).is_none());
251        assert!(ui.current_baked_font().size() > 0.0);
252    }
253
254    #[test]
255    fn managed_atlas_can_create_an_arbitrary_baked_size_in_frame() {
256        let mut ctx = crate::Context::create();
257        let consumer = ctx
258            .create_synchronous_renderer_consumer()
259            .expect("the managed renderer consumer should attach");
260        let font_id = ctx
261            .font_atlas()
262            .add_font(&[crate::FontSource::default_font()]);
263        ctx.io_mut().set_display_size([128.0, 128.0]);
264        ctx.io_mut().set_delta_time(1.0 / 60.0);
265        ctx.io_mut()
266            .set_backend_flags(crate::BackendFlags::RENDERER_HAS_TEXTURES);
267
268        {
269            let ui = ctx.frame();
270            let baked = ui
271                .baked_font_with_density(font_id, 18.0, 2.0)
272                .expect("managed atlases should allow dynamic baked sizes");
273            assert_eq!(baked.size(), 18.0);
274            assert_eq!(baked.rasterizer_density(), 2.0);
275        }
276        let _ = ctx.render(&consumer);
277    }
278
279    #[test]
280    fn baked_font_validates_size_and_density_before_ffi() {
281        let (mut ctx, font_id) = setup_context();
282        let ui = ctx.frame();
283
284        assert!(
285            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
286                let _ = ui.baked_font(font_id, 0.0);
287            }))
288            .is_err()
289        );
290        assert!(
291            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
292                let _ = ui.baked_font_with_density(font_id, 13.0, f32::NAN);
293            }))
294            .is_err()
295        );
296        assert!(
297            std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
298                let _ = ui.baked_font(font_id, 513.0);
299            }))
300            .is_err()
301        );
302    }
303}