dear_imgui_rs/fonts/
baked.rs1use 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#[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 pub fn size(&self) -> f32 {
76 self.size
77 }
78
79 pub fn rasterizer_density(&self) -> f32 {
81 self.rasterizer_density
82 }
83
84 pub fn ascent(&self) -> f32 {
86 unsafe { (*self.raw()).Ascent }
87 }
88
89 pub fn descent(&self) -> f32 {
91 unsafe { (*self.raw()).Descent }
92 }
93
94 pub fn metrics_total_surface(&self) -> u32 {
96 unsafe { (*self.raw()).MetricsTotalSurface() }
97 }
98
99 pub fn font_id(&self) -> FontId {
101 self.font
102 }
103
104 #[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 #[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 #[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 #[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 #[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 #[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 #[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}