Skip to main content

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