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            // The shaper can place a trailing zero-width wrap boundary glyph slightly past
465            // the line's width, so the row can extend past where `index_for_x` has glyphs.
466            self.unwrapped_layout
467                .index_for_x(position_in_unwrapped_line.x)
468                .ok_or(wrapped_line_end_index)
469        }
470    }
471
472    /// Returns the pixel position for the given byte index.
473    pub fn position_for_index(&self, index: usize, line_height: Pixels) -> Option<Point<Pixels>> {
474        let mut line_start_ix = 0;
475        let line_end_indices = self
476            .wrap_boundaries
477            .iter()
478            .map(|wrap_boundary| {
479                let run = &self.unwrapped_layout.runs[wrap_boundary.run_ix];
480                let glyph = &run.glyphs[wrap_boundary.glyph_ix];
481                glyph.index
482            })
483            .chain([self.len()])
484            .enumerate();
485        for (ix, line_end_ix) in line_end_indices {
486            let line_y = ix as f32 * line_height;
487            if index < line_start_ix {
488                break;
489            } else if index > line_end_ix {
490                line_start_ix = line_end_ix;
491                continue;
492            } else {
493                let line_start_x = self.unwrapped_layout.x_for_index(line_start_ix);
494                let x = self.unwrapped_layout.x_for_index(index) - line_start_x;
495                return Some(point(x, line_y));
496            }
497        }
498
499        None
500    }
501}
502
503/// A saved position in a line-layout cache, used to reuse or truncate the
504/// layouts produced during a frame.
505#[derive(Clone, Default)]
506pub struct LineLayoutIndex {
507    /// The font generation the retained layouts were shaped against.
508    pub font_generation: usize,
509    /// The number of shaped lines retained at this position.
510    pub lines_index: usize,
511    /// The number of wrapped lines retained at this position.
512    pub wrapped_lines_index: usize,
513    /// The number of content-hashed shaped lines retained at this position.
514    pub lines_by_hash_index: usize,
515    /// The number of content-hashed wrapped lines retained at this position.
516    pub wrapped_lines_by_hash_index: usize,
517}
518
519impl LineLayout {
520    /// The index for the character at the given x coordinate
521    pub fn index_for_x(&self, x: Pixels) -> Option<usize> {
522        if x >= self.width {
523            None
524        } else {
525            for run in self.runs.iter().rev() {
526                for glyph in run.glyphs.iter().rev() {
527                    if glyph.position.x <= x {
528                        return Some(glyph.index);
529                    }
530                }
531            }
532            Some(0)
533        }
534    }
535
536    /// closest_index_for_x returns the character boundary closest to the given x coordinate
537    /// (e.g. to handle aligning up/down arrow keys)
538    pub fn closest_index_for_x(&self, x: Pixels) -> usize {
539        let mut prev_index = 0;
540        let mut prev_x = px(0.);
541
542        for run in self.runs.iter() {
543            for glyph in run.glyphs.iter() {
544                if glyph.position.x >= x {
545                    if glyph.position.x - x < x - prev_x {
546                        return glyph.index;
547                    } else {
548                        return prev_index;
549                    }
550                }
551                prev_index = glyph.index;
552                prev_x = glyph.position.x;
553            }
554        }
555
556        if self.len == 1 {
557            if x > self.width / 2. {
558                return 1;
559            } else {
560                return 0;
561            }
562        }
563
564        self.len
565    }
566
567    /// The x position of the character at the given index
568    pub fn x_for_index(&self, index: usize) -> Pixels {
569        for run in &self.runs {
570            for glyph in &run.glyphs {
571                if glyph.index >= index {
572                    return glyph.position.x;
573                }
574            }
575        }
576        self.width
577    }
578
579    /// The corresponding Font at the given index
580    pub fn font_id_for_index(&self, index: usize) -> Option<FontId> {
581        for run in &self.runs {
582            for glyph in &run.glyphs {
583                if glyph.index >= index {
584                    return Some(run.font_id);
585                }
586            }
587        }
588        None
589    }
590
591    /// Split this layout at a byte index, returning `(prefix, suffix)`.
592    ///
593    /// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions.
594    ///   Its width equals the x-advance up to the split point.
595    /// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions
596    ///   shifted left so the first glyph starts at x=0, and byte indices rebased to 0.
597    /// - `font_size`, `ascent`, and `descent` are copied to both halves.
598    pub fn split_at(&self, byte_index: usize) -> (LineLayout, LineLayout) {
599        let x_offset = self.x_for_index(byte_index);
600
601        // Partition glyph runs. A single run may contribute glyphs to both halves.
602        let mut left_runs = Vec::new();
603        let mut right_runs = Vec::new();
604
605        for run in &self.runs {
606            let split_pos = run.glyphs.partition_point(|g| g.index < byte_index);
607
608            if split_pos > 0 {
609                left_runs.push(ShapedRun {
610                    font_id: run.font_id,
611                    glyphs: run.glyphs[..split_pos].to_vec(),
612                });
613            }
614
615            if split_pos < run.glyphs.len() {
616                let right_glyphs = run.glyphs[split_pos..]
617                    .iter()
618                    .map(|g| ShapedGlyph {
619                        id: g.id,
620                        position: point(g.position.x - x_offset, g.position.y),
621                        index: g.index - byte_index,
622                        is_emoji: g.is_emoji,
623                    })
624                    .collect();
625                right_runs.push(ShapedRun {
626                    font_id: run.font_id,
627                    glyphs: right_glyphs,
628                });
629            }
630        }
631
632        let left = LineLayout {
633            font_size: self.font_size,
634            width: x_offset,
635            ascent: self.ascent,
636            descent: self.descent,
637            runs: left_runs,
638            len: byte_index,
639        };
640
641        let right = LineLayout {
642            font_size: self.font_size,
643            width: self.width - x_offset,
644            ascent: self.ascent,
645            descent: self.descent,
646            runs: right_runs,
647            len: self.len - byte_index,
648        };
649
650        (left, right)
651    }
652}
653
654#[expect(missing_docs)]
655pub trait PlatformTextSystem: Send + Sync {
656    fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()>;
657    /// Installs a nonblocking sink for unresolved grapheme clusters.
658    fn set_missing_glyph_sink(&self, _sink: Option<Arc<dyn MissingGlyphSink>>) {}
659    /// Get all available font names.
660    fn all_font_names(&self) -> Vec<String>;
661    /// Get the font ID for a font descriptor.
662    fn font_id(&self, descriptor: &Font) -> Result<FontId>;
663    /// Prewarm any system font caches needed to shape text.
664    fn prewarm_fonts(&self, _font_ids: &[FontId]) {}
665    /// Get metrics for a font.
666    fn font_metrics(&self, font_id: FontId) -> FontMetrics;
667    /// Get typographic bounds for a glyph.
668    fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>>;
669    /// Get the advance width for a glyph.
670    fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>>;
671    /// Get the glyph ID for a character.
672    fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId>;
673    /// Get raster bounds for a glyph.
674    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>>;
675    /// Rasterize a glyph.
676    fn rasterize_glyph(
677        &self,
678        params: &RenderGlyphParams,
679        raster_bounds: Bounds<DevicePixels>,
680    ) -> Result<(Size<DevicePixels>, Vec<u8>)>;
681    /// Layout a line of text with the given font runs.
682    fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
683    /// Returns the recommended text rendering mode for the given font and size.
684    fn recommended_rendering_mode(&self, _font_id: FontId, _font_size: Pixels)
685    -> TextRenderingMode;
686    /// Returns the dilation level to use for a glyph painted in the given color.
687    fn glyph_dilation_for_color(&self, _color: Hsla) -> u8 {
688        0
689    }
690}
691
692/// The spacing behavior required of a fallback font.
693#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
694pub enum FallbackFontClass {
695    /// A proportionally spaced fallback font.
696    Proportional,
697    /// A fixed-width fallback font.
698    Monospace,
699}
700
701/// A grapheme cluster that could not be represented by any available font.
702#[derive(Clone, Debug, Eq, Hash, PartialEq)]
703pub struct MissingGlyph {
704    grapheme: SharedString,
705    font_class: FallbackFontClass,
706}
707
708impl MissingGlyph {
709    /// Creates a missing-glyph report.
710    pub fn new(grapheme: SharedString, font_class: FallbackFontClass) -> Self {
711        Self {
712            grapheme,
713            font_class,
714        }
715    }
716
717    /// Returns the unresolved grapheme cluster.
718    pub fn grapheme(&self) -> &str {
719        &self.grapheme
720    }
721
722    /// Returns the spacing behavior required of a fallback font.
723    pub fn font_class(&self) -> FallbackFontClass {
724        self.font_class
725    }
726}
727
728/// Accepts missing glyphs detected by a platform text system.
729pub trait MissingGlyphSink: Send + Sync {
730    /// Reports grapheme clusters that exhausted font fallback.
731    fn report(&self, missing_glyphs: Vec<MissingGlyph>);
732}
733
734#[expect(missing_docs)]
735pub struct NoopTextSystem;
736
737#[expect(missing_docs)]
738impl NoopTextSystem {
739    #[allow(dead_code)]
740    pub fn new() -> Self {
741        Self
742    }
743}
744
745impl PlatformTextSystem for NoopTextSystem {
746    fn add_fonts(&self, _fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
747        Ok(())
748    }
749
750    fn all_font_names(&self) -> Vec<String> {
751        Vec::new()
752    }
753
754    fn font_id(&self, _descriptor: &Font) -> Result<FontId> {
755        Ok(FontId(1))
756    }
757
758    fn font_metrics(&self, _font_id: FontId) -> FontMetrics {
759        FontMetrics {
760            units_per_em: 1000,
761            ascent: 1025.0,
762            descent: -275.0,
763            line_gap: 0.0,
764            underline_position: -95.0,
765            underline_thickness: 60.0,
766            cap_height: 698.0,
767            x_height: 516.0,
768            bounding_box: Bounds {
769                origin: Point {
770                    x: -260.0,
771                    y: -245.0,
772                },
773                size: Size {
774                    width: 1501.0,
775                    height: 1364.0,
776                },
777            },
778        }
779    }
780
781    fn typographic_bounds(&self, _font_id: FontId, _glyph_id: GlyphId) -> Result<Bounds<f32>> {
782        Ok(Bounds {
783            origin: Point { x: 54.0, y: 0.0 },
784            size: size(392.0, 528.0),
785        })
786    }
787
788    fn advance(&self, _font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
789        Ok(size(600.0 * glyph_id.0 as f32, 0.0))
790    }
791
792    fn glyph_for_char(&self, _font_id: FontId, ch: char) -> Option<GlyphId> {
793        Some(GlyphId(ch.len_utf16() as u32))
794    }
795
796    fn glyph_raster_bounds(&self, _params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
797        Ok(Default::default())
798    }
799
800    fn rasterize_glyph(
801        &self,
802        _params: &RenderGlyphParams,
803        raster_bounds: Bounds<DevicePixels>,
804    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
805        Ok((raster_bounds.size, Vec::new()))
806    }
807
808    fn layout_line(&self, text: &str, font_size: Pixels, _runs: &[FontRun]) -> LineLayout {
809        let mut position = px(0.);
810        let metrics = self.font_metrics(FontId(0));
811        let em_width = font_size
812            * self
813                .advance(FontId(0), self.glyph_for_char(FontId(0), 'm').unwrap())
814                .unwrap()
815                .width
816            / metrics.units_per_em as f32;
817        let mut glyphs = Vec::new();
818        for (ix, c) in text.char_indices() {
819            if let Some(glyph) = self.glyph_for_char(FontId(0), c) {
820                glyphs.push(ShapedGlyph {
821                    id: glyph,
822                    position: point(position, px(0.)),
823                    index: ix,
824                    is_emoji: glyph.0 == 2,
825                });
826                if glyph.0 == 2 {
827                    position += em_width * 2.0;
828                } else {
829                    position += em_width;
830                }
831            } else {
832                position += em_width
833            }
834        }
835        let mut runs = Vec::default();
836        if !glyphs.is_empty() {
837            runs.push(ShapedRun {
838                font_id: FontId(0),
839                glyphs,
840            });
841        } else {
842            position = px(0.);
843        }
844
845        LineLayout {
846            font_size,
847            width: position,
848            ascent: font_size * (metrics.ascent / metrics.units_per_em as f32),
849            descent: font_size * (metrics.descent / metrics.units_per_em as f32),
850            runs,
851            len: text.len(),
852        }
853    }
854
855    fn recommended_rendering_mode(
856        &self,
857        _font_id: FontId,
858        _font_size: Pixels,
859    ) -> TextRenderingMode {
860        TextRenderingMode::Grayscale
861    }
862}
863
864/// Maps well-known virtual font names to their concrete equivalents.
865#[allow(unused)]
866pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
867    // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
868    // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
869    // and so retained here for backward compatibility.
870    match name {
871        ".SystemUIFont" => system,
872        ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
873        ".ZedMono" | "Zed Plex Mono" => "Lilex",
874        _ => name,
875    }
876}
877
878/// Like [`font_name_with_fallbacks`] but accepts and returns [`SharedString`] references.
879#[allow(unused)]
880pub fn font_name_with_fallbacks_shared<'a>(
881    name: &'a SharedString,
882    system: &'a SharedString,
883) -> &'a SharedString {
884    // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
885    // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
886    // and so retained here for backward compatibility.
887    match name.as_str() {
888        ".SystemUIFont" => system,
889        ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") },
890        ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") },
891        _ => name,
892    }
893}
894
895/// A styled run of text.
896#[derive(Clone, Debug, PartialEq, Eq, Default)]
897pub struct TextRun {
898    /// A number of utf8 bytes
899    pub len: usize,
900    /// The font to use for this run.
901    pub font: Font,
902    /// The color
903    pub color: Hsla,
904    /// The background color (if any)
905    pub background_color: Option<Hsla>,
906    /// The underline style (if any)
907    pub underline: Option<UnderlineStyle>,
908    /// The strikethrough style (if any)
909    pub strikethrough: Option<StrikethroughStyle>,
910}