Skip to main content

gpui/
text_system.rs

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