1use anyhow::{Context as _, Result, anyhow};
8use collections::{FxHashMap, FxHashSet};
9use gpui_engine::{
10 Font, FontId, FontMetrics, FontRun, LineLayout, LineLayoutIndex, LineWrapper, LineWrapperHandle,
11 MissingGlyph, MissingGlyphReports, MissingGlyphSink, PlatformTextSystem, RenderGlyphParams,
12 TextRenderingMode, TextSystem, WrappedLineLayout, font,
13};
14use gpui_shared_string::SharedString;
15use gpui_types::{Bounds, DevicePixels, Hsla, Pixels, Size, px};
16use itertools::Itertools;
17use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
18use smallvec::{SmallVec, smallvec};
19use std::borrow::Cow;
20use std::collections::VecDeque;
21use std::future::Future;
22use std::ops::Range;
23use std::pin::Pin;
24use std::sync::{
25 Arc,
26 atomic::{AtomicUsize, Ordering},
27};
28
29const MAX_REPORTED_MISSING_GLYPHS: usize = 1024;
30
31#[derive(Default)]
32struct MissingGlyphState {
33 reported: FxHashSet<MissingGlyph>,
34 reported_order: VecDeque<MissingGlyph>,
35 generation: usize,
36}
37
38impl MissingGlyphState {
39 fn reset(&mut self, generation: usize) {
40 self.reported.clear();
41 self.reported_order.clear();
42 self.generation = generation;
43 }
44}
45
46struct QueuedMissingGlyph {
47 generation: usize,
48 missing_glyph: MissingGlyph,
49}
50
51struct MissingGlyphReporter {
53 generation: Arc<AtomicUsize>,
54 sender: async_channel::Sender<QueuedMissingGlyph>,
55}
56
57impl MissingGlyphSink for MissingGlyphReporter {
58 fn report(&self, missing_glyphs: Vec<MissingGlyph>) {
59 if self.sender.is_closed() {
60 return;
61 }
62
63 let generation = self.generation.load(Ordering::Acquire);
64 for missing_glyph in missing_glyphs.into_iter().unique() {
67 let queued = QueuedMissingGlyph {
68 generation,
69 missing_glyph,
70 };
71 if self.sender.try_send(queued).is_err() {
72 break;
73 }
74 }
75 }
76}
77
78impl MissingGlyphReporter {
79 fn reset(&self) {
80 self.generation.fetch_add(1, Ordering::AcqRel);
81 }
82}
83
84pub struct MissingGlyphReceiver {
86 state: MissingGlyphState,
87 generation: Arc<AtomicUsize>,
88 receiver: async_channel::Receiver<QueuedMissingGlyph>,
89}
90
91impl MissingGlyphReceiver {
92 pub async fn recv(
98 &mut self,
99 ) -> std::result::Result<Vec<MissingGlyph>, async_channel::RecvError> {
100 loop {
101 let queued = self.receiver.recv().await?;
102 let mut missing_glyphs = Vec::new();
103 for queued in std::iter::once(queued)
104 .chain(std::iter::from_fn(|| self.receiver.try_recv().ok()))
105 .take(MAX_REPORTED_MISSING_GLYPHS)
106 {
107 let generation = self.generation.load(Ordering::Acquire);
108 if self.state.generation != generation {
109 self.state.reset(generation);
110 missing_glyphs.clear();
111 }
112 if queued.generation != generation
113 || !self.state.reported.insert(queued.missing_glyph.clone())
114 {
115 continue;
116 }
117 self.state
118 .reported_order
119 .push_back(queued.missing_glyph.clone());
120 missing_glyphs.push(queued.missing_glyph);
121 if self.state.reported.len() > MAX_REPORTED_MISSING_GLYPHS
122 && let Some(expired) = self.state.reported_order.pop_front()
123 {
124 self.state.reported.remove(&expired);
125 }
126 }
127 if !missing_glyphs.is_empty() {
128 return Ok(missing_glyphs);
129 }
130 let mut yielded = false;
133 std::future::poll_fn(|cx| {
134 if std::mem::replace(&mut yielded, true) {
135 std::task::Poll::Ready(())
136 } else {
137 cx.waker().wake_by_ref();
138 std::task::Poll::Pending
139 }
140 })
141 .await;
142 }
143 }
144}
145
146impl Drop for MissingGlyphReceiver {
147 fn drop(&mut self) {
148 self.receiver.close();
149 while self.receiver.try_recv().is_ok() {}
150 }
151}
152
153impl MissingGlyphReports for MissingGlyphReceiver {
154 fn recv(&mut self) -> Pin<Box<dyn Future<Output = Option<Vec<MissingGlyph>>> + Send + '_>> {
155 Box::pin(async move { MissingGlyphReceiver::recv(self).await.ok() })
156 }
157}
158
159use crate::LineLayoutCache;
160
161pub struct DefaultTextSystem {
163 platform_text_system: Arc<dyn PlatformTextSystem>,
164 font_ids_by_font: RwLock<FxHashMap<Font, Result<FontId>>>,
165 font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
166 raster_bounds: RwLock<FxHashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
167 wrapper_pool: Mutex<FxHashMap<FontIdWithSize, Vec<LineWrapper>>>,
168 font_runs_pool: Mutex<Vec<Vec<FontRun>>>,
169 fallback_font_stack: SmallVec<[Font; 2]>,
170 font_generation: Arc<AtomicUsize>,
171 missing_glyph_reporter: Arc<MissingGlyphReporter>,
172 missing_glyph_receiver: Mutex<Option<MissingGlyphReceiver>>,
173 line_layout_cache: LineLayoutCache,
174}
175
176impl DefaultTextSystem {
177 pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
179 let font_generation = Arc::<AtomicUsize>::default();
180 let line_layout_cache =
181 LineLayoutCache::new(platform_text_system.clone(), font_generation.clone());
182 let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS);
183 let missing_glyph_generation = Arc::<AtomicUsize>::default();
184 DefaultTextSystem {
185 platform_text_system,
186 font_metrics: RwLock::default(),
187 raster_bounds: RwLock::default(),
188 font_ids_by_font: RwLock::default(),
189 wrapper_pool: Mutex::default(),
190 font_runs_pool: Mutex::default(),
191 fallback_font_stack: smallvec![
192 font(".ZedMono"),
194 font(".ZedSans"),
195 font("Helvetica"),
196 font("Segoe UI"), font("Ubuntu"), font("Adwaita Sans"), font("Cantarell"), font("Noto Sans"), font("DejaVu Sans"),
202 font("Arial"), ],
204 font_generation,
205 missing_glyph_reporter: Arc::new(MissingGlyphReporter {
206 generation: missing_glyph_generation.clone(),
207 sender,
208 }),
209 missing_glyph_receiver: Mutex::new(Some(MissingGlyphReceiver {
210 state: MissingGlyphState::default(),
211 generation: missing_glyph_generation,
212 receiver,
213 })),
214 line_layout_cache,
215 }
216 }
217
218 pub fn platform_text_system(&self) -> &Arc<dyn PlatformTextSystem> {
220 &self.platform_text_system
221 }
222
223 pub fn font_generation(&self) -> &Arc<AtomicUsize> {
225 &self.font_generation
226 }
227
228 pub fn take_font_runs(&self) -> Vec<FontRun> {
230 self.font_runs_pool.lock().pop().unwrap_or_default()
231 }
232
233 pub fn recycle_font_runs(&self, font_runs: Vec<FontRun>) {
235 self.font_runs_pool.lock().push(font_runs);
236 }
237
238 pub fn all_font_names(&self) -> Vec<String> {
242 let mut names = self.platform_text_system.all_font_names();
243 names.sort_unstable();
244 names.dedup();
245 names
246 }
247
248 pub fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
253 self.platform_text_system.add_fonts(fonts)?;
254 self.font_ids_by_font.write().clear();
255 self.missing_glyph_reporter.reset();
256 self.font_generation.fetch_add(1, Ordering::Release);
257 Ok(())
258 }
259
260 fn font_id(&self, font: &Font) -> Result<FontId> {
262 fn clone_font_id_result(font_id: &Result<FontId>) -> Result<FontId> {
263 match font_id {
264 Ok(font_id) => Ok(*font_id),
265 Err(err) => Err(anyhow!("{err}")),
266 }
267 }
268
269 let font_id = self
270 .font_ids_by_font
271 .read()
272 .get(font)
273 .map(clone_font_id_result);
274 if let Some(font_id) = font_id {
275 font_id
276 } else {
277 let font_id = self.platform_text_system.font_id(font);
278 self.font_ids_by_font
279 .write()
280 .insert(font.clone(), clone_font_id_result(&font_id));
281 font_id
282 }
283 }
284
285 pub fn get_font_for_id(&self, id: FontId) -> Option<Font> {
287 let lock = self.font_ids_by_font.read();
288 lock.iter()
289 .filter_map(|(font, result)| match result {
290 Ok(font_id) if *font_id == id => Some(font.clone()),
291 _ => None,
292 })
293 .next()
294 }
295
296 pub fn resolve_font(&self, font: &Font) -> FontId {
303 if let Ok(font_id) = self.font_id(font) {
304 return font_id;
305 }
306 for fallback in &self.fallback_font_stack {
307 if let Ok(font_id) = self.font_id(fallback) {
308 return font_id;
309 }
310 }
311
312 panic!(
313 "failed to resolve font '{}' or any of the fallbacks: {}",
314 font.family,
315 self.fallback_font_stack
316 .iter()
317 .map(|fallback| &fallback.family)
318 .join(", ")
319 );
320 }
321
322 pub fn prewarm_fonts(&self, fonts: &[Font]) {
328 let mut font_ids = SmallVec::<[FontId; 8]>::new();
329 for font in fonts {
330 let font_id = self.resolve_font(font);
331 if !font_ids.contains(&font_id) {
332 font_ids.push(font_id);
333 }
334 }
335 self.platform_text_system.prewarm_fonts(&font_ids);
336 }
337
338 pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
342 self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size))
343 }
344
345 pub fn typographic_bounds(
347 &self,
348 font_id: FontId,
349 font_size: Pixels,
350 character: char,
351 ) -> Result<Bounds<Pixels>> {
352 let glyph_id = self
353 .platform_text_system
354 .glyph_for_char(font_id, character)
355 .with_context(|| format!("glyph not found for character '{character}'"))?;
356 let bounds = self
357 .platform_text_system
358 .typographic_bounds(font_id, glyph_id)?;
359 Ok(self.read_metrics(font_id, |metrics| {
360 (bounds / metrics.units_per_em as f32 * font_size.0).map(px)
361 }))
362 }
363
364 pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
366 let glyph_id = self
367 .platform_text_system
368 .glyph_for_char(font_id, ch)
369 .with_context(|| format!("glyph not found for character '{ch}'"))?;
370 let result = self.platform_text_system.advance(font_id, glyph_id)?
371 / self.units_per_em(font_id) as f32;
372
373 Ok(result * font_size)
374 }
375
376 pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels {
379 let mut buffer = [0; 4];
380 let buffer = ch.encode_utf8(&mut buffer);
381 self.platform_text_system
382 .layout_line(
383 buffer,
384 font_size,
385 &[FontRun {
386 len: buffer.len(),
387 font_id,
388 }],
389 )
390 .width
391 }
392
393 pub fn em_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
397 Ok(self.typographic_bounds(font_id, font_size, 'm')?.size.width)
398 }
399
400 pub fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
404 Ok(self.advance(font_id, font_size, 'm')?.width)
405 }
406
407 pub fn ch_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
411 Ok(self.typographic_bounds(font_id, font_size, '0')?.size.width)
412 }
413
414 pub fn ch_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
418 Ok(self.advance(font_id, font_size, '0')?.width)
419 }
420
421 pub fn units_per_em(&self, font_id: FontId) -> u32 {
425 self.read_metrics(font_id, |metrics| metrics.units_per_em)
426 }
427
428 pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
430 self.read_metrics(font_id, |metrics| metrics.cap_height(font_size))
431 }
432
433 pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
435 self.read_metrics(font_id, |metrics| metrics.x_height(font_size))
436 }
437
438 pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
440 self.read_metrics(font_id, |metrics| metrics.ascent(font_size))
441 }
442
443 pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
446 self.read_metrics(font_id, |metrics| metrics.descent(font_size))
447 }
448
449 pub fn baseline_offset(
451 &self,
452 font_id: FontId,
453 font_size: Pixels,
454 line_height: Pixels,
455 ) -> Pixels {
456 let ascent = self.ascent(font_id, font_size);
457 let descent = self.descent(font_id, font_size);
458 let padding_top = (line_height - ascent - descent) / 2.;
459 padding_top + ascent
460 }
461
462 fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T {
463 let lock = self.font_metrics.upgradable_read();
464
465 if let Some(metrics) = lock.get(&font_id) {
466 read(metrics)
467 } else {
468 let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
469 let metrics = lock
470 .entry(font_id)
471 .or_insert_with(|| self.platform_text_system.font_metrics(font_id));
472 read(metrics)
473 }
474 }
475
476 pub fn line_wrapper(self: Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
478 let font_id = self.resolve_font(&font);
479 let wrapper = {
480 let mut lock = self.wrapper_pool.lock();
481 lock.entry(FontIdWithSize { font_id, font_size })
482 .or_default()
483 .pop()
484 .unwrap_or_else(|| LineWrapper::new(font_id, font_size, self.clone()))
485 };
486 LineWrapperHandle::new(wrapper, move |wrapper| {
487 let mut lock = self.wrapper_pool.lock();
488 lock.get_mut(&FontIdWithSize { font_id, font_size })
489 .expect("wrapper pool entry exists")
490 .push(wrapper);
491 })
492 }
493
494 pub fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
496 let raster_bounds = self.raster_bounds.upgradable_read();
497 if let Some(bounds) = raster_bounds.get(params) {
498 Ok(*bounds)
499 } else {
500 let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
501 let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
502 raster_bounds.insert(params.clone(), bounds);
503 Ok(bounds)
504 }
505 }
506
507 pub fn rasterize_glyph(
509 &self,
510 params: &RenderGlyphParams,
511 ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
512 let raster_bounds = self.raster_bounds(params)?;
513 self.platform_text_system
514 .rasterize_glyph(params, raster_bounds)
515 }
516
517 pub fn glyph_dilation_for_color(&self, color: Hsla) -> u8 {
519 self.platform_text_system.glyph_dilation_for_color(color)
520 }
521
522 pub fn recommended_rendering_mode(
525 &self,
526 font_id: FontId,
527 font_size: Pixels,
528 ) -> TextRenderingMode {
529 self.platform_text_system
530 .recommended_rendering_mode(font_id, font_size)
531 }
532}
533
534#[derive(Hash, Eq, PartialEq)]
535struct FontIdWithSize {
536 font_id: FontId,
537 font_size: Pixels,
538}
539
540pub trait DefaultTextSystemExt {
543 fn as_default_text_system(&self) -> Option<&DefaultTextSystem>;
545}
546
547impl DefaultTextSystemExt for dyn TextSystem + '_ {
548 fn as_default_text_system(&self) -> Option<&DefaultTextSystem> {
549 self.as_any().downcast_ref::<DefaultTextSystem>()
550 }
551}
552
553impl DefaultTextSystem {
554 #[cfg(any(test, feature = "test-support"))]
561 pub fn report_missing_glyphs_in_test(&self, missing_glyphs: Vec<MissingGlyph>) {
562 self.missing_glyph_reporter.report(missing_glyphs);
563 }
564}
565
566impl TextSystem for DefaultTextSystem {
567 fn take_missing_glyph_receiver(&self) -> Option<Box<dyn MissingGlyphReports>> {
568 self.missing_glyph_receiver
569 .try_lock()
570 .and_then(|mut receiver| receiver.take())
571 .map(|receiver| Box::new(receiver) as Box<dyn MissingGlyphReports>)
572 }
573
574 fn enable_missing_glyph_reporting(&self) {
575 self.platform_text_system
576 .set_missing_glyph_sink(Some(self.missing_glyph_reporter.clone()));
577 }
578
579 fn disable_missing_glyph_reporting(&self) {
580 self.platform_text_system.set_missing_glyph_sink(None);
581 self.missing_glyph_reporter.reset();
582 }
583
584 fn platform_text_system(&self) -> &Arc<dyn PlatformTextSystem> {
585 &self.platform_text_system
586 }
587
588 fn all_font_names(&self) -> Vec<String> {
589 self.all_font_names()
590 }
591
592 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
593 self.add_fonts(fonts)
594 }
595
596 fn get_font_for_id(&self, id: FontId) -> Option<Font> {
597 self.get_font_for_id(id)
598 }
599
600 fn resolve_font(&self, font: &Font) -> FontId {
601 self.resolve_font(font)
602 }
603
604 fn prewarm_fonts(&self, fonts: &[Font]) {
605 self.prewarm_fonts(fonts)
606 }
607
608 fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
609 self.bounding_box(font_id, font_size)
610 }
611
612 fn typographic_bounds(
613 &self,
614 font_id: FontId,
615 font_size: Pixels,
616 character: char,
617 ) -> Result<Bounds<Pixels>> {
618 self.typographic_bounds(font_id, font_size, character)
619 }
620
621 fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
622 self.advance(font_id, font_size, ch)
623 }
624
625 fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels {
626 self.layout_width(font_id, font_size, ch)
627 }
628
629 fn em_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
630 self.em_width(font_id, font_size)
631 }
632
633 fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
634 self.em_advance(font_id, font_size)
635 }
636
637 fn ch_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
638 self.ch_width(font_id, font_size)
639 }
640
641 fn ch_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
642 self.ch_advance(font_id, font_size)
643 }
644
645 fn units_per_em(&self, font_id: FontId) -> u32 {
646 self.units_per_em(font_id)
647 }
648
649 fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
650 self.cap_height(font_id, font_size)
651 }
652
653 fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
654 self.x_height(font_id, font_size)
655 }
656
657 fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
658 self.ascent(font_id, font_size)
659 }
660
661 fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
662 self.descent(font_id, font_size)
663 }
664
665 fn baseline_offset(&self, font_id: FontId, font_size: Pixels, line_height: Pixels) -> Pixels {
666 self.baseline_offset(font_id, font_size, line_height)
667 }
668
669 fn take_font_runs(&self) -> Vec<FontRun> {
670 self.take_font_runs()
671 }
672
673 fn recycle_font_runs(&self, font_runs: Vec<FontRun>) {
674 self.recycle_font_runs(font_runs)
675 }
676
677 fn line_wrapper(self: Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
678 DefaultTextSystem::line_wrapper(self, font, font_size)
679 }
680
681 fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
682 self.raster_bounds(params)
683 }
684
685 fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)> {
686 self.rasterize_glyph(params)
687 }
688
689 fn glyph_dilation_for_color(&self, color: Hsla) -> u8 {
690 self.glyph_dilation_for_color(color)
691 }
692
693 fn recommended_rendering_mode(&self, font_id: FontId, font_size: Pixels) -> TextRenderingMode {
694 self.recommended_rendering_mode(font_id, font_size)
695 }
696
697 fn layout_index(&self) -> LineLayoutIndex {
698 self.line_layout_cache.layout_index()
699 }
700
701 fn reuse_layouts(&self, range: Range<LineLayoutIndex>) {
702 self.line_layout_cache.reuse_layouts(range)
703 }
704
705 fn truncate_layouts(&self, index: LineLayoutIndex) {
706 self.line_layout_cache.truncate_layouts(index)
707 }
708
709 fn finish_frame(&self) {
710 self.line_layout_cache.finish_frame()
711 }
712
713 fn layout_wrapped_line(
714 &self,
715 text: &str,
716 font_size: Pixels,
717 runs: &[FontRun],
718 wrap_width: Option<Pixels>,
719 max_lines: Option<usize>,
720 ) -> Arc<WrappedLineLayout> {
721 self.line_layout_cache
722 .layout_wrapped_line(text, font_size, runs, wrap_width, max_lines)
723 }
724
725 fn layout_line(
726 &self,
727 text: &str,
728 font_size: Pixels,
729 runs: &[FontRun],
730 force_width: Option<Pixels>,
731 ) -> Arc<LineLayout> {
732 self.line_layout_cache
733 .layout_line(text, font_size, runs, force_width)
734 }
735
736 fn try_layout_line_by_hash(
737 &self,
738 text_hash: u64,
739 text_len: usize,
740 font_size: Pixels,
741 runs: &[FontRun],
742 force_width: Option<Pixels>,
743 ) -> Option<Arc<LineLayout>> {
744 self.line_layout_cache.try_layout_line_by_hash(
745 text_hash,
746 text_len,
747 font_size,
748 runs,
749 force_width,
750 )
751 }
752
753 fn layout_line_by_hash(
754 &self,
755 text_hash: u64,
756 text_len: usize,
757 font_size: Pixels,
758 runs: &[FontRun],
759 force_width: Option<Pixels>,
760 materialize_text: Box<dyn FnOnce() -> SharedString>,
761 ) -> Arc<LineLayout> {
762 self.line_layout_cache.layout_line_by_hash(
763 text_hash,
764 text_len,
765 font_size,
766 runs,
767 force_width,
768 materialize_text,
769 )
770 }
771}
772
773#[cfg(test)]
774mod missing_glyph_tests {
775 use super::*;
776 use futures::FutureExt as _;
777 use gpui_engine::FallbackFontClass;
778
779 #[test]
780 fn bounds_retained_missing_glyphs() {
781 let (reporter, mut receiver) = missing_glyph_channel();
782 reporter.report(
783 (0..MAX_REPORTED_MISSING_GLYPHS)
784 .map(|index| {
785 MissingGlyph::new(index.to_string().into(), FallbackFontClass::Proportional)
786 })
787 .collect(),
788 );
789 assert!(receiver.recv().now_or_never().unwrap().is_ok());
790
791 let newest = MissingGlyph::new("newest".into(), FallbackFontClass::Monospace);
792 reporter.report(vec![newest.clone()]);
793 assert!(receiver.recv().now_or_never().unwrap().is_ok());
794
795 let state = &receiver.state;
796 assert_eq!(state.reported.len(), MAX_REPORTED_MISSING_GLYPHS);
797 assert_eq!(state.reported_order.len(), MAX_REPORTED_MISSING_GLYPHS);
798 assert!(state.reported.contains(&newest));
799 }
800
801 #[test]
802 fn dropping_receiver_closes_and_clears_reports() {
803 let (reporter, receiver) = missing_glyph_channel();
804 reporter.report(vec![missing_glyph("missing")]);
805
806 drop(receiver);
807
808 assert!(reporter.sender.is_closed());
809 assert!(reporter.sender.is_empty());
810 }
811
812 fn missing_glyph_channel() -> (MissingGlyphReporter, MissingGlyphReceiver) {
813 let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS);
814 let generation = Arc::<AtomicUsize>::default();
815 let reporter = MissingGlyphReporter {
816 generation: generation.clone(),
817 sender,
818 };
819 let receiver = MissingGlyphReceiver {
820 state: MissingGlyphState::default(),
821 generation,
822 receiver,
823 };
824 (reporter, receiver)
825 }
826
827 fn missing_glyph(grapheme: &'static str) -> MissingGlyph {
828 MissingGlyph::new(grapheme.into(), FallbackFontClass::Proportional)
829 }
830}