Skip to main content

gpui_engine/
text.rs

1//! Font identity, metrics, and rendering parameter types shared by `gpui` and its platform backends.
2
3use anyhow::Result;
4use derive_more::{Add, FromStr, Sub};
5use gpui_shared_string::SharedString;
6use gpui_types::{
7    Bounds, DevicePixels, Hsla, Pixels, Point, Size, StrikethroughStyle, UnderlineStyle, point, px,
8    size,
9};
10use schemars::JsonSchema;
11use serde::{Deserialize, Serialize};
12use smallvec::SmallVec;
13use std::borrow::Cow;
14use std::fmt::{Debug, Display, Formatter};
15use std::hash::{Hash, Hasher};
16use std::sync::Arc;
17
18/// Number of subpixel glyph variants along the X axis.
19pub const SUBPIXEL_VARIANTS_X: u8 = 4;
20
21/// Number of subpixel glyph variants along the Y axis.
22pub const SUBPIXEL_VARIANTS_Y: u8 = 1;
23
24use crate::{FontFallbacks, FontFeatures, FontId, GlyphId, RenderGlyphParams};
25
26/// An opaque identifier for a specific font family.
27#[derive(Hash, PartialEq, Eq, Clone, Copy, Debug)]
28pub struct FontFamilyId(pub usize);
29
30/// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0,
31/// with 400.0 as normal.
32#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)]
33#[serde(transparent)]
34pub struct FontWeight(pub f32);
35
36impl Display for FontWeight {
37    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
38        write!(f, "{}", self.0)
39    }
40}
41
42impl From<f32> for FontWeight {
43    fn from(weight: f32) -> Self {
44        FontWeight(weight)
45    }
46}
47
48impl Default for FontWeight {
49    #[inline]
50    fn default() -> FontWeight {
51        FontWeight::NORMAL
52    }
53}
54
55impl Hash for FontWeight {
56    fn hash<H: Hasher>(&self, state: &mut H) {
57        state.write_u32(u32::from_be_bytes(self.0.to_be_bytes()));
58    }
59}
60
61impl Eq for FontWeight {}
62
63impl FontWeight {
64    /// Thin weight (100), the thinnest value.
65    pub const THIN: FontWeight = FontWeight(100.0);
66    /// Extra light weight (200).
67    pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0);
68    /// Light weight (300).
69    pub const LIGHT: FontWeight = FontWeight(300.0);
70    /// Normal (400).
71    pub const NORMAL: FontWeight = FontWeight(400.0);
72    /// Medium weight (500, higher than normal).
73    pub const MEDIUM: FontWeight = FontWeight(500.0);
74    /// Semibold weight (600).
75    pub const SEMIBOLD: FontWeight = FontWeight(600.0);
76    /// Bold weight (700).
77    pub const BOLD: FontWeight = FontWeight(700.0);
78    /// Extra-bold weight (800).
79    pub const EXTRA_BOLD: FontWeight = FontWeight(800.0);
80    /// Black weight (900), the thickest value.
81    pub const BLACK: FontWeight = FontWeight(900.0);
82
83    /// All of the font weights, in order from thinnest to thickest.
84    pub const ALL: [FontWeight; 9] = [
85        Self::THIN,
86        Self::EXTRA_LIGHT,
87        Self::LIGHT,
88        Self::NORMAL,
89        Self::MEDIUM,
90        Self::SEMIBOLD,
91        Self::BOLD,
92        Self::EXTRA_BOLD,
93        Self::BLACK,
94    ];
95}
96
97impl schemars::JsonSchema for FontWeight {
98    fn schema_name() -> Cow<'static, str> {
99        "FontWeight".into()
100    }
101
102    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
103        use schemars::json_schema;
104        json_schema!({
105            "type": "number",
106            "minimum": Self::THIN,
107            "maximum": Self::BLACK,
108            "default": Self::default(),
109            "description": "Font weight value between 100 (thin) and 900 (black)"
110        })
111    }
112}
113
114/// Allows italic or oblique faces to be selected.
115#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)]
116pub enum FontStyle {
117    /// A face that is neither italic not obliqued.
118    #[default]
119    Normal,
120    /// A form that is generally cursive in nature.
121    Italic,
122    /// A typically-sloped version of the regular face.
123    Oblique,
124}
125
126impl Display for FontStyle {
127    fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
128        Debug::fmt(self, f)
129    }
130}
131
132/// The configuration details for identifying a specific font.
133#[derive(Clone, Debug, Eq, PartialEq, Hash)]
134pub struct Font {
135    /// The font family name.
136    ///
137    /// The special name ".SystemUIFont" is used to identify the system UI font, which varies based on platform.
138    pub family: SharedString,
139
140    /// The font features to use.
141    pub features: FontFeatures,
142
143    /// The fallbacks fonts to use.
144    pub fallbacks: Option<FontFallbacks>,
145
146    /// The font weight.
147    pub weight: FontWeight,
148
149    /// The font style.
150    pub style: FontStyle,
151}
152
153impl Default for Font {
154    fn default() -> Self {
155        font(".SystemUIFont")
156    }
157}
158
159/// Get a [`Font`] for a given name.
160pub fn font(family: impl Into<SharedString>) -> Font {
161    Font {
162        family: family.into(),
163        features: FontFeatures::default(),
164        weight: FontWeight::default(),
165        style: FontStyle::default(),
166        fallbacks: None,
167    }
168}
169
170impl Font {
171    /// Set this Font to be bold
172    pub fn bold(mut self) -> Self {
173        self.weight = FontWeight::BOLD;
174        self
175    }
176
177    /// Set this Font to be italic
178    pub fn italic(mut self) -> Self {
179        self.style = FontStyle::Italic;
180        self
181    }
182}
183
184/// A struct for storing font metrics.
185/// It is used to define the measurements of a typeface.
186#[derive(Clone, Copy, Debug)]
187pub struct FontMetrics {
188    /// The number of font units that make up the "em square",
189    /// a scalable grid for determining the size of a typeface.
190    pub units_per_em: u32,
191
192    /// The vertical distance from the baseline of the font to the top of the glyph covers.
193    pub ascent: f32,
194
195    /// The vertical distance from the baseline of the font to the bottom of the glyph covers.
196    pub descent: f32,
197
198    /// The recommended additional space to add between lines of type.
199    pub line_gap: f32,
200
201    /// The suggested position of the underline.
202    pub underline_position: f32,
203
204    /// The suggested thickness of the underline.
205    pub underline_thickness: f32,
206
207    /// The height of a capital letter measured from the baseline of the font.
208    pub cap_height: f32,
209
210    /// The height of a lowercase x.
211    pub x_height: f32,
212
213    /// The outer limits of the area that the font covers.
214    /// Corresponds to the xMin / xMax / yMin / yMax values in the OpenType `head` table
215    pub bounding_box: Bounds<f32>,
216}
217
218impl FontMetrics {
219    /// Returns the vertical distance from the baseline of the font to the top of the glyph covers in pixels.
220    pub fn ascent(&self, font_size: Pixels) -> Pixels {
221        Pixels((self.ascent / self.units_per_em as f32) * font_size.0)
222    }
223
224    /// Returns the vertical distance from the baseline of the font to the bottom of the glyph covers in pixels.
225    pub fn descent(&self, font_size: Pixels) -> Pixels {
226        Pixels((self.descent / self.units_per_em as f32) * font_size.0)
227    }
228
229    /// Returns the recommended additional space to add between lines of type in pixels.
230    pub fn line_gap(&self, font_size: Pixels) -> Pixels {
231        Pixels((self.line_gap / self.units_per_em as f32) * font_size.0)
232    }
233
234    /// Returns the suggested position of the underline in pixels.
235    pub fn underline_position(&self, font_size: Pixels) -> Pixels {
236        Pixels((self.underline_position / self.units_per_em as f32) * font_size.0)
237    }
238
239    /// Returns the suggested thickness of the underline in pixels.
240    pub fn underline_thickness(&self, font_size: Pixels) -> Pixels {
241        Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0)
242    }
243
244    /// Returns the height of a capital letter measured from the baseline of the font in pixels.
245    pub fn cap_height(&self, font_size: Pixels) -> Pixels {
246        Pixels((self.cap_height / self.units_per_em as f32) * font_size.0)
247    }
248
249    /// Returns the height of a lowercase x in pixels.
250    pub fn x_height(&self, font_size: Pixels) -> Pixels {
251        Pixels((self.x_height / self.units_per_em as f32) * font_size.0)
252    }
253
254    /// Returns the outer limits of the area that the font covers in pixels.
255    pub fn bounding_box(&self, font_size: Pixels) -> Bounds<Pixels> {
256        (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
257    }
258}
259
260/// A run of text with a single font.
261#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
262#[expect(missing_docs)]
263pub struct FontRun {
264    pub len: usize,
265    pub font_id: FontId,
266}
267
268/// The text rendering mode to use for drawing glyphs.
269#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
270pub enum TextRenderingMode {
271    /// Use the platform's default text rendering mode.
272    #[default]
273    PlatformDefault,
274    /// Use subpixel (ClearType-style) text rendering.
275    Subpixel,
276    /// Use grayscale text rendering.
277    Grayscale,
278}
279
280/// A laid out and styled line of text
281#[derive(Default, Debug)]
282pub struct LineLayout {
283    /// The font size for this line
284    pub font_size: Pixels,
285    /// The width of the line
286    pub width: Pixels,
287    /// The ascent of the line
288    pub ascent: Pixels,
289    /// The descent of the line
290    pub descent: Pixels,
291    /// The shaped runs that make up this line
292    pub runs: Vec<ShapedRun>,
293    /// The length of the line in utf-8 bytes
294    pub len: usize,
295}
296
297/// A run of text that has been shaped .
298#[derive(Debug, Clone)]
299pub struct ShapedRun {
300    /// The font id for this run
301    pub font_id: FontId,
302    /// The glyphs that make up this run
303    pub glyphs: Vec<ShapedGlyph>,
304}
305
306/// A single glyph, ready to paint.
307#[derive(Clone, Debug)]
308pub struct ShapedGlyph {
309    /// The ID for this glyph, as determined by the text system.
310    pub id: GlyphId,
311
312    /// The position of this glyph in its containing line.
313    pub position: Point<Pixels>,
314
315    /// The index of this glyph in the original text.
316    pub index: usize,
317
318    /// Whether this glyph is an emoji
319    pub is_emoji: bool,
320}
321
322/// A line of text that has been wrapped to fit a given width.
323#[derive(Default, Debug)]
324pub struct WrappedLineLayout {
325    /// The line layout, pre-wrapping.
326    pub unwrapped_layout: Arc<LineLayout>,
327
328    /// The boundaries at which the line was wrapped.
329    pub wrap_boundaries: SmallVec<[WrapBoundary; 1]>,
330
331    /// The width of the line, if it was wrapped.
332    pub wrap_width: Option<Pixels>,
333}
334
335/// A boundary at which a line was wrapped.
336#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
337pub struct WrapBoundary {
338    /// The index in the run just before the line was wrapped.
339    pub run_ix: usize,
340    /// The index of the glyph just before the line was wrapped.
341    pub glyph_ix: usize,
342}
343
344impl WrappedLineLayout {
345    /// The length of the underlying text, in utf8 bytes.
346    #[allow(clippy::len_without_is_empty)]
347    pub fn len(&self) -> usize {
348        self.unwrapped_layout.len
349    }
350
351    /// The width of this line, in pixels, whether or not it was wrapped.
352    pub fn width(&self) -> Pixels {
353        self.wrap_width
354            .unwrap_or(Pixels::MAX)
355            .min(self.unwrapped_layout.width)
356    }
357
358    /// The size of the whole wrapped text, for the given line_height.
359    /// can span multiple lines if there are multiple wrap boundaries.
360    pub fn size(&self, line_height: Pixels) -> Size<Pixels> {
361        Size {
362            width: self.width(),
363            height: line_height * (self.wrap_boundaries.len() + 1),
364        }
365    }
366
367    /// The ascent of a line in this layout
368    pub fn ascent(&self) -> Pixels {
369        self.unwrapped_layout.ascent
370    }
371
372    /// The descent of a line in this layout
373    pub fn descent(&self) -> Pixels {
374        self.unwrapped_layout.descent
375    }
376
377    /// The wrap boundaries in this layout
378    pub fn wrap_boundaries(&self) -> &[WrapBoundary] {
379        &self.wrap_boundaries
380    }
381
382    /// The font size of this layout
383    pub fn font_size(&self) -> Pixels {
384        self.unwrapped_layout.font_size
385    }
386
387    /// The runs in this layout, sans wrapping
388    pub fn runs(&self) -> &[ShapedRun] {
389        &self.unwrapped_layout.runs
390    }
391
392    /// The index corresponding to a given position in this layout for the given line height.
393    ///
394    /// See also [`Self::closest_index_for_position`].
395    pub fn index_for_position(
396        &self,
397        position: Point<Pixels>,
398        line_height: Pixels,
399    ) -> Result<usize, usize> {
400        self._index_for_position(position, line_height, false)
401    }
402
403    /// The closest index to a given position in this layout for the given line height.
404    ///
405    /// Closest means the character boundary closest to the given position.
406    ///
407    /// See also [`LineLayout::closest_index_for_x`].
408    pub fn closest_index_for_position(
409        &self,
410        position: Point<Pixels>,
411        line_height: Pixels,
412    ) -> Result<usize, usize> {
413        self._index_for_position(position, line_height, true)
414    }
415
416    fn _index_for_position(
417        &self,
418        position: Point<Pixels>,
419        line_height: Pixels,
420        closest: bool,
421    ) -> Result<usize, usize> {
422        let wrapped_line_ix = (position.y / line_height) as usize;
423
424        let wrapped_line_start_index;
425        let wrapped_line_start_x;
426        if wrapped_line_ix > 0 {
427            let Some(line_start_boundary) = self.wrap_boundaries.get(wrapped_line_ix - 1) else {
428                return Err(0);
429            };
430            let run = &self.unwrapped_layout.runs[line_start_boundary.run_ix];
431            let glyph = &run.glyphs[line_start_boundary.glyph_ix];
432            wrapped_line_start_index = glyph.index;
433            wrapped_line_start_x = glyph.position.x;
434        } else {
435            wrapped_line_start_index = 0;
436            wrapped_line_start_x = Pixels::ZERO;
437        };
438
439        let wrapped_line_end_index;
440        let wrapped_line_end_x;
441        if wrapped_line_ix < self.wrap_boundaries.len() {
442            let next_wrap_boundary_ix = wrapped_line_ix;
443            let next_wrap_boundary = self.wrap_boundaries[next_wrap_boundary_ix];
444            let run = &self.unwrapped_layout.runs[next_wrap_boundary.run_ix];
445            let glyph = &run.glyphs[next_wrap_boundary.glyph_ix];
446            wrapped_line_end_index = glyph.index;
447            wrapped_line_end_x = glyph.position.x;
448        } else {
449            wrapped_line_end_index = self.unwrapped_layout.len;
450            wrapped_line_end_x = self.unwrapped_layout.width;
451        };
452
453        let mut position_in_unwrapped_line = position;
454        position_in_unwrapped_line.x += wrapped_line_start_x;
455        if position_in_unwrapped_line.x < wrapped_line_start_x {
456            Err(wrapped_line_start_index)
457        } else if position_in_unwrapped_line.x >= wrapped_line_end_x {
458            Err(wrapped_line_end_index)
459        } else if closest {
460            Ok(self
461                .unwrapped_layout
462                .closest_index_for_x(position_in_unwrapped_line.x))
463        } else {
464            Ok(self
465                .unwrapped_layout
466                .index_for_x(position_in_unwrapped_line.x)
467                .unwrap())
468        }
469    }
470
471    /// Returns the pixel position for the given byte index.
472    pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
473        let mut line_start_ix = 0;
474        let line_end_indices = self
475            .wrap_boundaries
476            .iter()
477            .map(|wrap_boundary| {
478                let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
479                let glyph = &run.glyphs[wrap_boundary.glyph_ix];
480                glyph.index
481            })
482            .chain([self.len()])
483            .enumerate();
484        for (ix, line_end_ix) in line_end_indices {
485            let line_y = ix as f32 * line_height;
486            if index < line_start_ix {
487                break;
488            } else if index > line_end_ix {
489                line_start_ix = line_end_ix;
490                continue;
491            } else {
492                let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
493                let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
494                return Some(point(x, line_y));
495            }
496        }
497
498        None
499    }
500}
501
502/// A saved position in a line-layout cache, used to reuse or truncate the
503/// layouts produced during a frame.
504#[derive(Clone, Default)]
505pub struct LineLayoutIndex {
506    /// The font generation the retained layouts were shaped against.
507    pub font_generation: usize,
508    /// The number of shaped lines retained at this position.
509    pub lines_index: usize,
510    /// The number of wrapped lines retained at this position.
511    pub wrapped_lines_index: usize,
512    /// The number of content-hashed shaped lines retained at this position.
513    pub lines_by_hash_index: usize,
514    /// The number of content-hashed wrapped lines retained at this position.
515    pub wrapped_lines_by_hash_index: usize,
516}
517
518impl LineLayout {
519    /// The index for the character at the given x coordinate
520    pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
521        if x >= self.width {
522            None
523        } else {
524            for run in self.runs.iter().rev() {
525                for glyph in run.glyphs.iter().rev() {
526                    if glyph.position.x <= x {
527                        return Some(glyph.index);
528                    }
529                }
530            }
531            Some(0)
532        }
533    }
534
535    /// closest_index_for_x returns the character boundary closest to the given x coordinate
536    /// (e.g. to handle aligning up/down arrow keys)
537    pub fn closest_index_for_x(&self, x: Pixels) -> usize {
538        let mut prev_index = 0;
539        let mut prev_x = px(0.);
540
541        for run in self.runs.iter() {
542            for glyph in run.glyphs.iter() {
543                if glyph.position.x >= x {
544                    if glyph.position.x - x < x - prev_x {
545                        return glyph.index;
546                    } else {
547                        return prev_index;
548                    }
549                }
550                prev_index = glyph.index;
551                prev_x = glyph.position.x;
552            }
553        }
554
555        if self.len == 1 {
556            if x > self.width / 2. {
557                return 1;
558            } else {
559                return 0;
560            }
561        }
562
563        self.len
564    }
565
566    /// The x position of the character at the given index
567    pub fn x_for_index(&self, index: usize) -> Pixels {
568        for run in &self.runs {
569            for glyph in &run.glyphs {
570                if glyph.index >= index {
571                    return glyph.position.x;
572                }
573            }
574        }
575        self.width
576    }
577
578    /// The corresponding Font at the given index
579    pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
580        for run in &self.runs {
581            for glyph in &run.glyphs {
582                if glyph.index >= index {
583                    return Some(run.font_id);
584                }
585            }
586        }
587        None
588    }
589
590    /// Split this layout at a byte index, returning `(prefix, suffix)`.
591    ///
592    /// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions.
593    ///   Its width equals the x-advance up to the split point.
594    /// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions
595    ///   shifted left so the first glyph starts at x=0, and byte indices rebased to 0.
596    /// - `font_size`, `ascent`, and `descent` are copied to both halves.
597    pub fn split_at(&self, byte_index: usize) -> (LineLayout, LineLayout) {
598        let x_offset = self.x_for_index(byte_index);
599
600        // Partition glyph runs. A single run may contribute glyphs to both halves.
601        let mut left_runs = Vec::new();
602        let mut right_runs = Vec::new();
603
604        for run in &self.runs {
605            let split_pos = run.glyphs.partition_point(|g| g.index < byte_index);
606
607            if split_pos > 0 {
608                left_runs.push(ShapedRun {
609                    font_id: run.font_id,
610                    glyphs: run.glyphs[..split_pos].to_vec(),
611                });
612            }
613
614            if split_pos < run.glyphs.len() {
615                let right_glyphs = run.glyphs[split_pos..]
616                    .iter()
617                    .map(|g| ShapedGlyph {
618                        id: g.id,
619                        position: point(g.position.x - x_offset, g.position.y),
620                        index: g.index - byte_index,
621                        is_emoji: g.is_emoji,
622                    })
623                    .collect();
624                right_runs.push(ShapedRun {
625                    font_id: run.font_id,
626                    glyphs: right_glyphs,
627                });
628            }
629        }
630
631        let left = LineLayout {
632            font_size: self.font_size,
633            width: x_offset,
634            ascent: self.ascent,
635            descent: self.descent,
636            runs: left_runs,
637            len: byte_index,
638        };
639
640        let right = LineLayout {
641            font_size: self.font_size,
642            width: self.width - x_offset,
643            ascent: self.ascent,
644            descent: self.descent,
645            runs: right_runs,
646            len: self.len - byte_index,
647        };
648
649        (left, right)
650    }
651}
652
653#[expect(missing_docs)]
654pub trait PlatformTextSystem: Send + Sync {
655    fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
656    /// Installs a nonblocking sink for unresolved grapheme clusters.
657    fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
658    /// Get all available font names.
659    fn all_font_names(&self) -> Vec<String>;
660    /// Get the font ID for a font descriptor.
661    fn font_id(&self, descriptor: &Font) -> Result<FontId>;
662    /// Prewarm any system font caches needed to shape text.
663    fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
664    /// Get metrics for a font.
665    fn font_metrics(&self, font_id: FontId) -> FontMetrics;
666    /// Get typographic bounds for a glyph.
667    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
668    /// Get the advance width for a glyph.
669    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
670    /// Get the glyph ID for a character.
671    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
672    /// Get raster bounds for a glyph.
673    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
674    /// Rasterize a glyph.
675    fn rasterize_glyph(
676        &self,
677        params: &RenderGlyphParams,
678        raster_bounds: Bounds<DevicePixels>,
679    ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
680    /// Layout a line of text with the given font runs.
681    fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
682    /// Returns the recommended text rendering mode for the given font and size.
683    fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
684    -> TextRenderingMode;
685    /// Returns the dilation level to use for a glyph painted in the given color.
686    fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
687        0
688    }
689}
690
691/// The spacing behavior required of a fallback font.
692#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
693pub enum FallbackFontClass {
694    /// A proportionally spaced fallback font.
695    Proportional,
696    /// A fixed-width fallback font.
697    Monospace,
698}
699
700/// A grapheme cluster that could not be represented by any available font.
701#[derive(Clone, Debug, Eq, Hash, PartialEq)]
702pub struct MissingGlyph {
703    grapheme: SharedString,
704    font_class: FallbackFontClass,
705}
706
707impl MissingGlyph {
708    /// Creates a missing-glyph report.
709    pub fn new(grapheme: SharedString, font_class: FallbackFontClass) -> Self {
710        Self {
711            grapheme,
712            font_class,
713        }
714    }
715
716    /// Returns the unresolved grapheme cluster.
717    pub fn grapheme(&self) -> &str {
718        &self.grapheme
719    }
720
721    /// Returns the spacing behavior required of a fallback font.
722    pub fn font_class(&self) -> FallbackFontClass {
723        self.font_class
724    }
725}
726
727/// Accepts missing glyphs detected by a platform text system.
728pub trait MissingGlyphSink: Send + Sync {
729    /// Reports grapheme clusters that exhausted font fallback.
730    fn report(&self, missing_glyphs: Vec<MissingGlyph>);
731}
732
733#[expect(missing_docs)]
734pub struct NoopTextSystem;
735
736#[expect(missing_docs)]
737impl NoopTextSystem {
738    #[allow(dead_code)]
739    pub fn new() -> Self {
740        Self
741    }
742}
743
744impl PlatformTextSystem for NoopTextSystem {
745    fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
746        Ok(())
747    }
748
749    fn all_font_names(&self) -> Vec<String> {
750        Vec::new()
751    }
752
753    fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
754        Ok(FontId(1))
755    }
756
757    fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
758        FontMetrics {
759            units_per_em: 1000,
760            ascent: 1025.0,
761            descent: -275.0,
762            line_gap: 0.0,
763            underline_position: -95.0,
764            underline_thickness: 60.0,
765            cap_height: 698.0,
766            x_height: 516.0,
767            bounding_box: Bounds {
768                origin: Point {
769                    x: -260.0,
770                    y: -245.0,
771                },
772                size: Size {
773                    width: 1501.0,
774                    height: 1364.0,
775                },
776            },
777        }
778    }
779
780    fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
781        Ok(Bounds {
782            origin: Point { x: 54.0, y: 0.0 },
783            size: size(392.0, 528.0),
784        })
785    }
786
787    fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
788        Ok(size(600.0 * glyph_id.0 as f32, 0.0))
789    }
790
791    fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
792        Some(GlyphId(ch.len_utf16() as u32))
793    }
794
795    fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
796        Ok(Default::default())
797    }
798
799    fn rasterize_glyph(
800        &self,
801        _params: &RenderGlyphParams,
802        raster_bounds: Bounds<DevicePixels>,
803    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
804        Ok((raster_bounds.size, Vec::new()))
805    }
806
807    fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
808        let mut position = px(0.);
809        let metrics = self.font_metrics(FontId(0));
810        let em_width = font_size
811            * self
812                .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
813                .unwrap()
814                .width
815            / metrics.units_per_em as f32;
816        let mut glyphs = Vec::new();
817        for (ix, c) in text.char_indices() {
818            if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
819                glyphs.push(ShapedGlyph {
820                    id: glyph,
821                    position: point(position, px(0.)),
822                    index: ix,
823                    is_emoji: glyph.0 == 2,
824                });
825                if glyph.0 == 2 {
826                    position += em_width * 2.0;
827                } else {
828                    position += em_width;
829                }
830            } else {
831                position += em_width
832            }
833        }
834        let mut runs = Vec::default();
835        if !glyphs.is_empty() {
836            runs.push(ShapedRun {
837                font_id: FontId(0),
838                glyphs,
839            });
840        } else {
841            position = px(0.);
842        }
843
844        LineLayout {
845            font_size,
846            width: position,
847            ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
848            descent: font_size * (metrics.descent / metrics.units_per_em as f32),
849            runs,
850            len: text.len(),
851        }
852    }
853
854    fn recommended_rendering_mode(
855        &self,
856        _font_id: FontId,
857        _font_size: Pixels,
858    ) -> TextRenderingMode {
859        TextRenderingMode::Grayscale
860    }
861}
862
863/// Maps well-known virtual font names to their concrete equivalents.
864#[allow(unused)]
865pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
866    // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
867    // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
868    // and so retained here for backward compatibility.
869    match name {
870        ".SystemUIFont" => system,
871        ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
872        ".ZedMono" | "Zed Plex Mono" => "Lilex",
873        _ => name,
874    }
875}
876
877/// Like [`font_name_with_fallbacks`] but accepts and returns [`SharedString`] references.
878#[allow(unused)]
879pub fn font_name_with_fallbacks_shared<'a>(
880    name: &'a SharedString,
881    system: &'a SharedString,
882) -> &'a SharedString {
883    // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
884    // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
885    // and so retained here for backward compatibility.
886    match name.as_str() {
887        ".SystemUIFont" => system,
888        ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") },
889        ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") },
890        _ => name,
891    }
892}
893
894/// A styled run of text.
895#[derive(Clone, Debug, PartialEq, Eq, Default)]
896pub struct TextRun {
897    /// A number of utf8 bytes
898    pub len: usize,
899    /// The font to use for this run.
900    pub font: Font,
901    /// The color
902    pub color: Hsla,
903    /// The background color (if any)
904    pub background_color: Option<Hsla>,
905    /// The underline style (if any)
906    pub underline: Option<UnderlineStyle>,
907    /// The strikethrough style (if any)
908    pub strikethrough: Option<StrikethroughStyle>,
909}