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