Skip to main content

gpui/
text_system.rs

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