1use std::collections::HashMap;
8
9use cosmic_text::{
10 Align, Attrs, Buffer, CacheKey, CacheKeyFlags, Family, FontSystem, Metrics, Shaping, Style,
11 SubpixelBin, SwashCache, SwashContent, SwashImage, Weight, Wrap,
12};
13
14mod gpu;
15#[cfg(feature = "experimental-msdf")]
16mod msdf;
17pub use gpu::*;
18#[cfg(feature = "experimental-msdf")]
19pub use msdf::*;
20
21pub const ALPHA_TEXT_SHADER: &str = include_str!("shaders/alpha_text.wgsl");
22#[cfg(feature = "experimental-msdf")]
23pub const MSDF_TEXT_SHADER: &str = include_str!("shaders/msdf_text.wgsl");
24#[cfg(feature = "experimental-msdf")]
25pub const MSDF_GENERATOR_SHADER: &str = include_str!("shaders/msdf_generate.wgsl");
26pub const DEFAULT_FONT_FAMILY: &str = "Roboto";
27pub const ROBOTO_REGULAR_BYTES: &[u8] = include_bytes!("../assets/roboto/Roboto-Regular.ttf");
28
29#[derive(Debug)]
30pub struct TextSystem {
31 font_system: FontSystem,
32 swash_cache: SwashCache,
33 #[cfg(feature = "experimental-msdf")]
34 msdf_job_cache: HashMap<GlyphKey, Option<MsdfGlyphJob>>,
35}
36
37impl TextSystem {
38 pub fn new() -> Self {
39 let mut font_system = FontSystem::new_with_fonts(std::iter::empty());
40 load_default_fonts(&mut font_system);
41 Self {
42 font_system,
43 swash_cache: SwashCache::new(),
44 #[cfg(feature = "experimental-msdf")]
45 msdf_job_cache: HashMap::new(),
46 }
47 }
48
49 pub fn load_font_data(&mut self, data: Vec<u8>) {
50 self.font_system.db_mut().load_font_data(data);
51 #[cfg(feature = "experimental-msdf")]
52 self.msdf_job_cache.clear();
53 }
54
55 pub fn measure(&mut self, request: &TextLayoutRequest) -> TextMeasurement {
56 self.layout(request).measurement
57 }
58
59 pub fn layout(&mut self, request: &TextLayoutRequest) -> TextLayout {
60 let metrics = Metrics::new(request.font_size.max(1.0), request.line_height());
61 let mut buffer = Buffer::new(&mut self.font_system, metrics);
62 buffer.set_size(
63 &mut self.font_system,
64 request.max_width.filter(|width| *width > 0.0),
65 None,
66 );
67 buffer.set_wrap(
68 &mut self.font_system,
69 if request.max_width.is_some_and(|width| width > 0.0) {
70 Wrap::WordOrGlyph
71 } else {
72 Wrap::None
73 },
74 );
75 let attrs = Attrs::new()
76 .family(Family::Name(&request.font_family))
77 .weight(Weight(request.font_weight.clamp(1, 1000)))
78 .style(match request.font_style {
79 TextFontStyle::Italic => Style::Italic,
80 TextFontStyle::Oblique => Style::Oblique,
81 TextFontStyle::Normal => Style::Normal,
82 })
83 .cache_key_flags(if request.disable_hinting {
84 CacheKeyFlags::DISABLE_HINTING
85 } else {
86 CacheKeyFlags::empty()
87 });
88 buffer.set_text(
89 &mut self.font_system,
90 &request.content,
91 &attrs,
92 Shaping::Advanced,
93 Some(match request.align {
94 TextAlign::Left => Align::Left,
95 TextAlign::Center => Align::Center,
96 TextAlign::Right => Align::Right,
97 TextAlign::Justified => Align::Justified,
98 }),
99 );
100 buffer.shape_until_scroll(&mut self.font_system, true);
101
102 let mut glyphs = Vec::new();
103 let mut width: f32 = 0.0;
104 let mut height: f32 = 0.0;
105 for run in buffer.layout_runs() {
106 width = width.max(run.line_w);
107 height = height.max(run.line_top + run.line_height);
108 for glyph in run.glyphs {
109 let physical =
110 glyph.physical((request.origin[0], request.origin[1] + run.line_y), 1.0);
111 let mut cache_key = physical.cache_key;
112 cache_key.x_bin = SubpixelBin::Zero;
113 cache_key.y_bin = SubpixelBin::Zero;
114 let x = request.origin[0] + glyph.x + (glyph.font_size * glyph.x_offset);
115 let y =
116 request.origin[1] + run.line_y + glyph.y - (glyph.font_size * glyph.y_offset);
117 glyphs.push(TextGlyph {
118 key: GlyphKey(cache_key),
119 x,
120 y,
121 width: glyph.w,
122 height: run.line_height,
123 x_offset: glyph.x_offset,
124 y_offset: glyph.y_offset,
125 color: request.color,
126 });
127 }
128 }
129
130 TextLayout {
131 measurement: TextMeasurement { width, height },
132 glyphs,
133 }
134 }
135
136 pub fn render_alpha_atlas(
137 &mut self,
138 layout: &TextLayout,
139 config: AtlasConfig,
140 max_glyphs: usize,
141 ) -> AlphaAtlasRender {
142 self.render_atlas(layout, config, max_glyphs)
143 }
144
145 #[cfg(feature = "experimental-msdf")]
146 pub fn render_gpu_hybrid_atlas(
147 &mut self,
148 layout: &TextLayout,
149 config: AtlasConfig,
150 max_glyphs: usize,
151 max_segments: usize,
152 max_msdf_pixels: u32,
153 ) -> GpuHybridAtlasRender {
154 let mut atlas = GlyphAtlas::new(config);
155 let mut pixels = vec![0_u8; config.width as usize * config.height as usize * 4];
156 let mut instances = Vec::with_capacity(max_glyphs.min(layout.glyphs.len()));
157 let mut jobs = Vec::new();
158 let mut segments = Vec::new();
159 let mut pixel_jobs = Vec::new();
160 let mut msdf_pixel_count = 0_u32;
161 let mut glyph_count = 0;
162
163 for glyph in layout.glyphs.iter().take(max_glyphs) {
164 let msdf_key = glyph.key.msdf_key();
165 if let Some(msdf) = self.glyph_msdf_job(msdf_key, config) {
166 let glyph_size = [msdf.placement.width, msdf.placement.height];
167 let glyph_pixels = glyph_size[0].saturating_mul(glyph_size[1]);
168 if !msdf.segments.is_empty()
169 && segments.len().saturating_add(msdf.segments.len()) <= max_segments
170 && msdf_pixel_count.saturating_add(glyph_pixels) <= max_msdf_pixels
171 {
172 let Some(entry) = atlas.ensure_glyph(msdf_key, glyph_size) else {
173 continue;
174 };
175 let segment_start = segments.len() as u32;
176 let pixel_start = msdf_pixel_count;
177 let job_index = jobs.len() as u32;
178 segments.extend(msdf.segments);
179 msdf_pixel_count = msdf_pixel_count.saturating_add(glyph_pixels);
180 pixel_jobs.extend(std::iter::repeat(job_index).take(glyph_pixels as usize));
181 jobs.push(GpuMsdfJob {
182 atlas_rect: [
183 entry.origin[0],
184 entry.origin[1],
185 entry.size[0],
186 entry.size[1],
187 ],
188 segment_range: [segment_start, segments.len() as u32 - segment_start],
189 pixel_range: [pixel_start, glyph_pixels],
190 px_range: config.px_range.max(1) as f32,
191 _padding: [0; 3],
192 });
193 instances.push(msdf_glyph_instance_for(glyph, entry, &msdf.placement));
194 } else {
195 let Some(instance) =
196 self.raster_glyph_instance(glyph, &mut atlas, config, &mut pixels)
197 else {
198 continue;
199 };
200 instances.push(instance);
201 }
202 } else {
203 let Some(instance) =
204 self.raster_glyph_instance(glyph, &mut atlas, config, &mut pixels)
205 else {
206 continue;
207 };
208 instances.push(instance);
209 }
210 glyph_count += 1;
211 }
212
213 GpuHybridAtlasRender {
214 atlas,
215 pixels,
216 instances,
217 jobs,
218 segments,
219 pixel_jobs,
220 msdf_pixel_count,
221 glyph_count,
222 }
223 }
224
225 fn render_atlas(
226 &mut self,
227 layout: &TextLayout,
228 config: AtlasConfig,
229 max_glyphs: usize,
230 ) -> AlphaAtlasRender {
231 let mut atlas = GlyphAtlas::new(config);
232 let mut pixels = vec![0_u8; config.width as usize * config.height as usize * 4];
233 let mut instances = Vec::with_capacity(max_glyphs.min(layout.glyphs.len()));
234 let mut glyph_count = 0;
235
236 for glyph in layout.glyphs.iter().take(max_glyphs) {
237 let Some(image) = self.glyph_image(glyph.key) else {
238 continue;
239 };
240 let glyph_size = [image.placement.width, image.placement.height];
241 let Some(entry) = atlas.ensure_glyph(glyph.key, glyph_size) else {
242 continue;
243 };
244 write_glyph_to_atlas(&mut pixels, config, entry, &image);
245 instances.push(glyph_instance_for(glyph, entry, &image));
246 glyph_count += 1;
247 }
248
249 AlphaAtlasRender {
250 atlas,
251 pixels,
252 instances,
253 glyph_count,
254 }
255 }
256
257 fn glyph_image(&mut self, key: GlyphKey) -> Option<SwashImage> {
258 self.swash_cache
259 .get_image(&mut self.font_system, key.0)
260 .as_ref()
261 .cloned()
262 }
263
264 #[cfg(feature = "experimental-msdf")]
265 fn glyph_msdf_job(&mut self, key: GlyphKey, config: AtlasConfig) -> Option<MsdfGlyphJob> {
266 if let Some(cached) = self.msdf_job_cache.get(&key) {
267 return cached.clone();
268 }
269 self.font_system
270 .db()
271 .with_face_data(key.0.font_id, |data, face_index| {
272 generate_msdf_job(data, face_index, key.0, config)
273 })
274 .flatten()
275 .inspect(|job| {
276 self.msdf_job_cache.insert(key, Some(job.clone()));
277 })
278 .or_else(|| {
279 self.msdf_job_cache.insert(key, None);
280 None
281 })
282 }
283
284 #[cfg(feature = "experimental-msdf")]
285 fn raster_glyph_instance(
286 &mut self,
287 glyph: &TextGlyph,
288 atlas: &mut GlyphAtlas,
289 config: AtlasConfig,
290 pixels: &mut [u8],
291 ) -> Option<GpuGlyphInstance> {
292 let image = self.glyph_image(glyph.key)?;
293 let glyph_size = [image.placement.width, image.placement.height];
294 let entry = atlas.ensure_glyph(glyph.key, glyph_size)?;
295 write_glyph_to_atlas(pixels, config, entry, &image);
296 Some(glyph_instance_for(glyph, entry, &image))
297 }
298}
299
300impl Default for TextSystem {
301 fn default() -> Self {
302 Self::new()
303 }
304}
305
306#[cfg(feature = "experimental-msdf")]
307pub fn rgba8_to_rgba16_float(pixels: &[u8]) -> Vec<u16> {
308 pixels
309 .iter()
310 .map(|value| half::f16::from_f32(f32::from(*value) / 255.0).to_bits())
311 .collect()
312}
313
314fn load_default_fonts(font_system: &mut FontSystem) {
315 let db = font_system.db_mut();
316 db.load_font_data(ROBOTO_REGULAR_BYTES.to_vec());
317 db.set_sans_serif_family(DEFAULT_FONT_FAMILY);
318}
319
320#[derive(Debug, Clone)]
321pub struct TextLayoutRequest {
322 pub content: String,
323 pub font_family: String,
324 pub font_size: f32,
325 pub font_weight: u16,
326 pub font_style: TextFontStyle,
327 pub max_width: Option<f32>,
328 pub line_height: Option<f32>,
329 pub align: TextAlign,
330 pub origin: [f32; 2],
331 pub color: [f32; 4],
332 pub disable_hinting: bool,
333}
334
335impl TextLayoutRequest {
336 pub fn new(content: impl Into<String>) -> Self {
337 Self {
338 content: content.into(),
339 font_family: DEFAULT_FONT_FAMILY.to_string(),
340 font_size: 16.0,
341 font_weight: 400,
342 font_style: TextFontStyle::Normal,
343 max_width: None,
344 line_height: None,
345 align: TextAlign::Left,
346 origin: [0.0, 0.0],
347 color: [1.0, 1.0, 1.0, 1.0],
348 disable_hinting: true,
349 }
350 }
351
352 pub fn line_height(&self) -> f32 {
353 self.line_height.unwrap_or(self.font_size * 1.2).max(1.0)
354 }
355}
356
357#[derive(Debug, Clone, Copy, PartialEq, Eq)]
358pub enum TextFontStyle {
359 Normal,
360 Italic,
361 Oblique,
362}
363
364#[derive(Debug, Clone, Copy, PartialEq, Eq)]
365pub enum TextAlign {
366 Left,
367 Center,
368 Right,
369 Justified,
370}
371
372#[derive(Debug, Clone, Copy, PartialEq)]
373pub struct TextMeasurement {
374 pub width: f32,
375 pub height: f32,
376}
377
378#[derive(Debug, Clone, PartialEq)]
379pub struct TextLayout {
380 pub measurement: TextMeasurement,
381 pub glyphs: Vec<TextGlyph>,
382}
383
384#[derive(Debug, Clone, PartialEq)]
385pub struct TextGlyph {
386 pub key: GlyphKey,
387 pub x: f32,
388 pub y: f32,
389 pub width: f32,
390 pub height: f32,
391 pub x_offset: f32,
392 pub y_offset: f32,
393 pub color: [f32; 4],
394}
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
397pub struct GlyphKey(pub CacheKey);
398
399impl GlyphKey {
400 #[cfg(feature = "experimental-msdf")]
401 fn msdf_key(self) -> Self {
402 let mut key = self.0;
403 key.x_bin = cosmic_text::SubpixelBin::Zero;
404 key.y_bin = cosmic_text::SubpixelBin::Zero;
405 Self(key)
406 }
407}
408
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub struct AtlasConfig {
411 pub width: u32,
412 pub height: u32,
413 pub px_range: u32,
414}
415
416impl Default for AtlasConfig {
417 fn default() -> Self {
418 Self {
419 width: 2048,
420 height: 2048,
421 px_range: 8,
422 }
423 }
424}
425
426#[derive(Debug, Clone)]
427pub struct GlyphAtlas {
428 config: AtlasConfig,
429 entries: HashMap<GlyphKey, AtlasEntry>,
430 cursor_x: u32,
431 cursor_y: u32,
432 row_height: u32,
433}
434
435impl GlyphAtlas {
436 pub fn new(config: AtlasConfig) -> Self {
437 Self {
438 config,
439 entries: HashMap::new(),
440 cursor_x: 0,
441 cursor_y: 0,
442 row_height: 0,
443 }
444 }
445
446 pub fn config(&self) -> AtlasConfig {
447 self.config
448 }
449
450 pub fn used_size(&self) -> [u32; 2] {
451 [
452 self.config.width,
453 self.cursor_y
454 .saturating_add(self.row_height)
455 .min(self.config.height),
456 ]
457 }
458
459 pub fn entry(&self, key: &GlyphKey) -> Option<AtlasEntry> {
460 self.entries.get(key).copied()
461 }
462
463 pub fn ensure_glyph(&mut self, key: GlyphKey, size: [u32; 2]) -> Option<AtlasEntry> {
464 if let Some(entry) = self.entry(&key) {
465 return Some(entry);
466 }
467 let padding = self.config.px_range.max(1);
468 let width = size[0].saturating_add(padding * 2).max(1);
469 let height = size[1].saturating_add(padding * 2).max(1);
470 if width > self.config.width || height > self.config.height {
471 return None;
472 }
473 if self.cursor_x + width > self.config.width {
474 self.cursor_x = 0;
475 self.cursor_y = self.cursor_y.saturating_add(self.row_height);
476 self.row_height = 0;
477 }
478 if self.cursor_y + height > self.config.height {
479 return None;
480 }
481 let entry = AtlasEntry {
482 origin: [self.cursor_x + padding, self.cursor_y + padding],
483 size,
484 uv_min: [
485 (self.cursor_x + padding) as f32 / self.config.width as f32,
486 (self.cursor_y + padding) as f32 / self.config.height as f32,
487 ],
488 uv_max: [
489 (self.cursor_x + padding + size[0]) as f32 / self.config.width as f32,
490 (self.cursor_y + padding + size[1]) as f32 / self.config.height as f32,
491 ],
492 };
493 self.cursor_x += width;
494 self.row_height = self.row_height.max(height);
495 self.entries.insert(key, entry);
496 Some(entry)
497 }
498}
499
500#[derive(Debug, Clone)]
501pub struct AlphaAtlasRender {
502 pub atlas: GlyphAtlas,
503 pub pixels: Vec<u8>,
504 pub instances: Vec<GpuGlyphInstance>,
505 pub glyph_count: usize,
506}
507
508#[cfg(feature = "experimental-msdf")]
509#[derive(Debug, Clone)]
510pub struct GpuHybridAtlasRender {
511 pub atlas: GlyphAtlas,
512 pub pixels: Vec<u8>,
513 pub instances: Vec<GpuGlyphInstance>,
514 pub jobs: Vec<GpuMsdfJob>,
515 pub segments: Vec<GpuMsdfSegment>,
516 pub pixel_jobs: Vec<u32>,
517 pub msdf_pixel_count: u32,
518 pub glyph_count: usize,
519}
520
521#[derive(Debug, Clone, Copy, PartialEq)]
522pub struct AtlasEntry {
523 pub origin: [u32; 2],
524 pub size: [u32; 2],
525 pub uv_min: [f32; 2],
526 pub uv_max: [f32; 2],
527}
528
529fn write_glyph_to_atlas(
530 pixels: &mut [u8],
531 config: AtlasConfig,
532 entry: AtlasEntry,
533 image: &SwashImage,
534) {
535 match image.content {
536 SwashContent::Mask => {
537 for y in 0..image.placement.height as usize {
538 for x in 0..image.placement.width as usize {
539 let src = y * image.placement.width as usize + x;
540 let dst_x = entry.origin[0] as usize + x;
541 let dst_y = entry.origin[1] as usize + y;
542 let dst = (dst_y * config.width as usize + dst_x) * 4;
543 let alpha = image.data[src];
544 pixels[dst] = 255;
545 pixels[dst + 1] = 255;
546 pixels[dst + 2] = 255;
547 pixels[dst + 3] = alpha;
548 }
549 }
550 }
551 SwashContent::Color => {
552 for y in 0..image.placement.height as usize {
553 for x in 0..image.placement.width as usize {
554 let src = (y * image.placement.width as usize + x) * 4;
555 let dst_x = entry.origin[0] as usize + x;
556 let dst_y = entry.origin[1] as usize + y;
557 let dst = (dst_y * config.width as usize + dst_x) * 4;
558 pixels[dst..dst + 4].copy_from_slice(&image.data[src..src + 4]);
559 }
560 }
561 }
562 SwashContent::SubpixelMask => {
563 for y in 0..image.placement.height as usize {
564 for x in 0..image.placement.width as usize {
565 let src = (y * image.placement.width as usize + x) * 3;
566 let dst_x = entry.origin[0] as usize + x;
567 let dst_y = entry.origin[1] as usize + y;
568 let dst = (dst_y * config.width as usize + dst_x) * 4;
569 let alpha = image.data[src]
570 .max(image.data[src + 1])
571 .max(image.data[src + 2]);
572 pixels[dst] = 255;
573 pixels[dst + 1] = 255;
574 pixels[dst + 2] = 255;
575 pixels[dst + 3] = alpha;
576 }
577 }
578 }
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn measures_roboto_text() {
588 let mut system = TextSystem::new();
589 let mut request = TextLayoutRequest::new("External URL media");
590 request.font_size = 44.0;
591 request.max_width = Some(900.0);
592
593 let measurement = system.measure(&request);
594
595 assert!(measurement.width > 100.0);
596 assert!(measurement.height > 40.0);
597 }
598
599 #[test]
600 fn creates_gpu_glyph_instances_from_layout() {
601 let mut system = TextSystem::new();
602 let mut request = TextLayoutRequest::new("Roboto");
603 request.font_size = 32.0;
604 let layout = system.layout(&request);
605 let render = system.render_alpha_atlas(&layout, AtlasConfig::default(), 128);
606 let instance = render
607 .instances
608 .iter()
609 .find(|instance| instance.rect[2] > 0.0)
610 .expect("expected a visible glyph instance");
611
612 assert!(instance.rect[2] > 0.0);
613 assert!(instance.uv_rect[2] > instance.uv_rect[0]);
614 }
615
616 #[test]
617 fn wraps_text_to_max_width() {
618 let mut system = TextSystem::new();
619 let mut unwrapped = TextLayoutRequest::new("one two three four five");
620 unwrapped.font_size = 24.0;
621 let mut wrapped = unwrapped.clone();
622 wrapped.max_width = Some(80.0);
623
624 let wide = system.measure(&unwrapped);
625 let narrow = system.measure(&wrapped);
626
627 assert!(wide.width > narrow.width);
628 assert!(narrow.height > wide.height);
629 }
630
631 #[test]
632 fn alpha_atlas_contains_non_empty_pixels() {
633 let mut system = TextSystem::new();
634 let mut request = TextLayoutRequest::new("A");
635 request.font_size = 48.0;
636 let layout = system.layout(&request);
637 let render = system.render_alpha_atlas(
638 &layout,
639 AtlasConfig {
640 width: 128,
641 height: 128,
642 px_range: 2,
643 },
644 16,
645 );
646
647 assert_eq!(render.instances.len(), render.glyph_count);
648 assert!(render.glyph_count > 0);
649 assert!(render.pixels.chunks_exact(4).any(|pixel| pixel[3] > 0));
650 }
651
652 #[test]
653 fn alpha_atlas_preserves_color_glyph_pixels() {
654 let mut image = SwashImage::new();
655 image.content = SwashContent::Color;
656 image.placement.width = 2;
657 image.placement.height = 1;
658 image.data = vec![255, 0, 0, 200, 0, 128, 255, 180];
659 let entry = AtlasEntry {
660 origin: [1, 1],
661 size: [2, 1],
662 uv_min: [0.0, 0.0],
663 uv_max: [1.0, 1.0],
664 };
665 let config = AtlasConfig {
666 width: 4,
667 height: 4,
668 px_range: 1,
669 };
670 let mut pixels = vec![0_u8; config.width as usize * config.height as usize * 4];
671
672 write_glyph_to_atlas(&mut pixels, config, entry, &image);
673
674 let first = ((config.width + 1) * 4) as usize;
675 let second = first + 4;
676 assert_eq!(&pixels[first..first + 4], &[255, 0, 0, 200]);
677 assert_eq!(&pixels[second..second + 4], &[0, 128, 255, 180]);
678 }
679
680 #[test]
681 fn raster_keys_ignore_subpixel_position() {
682 let mut system = TextSystem::new();
683 let mut first = TextLayoutRequest::new("A");
684 first.origin = [0.1, 0.0];
685 let mut second = first.clone();
686 second.origin = [0.6, 0.0];
687
688 let first_glyph = system.layout(&first).glyphs[0].clone();
689 let second_glyph = system.layout(&second).glyphs[0].clone();
690
691 assert_eq!(first_glyph.key, second_glyph.key);
692 assert_ne!(first_glyph.x, second_glyph.x);
693 }
694
695 #[cfg(feature = "experimental-msdf")]
696 #[test]
697 fn hybrid_atlas_uses_msdf_for_outline_glyphs() {
698 let mut system = TextSystem::new();
699 let layout = system.layout(&TextLayoutRequest::new("A"));
700 let render =
701 system.render_gpu_hybrid_atlas(&layout, AtlasConfig::default(), 16, 4096, 4096);
702
703 assert!(render.instances.iter().any(is_msdf_instance));
704 }
705
706 #[cfg(feature = "experimental-msdf")]
707 #[test]
708 fn msdf_keys_ignore_subpixel_position() {
709 let mut system = TextSystem::new();
710 let mut first = TextLayoutRequest::new("A");
711 first.origin = [0.1, 0.0];
712 let mut second = first.clone();
713 second.origin = [0.6, 0.0];
714
715 let first_key = system.layout(&first).glyphs[0].key;
716 let second_key = system.layout(&second).glyphs[0].key;
717
718 assert_eq!(first_key, second_key);
719 assert_eq!(first_key.msdf_key(), second_key.msdf_key());
720 }
721
722 #[cfg(feature = "experimental-msdf")]
723 #[test]
724 fn msdf_placement_keeps_fractional_bearings() {
725 let mut system = TextSystem::new();
726 let mut request = TextLayoutRequest::new("B");
727 request.font_size = 48.3;
728 let layout = system.layout(&request);
729 let job = system
730 .glyph_msdf_job(layout.glyphs[0].key.msdf_key(), AtlasConfig::default())
731 .expect("expected outline glyph to produce an MSDF job");
732
733 assert_ne!(job.placement.top.fract(), 0.0);
734 }
735
736 #[cfg(feature = "experimental-msdf")]
737 #[test]
738 fn msdf_budget_fallback_uses_raster_atlas_entry() {
739 let mut system = TextSystem::new();
740 let mut request = TextLayoutRequest::new("A");
741 request.origin = [0.25, 0.0];
742 let layout = system.layout(&request);
743 let glyph_key = layout.glyphs[0].key;
744 let msdf_key = glyph_key.msdf_key();
745 assert_eq!(glyph_key, msdf_key);
746
747 let render =
748 system.render_gpu_hybrid_atlas(&layout, AtlasConfig::default(), 16, 0, u32::MAX);
749
750 assert!(render.jobs.is_empty());
751 assert!(
752 render
753 .instances
754 .iter()
755 .all(|instance| !is_msdf_instance(instance))
756 );
757 assert!(render.atlas.entry(&glyph_key).is_some());
758 }
759
760 #[test]
761 fn atlas_reuses_existing_glyph_entries() {
762 let key = {
763 let mut system = TextSystem::new();
764 let layout = system.layout(&TextLayoutRequest::new("A"));
765 layout.glyphs[0].key
766 };
767 let mut atlas = GlyphAtlas::new(AtlasConfig {
768 width: 64,
769 height: 64,
770 px_range: 2,
771 });
772
773 let first = atlas.ensure_glyph(key, [12, 16]).unwrap();
774 let second = atlas.ensure_glyph(key, [12, 16]).unwrap();
775
776 assert_eq!(first, second);
777 }
778
779 #[test]
780 fn gpu_structs_have_stable_sizes() {
781 assert_eq!(std::mem::size_of::<GpuTextGlobals>(), 16);
782 assert_eq!(std::mem::size_of::<GpuGlyphInstance>(), 64);
783 #[cfg(feature = "experimental-msdf")]
784 {
785 assert_eq!(std::mem::size_of::<GpuMsdfGlobals>(), 24);
786 assert_eq!(std::mem::size_of::<GpuMsdfJob>(), 48);
787 assert_eq!(std::mem::size_of::<GpuMsdfSegment>(), 48);
788 }
789 }
790
791 #[test]
792 fn exposes_wgsl_shader_sources() {
793 assert!(ALPHA_TEXT_SHADER.contains("textureSample"));
794 assert!(ALPHA_TEXT_SHADER.contains("@binding(4) var<uniform> paint: Paint"));
795 assert!(ALPHA_TEXT_SHADER.contains("sample_paint(in.position.xy)"));
796 #[cfg(feature = "experimental-msdf")]
797 {
798 assert!(MSDF_TEXT_SHADER.contains("median3"));
799 assert!(MSDF_TEXT_SHADER.contains("textureSample"));
800 assert!(MSDF_GENERATOR_SHADER.contains("pixel_jobs"));
801 }
802 }
803}