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