Skip to main content

gpui/
text_system.rs

1mod font_fallbacks;
2mod font_features;
3pub(crate) mod font_inspection;
4mod font_introspection;
5mod font_variations;
6mod line;
7mod line_layout;
8mod line_wrapper;
9mod paragraph;
10
11pub use font_fallbacks::*;
12pub use font_features::*;
13pub use font_introspection::*;
14pub use font_variations::*;
15pub use line::*;
16pub use line_layout::*;
17pub use line_wrapper::*;
18pub use paragraph::*;
19use schemars::JsonSchema;
20use serde::{Deserialize, Serialize};
21
22use crate::{
23    Bounds, DevicePixels, Hsla, Pixels, PlatformTextSystem, Point, Result, SharedString, Size,
24    StrikethroughStyle, TextRenderingMode, UnderlineStyle, px,
25};
26use anyhow::Context as _;
27use collections::{FxHashMap, FxHashSet};
28use core::fmt;
29use derive_more::{Add, Deref, FromStr, Sub};
30use itertools::Itertools;
31use parking_lot::{Mutex, RwLock, RwLockUpgradableReadGuard};
32use smallvec::{SmallVec, smallvec};
33use std::{
34    borrow::Cow,
35    cell::Cell,
36    cmp,
37    collections::VecDeque,
38    fmt::{Debug, Display, Formatter},
39    hash::{Hash, Hasher},
40    ops::{Deref, DerefMut, Range},
41    sync::{
42        Arc,
43        atomic::{AtomicUsize, Ordering},
44    },
45};
46
47/// An opaque identifier for a specific font.
48#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
49#[repr(C)]
50pub struct FontId(pub usize);
51
52/// An opaque identifier for a specific font family.
53#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
54pub struct FontFamilyId(pub usize);
55
56/// Number of subpixel glyph variants along the X axis.
57pub const SUBPIXEL_VARIANTS_X: u8 = 4;
58
59/// Number of subpixel glyph variants along the Y axis.
60pub const SUBPIXEL_VARIANTS_Y: u8 = 1;
61
62// Leave enough room below the underline for its stroke while keeping it below the baseline.
63const UNDERLINE_DESCENT_OFFSET_FACTOR: f32 = 0.618;
64
65/// Returns the vertical offset used to paint an underline within a line.
66pub fn underline_y_offset(line_height: Pixels, ascent: Pixels, descent: Pixels) -> Pixels {
67    let padding_top = (line_height - ascent - descent) / 2.;
68    padding_top + ascent + descent * UNDERLINE_DESCENT_OFFSET_FACTOR
69}
70
71const MAX_REPORTED_MISSING_GLYPHS: usize = 1024;
72
73/// The spacing behavior required of a fallback font.
74#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
75pub enum FallbackFontClass {
76    /// A proportionally spaced fallback font.
77    Proportional,
78    /// A fixed-width fallback font.
79    Monospace,
80}
81
82/// A grapheme cluster that could not be represented by any available font.
83#[derive(Clone, Debug, Eq, Hash, PartialEq)]
84pub struct MissingGlyph {
85    grapheme: SharedString,
86    font_class: FallbackFontClass,
87}
88
89impl MissingGlyph {
90    /// Creates a missing-glyph report.
91    pub fn new(grapheme: SharedString, font_class: FallbackFontClass) -> Self {
92        Self {
93            grapheme,
94            font_class,
95        }
96    }
97
98    /// Returns the unresolved grapheme cluster.
99    pub fn grapheme(&self) -> &str {
100        &self.grapheme
101    }
102
103    /// Returns the spacing behavior required of a fallback font.
104    pub fn font_class(&self) -> FallbackFontClass {
105        self.font_class
106    }
107}
108
109/// Accepts missing glyphs detected by a platform text system.
110pub trait MissingGlyphSink: Send + Sync {
111    /// Reports grapheme clusters that exhausted font fallback.
112    fn report(&self, missing_glyphs: Vec<MissingGlyph>);
113}
114
115#[derive(Default)]
116struct MissingGlyphState {
117    reported: FxHashSet<MissingGlyph>,
118    reported_order: VecDeque<MissingGlyph>,
119    generation: usize,
120}
121
122impl MissingGlyphState {
123    fn reset(&mut self, generation: usize) {
124        self.reported.clear();
125        self.reported_order.clear();
126        self.generation = generation;
127    }
128}
129
130struct QueuedMissingGlyph {
131    generation: usize,
132    missing_glyph: MissingGlyph,
133}
134
135/// Collects missing-glyph reports without invoking application code during layout.
136struct MissingGlyphReporter {
137    generation: Arc<AtomicUsize>,
138    sender: async_channel::Sender<QueuedMissingGlyph>,
139}
140
141impl MissingGlyphSink for MissingGlyphReporter {
142    fn report(&self, missing_glyphs: Vec<MissingGlyph>) {
143        if self.sender.is_closed() {
144            return;
145        }
146
147        let generation = self.generation.load(Ordering::Acquire);
148        // Repetitions within a line must not fill the queue before its other
149        // missing glyphs. Cross-report deduplication belongs to the receiver.
150        for missing_glyph in missing_glyphs.into_iter().unique() {
151            let queued = QueuedMissingGlyph {
152                generation,
153                missing_glyph,
154            };
155            if self.sender.try_send(queued).is_err() {
156                break;
157            }
158        }
159    }
160}
161
162impl MissingGlyphReporter {
163    fn reset(&self) {
164        self.generation.fetch_add(1, Ordering::AcqRel);
165    }
166}
167
168/// Receives batches of grapheme clusters that exhausted font fallback.
169pub(crate) struct MissingGlyphReceiver {
170    state: MissingGlyphState,
171    generation: Arc<AtomicUsize>,
172    receiver: async_channel::Receiver<QueuedMissingGlyph>,
173}
174
175impl MissingGlyphReceiver {
176    /// Waits until at least one new missing glyph has been observed.
177    ///
178    /// # Errors
179    ///
180    /// Returns [`async_channel::RecvError`] if the reporting channel is closed.
181    pub(crate) async fn recv(
182        &mut self,
183    ) -> std::result::Result<Vec<MissingGlyph>, async_channel::RecvError> {
184        loop {
185            let queued = self.receiver.recv().await?;
186            let mut missing_glyphs = Vec::new();
187            for queued in std::iter::once(queued)
188                .chain(std::iter::from_fn(|| self.receiver.try_recv().ok()))
189                .take(MAX_REPORTED_MISSING_GLYPHS)
190            {
191                let generation = self.generation.load(Ordering::Acquire);
192                if self.state.generation != generation {
193                    self.state.reset(generation);
194                    missing_glyphs.clear();
195                }
196                if queued.generation != generation
197                    || !self.state.reported.insert(queued.missing_glyph.clone())
198                {
199                    continue;
200                }
201                self.state
202                    .reported_order
203                    .push_back(queued.missing_glyph.clone());
204                missing_glyphs.push(queued.missing_glyph);
205                if self.state.reported.len() > MAX_REPORTED_MISSING_GLYPHS
206                    && let Some(expired) = self.state.reported_order.pop_front()
207                {
208                    self.state.reported.remove(&expired);
209                }
210            }
211            if !missing_glyphs.is_empty() {
212                return Ok(missing_glyphs);
213            }
214            // A producer can keep refilling the queue with already-reported
215            // glyphs. Bound work per poll even when every report is filtered out.
216            let mut yielded = false;
217            std::future::poll_fn(|cx| {
218                if std::mem::replace(&mut yielded, true) {
219                    std::task::Poll::Ready(())
220                } else {
221                    cx.waker().wake_by_ref();
222                    std::task::Poll::Pending
223                }
224            })
225            .await;
226        }
227    }
228}
229
230impl Drop for MissingGlyphReceiver {
231    fn drop(&mut self) {
232        self.receiver.close();
233        while self.receiver.try_recv().is_ok() {}
234    }
235}
236
237/// The GPUI text rendering sub system.
238pub struct TextSystem {
239    platform_text_system: Arc<dyn PlatformTextSystem>,
240    font_ids_by_font: RwLock<FxHashMap<Font, CachedFont>>,
241    font_metrics: RwLock<FxHashMap<FontId, FontMetrics>>,
242    raster_bounds: RwLock<FxHashMap<RenderGlyphParams, Bounds<DevicePixels>>>,
243    wrapper_pool: Mutex<FxHashMap<FontIdWithSize, Vec<CharWidthCache>>>,
244    fallback_font_stack: SmallVec<[Font; 2]>,
245    font_generation: Arc<AtomicUsize>,
246    missing_glyph_reporter: Arc<MissingGlyphReporter>,
247    missing_glyph_receiver: Mutex<Option<MissingGlyphReceiver>>,
248}
249
250#[derive(Clone)]
251enum CachedFont {
252    Direct(FontId),
253    Fallback(FontId),
254    Missing(Arc<anyhow::Error>),
255}
256
257impl TextSystem {
258    /// Create a new TextSystem with the given platform text system.
259    pub fn new(platform_text_system: Arc<dyn PlatformTextSystem>) -> Self {
260        let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS);
261        let missing_glyph_generation = Arc::<AtomicUsize>::default();
262        TextSystem {
263            platform_text_system,
264            font_metrics: RwLock::default(),
265            raster_bounds: RwLock::default(),
266            font_ids_by_font: RwLock::default(),
267            wrapper_pool: Mutex::default(),
268            fallback_font_stack: smallvec![
269                // TODO: Remove this when Linux have implemented setting fallbacks.
270                font(".ZedMono"),
271                font(".ZedSans"),
272                font("Helvetica"),
273                font("Segoe UI"),     // Windows
274                font("Ubuntu"),       // Gnome (Ubuntu)
275                font("Adwaita Sans"), // Gnome 47
276                font("Cantarell"),    // Gnome
277                font("Noto Sans"),    // KDE
278                font("DejaVu Sans"),
279                font("Arial"), // macOS, Windows
280            ],
281            font_generation: Arc::default(),
282            missing_glyph_reporter: Arc::new(MissingGlyphReporter {
283                generation: missing_glyph_generation.clone(),
284                sender,
285            }),
286            missing_glyph_receiver: Mutex::new(Some(MissingGlyphReceiver {
287                state: MissingGlyphState::default(),
288                generation: missing_glyph_generation,
289                receiver,
290            })),
291        }
292    }
293
294    /// Get sorted, unique font family names available to the platform text system.
295    ///
296    /// Includes fonts registered with [`Self::add_fonts`].
297    pub fn all_font_names(&self) -> Vec<String> {
298        let mut names = self.platform_text_system.all_font_names();
299        names.sort_unstable();
300        names.dedup();
301        names
302    }
303
304    /// Add a font's data to the text system.
305    ///
306    /// Cached font resolution and line layouts are invalidated after installation.
307    /// Layouts already in progress may complete against the previous font set.
308    pub fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
309        self.platform_text_system.add_fonts(fonts)?;
310        self.font_ids_by_font.write().clear();
311        self.missing_glyph_reporter.reset();
312        self.font_generation.fetch_add(1, Ordering::Release);
313        Ok(())
314    }
315
316    /// Takes the receiver for missing-glyph reports.
317    ///
318    /// Only one receiver is available for each text system. Returns `None` when
319    /// the receiver was already taken or another caller is taking it.
320    pub(crate) fn take_missing_glyph_receiver(&self) -> Option<MissingGlyphReceiver> {
321        self.missing_glyph_receiver
322            .try_lock()
323            .and_then(|mut receiver| receiver.take())
324    }
325
326    pub(crate) fn enable_missing_glyph_reporting(&self) {
327        self.platform_text_system
328            .set_missing_glyph_sink(Some(self.missing_glyph_reporter.clone()));
329    }
330
331    pub(crate) fn disable_missing_glyph_reporting(&self) {
332        self.platform_text_system.set_missing_glyph_sink(None);
333        self.missing_glyph_reporter.reset();
334    }
335
336    #[cfg(test)]
337    pub(crate) fn report_missing_glyphs_in_test(&self, missing_glyphs: Vec<MissingGlyph>) {
338        self.missing_glyph_reporter.report(missing_glyphs);
339    }
340
341    /// Reports this backend's font inspection and static variation capabilities.
342    pub fn font_capabilities(&self) -> FontCapabilities {
343        self.platform_text_system.font_capabilities()
344    }
345    /// Queries available axis ranges for a resolved face.
346    pub fn font_axes(&self, font_id: FontId) -> Result<FontQuery<Vec<VariationAxisInfo>>> {
347        self.platform_text_system.font_axes(font_id)
348    }
349
350    /// Queries GSUB/GPOS features on this resolved face. A successful empty list
351    /// differs from an unsupported query or an error. Metadata does not guarantee
352    /// applicability to a particular script, language, or input text.
353    pub fn opentype_features(
354        &self,
355        font_id: FontId,
356    ) -> Result<FontQuery<Vec<OpenTypeFeatureInfo>>> {
357        self.platform_text_system.opentype_features(font_id)
358    }
359
360    /// Get the Font for the Font Id.
361    pub fn get_font_for_id(&self, id: FontId) -> Option<Font> {
362        let lock = self.font_ids_by_font.read();
363        lock.iter()
364            .filter_map(|(font, result)| match result {
365                CachedFont::Direct(font_id) if *font_id == id => Some(font.clone()),
366                _ => None,
367            })
368            .next()
369    }
370
371    /// Resolves the specified font, falling back to the default font stack if
372    /// the font fails to load.
373    ///
374    /// # Panics
375    ///
376    /// Panics if the font and none of the fallbacks can be resolved.
377    pub fn resolve_font(&self, font: &Font) -> FontId {
378        if let Some(CachedFont::Direct(font_id) | CachedFont::Fallback(font_id)) =
379            self.font_ids_by_font.read().get(font)
380        {
381            return *font_id;
382        }
383        self.resolve_font_uncached(font)
384    }
385
386    /// Prewarm any system font caches needed to shape text.
387    ///
388    /// This may be expensive, so callers should generally invoke it on a
389    /// background executor. Missing entries are still populated on demand by
390    /// the normal shaping path.
391    pub fn prewarm_fonts(&self, fonts: &[Font]) {
392        let mut font_ids = SmallVec::<[FontId; 8]>::new();
393        for font in fonts {
394            let font_id = self.resolve_font(font);
395            if !font_ids.contains(&font_id) {
396                font_ids.push(font_id);
397            }
398        }
399        self.platform_text_system.prewarm_fonts(&font_ids);
400    }
401
402    /// Get the bounding box for the given font and font size.
403    /// A font's bounding box is the smallest rectangle that could enclose all glyphs
404    /// in the font. superimposed over one another.
405    pub fn bounding_box(&self, font_id: FontId, font_size: Pixels) -> Bounds<Pixels> {
406        self.read_metrics(font_id, |metrics| metrics.bounding_box(font_size))
407    }
408
409    /// Get the typographic bounds for the given character, in the given font and size.
410    pub fn typographic_bounds(
411        &self,
412        font_id: FontId,
413        font_size: Pixels,
414        character: char,
415    ) -> Result<Bounds<Pixels>> {
416        let glyph_id = self
417            .platform_text_system
418            .glyph_for_char(font_id, character)
419            .with_context(|| format!("glyph not found for character '{character}'"))?;
420        let bounds = self
421            .platform_text_system
422            .typographic_bounds(font_id, glyph_id)?;
423        Ok(self.read_metrics(font_id, |metrics| {
424            (bounds / metrics.units_per_em as f32 * font_size.0).map(px)
425        }))
426    }
427
428    /// Get the advance width for the given character, in the given font and size.
429    pub fn advance(&self, font_id: FontId, font_size: Pixels, ch: char) -> Result<Size<Pixels>> {
430        let glyph_id = self
431            .platform_text_system
432            .glyph_for_char(font_id, ch)
433            .with_context(|| format!("glyph not found for character '{ch}'"))?;
434        let result = self.platform_text_system.advance(font_id, glyph_id)?
435            / self.units_per_em(font_id) as f32;
436
437        Ok(result * font_size)
438    }
439
440    // Consider removing this?
441    /// Returns the shaped layout width of for the given character, in the given font and size.
442    pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels {
443        let mut buffer = [0; 4];
444        let buffer = ch.encode_utf8(&mut buffer);
445        self.platform_text_system
446            .layout_line(
447                buffer,
448                font_size,
449                &[FontRun {
450                    len: buffer.len(),
451                    font_id,
452                }],
453            )
454            .width
455    }
456
457    /// Returns the width of an `em`.
458    ///
459    /// Uses the width of the `m` character in the given font and size.
460    pub fn em_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
461        Ok(self.typographic_bounds(font_id, font_size, 'm')?.size.width)
462    }
463
464    /// Returns the advance width of an `em`.
465    ///
466    /// Uses the advance width of the `m` character in the given font and size.
467    pub fn em_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
468        Ok(self.advance(font_id, font_size, 'm')?.width)
469    }
470
471    /// Returns the width of an `ch`.
472    ///
473    /// Uses the width of the `0` character in the given font and size.
474    pub fn ch_width(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
475        Ok(self.typographic_bounds(font_id, font_size, '0')?.size.width)
476    }
477
478    /// Returns the advance width of an `ch`.
479    ///
480    /// Uses the advance width of the `0` character in the given font and size.
481    pub fn ch_advance(&self, font_id: FontId, font_size: Pixels) -> Result<Pixels> {
482        Ok(self.advance(font_id, font_size, '0')?.width)
483    }
484
485    /// Get the number of font size units per 'em square',
486    /// Per MDN: "an abstract square whose height is the intended distance between
487    /// lines of type in the same type size"
488    pub fn units_per_em(&self, font_id: FontId) -> u32 {
489        self.read_metrics(font_id, |metrics| metrics.units_per_em)
490    }
491
492    /// Get the height of a capital letter in the given font and size.
493    pub fn cap_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
494        self.read_metrics(font_id, |metrics| metrics.cap_height(font_size))
495    }
496
497    /// Get the height of the x character in the given font and size.
498    pub fn x_height(&self, font_id: FontId, font_size: Pixels) -> Pixels {
499        self.read_metrics(font_id, |metrics| metrics.x_height(font_size))
500    }
501
502    /// Get the recommended distance from the baseline for the given font
503    pub fn ascent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
504        self.read_metrics(font_id, |metrics| metrics.ascent(font_size))
505    }
506
507    /// Get the recommended distance below the baseline for the given font,
508    /// in single spaced text.
509    pub fn descent(&self, font_id: FontId, font_size: Pixels) -> Pixels {
510        self.read_metrics(font_id, |metrics| metrics.descent(font_size))
511    }
512
513    /// Get the recommended baseline offset for the given font and line height.
514    pub fn baseline_offset(
515        &self,
516        font_id: FontId,
517        font_size: Pixels,
518        line_height: Pixels,
519    ) -> Pixels {
520        let ascent = self.ascent(font_id, font_size);
521        let descent = self.descent(font_id, font_size);
522        let padding_top = (line_height - ascent - descent) / 2.;
523        padding_top + ascent
524    }
525
526    fn read_metrics<T>(&self, font_id: FontId, read: impl FnOnce(&FontMetrics) -> T) -> T {
527        let lock = self.font_metrics.upgradable_read();
528
529        if let Some(metrics) = lock.get(&font_id) {
530            read(metrics)
531        } else {
532            let mut lock = RwLockUpgradableReadGuard::upgrade(lock);
533            let metrics = lock
534                .entry(font_id)
535                .or_insert_with(|| self.platform_text_system.font_metrics(font_id));
536            read(metrics)
537        }
538    }
539
540    /// Returns a handle to a line wrapper, for the given font and font size.
541    pub fn line_wrapper(self: &Arc<Self>, font: Font, font_size: Pixels) -> LineWrapperHandle {
542        let lock = &mut self.wrapper_pool.lock();
543        let font_id = self.resolve_font(&font);
544        let char_widths = lock
545            .entry(FontIdWithSize { font_id, font_size })
546            .or_default()
547            .pop()
548            .unwrap_or_default();
549        let wrapper = LineWrapper::new(font_id, font_size, self.clone(), char_widths);
550
551        LineWrapperHandle {
552            wrapper: Some(wrapper),
553            text_system: self.clone(),
554        }
555    }
556
557    /// Get the rasterized size and location of a specific, rendered glyph.
558    pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
559        let raster_bounds = self.raster_bounds.upgradable_read();
560        if let Some(bounds) = raster_bounds.get(params) {
561            Ok(*bounds)
562        } else {
563            let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
564            let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
565            raster_bounds.insert(params.clone(), bounds);
566            Ok(bounds)
567        }
568    }
569
570    pub(crate) fn rasterize_glyph(
571        &self,
572        params: &RenderGlyphParams,
573    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
574        let raster_bounds = self.raster_bounds(params)?;
575        self.platform_text_system
576            .rasterize_glyph(params, raster_bounds)
577    }
578
579    /// Returns the dilation level to use for a glyph painted in the given color.
580    pub(crate) fn glyph_dilation_for_color(&self, color: Hsla) -> u8 {
581        self.platform_text_system.glyph_dilation_for_color(color)
582    }
583
584    /// Returns the text rendering mode recommended by the platform for the given font and size.
585    /// The return value will never be [`TextRenderingMode::PlatformDefault`].
586    pub(crate) fn recommended_rendering_mode(
587        &self,
588        font_id: FontId,
589        font_size: Pixels,
590    ) -> TextRenderingMode {
591        self.platform_text_system
592            .recommended_rendering_mode(font_id, font_size)
593    }
594
595    fn resolve_font_uncached(&self, font: &Font) -> FontId {
596        let mut font_error = None;
597        let font_id = {
598            let mut font_ids = self.font_ids_by_font.upgradable_read();
599            if let Some(CachedFont::Direct(font_id) | CachedFont::Fallback(font_id)) =
600                font_ids.get(font)
601            {
602                return *font_id;
603            }
604
605            let mut lookup_font = |candidate: &Font| {
606                let cached = if let Some(cached) = font_ids.get(candidate) {
607                    cached.clone()
608                } else {
609                    let cached = match self.platform_text_system.font_id(candidate) {
610                        Ok(font_id) => CachedFont::Direct(font_id),
611                        Err(error) => CachedFont::Missing(Arc::new(error)),
612                    };
613                    font_ids.with_upgraded(|font_ids| {
614                        font_ids.insert(candidate.clone(), cached.clone());
615                    });
616                    cached
617                };
618                match cached {
619                    CachedFont::Direct(font_id) => Some(font_id),
620                    CachedFont::Fallback(_) => None,
621                    CachedFont::Missing(error) => {
622                        if candidate == font {
623                            font_error = Some(error);
624                        }
625                        None
626                    }
627                }
628            };
629
630            if let Some(font_id) = lookup_font(font) {
631                Some(font_id)
632            } else {
633                let font_id = self.fallback_font_stack.iter().find_map(lookup_font);
634                if let Some(font_id) = font_id {
635                    font_ids.with_upgraded(|font_ids| {
636                        font_ids.insert(font.clone(), CachedFont::Fallback(font_id));
637                    });
638                }
639                font_id
640            }
641        };
642        if let Some(error) = font_error {
643            log::warn!("failed to load font {font:?}: {error:#}");
644        }
645        font_id.unwrap_or_else(|| {
646            panic!(
647                "failed to resolve font '{}' or any of the fallbacks: {}",
648                font.family,
649                self.fallback_font_stack
650                    .iter()
651                    .map(|fallback| &fallback.family)
652                    .join(", ")
653            );
654        })
655    }
656}
657
658/// The GPUI text layout subsystem.
659#[derive(Deref)]
660pub struct WindowTextSystem {
661    line_layout_cache: LineLayoutCache,
662    font_runs_buffer: Cell<Vec<FontRun>>,
663    #[deref]
664    text_system: Arc<TextSystem>,
665}
666
667impl WindowTextSystem {
668    /// Create a new WindowTextSystem with the given TextSystem.
669    pub fn new(text_system: Arc<TextSystem>) -> Self {
670        Self {
671            line_layout_cache: LineLayoutCache::new(
672                text_system.platform_text_system.clone(),
673                text_system.font_generation.clone(),
674            ),
675            font_runs_buffer: Cell::default(),
676            text_system,
677        }
678    }
679
680    pub(crate) fn layout_index(&self) -> LineLayoutIndex {
681        self.line_layout_cache.layout_index()
682    }
683
684    pub(crate) fn reuse_layouts(&self, index: Range<LineLayoutIndex>) {
685        self.line_layout_cache.reuse_layouts(index)
686    }
687
688    pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) {
689        self.line_layout_cache.truncate_layouts(index)
690    }
691
692    /// Shape the given line, at the given font_size, for painting to the screen.
693    /// Subsets of the line can be styled independently with the `runs` parameter.
694    ///
695    /// Note that this method can only shape a single line of text. It will panic
696    /// if the text contains newlines. If you need to shape multiple lines of text,
697    /// use [`Self::shape_text`] instead.
698    pub fn shape_line(
699        &self,
700        text: SharedString,
701        font_size: Pixels,
702        runs: &[TextRun],
703        force_width: Option<Pixels>,
704    ) -> ShapedLine {
705        debug_assert!(
706            text.find('\n').is_none(),
707            "text argument should not contain newlines"
708        );
709
710        let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
711        for run in runs {
712            if let Some(last_run) = decoration_runs.last_mut()
713                && last_run.color == run.color
714                && last_run.underline == run.underline
715                && last_run.strikethrough == run.strikethrough
716                && last_run.background_color == run.background_color
717            {
718                last_run.len += run.len as u32;
719                continue;
720            }
721            decoration_runs.push(DecorationRun {
722                len: run.len as u32,
723                color: run.color,
724                background_color: run.background_color,
725                underline: run.underline,
726                strikethrough: run.strikethrough,
727            });
728        }
729
730        let layout = self.layout_line(&text, font_size, runs, force_width);
731
732        ShapedLine {
733            layout,
734            text,
735            decoration_runs,
736        }
737    }
738
739    /// Shape a multi line string of text, at the given font_size, for painting to the screen.
740    /// Subsets of the text can be styled independently with the `runs` parameter.
741    /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width.
742    pub fn shape_text(
743        &self,
744        text: SharedString,
745        font_size: Pixels,
746        runs: &[TextRun],
747        wrap_width: Option<Pixels>,
748        line_clamp: Option<usize>,
749    ) -> Result<SmallVec<[WrappedLine; 1]>> {
750        let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable();
751        let mut font_runs = self.font_runs_buffer.take();
752
753        let mut lines = SmallVec::new();
754        let max_wrap_lines = line_clamp;
755        let mut wrapped_lines = 0;
756
757        let mut process_line = |line_text: SharedString, line_start, line_end| {
758            font_runs.clear();
759
760            let mut decoration_runs = <Vec<DecorationRun>>::with_capacity(32);
761            let mut run_start = line_start;
762            while run_start < line_end {
763                let Some(run) = runs.peek_mut() else {
764                    log::warn!("`TextRun`s do not cover the entire to be shaped text");
765                    break;
766                };
767
768                let run_len_within_line = cmp::min(line_end - run_start, run.len);
769
770                let decoration_changed = if let Some(last_run) = decoration_runs.last_mut()
771                    && last_run.color == run.color
772                    && last_run.underline == run.underline
773                    && last_run.strikethrough == run.strikethrough
774                    && last_run.background_color == run.background_color
775                {
776                    last_run.len += run_len_within_line as u32;
777                    false
778                } else {
779                    decoration_runs.push(DecorationRun {
780                        len: run_len_within_line as u32,
781                        color: run.color,
782                        background_color: run.background_color,
783                        underline: run.underline,
784                        strikethrough: run.strikethrough,
785                    });
786                    true
787                };
788
789                let font_id = self.resolve_font(&run.font);
790                if let Some(font_run) = font_runs.last_mut()
791                    && font_id == font_run.font_id
792                    && !decoration_changed
793                {
794                    font_run.len += run_len_within_line;
795                } else {
796                    font_runs.push(FontRun {
797                        len: run_len_within_line,
798                        font_id,
799                    });
800                }
801
802                // Preserve the remainder of the run for the next line
803                run.len -= run_len_within_line;
804                if run.len == 0 {
805                    runs.next();
806                }
807                run_start += run_len_within_line;
808            }
809
810            let layout = self.line_layout_cache.layout_wrapped_line(
811                &line_text,
812                font_size,
813                &font_runs,
814                wrap_width,
815                max_wrap_lines.map(|max| max.saturating_sub(wrapped_lines)),
816            );
817            wrapped_lines += layout.wrap_boundaries.len();
818
819            lines.push(WrappedLine {
820                layout,
821                decoration_runs,
822                text: line_text,
823            });
824
825            // Skip `\n` character.
826            if let Some(run) = runs.peek_mut() {
827                run.len -= 1;
828                if run.len == 0 {
829                    runs.next();
830                }
831            }
832        };
833
834        let mut split_lines = text.split('\n');
835
836        // Special case single lines to prevent allocating a sharedstring
837        if let Some(first_line) = split_lines.next()
838            && let Some(second_line) = split_lines.next()
839        {
840            let mut line_start = 0;
841            process_line(
842                SharedString::new(first_line),
843                line_start,
844                line_start + first_line.len(),
845            );
846            line_start += first_line.len() + '\n'.len_utf8();
847            process_line(
848                SharedString::new(second_line),
849                line_start,
850                line_start + second_line.len(),
851            );
852            for line_text in split_lines {
853                line_start += line_text.len() + '\n'.len_utf8();
854                process_line(
855                    SharedString::new(line_text),
856                    line_start,
857                    line_start + line_text.len(),
858                );
859            }
860        } else {
861            let end = text.len();
862            process_line(text, 0, end);
863        }
864
865        self.font_runs_buffer.set(font_runs);
866
867        Ok(lines)
868    }
869
870    pub(crate) fn finish_frame(&self) {
871        self.line_layout_cache.finish_frame()
872    }
873
874    /// Layout the given line of text, at the given font_size.
875    /// Subsets of the line can be styled independently with the `runs` parameter.
876    /// Generally, you should prefer to use [`Self::shape_line`] instead, which
877    /// can be painted directly.
878    pub fn layout_line(
879        &self,
880        text: &str,
881        font_size: Pixels,
882        runs: &[TextRun],
883        force_width: Option<Pixels>,
884    ) -> Arc<LineLayout> {
885        let mut last_run = None::<&TextRun>;
886        let mut font_runs = self.font_runs_buffer.take();
887        font_runs.clear();
888
889        for run in runs.iter() {
890            let decoration_changed = if let Some(last_run) = last_run
891                && last_run.color == run.color
892                && last_run.underline == run.underline
893                && last_run.strikethrough == run.strikethrough
894            // we do not consider differing background color relevant, as it does not affect glyphs
895            // && last_run.background_color == run.background_color
896            {
897                false
898            } else {
899                last_run = Some(run);
900                true
901            };
902
903            let font_id = self.resolve_font(&run.font);
904            if let Some(font_run) = font_runs.last_mut()
905                && font_id == font_run.font_id
906                && !decoration_changed
907            {
908                font_run.len += run.len;
909            } else {
910                font_runs.push(FontRun {
911                    len: run.len,
912                    font_id,
913                });
914            }
915        }
916
917        let layout = self.line_layout_cache.layout_line(
918            &SharedString::new(text),
919            font_size,
920            &font_runs,
921            force_width,
922        );
923
924        self.font_runs_buffer.set(font_runs);
925
926        layout
927    }
928
929    /// Layout one paragraph through an optional backend capability. Uses the
930    /// existing frame cache lifecycle; colors/decorations are paint-only inputs.
931    /// This initial API takes resolved FontRuns and automatic direction/start alignment.
932    pub fn layout_paragraph(
933        &self,
934        text: SharedString,
935        font_size: Pixels,
936        runs: &[FontRun],
937        width: Pixels,
938    ) -> Result<Arc<dyn PlatformParagraphLayout>> {
939        self.line_layout_cache
940            .layout_paragraph(text, font_size, runs, width)
941    }
942
943    /// Returns the shaped layout width of for the given character, in the given font and size.
944    pub fn layout_width(&self, font_id: FontId, font_size: Pixels, ch: char) -> Pixels {
945        let mut buffer = [0; 4];
946        let buffer: &_ = ch.encode_utf8(&mut buffer);
947        self.line_layout_cache
948            .layout_line(
949                buffer,
950                font_size,
951                &[FontRun {
952                    len: buffer.len(),
953                    font_id,
954                }],
955                None,
956            )
957            .width
958    }
959
960    /// Returns the shaped layout width of an `em`.
961    pub fn em_layout_width(&self, font_id: FontId, font_size: Pixels) -> Pixels {
962        self.layout_width(font_id, font_size, 'm')
963    }
964}
965
966#[derive(Hash, Eq, PartialEq)]
967struct FontIdWithSize {
968    font_id: FontId,
969    font_size: Pixels,
970}
971
972/// A handle into the text system, which can be used to compute the wrapped layout of text
973pub struct LineWrapperHandle {
974    wrapper: Option<LineWrapper>,
975    text_system: Arc<TextSystem>,
976}
977
978impl Drop for LineWrapperHandle {
979    fn drop(&mut self) {
980        let mut state = self.text_system.wrapper_pool.lock();
981        let wrapper = self.wrapper.take().unwrap();
982        state
983            .get_mut(&FontIdWithSize {
984                font_id: wrapper.font_id,
985                font_size: wrapper.font_size,
986            })
987            .unwrap()
988            .push(wrapper.into_char_widths());
989    }
990}
991
992impl Deref for LineWrapperHandle {
993    type Target = LineWrapper;
994
995    fn deref(&self) -> &Self::Target {
996        self.wrapper.as_ref().unwrap()
997    }
998}
999
1000impl DerefMut for LineWrapperHandle {
1001    fn deref_mut(&mut self) -> &mut Self::Target {
1002        self.wrapper.as_mut().unwrap()
1003    }
1004}
1005
1006/// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0,
1007/// with 400.0 as normal.
1008#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)]
1009#[serde(transparent)]
1010pub struct FontWeight(pub f32);
1011
1012impl Display for FontWeight {
1013    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1014        write!(f, "{}", self.0)
1015    }
1016}
1017
1018impl From<f32> for FontWeight {
1019    fn from(weight: f32) -> Self {
1020        FontWeight(weight)
1021    }
1022}
1023
1024impl Default for FontWeight {
1025    #[inline]
1026    fn default() -> FontWeight {
1027        FontWeight::NORMAL
1028    }
1029}
1030
1031impl Hash for FontWeight {
1032    fn hash<H: Hasher>(&self, state: &mut H) {
1033        state.write_u32(u32::from_be_bytes(self.0.to_be_bytes()));
1034    }
1035}
1036
1037impl Eq for FontWeight {}
1038
1039impl FontWeight {
1040    /// Thin weight (100), the thinnest value.
1041    pub const THIN: FontWeight = FontWeight(100.0);
1042    /// Extra light weight (200).
1043    pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0);
1044    /// Light weight (300).
1045    pub const LIGHT: FontWeight = FontWeight(300.0);
1046    /// Normal (400).
1047    pub const NORMAL: FontWeight = FontWeight(400.0);
1048    /// Medium weight (500, higher than normal).
1049    pub const MEDIUM: FontWeight = FontWeight(500.0);
1050    /// Semibold weight (600).
1051    pub const SEMIBOLD: FontWeight = FontWeight(600.0);
1052    /// Bold weight (700).
1053    pub const BOLD: FontWeight = FontWeight(700.0);
1054    /// Extra-bold weight (800).
1055    pub const EXTRA_BOLD: FontWeight = FontWeight(800.0);
1056    /// Black weight (900), the thickest value.
1057    pub const BLACK: FontWeight = FontWeight(900.0);
1058
1059    /// All of the font weights, in order from thinnest to thickest.
1060    pub const ALL: [FontWeight; 9] = [
1061        Self::THIN,
1062        Self::EXTRA_LIGHT,
1063        Self::LIGHT,
1064        Self::NORMAL,
1065        Self::MEDIUM,
1066        Self::SEMIBOLD,
1067        Self::BOLD,
1068        Self::EXTRA_BOLD,
1069        Self::BLACK,
1070    ];
1071}
1072
1073impl schemars::JsonSchema for FontWeight {
1074    fn schema_name() -> std::borrow::Cow<'static, str> {
1075        "FontWeight".into()
1076    }
1077
1078    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
1079        use schemars::json_schema;
1080        json_schema!({
1081            "type": "number",
1082            "minimum": Self::THIN,
1083            "maximum": Self::BLACK,
1084            "default": Self::default(),
1085            "description": "Font weight value between 100 (thin) and 900 (black)"
1086        })
1087    }
1088}
1089
1090/// Allows italic or oblique faces to be selected.
1091#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)]
1092pub enum FontStyle {
1093    /// A face that is neither italic not obliqued.
1094    #[default]
1095    Normal,
1096    /// A form that is generally cursive in nature.
1097    Italic,
1098    /// A typically-sloped version of the regular face.
1099    Oblique,
1100}
1101
1102impl Display for FontStyle {
1103    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1104        Debug::fmt(self, f)
1105    }
1106}
1107
1108/// A styled run of text, for use in [`crate::TextLayout`].
1109#[derive(Clone, Debug, PartialEq, Eq, Default)]
1110pub struct TextRun {
1111    /// A number of utf8 bytes
1112    pub len: usize,
1113    /// The font to use for this run.
1114    pub font: Font,
1115    /// The color
1116    pub color: Hsla,
1117    /// The background color (if any)
1118    pub background_color: Option<Hsla>,
1119    /// The underline style (if any)
1120    pub underline: Option<UnderlineStyle>,
1121    /// The strikethrough style (if any)
1122    pub strikethrough: Option<StrikethroughStyle>,
1123}
1124
1125#[cfg(all(target_os = "macos", test))]
1126impl TextRun {
1127    fn with_len(&self, len: usize) -> Self {
1128        let mut this = self.clone();
1129        this.len = len;
1130        this
1131    }
1132}
1133
1134/// An identifier for a specific glyph, as returned by [`WindowTextSystem::layout_line`].
1135#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
1136#[repr(C)]
1137pub struct GlyphId(pub u32);
1138
1139/// Parameters for rendering a glyph, used as cache keys for raster bounds.
1140///
1141/// This struct identifies a specific glyph rendering configuration including
1142/// font, size, subpixel positioning, and scale factor. It's used to look up
1143/// cached raster bounds and sprite atlas entries.
1144#[derive(Clone, Debug, PartialEq)]
1145#[expect(missing_docs)]
1146pub struct RenderGlyphParams {
1147    pub font_id: FontId,
1148    pub glyph_id: GlyphId,
1149    pub font_size: Pixels,
1150    pub subpixel_variant: Point<u8>,
1151    pub scale_factor: f32,
1152    pub is_emoji: bool,
1153    pub subpixel_rendering: bool,
1154    pub dilation: u8,
1155}
1156
1157impl Eq for RenderGlyphParams {}
1158
1159impl Hash for RenderGlyphParams {
1160    fn hash<H: Hasher>(&self, state: &mut H) {
1161        self.font_id.0.hash(state);
1162        self.glyph_id.0.hash(state);
1163        self.font_size.0.to_bits().hash(state);
1164        self.subpixel_variant.hash(state);
1165        self.scale_factor.to_bits().hash(state);
1166        self.is_emoji.hash(state);
1167        self.subpixel_rendering.hash(state);
1168        self.dilation.hash(state);
1169    }
1170}
1171
1172/// The configuration details for identifying a specific font.
1173#[derive(Clone, Debug, Eq, PartialEq, Hash)]
1174pub struct Font {
1175    /// The font family name.
1176    ///
1177    /// The special name ".SystemUIFont" is used to identify the system UI font, which varies based on platform.
1178    pub family: SharedString,
1179
1180    /// The font features to use.
1181    pub features: FontFeatures,
1182
1183    /// Precise explicit variable-font coordinates in design units.
1184    pub variations: FontVariations,
1185
1186    /// The fallbacks fonts to use.
1187    pub fallbacks: Option<FontFallbacks>,
1188
1189    /// The font weight.
1190    pub weight: FontWeight,
1191
1192    /// The font style.
1193    pub style: FontStyle,
1194}
1195
1196impl Default for Font {
1197    fn default() -> Self {
1198        font(".SystemUIFont")
1199    }
1200}
1201
1202/// Get a [`Font`] for a given name.
1203pub fn font(family: impl Into<SharedString>) -> Font {
1204    Font {
1205        family: family.into(),
1206        features: FontFeatures::default(),
1207        variations: FontVariations::default(),
1208        weight: FontWeight::default(),
1209        style: FontStyle::default(),
1210        fallbacks: None,
1211    }
1212}
1213
1214impl Font {
1215    /// Set this Font to be bold
1216    pub fn bold(mut self) -> Self {
1217        self.weight = FontWeight::BOLD;
1218        self
1219    }
1220
1221    /// Set this Font to be italic
1222    pub fn italic(mut self) -> Self {
1223        self.style = FontStyle::Italic;
1224        self
1225    }
1226}
1227
1228/// A struct for storing font metrics.
1229/// It is used to define the measurements of a typeface.
1230#[derive(Clone, Copy, Debug)]
1231pub struct FontMetrics {
1232    /// The number of font units that make up the "em square",
1233    /// a scalable grid for determining the size of a typeface.
1234    pub units_per_em: u32,
1235
1236    /// The vertical distance from the baseline of the font to the top of the glyph covers.
1237    pub ascent: f32,
1238
1239    /// The vertical distance from the baseline of the font to the bottom of the glyph covers.
1240    pub descent: f32,
1241
1242    /// The recommended additional space to add between lines of type.
1243    pub line_gap: f32,
1244
1245    /// The suggested position of the underline.
1246    pub underline_position: f32,
1247
1248    /// The suggested thickness of the underline.
1249    pub underline_thickness: f32,
1250
1251    /// The height of a capital letter measured from the baseline of the font.
1252    pub cap_height: f32,
1253
1254    /// The height of a lowercase x.
1255    pub x_height: f32,
1256
1257    /// The outer limits of the area that the font covers.
1258    /// Corresponds to the xMin / xMax / yMin / yMax values in the OpenType `head` table
1259    pub bounding_box: Bounds<f32>,
1260}
1261
1262impl FontMetrics {
1263    /// Returns the vertical distance from the baseline of the font to the top of the glyph covers in pixels.
1264    pub fn ascent(&self, font_size: Pixels) -> Pixels {
1265        Pixels((self.ascent / self.units_per_em as f32) * font_size.0)
1266    }
1267
1268    /// Returns the vertical distance from the baseline of the font to the bottom of the glyph covers in pixels.
1269    pub fn descent(&self, font_size: Pixels) -> Pixels {
1270        Pixels((self.descent / self.units_per_em as f32) * font_size.0)
1271    }
1272
1273    /// Returns the recommended additional space to add between lines of type in pixels.
1274    pub fn line_gap(&self, font_size: Pixels) -> Pixels {
1275        Pixels((self.line_gap / self.units_per_em as f32) * font_size.0)
1276    }
1277
1278    /// Returns the suggested position of the underline in pixels.
1279    pub fn underline_position(&self, font_size: Pixels) -> Pixels {
1280        Pixels((self.underline_position / self.units_per_em as f32) * font_size.0)
1281    }
1282
1283    /// Returns the suggested thickness of the underline in pixels.
1284    pub fn underline_thickness(&self, font_size: Pixels) -> Pixels {
1285        Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0)
1286    }
1287
1288    /// Returns the height of a capital letter measured from the baseline of the font in pixels.
1289    pub fn cap_height(&self, font_size: Pixels) -> Pixels {
1290        Pixels((self.cap_height / self.units_per_em as f32) * font_size.0)
1291    }
1292
1293    /// Returns the height of a lowercase x in pixels.
1294    pub fn x_height(&self, font_size: Pixels) -> Pixels {
1295        Pixels((self.x_height / self.units_per_em as f32) * font_size.0)
1296    }
1297
1298    /// Returns the outer limits of the area that the font covers in pixels.
1299    pub fn bounding_box(&self, font_size: Pixels) -> Bounds<Pixels> {
1300        (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
1301    }
1302}
1303
1304/// Maps well-known virtual font names to their concrete equivalents.
1305#[allow(unused)]
1306pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
1307    // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
1308    // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
1309    // and so retained here for backward compatibility.
1310    match name {
1311        ".SystemUIFont" => system,
1312        ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
1313        ".ZedMono" | "Zed Plex Mono" => "Lilex",
1314        _ => name,
1315    }
1316}
1317
1318/// Like [`font_name_with_fallbacks`] but accepts and returns [`SharedString`] references.
1319#[allow(unused)]
1320pub fn font_name_with_fallbacks_shared<'a>(
1321    name: &'a SharedString,
1322    system: &'a SharedString,
1323) -> &'a SharedString {
1324    // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
1325    // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
1326    // and so retained here for backward compatibility.
1327    match name.as_str() {
1328        ".SystemUIFont" => system,
1329        ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") },
1330        ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") },
1331        _ => name,
1332    }
1333}
1334
1335#[cfg(test)]
1336mod font_resolution_tests {
1337    use super::*;
1338    use crate::NoopTextSystem;
1339
1340    #[test]
1341    fn caches_resolved_font_fallbacks() {
1342        let mut text_system = TextSystem::new(Arc::new(NoopTextSystem::new()));
1343        let requested = font("Missing Font").bold().italic();
1344        let missing = CachedFont::Missing(Arc::new(anyhow::anyhow!("missing font")));
1345        {
1346            let mut font_ids = text_system.font_ids_by_font.write();
1347            font_ids.insert(requested.clone(), missing.clone());
1348            for fallback in &text_system.fallback_font_stack {
1349                font_ids.insert(fallback.clone(), missing.clone());
1350            }
1351            font_ids.insert(font("Arial"), CachedFont::Direct(FontId(42)));
1352        }
1353
1354        assert_eq!(text_system.resolve_font(&requested), FontId(42));
1355        assert_eq!(text_system.get_font_for_id(FontId(42)), Some(font("Arial")));
1356
1357        text_system.fallback_font_stack.clear();
1358        assert_eq!(text_system.resolve_font(&requested), FontId(42));
1359    }
1360}
1361
1362#[cfg(test)]
1363mod line_wrapper_pool_tests {
1364    use super::*;
1365    use crate::NoopTextSystem;
1366
1367    #[test]
1368    fn pooled_line_wrappers_do_not_retain_text_system() {
1369        let text_system = Arc::new(TextSystem::new(Arc::new(NoopTextSystem::new())));
1370        let weak_text_system = Arc::downgrade(&text_system);
1371
1372        let mut wrapper = text_system.line_wrapper(font(".ZedMono"), px(16.));
1373        let fragments = [LineFragment::text("hello world")];
1374        wrapper
1375            .wrap_line(&fragments, px(40.), IndentAdjustment::default())
1376            .for_each(drop);
1377        drop(wrapper);
1378        drop(text_system);
1379
1380        assert!(weak_text_system.upgrade().is_none());
1381    }
1382}
1383
1384#[cfg(test)]
1385mod missing_glyph_tests {
1386    use super::*;
1387    use futures::FutureExt as _;
1388
1389    #[test]
1390    fn bounds_retained_missing_glyphs() {
1391        let (reporter, mut receiver) = missing_glyph_channel();
1392        reporter.report(
1393            (0..MAX_REPORTED_MISSING_GLYPHS)
1394                .map(|index| {
1395                    MissingGlyph::new(index.to_string().into(), FallbackFontClass::Proportional)
1396                })
1397                .collect(),
1398        );
1399        assert!(receiver.recv().now_or_never().unwrap().is_ok());
1400
1401        let newest = MissingGlyph::new("newest".into(), FallbackFontClass::Monospace);
1402        reporter.report(vec![newest.clone()]);
1403        assert!(receiver.recv().now_or_never().unwrap().is_ok());
1404
1405        let state = &receiver.state;
1406        assert_eq!(state.reported.len(), MAX_REPORTED_MISSING_GLYPHS);
1407        assert_eq!(state.reported_order.len(), MAX_REPORTED_MISSING_GLYPHS);
1408        assert!(state.reported.contains(&newest));
1409    }
1410
1411    #[test]
1412    fn dropping_receiver_closes_and_clears_reports() {
1413        let (reporter, receiver) = missing_glyph_channel();
1414        reporter.report(vec![missing_glyph("missing")]);
1415
1416        drop(receiver);
1417
1418        assert!(reporter.sender.is_closed());
1419        assert!(reporter.sender.is_empty());
1420    }
1421
1422    fn missing_glyph_channel() -> (MissingGlyphReporter, MissingGlyphReceiver) {
1423        let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS);
1424        let generation = Arc::<AtomicUsize>::default();
1425        let reporter = MissingGlyphReporter {
1426            generation: generation.clone(),
1427            sender,
1428        };
1429        let receiver = MissingGlyphReceiver {
1430            state: MissingGlyphState::default(),
1431            generation,
1432            receiver,
1433        };
1434        (reporter, receiver)
1435    }
1436
1437    fn missing_glyph(grapheme: &'static str) -> MissingGlyph {
1438        MissingGlyph::new(grapheme.into(), FallbackFontClass::Proportional)
1439    }
1440}