Skip to main content

gpui/
text_system.rs

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