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
540impl TextSystem for DefaultTextSystem {
541 fn take_missing_glyph_receiver(&self) -> Option<Box<dyn MissingGlyphReports>> {
542 self.missing_glyph_receiver
543 .try_lock()
544 .and_then(|mut receiver| receiver.take())
545 .map(|receiver| Box::new(receiver) as Box<dyn MissingGlyphReports>)
546 }
547
548 fn enable_missing_glyph_reporting(&self) {
549 self.platform_text_system
550 .set_missing_glyph_sink(Some(self.missing_glyph_reporter.clone()));
551 }
552
553 fn disable_missing_glyph_reporting(&self) {
554 self.platform_text_system.set_missing_glyph_sink(None);
555 self.missing_glyph_reporter.reset();
556 }
557
558 fn report_missing_glyphs_in_test(&self, missing_glyphs: Vec<MissingGlyph>) {
559 self.missing_glyph_reporter.report(missing_glyphs);
560 }
561
562 fn platform_text_system(&self) -> &Arc<dyn PlatformTextSystem> {
563 &self.platform_text_system
564 }
565
566 fn all_font_names(&self) -> Vec<String> {
567 self.all_font_names()
568 }
569
570 fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
571 self.add_fonts(fonts)
572 }
573
574 fn get_font_for_id(&self, id: FontId) -> Option<Font> {
575 self.get_font_for_id(id)
576 }
577
578 fn resolve_font(&self, font: &Font) -> FontId {
579 self.resolve_font(font)
580 }
581
582 fn prewarm_fonts(&self, fonts: &[Font]) {
583 self.prewarm_fonts(fonts)
584 }
585
586 fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
587 self.bounding_box(font_id, font_size)
588 }
589
590 fn typographic_bounds(
591 &self,
592 font_id: FontId,
593 font_size: Pixels,
594 character: char,
595 ) -> Result<Bounds<Pixels>> {
596 self.typographic_bounds(font_id, font_size, character)
597 }
598
599 fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
600 self.advance(font_id, font_size, ch)
601 }
602
603 fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels {
604 self.layout_width(font_id, font_size, ch)
605 }
606
607 fn em_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
608 self.em_width(font_id, font_size)
609 }
610
611 fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
612 self.em_advance(font_id, font_size)
613 }
614
615 fn ch_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
616 self.ch_width(font_id, font_size)
617 }
618
619 fn ch_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
620 self.ch_advance(font_id, font_size)
621 }
622
623 fn units_per_em(&self, font_id: FontId) -> u32 {
624 self.units_per_em(font_id)
625 }
626
627 fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
628 self.cap_height(font_id, font_size)
629 }
630
631 fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
632 self.x_height(font_id, font_size)
633 }
634
635 fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
636 self.ascent(font_id, font_size)
637 }
638
639 fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
640 self.descent(font_id, font_size)
641 }
642
643 fn baseline_offset(&self, font_id: FontId, font_size: Pixels, line_height: Pixels) -> Pixels {
644 self.baseline_offset(font_id, font_size, line_height)
645 }
646
647 fn take_font_runs(&self) -> Vec<FontRun> {
648 self.take_font_runs()
649 }
650
651 fn recycle_font_runs(&self, font_runs: Vec<FontRun>) {
652 self.recycle_font_runs(font_runs)
653 }
654
655 fn line_wrapper(self: Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
656 DefaultTextSystem::line_wrapper(self, font, font_size)
657 }
658
659 fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
660 self.raster_bounds(params)
661 }
662
663 fn rasterize_glyph(&self, params: &RenderGlyphParams) -> Result<(Size<DevicePixels>, Vec<u8>)> {
664 self.rasterize_glyph(params)
665 }
666
667 fn glyph_dilation_for_color(&self, color: Hsla) -> u8 {
668 self.glyph_dilation_for_color(color)
669 }
670
671 fn recommended_rendering_mode(&self, font_id: FontId, font_size: Pixels) -> TextRenderingMode {
672 self.recommended_rendering_mode(font_id, font_size)
673 }
674
675 fn layout_index(&self) -> LineLayoutIndex {
676 self.line_layout_cache.layout_index()
677 }
678
679 fn reuse_layouts(&self, range: Range<LineLayoutIndex>) {
680 self.line_layout_cache.reuse_layouts(range)
681 }
682
683 fn truncate_layouts(&self, index: LineLayoutIndex) {
684 self.line_layout_cache.truncate_layouts(index)
685 }
686
687 fn finish_frame(&self) {
688 self.line_layout_cache.finish_frame()
689 }
690
691 fn layout_wrapped_line(
692 &self,
693 text: &str,
694 font_size: Pixels,
695 runs: &[FontRun],
696 wrap_width: Option<Pixels>,
697 max_lines: Option<usize>,
698 ) -> Arc<WrappedLineLayout> {
699 self.line_layout_cache
700 .layout_wrapped_line(text, font_size, runs, wrap_width, max_lines)
701 }
702
703 fn layout_line(
704 &self,
705 text: &str,
706 font_size: Pixels,
707 runs: &[FontRun],
708 force_width: Option<Pixels>,
709 ) -> Arc<LineLayout> {
710 self.line_layout_cache
711 .layout_line(text, font_size, runs, force_width)
712 }
713
714 fn try_layout_line_by_hash(
715 &self,
716 text_hash: u64,
717 text_len: usize,
718 font_size: Pixels,
719 runs: &[FontRun],
720 force_width: Option<Pixels>,
721 ) -> Option<Arc<LineLayout>> {
722 self.line_layout_cache.try_layout_line_by_hash(
723 text_hash,
724 text_len,
725 font_size,
726 runs,
727 force_width,
728 )
729 }
730
731 fn layout_line_by_hash(
732 &self,
733 text_hash: u64,
734 text_len: usize,
735 font_size: Pixels,
736 runs: &[FontRun],
737 force_width: Option<Pixels>,
738 materialize_text: Box<dyn FnOnce() -> SharedString>,
739 ) -> Arc<LineLayout> {
740 self.line_layout_cache.layout_line_by_hash(
741 text_hash,
742 text_len,
743 font_size,
744 runs,
745 force_width,
746 materialize_text,
747 )
748 }
749}
750
751#[cfg(test)]
752mod missing_glyph_tests {
753 use super::*;
754 use futures::FutureExt as _;
755 use gpui_engine::FallbackFontClass;
756
757 #[test]
758 fn bounds_retained_missing_glyphs() {
759 let (reporter, mut receiver) = missing_glyph_channel();
760 reporter.report(
761 (0..MAX_REPORTED_MISSING_GLYPHS)
762 .map(|index| {
763 MissingGlyph::new(index.to_string().into(), FallbackFontClass::Proportional)
764 })
765 .collect(),
766 );
767 assert!(receiver.recv().now_or_never().unwrap().is_ok());
768
769 let newest = MissingGlyph::new("newest".into(), FallbackFontClass::Monospace);
770 reporter.report(vec![newest.clone()]);
771 assert!(receiver.recv().now_or_never().unwrap().is_ok());
772
773 let state = &receiver.state;
774 assert_eq!(state.reported.len(), MAX_REPORTED_MISSING_GLYPHS);
775 assert_eq!(state.reported_order.len(), MAX_REPORTED_MISSING_GLYPHS);
776 assert!(state.reported.contains(&newest));
777 }
778
779 #[test]
780 fn dropping_receiver_closes_and_clears_reports() {
781 let (reporter, receiver) = missing_glyph_channel();
782 reporter.report(vec![missing_glyph("missing")]);
783
784 drop(receiver);
785
786 assert!(reporter.sender.is_closed());
787 assert!(reporter.sender.is_empty());
788 }
789
790 fn missing_glyph_channel() -> (MissingGlyphReporter, MissingGlyphReceiver) {
791 let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS);
792 let generation = Arc::<AtomicUsize>::default();
793 let reporter = MissingGlyphReporter {
794 generation: generation.clone(),
795 sender,
796 };
797 let receiver = MissingGlyphReceiver {
798 state: MissingGlyphState::default(),
799 generation,
800 receiver,
801 };
802 (reporter, receiver)
803 }
804
805 fn missing_glyph(grapheme: &'static str) -> MissingGlyph {
806 MissingGlyph::new(grapheme.into(), FallbackFontClass::Proportional)
807 }
808}