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<CharWidthCache>>>,
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 char_widths = lock
620            .entry(FontIdWithSize { font_id, font_size })
621            .or_default()
622            .pop()
623            .unwrap_or_default();
624        let wrapper = LineWrapper::new(
625            font_id,
626            font_size,
627            self.platform_text_system.clone(),
628            char_widths,
629        );
630
631        LineWrapperHandle {
632            wrapper: Some(wrapper),
633            text_system: self.clone(),
634        }
635    }
636
637    /// Get the rasterized size and location of a specific, rendered glyph.
638    pub(crate) fn raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
639        let raster_bounds = self.raster_bounds.upgradable_read();
640        if let Some(bounds) = raster_bounds.get(params) {
641            Ok(*bounds)
642        } else {
643            let mut raster_bounds = RwLockUpgradableReadGuard::upgrade(raster_bounds);
644            // If a limit is set and we've exceeded it, clear the cache to
645            // reclaim memory. This is a simple generational approach since
646            // glyph_raster_bounds is cheap to recompute.
647            if let Some(max) = self.raster_bounds_max_entries {
648                if raster_bounds.len() >= max {
649                    raster_bounds.clear();
650                    self.platform_text_system.clear_glyph_raster_cache();
651                }
652            }
653            let bounds = self.platform_text_system.glyph_raster_bounds(params)?;
654            raster_bounds.insert(params.clone(), bounds);
655            Ok(bounds)
656        }
657    }
658
659    pub(crate) fn rasterize_glyph(
660        &self,
661        params: &RenderGlyphParams,
662    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
663        let raster_bounds = self.raster_bounds(params)?;
664        self.platform_text_system
665            .rasterize_glyph(params, raster_bounds)
666    }
667}
668
669/// The GPUI text layout subsystem.
670#[derive(Deref)]
671pub struct WindowTextSystem {
672    line_layout_cache: LineLayoutCache,
673    #[deref]
674    text_system: Arc<TextSystem>,
675}
676
677impl WindowTextSystem {
678    #[doc(hidden)]
679    pub fn new(text_system: Arc<TextSystem>) -> Self {
680        Self {
681            line_layout_cache: LineLayoutCache::new(
682                text_system.platform_text_system.clone(),
683                text_system.global_line_layout_cache.clone(),
684                text_system.font_generation.clone(),
685            ),
686            text_system,
687        }
688    }
689
690    pub(crate) fn layout_index(&self) -> LineLayoutIndex {
691        self.line_layout_cache.layout_index()
692    }
693
694    pub(crate) fn reuse_layouts(&self, index: Range<LineLayoutIndex>) {
695        self.line_layout_cache.reuse_layouts(index)
696    }
697
698    pub(crate) fn truncate_layouts(&self, index: LineLayoutIndex) {
699        self.line_layout_cache.truncate_layouts(index)
700    }
701
702    /// Shape the given line, at the given font_size, for painting to the screen.
703    /// Subsets of the line can be styled independently with the `runs` parameter.
704    ///
705    /// Note that this method can only shape a single line of text. It will panic
706    /// if the text contains newlines. If you need to shape multiple lines of text,
707    /// use [`Self::shape_text`] instead.
708    pub fn shape_line(
709        &self,
710        text: SharedString,
711        font_size: Pixels,
712        runs: &[TextRun],
713        force_width: Option<Pixels>,
714    ) -> ShapedLine {
715        self.shape_line_with_spacing(text, font_size, runs, force_width, None)
716    }
717
718    /// Shape a line of text with optional letter spacing applied to glyph positions.
719    pub fn shape_line_with_spacing(
720        &self,
721        text: SharedString,
722        font_size: Pixels,
723        runs: &[TextRun],
724        force_width: Option<Pixels>,
725        letter_spacing: Option<Pixels>,
726    ) -> ShapedLine {
727        debug_assert!(
728            text.find('\n').is_none(),
729            "text argument should not contain newlines"
730        );
731
732        let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
733        for run in runs {
734            push_decoration_run(&mut decoration_runs, run, run.len as u32);
735        }
736
737        let layout =
738            self.layout_line_with_spacing(&text, font_size, runs, force_width, letter_spacing);
739
740        ShapedLine {
741            layout,
742            text,
743            decoration_runs,
744        }
745    }
746
747    /// Shape a multi line string of text, at the given font_size, for painting to the screen.
748    /// Subsets of the text can be styled independently with the `runs` parameter.
749    /// If `wrap_width` is provided, the line breaks will be adjusted to fit within the given width.
750    pub fn shape_text(
751        &self,
752        text: SharedString,
753        font_size: Pixels,
754        runs: &[TextRun],
755        wrap_width: Option<Pixels>,
756        line_clamp: Option<usize>,
757    ) -> Result<SmallVec<[WrappedLine; 1]>> {
758        let mut runs = runs.iter().filter(|run| run.len > 0).cloned().peekable();
759        let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
760
761        let mut lines = SmallVec::new();
762        let mut line_start = 0;
763        let mut max_wrap_lines = line_clamp.unwrap_or(usize::MAX);
764        let mut wrapped_lines = 0;
765
766        let mut process_line = |line_text: SharedString| {
767            font_runs.clear();
768            let line_end = line_start + line_text.len();
769
770            let mut last_font: Option<FontId> = None;
771            let mut decoration_runs = SmallVec::<[DecorationRun; 32]>::new();
772            let mut run_start = line_start;
773            while run_start < line_end {
774                let Some(run) = runs.peek_mut() else {
775                    break;
776                };
777
778                let run_len_within_line = cmp::min(line_end, run_start + run.len) - run_start;
779
780                let decoration_changed =
781                    push_decoration_run(&mut decoration_runs, run, run_len_within_line as u32);
782
783                if let Some(font_run) = font_runs.last_mut()
784                    && Some(font_run.font_id) == last_font
785                    && !decoration_changed
786                {
787                    font_run.len += run_len_within_line;
788                } else {
789                    let font_id = self.resolve_font(&run.font);
790                    last_font = Some(font_id);
791                    font_runs.push(FontRun {
792                        len: run_len_within_line,
793                        font_id,
794                    });
795                }
796
797                if run_len_within_line == run.len {
798                    runs.next();
799                } else {
800                    // Preserve the remainder of the run for the next line
801                    run.len -= run_len_within_line;
802                }
803                run_start += run_len_within_line;
804            }
805
806            let layout = self.line_layout_cache.layout_wrapped_line(
807                &line_text,
808                font_size,
809                &font_runs,
810                wrap_width,
811                Some(max_wrap_lines - wrapped_lines),
812            );
813            wrapped_lines += layout.wrap_boundaries.len();
814
815            lines.push(WrappedLine {
816                layout,
817                decoration_runs,
818                text: line_text,
819            });
820
821            // Skip `\n` character.
822            line_start = line_end + 1;
823            if let Some(run) = runs.peek_mut() {
824                run.len -= 1;
825                if run.len == 0 {
826                    runs.next();
827                }
828            }
829        };
830
831        let mut split_lines = text.split('\n');
832        let mut processed = false;
833
834        if let Some(first_line) = split_lines.next()
835            && let Some(second_line) = split_lines.next()
836        {
837            processed = true;
838            process_line(first_line.to_string().into());
839            process_line(second_line.to_string().into());
840            for line_text in split_lines {
841                process_line(line_text.to_string().into());
842            }
843        }
844
845        if !processed {
846            process_line(text);
847        }
848
849        self.font_runs_pool.lock().push(font_runs);
850
851        Ok(lines)
852    }
853
854    pub(crate) fn finish_frame(&self) {
855        self.line_layout_cache.finish_frame()
856    }
857
858    /// Layout the given line of text, at the given font_size.
859    /// Subsets of the line can be styled independently with the `runs` parameter.
860    /// Generally, you should prefer to use [`Self::shape_line`] instead, which
861    /// can be painted directly.
862    pub fn layout_line(
863        &self,
864        text: &str,
865        font_size: Pixels,
866        runs: &[TextRun],
867        force_width: Option<Pixels>,
868    ) -> Arc<LineLayout> {
869        self.layout_line_with_spacing(text, font_size, runs, force_width, None)
870    }
871
872    /// Layout a line of text with optional letter spacing applied to glyph positions.
873    pub fn layout_line_with_spacing(
874        &self,
875        text: &str,
876        font_size: Pixels,
877        runs: &[TextRun],
878        force_width: Option<Pixels>,
879        letter_spacing: Option<Pixels>,
880    ) -> Arc<LineLayout> {
881        let mut last_run = None::<&TextRun>;
882        let mut last_font: Option<FontId> = None;
883        let mut font_runs = self.font_runs_pool.lock().pop().unwrap_or_default();
884        font_runs.clear();
885
886        for run in runs.iter() {
887            let decoration_changed = match last_run {
888                Some(last) if decorations_equal(last, run) => false,
889                _ => {
890                    last_run = Some(run);
891                    true
892                }
893            };
894
895            if let Some(font_run) = font_runs.last_mut()
896                && Some(font_run.font_id) == last_font
897                && !decoration_changed
898            {
899                font_run.len += run.len;
900            } else {
901                let font_id = self.resolve_font(&run.font);
902                last_font = Some(font_id);
903                font_runs.push(FontRun {
904                    len: run.len,
905                    font_id,
906                });
907            }
908        }
909
910        let layout = self.line_layout_cache.layout_line_with_spacing(
911            &SharedString::new(text),
912            font_size,
913            &font_runs,
914            force_width,
915            letter_spacing,
916        );
917
918        self.font_runs_pool.lock().push(font_runs);
919
920        layout
921    }
922}
923
924#[derive(Hash, Eq, PartialEq)]
925struct FontIdWithSize {
926    font_id: FontId,
927    font_size: Pixels,
928}
929
930/// A handle into the text system, which can be used to compute the wrapped layout of text
931pub struct LineWrapperHandle {
932    wrapper: Option<LineWrapper>,
933    text_system: Arc<TextSystem>,
934}
935
936impl Drop for LineWrapperHandle {
937    fn drop(&mut self) {
938        let mut state = self.text_system.wrapper_pool.lock();
939        let wrapper = self.wrapper.take().unwrap();
940        state
941            .get_mut(&FontIdWithSize {
942                font_id: wrapper.font_id,
943                font_size: wrapper.font_size,
944            })
945            .unwrap()
946            .push(wrapper.into_char_widths());
947    }
948}
949
950impl Deref for LineWrapperHandle {
951    type Target = LineWrapper;
952
953    fn deref(&self) -> &Self::Target {
954        self.wrapper.as_ref().unwrap()
955    }
956}
957
958impl DerefMut for LineWrapperHandle {
959    fn deref_mut(&mut self) -> &mut Self::Target {
960        self.wrapper.as_mut().unwrap()
961    }
962}
963
964/// The degree of blackness or stroke thickness of a font. This value ranges from 100.0 to 900.0,
965/// with 400.0 as normal.
966#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Serialize, Deserialize, Add, Sub, FromStr)]
967#[serde(transparent)]
968pub struct FontWeight(pub f32);
969
970impl Display for FontWeight {
971    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
972        write!(f, "{}", self.0)
973    }
974}
975
976impl From<f32> for FontWeight {
977    fn from(weight: f32) -> Self {
978        FontWeight(weight)
979    }
980}
981
982impl Default for FontWeight {
983    #[inline]
984    fn default() -> FontWeight {
985        FontWeight::NORMAL
986    }
987}
988
989impl Hash for FontWeight {
990    fn hash<H: Hasher>(&self, state: &mut H) {
991        state.write_u32(u32::from_be_bytes(self.0.to_be_bytes()));
992    }
993}
994
995impl Eq for FontWeight {}
996
997impl FontWeight {
998    /// Thin weight (100), the thinnest value.
999    pub const THIN: FontWeight = FontWeight(100.0);
1000    /// Extra light weight (200).
1001    pub const EXTRA_LIGHT: FontWeight = FontWeight(200.0);
1002    /// Light weight (300).
1003    pub const LIGHT: FontWeight = FontWeight(300.0);
1004    /// Normal (400).
1005    pub const NORMAL: FontWeight = FontWeight(400.0);
1006    /// Medium weight (500, higher than normal).
1007    pub const MEDIUM: FontWeight = FontWeight(500.0);
1008    /// Semibold weight (600).
1009    pub const SEMIBOLD: FontWeight = FontWeight(600.0);
1010    /// Bold weight (700).
1011    pub const BOLD: FontWeight = FontWeight(700.0);
1012    /// Extra-bold weight (800).
1013    pub const EXTRA_BOLD: FontWeight = FontWeight(800.0);
1014    /// Black weight (900), the thickest value.
1015    pub const BLACK: FontWeight = FontWeight(900.0);
1016
1017    /// All of the font weights, in order from thinnest to thickest.
1018    pub const ALL: [FontWeight; 9] = [
1019        Self::THIN,
1020        Self::EXTRA_LIGHT,
1021        Self::LIGHT,
1022        Self::NORMAL,
1023        Self::MEDIUM,
1024        Self::SEMIBOLD,
1025        Self::BOLD,
1026        Self::EXTRA_BOLD,
1027        Self::BLACK,
1028    ];
1029}
1030
1031impl schemars::JsonSchema for FontWeight {
1032    fn schema_name() -> std::borrow::Cow<'static, str> {
1033        "FontWeight".into()
1034    }
1035
1036    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
1037        use schemars::json_schema;
1038        json_schema!({
1039            "type": "number",
1040            "minimum": Self::THIN,
1041            "maximum": Self::BLACK,
1042            "default": Self::default(),
1043            "description": "Font weight value between 100 (thin) and 900 (black)"
1044        })
1045    }
1046}
1047
1048/// Allows italic or oblique faces to be selected.
1049#[derive(Clone, Copy, Eq, PartialEq, Debug, Hash, Default, Serialize, Deserialize, JsonSchema)]
1050pub enum FontStyle {
1051    /// A face that is neither italic not obliqued.
1052    #[default]
1053    Normal,
1054    /// A form that is generally cursive in nature.
1055    Italic,
1056    /// A typically-sloped version of the regular face.
1057    Oblique,
1058}
1059
1060impl Display for FontStyle {
1061    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
1062        Debug::fmt(self, f)
1063    }
1064}
1065
1066/// A styled run of text, for use in [`crate::TextLayout`].
1067#[derive(Clone, Debug, PartialEq, Eq)]
1068pub struct TextRun {
1069    /// A number of utf8 bytes
1070    pub len: usize,
1071    /// The font to use for this run.
1072    pub font: Font,
1073    /// The color
1074    pub color: Hsla,
1075    /// The background color (if any)
1076    pub background_color: Option<Hsla>,
1077    /// The underline style (if any)
1078    pub underline: Option<UnderlineStyle>,
1079    /// The strikethrough style (if any)
1080    pub strikethrough: Option<StrikethroughStyle>,
1081}
1082
1083/// Whether two adjacent [`TextRun`]s share every decoration. The condition must
1084/// stay in sync with [`push_decoration_run`]'s merge check: a decoration change
1085/// starts both a new [`DecorationRun`] and a new [`FontRun`].
1086fn decorations_equal(a: &TextRun, b: &TextRun) -> bool {
1087    a.color == b.color
1088        && a.underline == b.underline
1089        && a.strikethrough == b.strikethrough
1090        && a.background_color == b.background_color
1091}
1092
1093/// Appends `len` bytes of `run` to `decoration_runs`, merging into the previous
1094/// run when `run` shares every decoration with it. Returns whether a new run was
1095/// started, i.e. the decoration changed at this boundary.
1096fn push_decoration_run(
1097    decoration_runs: &mut SmallVec<[DecorationRun; 32]>,
1098    run: &TextRun,
1099    len: u32,
1100) -> bool {
1101    if let Some(last_run) = decoration_runs.last_mut()
1102        && last_run.color == run.color
1103        && last_run.underline == run.underline
1104        && last_run.strikethrough == run.strikethrough
1105        && last_run.background_color == run.background_color
1106    {
1107        last_run.len += len;
1108        false
1109    } else {
1110        decoration_runs.push(DecorationRun {
1111            len,
1112            color: run.color,
1113            background_color: run.background_color,
1114            underline: run.underline,
1115            strikethrough: run.strikethrough,
1116        });
1117        true
1118    }
1119}
1120
1121#[cfg(all(target_os = "macos", test))]
1122impl TextRun {
1123    fn with_len(&self, len: usize) -> Self {
1124        let mut this = self.clone();
1125        this.len = len;
1126        this
1127    }
1128}
1129
1130/// An identifier for a specific glyph, as returned by [`WindowTextSystem::layout_line`].
1131#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
1132#[repr(C)]
1133pub struct GlyphId(pub u32);
1134
1135#[derive(Clone, Debug, PartialEq)]
1136#[allow(missing_docs)]
1137pub struct RenderGlyphParams {
1138    pub font_id: FontId,
1139    pub glyph_id: GlyphId,
1140    pub font_size: Pixels,
1141    pub subpixel_variant: Point<u8>,
1142    pub scale_factor: f32,
1143    pub is_emoji: bool,
1144    pub dilation: u8,
1145}
1146
1147impl Eq for RenderGlyphParams {}
1148
1149impl Hash for RenderGlyphParams {
1150    fn hash<H: Hasher>(&self, state: &mut H) {
1151        self.font_id.0.hash(state);
1152        self.glyph_id.0.hash(state);
1153        self.font_size.0.to_bits().hash(state);
1154        self.subpixel_variant.hash(state);
1155        self.scale_factor.to_bits().hash(state);
1156        self.is_emoji.hash(state);
1157        self.dilation.hash(state);
1158    }
1159}
1160
1161/// The configuration details for identifying a specific font.
1162#[derive(Clone, Debug, Eq, PartialEq, Hash)]
1163pub struct Font {
1164    /// The font family name.
1165    ///
1166    /// The special name ".SystemUIFont" is used to identify the system UI font, which varies based on platform.
1167    pub family: SharedString,
1168
1169    /// The font features to use.
1170    pub features: FontFeatures,
1171
1172    /// The fallbacks fonts to use.
1173    pub fallbacks: Option<FontFallbacks>,
1174
1175    /// The font weight.
1176    pub weight: FontWeight,
1177
1178    /// The font style.
1179    pub style: FontStyle,
1180}
1181
1182/// Get a [`Font`] for a given name.
1183pub fn font(family: impl Into<SharedString>) -> Font {
1184    Font {
1185        family: family.into(),
1186        features: FontFeatures::default(),
1187        weight: FontWeight::default(),
1188        style: FontStyle::default(),
1189        fallbacks: None,
1190    }
1191}
1192
1193impl Font {
1194    /// Set this Font to be bold
1195    pub fn bold(mut self) -> Self {
1196        self.weight = FontWeight::BOLD;
1197        self
1198    }
1199
1200    /// Set this Font to be italic
1201    pub fn italic(mut self) -> Self {
1202        self.style = FontStyle::Italic;
1203        self
1204    }
1205}
1206
1207/// A struct for storing font metrics.
1208/// It is used to define the measurements of a typeface.
1209#[derive(Clone, Copy, Debug)]
1210pub struct FontMetrics {
1211    /// The number of font units that make up the "em square",
1212    /// a scalable grid for determining the size of a typeface.
1213    pub units_per_em: u32,
1214
1215    /// The vertical distance from the baseline of the font to the top of the glyph covers.
1216    pub ascent: f32,
1217
1218    /// The vertical distance from the baseline of the font to the bottom of the glyph covers.
1219    pub descent: f32,
1220
1221    /// The recommended additional space to add between lines of type.
1222    pub line_gap: f32,
1223
1224    /// The suggested position of the underline.
1225    pub underline_position: f32,
1226
1227    /// The suggested thickness of the underline.
1228    pub underline_thickness: f32,
1229
1230    /// The height of a capital letter measured from the baseline of the font.
1231    pub cap_height: f32,
1232
1233    /// The height of a lowercase x.
1234    pub x_height: f32,
1235
1236    /// The outer limits of the area that the font covers.
1237    /// Corresponds to the xMin / xMax / yMin / yMax values in the OpenType `head` table
1238    pub bounding_box: Bounds<f32>,
1239}
1240
1241impl FontMetrics {
1242    /// Returns the vertical distance from the baseline of the font to the top of the glyph covers in pixels.
1243    pub fn ascent(&self, font_size: Pixels) -> Pixels {
1244        Pixels((self.ascent / self.units_per_em as f32) * font_size.0)
1245    }
1246
1247    /// Returns the vertical distance from the baseline of the font to the bottom of the glyph covers in pixels.
1248    pub fn descent(&self, font_size: Pixels) -> Pixels {
1249        Pixels((self.descent / self.units_per_em as f32) * font_size.0)
1250    }
1251
1252    /// Returns the recommended additional space to add between lines of type in pixels.
1253    pub fn line_gap(&self, font_size: Pixels) -> Pixels {
1254        Pixels((self.line_gap / self.units_per_em as f32) * font_size.0)
1255    }
1256
1257    /// Returns the suggested position of the underline in pixels.
1258    pub fn underline_position(&self, font_size: Pixels) -> Pixels {
1259        Pixels((self.underline_position / self.units_per_em as f32) * font_size.0)
1260    }
1261
1262    /// Returns the suggested thickness of the underline in pixels.
1263    pub fn underline_thickness(&self, font_size: Pixels) -> Pixels {
1264        Pixels((self.underline_thickness / self.units_per_em as f32) * font_size.0)
1265    }
1266
1267    /// Returns the height of a capital letter measured from the baseline of the font in pixels.
1268    pub fn cap_height(&self, font_size: Pixels) -> Pixels {
1269        Pixels((self.cap_height / self.units_per_em as f32) * font_size.0)
1270    }
1271
1272    /// Returns the height of a lowercase x in pixels.
1273    pub fn x_height(&self, font_size: Pixels) -> Pixels {
1274        Pixels((self.x_height / self.units_per_em as f32) * font_size.0)
1275    }
1276
1277    /// Returns the outer limits of the area that the font covers in pixels.
1278    pub fn bounding_box(&self, font_size: Pixels) -> Bounds<Pixels> {
1279        (self.bounding_box / self.units_per_em as f32 * font_size.0).map(px)
1280    }
1281}
1282
1283#[allow(unused)]
1284#[doc(hidden)]
1285pub fn font_name_with_fallbacks<'a>(name: &'a str, system: &'a str) -> &'a str {
1286    // Note: the "Zed Plex" fonts were deprecated as we are not allowed to use "Plex"
1287    // in a derived font name. They are essentially indistinguishable from IBM Plex/Lilex,
1288    // and so retained here for backward compatibility.
1289    match name {
1290        ".SystemUIFont" => system,
1291        ".ZedSans" | "Zed Plex Sans" => "IBM Plex Sans",
1292        ".ZedMono" | "Zed Plex Mono" => "Lilex",
1293        _ => name,
1294    }
1295}
1296
1297#[cfg(test)]
1298mod tests {
1299    use super::*;
1300    use crate::{AppResourceProfile, NoopTextSystem, point, size};
1301    use parking_lot::Mutex;
1302    use std::collections::HashSet;
1303    use std::sync::atomic::AtomicBool;
1304
1305    struct TrackingTextSystem {
1306        noop: NoopTextSystem,
1307        pending_glyph_images: Mutex<HashSet<RenderGlyphParams>>,
1308        missing_glyph_sink_installed: AtomicBool,
1309    }
1310
1311    impl TrackingTextSystem {
1312        fn new() -> Self {
1313            Self {
1314                noop: NoopTextSystem::new(),
1315                pending_glyph_images: Mutex::default(),
1316                missing_glyph_sink_installed: AtomicBool::new(false),
1317            }
1318        }
1319
1320        fn pending_glyph_images_len(&self) -> usize {
1321            self.pending_glyph_images.lock().len()
1322        }
1323    }
1324
1325    impl PlatformTextSystem for TrackingTextSystem {
1326        fn set_missing_glyph_sink(&self, _sink: Arc<dyn MissingGlyphSink>) {
1327            self.missing_glyph_sink_installed
1328                .store(true, Ordering::SeqCst);
1329        }
1330
1331        fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1332            self.noop.add_fonts(fonts)
1333        }
1334
1335        fn all_font_names(&self) -> Vec<String> {
1336            self.noop.all_font_names()
1337        }
1338
1339        fn font_id(&self, descriptor: &Font) -> Result<FontId> {
1340            self.noop.font_id(descriptor)
1341        }
1342
1343        fn clear_glyph_raster_cache(&self) {
1344            self.pending_glyph_images.lock().clear();
1345        }
1346
1347        fn font_metrics(&self, font_id: FontId) -> FontMetrics {
1348            self.noop.font_metrics(font_id)
1349        }
1350
1351        fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
1352            self.noop.typographic_bounds(font_id, glyph_id)
1353        }
1354
1355        fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1356            self.noop.advance(font_id, glyph_id)
1357        }
1358
1359        fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
1360            self.noop.glyph_for_char(font_id, ch)
1361        }
1362
1363        fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1364            self.pending_glyph_images.lock().insert(params.clone());
1365            Ok(Bounds {
1366                origin: point(DevicePixels(0), DevicePixels(0)),
1367                size: size(DevicePixels(1), DevicePixels(1)),
1368            })
1369        }
1370
1371        fn rasterize_glyph(
1372            &self,
1373            params: &RenderGlyphParams,
1374            raster_bounds: Bounds<DevicePixels>,
1375        ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1376            self.pending_glyph_images.lock().remove(params);
1377            Ok((raster_bounds.size, vec![0]))
1378        }
1379
1380        fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
1381            self.noop.layout_line(text, font_size, runs)
1382        }
1383    }
1384
1385    fn glyph_params(glyph_id: u32) -> RenderGlyphParams {
1386        RenderGlyphParams {
1387            font_id: FontId(0),
1388            glyph_id: GlyphId(glyph_id),
1389            font_size: px(16.),
1390            subpixel_variant: point(0, 0),
1391            scale_factor: 1.0,
1392            is_emoji: false,
1393            dilation: 0,
1394        }
1395    }
1396
1397    /// Delegates to [`NoopTextSystem`] while recording the `FontRun`s passed to each
1398    /// `layout_line` call, so tests can assert how the core text system split runs
1399    /// before handing them to the platform.
1400    struct RecordingTextSystem {
1401        noop: NoopTextSystem,
1402        layout_runs: Mutex<Vec<Vec<FontRun>>>,
1403    }
1404
1405    impl PlatformTextSystem for RecordingTextSystem {
1406        fn set_missing_glyph_sink(&self, sink: Arc<dyn MissingGlyphSink>) {
1407            self.noop.set_missing_glyph_sink(sink)
1408        }
1409
1410        fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
1411            self.noop.add_fonts(fonts)
1412        }
1413
1414        fn all_font_names(&self) -> Vec<String> {
1415            self.noop.all_font_names()
1416        }
1417
1418        fn font_id(&self, descriptor: &Font) -> Result<FontId> {
1419            self.noop.font_id(descriptor)
1420        }
1421
1422        fn clear_glyph_raster_cache(&self) {
1423            self.noop.clear_glyph_raster_cache()
1424        }
1425
1426        fn font_metrics(&self, font_id: FontId) -> FontMetrics {
1427            self.noop.font_metrics(font_id)
1428        }
1429
1430        fn typographic_bounds(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Bounds<f32>> {
1431            self.noop.typographic_bounds(font_id, glyph_id)
1432        }
1433
1434        fn advance(&self, font_id: FontId, glyph_id: GlyphId) -> Result<Size<f32>> {
1435            self.noop.advance(font_id, glyph_id)
1436        }
1437
1438        fn glyph_for_char(&self, font_id: FontId, ch: char) -> Option<GlyphId> {
1439            self.noop.glyph_for_char(font_id, ch)
1440        }
1441
1442        fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
1443            self.noop.glyph_raster_bounds(params)
1444        }
1445
1446        fn rasterize_glyph(
1447            &self,
1448            params: &RenderGlyphParams,
1449            raster_bounds: Bounds<DevicePixels>,
1450        ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
1451            self.noop.rasterize_glyph(params, raster_bounds)
1452        }
1453
1454        fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
1455            self.layout_runs.lock().push(runs.to_vec());
1456            self.noop.layout_line(text, font_size, runs)
1457        }
1458    }
1459
1460    #[test]
1461    fn background_color_runs_split_font_runs_in_both_shaping_paths() {
1462        let platform = Arc::new(RecordingTextSystem {
1463            noop: NoopTextSystem::new(),
1464            layout_runs: Mutex::default(),
1465        });
1466        let runs = || {
1467            let plain = TextRun {
1468                len: 1,
1469                font: font("Noop Font"),
1470                color: Hsla::default(),
1471                background_color: None,
1472                underline: None,
1473                strikethrough: None,
1474            };
1475            let mut highlighted = plain.clone();
1476            highlighted.background_color = Some(Hsla::default());
1477            [plain, highlighted]
1478        };
1479
1480        let line_shaper = WindowTextSystem::new(Arc::new(TextSystem::new(
1481            platform.clone(),
1482            &AppResourceProfile::desktop().text,
1483        )));
1484        let shaped_line = line_shaper.shape_line("ab".into(), px(16.), &runs(), None);
1485        assert_eq!(
1486            shaped_line.decoration_runs.len(),
1487            2,
1488            "decoration runs must split on background color"
1489        );
1490
1491        let text_shaper = WindowTextSystem::new(Arc::new(TextSystem::new(
1492            platform.clone(),
1493            &AppResourceProfile::desktop().text,
1494        )));
1495        let wrapped_lines = text_shaper
1496            .shape_text("ab".into(), px(16.), &runs(), None, None)
1497            .unwrap();
1498        assert_eq!(
1499            wrapped_lines[0].decoration_runs.len(),
1500            2,
1501            "decoration runs must split on background color"
1502        );
1503
1504        let recorded = platform.layout_runs.lock();
1505        assert_eq!(
1506            recorded.len(),
1507            2,
1508            "each shaping path should reach the platform once"
1509        );
1510        assert_eq!(
1511            recorded[0], recorded[1],
1512            "shape_line and shape_text must agree on font run boundaries"
1513        );
1514    }
1515
1516    #[test]
1517    fn all_font_names_lists_only_platform_families() {
1518        let text_system = TextSystem::new(
1519            Arc::new(NoopTextSystem::new()),
1520            &AppResourceProfile::desktop().text,
1521        );
1522        assert!(text_system.all_font_names().is_empty());
1523    }
1524
1525    #[test]
1526    fn resolve_font_caches_fallbacks() {
1527        let mut text_system = TextSystem::new(
1528            Arc::new(NoopTextSystem::new()),
1529            &AppResourceProfile::desktop().text,
1530        );
1531        let requested = font("Missing Font").bold().italic();
1532        let fallback = font("DejaVu Sans");
1533        {
1534            let mut font_ids = text_system.font_ids_by_font.write();
1535            let missing = CachedFont::Missing(Arc::new(anyhow::anyhow!("missing font")));
1536            font_ids.insert(requested.clone(), missing.clone());
1537            for candidate in &text_system.fallback_font_stack {
1538                font_ids.insert(candidate.clone(), missing.clone());
1539            }
1540            font_ids.insert(fallback.clone(), CachedFont::Direct(FontId(42)));
1541        }
1542
1543        assert_eq!(text_system.resolve_font(&requested), FontId(42));
1544        assert_eq!(text_system.get_font_for_id(FontId(42)), Some(fallback));
1545
1546        text_system.fallback_font_stack.clear();
1547        assert_eq!(text_system.resolve_font(&requested), FontId(42));
1548    }
1549
1550    #[test]
1551    fn text_system_installs_missing_glyph_sink_on_platform() {
1552        let platform = Arc::new(TrackingTextSystem::new());
1553        assert!(!platform.missing_glyph_sink_installed.load(Ordering::SeqCst));
1554        let _text_system = TextSystem::new(platform.clone(), &AppResourceProfile::desktop().text);
1555        assert!(platform.missing_glyph_sink_installed.load(Ordering::SeqCst));
1556    }
1557
1558    #[test]
1559    fn clearing_layout_cache_also_clears_platform_pending_glyph_images() {
1560        let platform = Arc::new(TrackingTextSystem::new());
1561        let text_system = TextSystem::new(platform.clone(), &AppResourceProfile::desktop().text);
1562
1563        text_system.raster_bounds(&glyph_params(1)).unwrap();
1564        assert_eq!(platform.pending_glyph_images_len(), 1);
1565
1566        text_system.clear_layout_cache();
1567
1568        assert_eq!(platform.pending_glyph_images_len(), 0);
1569    }
1570
1571    #[test]
1572    fn evicting_raster_bounds_also_clears_platform_pending_glyph_images() {
1573        let platform = Arc::new(TrackingTextSystem::new());
1574        let mut budget = AppResourceProfile::desktop().text;
1575        budget.raster_bounds_cache_max_entries = Some(1);
1576        let text_system = TextSystem::new(platform.clone(), &budget);
1577
1578        text_system.raster_bounds(&glyph_params(1)).unwrap();
1579        text_system.raster_bounds(&glyph_params(2)).unwrap();
1580
1581        assert_eq!(platform.pending_glyph_images_len(), 1);
1582    }
1583
1584    #[test]
1585    fn adding_fonts_invalidates_cached_line_layouts() {
1586        let text_system = Arc::new(TextSystem::new(
1587            Arc::new(NoopTextSystem::new()),
1588            &AppResourceProfile::desktop().text,
1589        ));
1590        let window_text_system = WindowTextSystem::new(text_system.clone());
1591        let text: SharedString = "cached text".into();
1592        let runs = [TextRun {
1593            len: text.len(),
1594            font: font("IBM Plex Sans"),
1595            color: Default::default(),
1596            background_color: None,
1597            underline: None,
1598            strikethrough: None,
1599        }];
1600
1601        let first_layout = window_text_system.shape_line(text.clone(), px(14.0), &runs, None);
1602        let cached_layout = window_text_system.shape_line(text.clone(), px(14.0), &runs, None);
1603        assert!(std::ptr::eq::<LineLayout>(
1604            &**first_layout,
1605            &**cached_layout
1606        ));
1607
1608        text_system
1609            .add_fonts(vec![Cow::Borrowed(b"unused")])
1610            .unwrap();
1611
1612        let refreshed_layout = window_text_system.shape_line(text, px(14.0), &runs, None);
1613        assert!(!std::ptr::eq::<LineLayout>(
1614            &**first_layout,
1615            &**refreshed_layout
1616        ));
1617    }
1618
1619    #[test]
1620    fn test_font_weight_from_str() {
1621        let weight: FontWeight = "450".parse().unwrap();
1622        assert_eq!(weight, FontWeight(450.0));
1623        assert!("not-a-number".parse::<FontWeight>().is_err());
1624        assert_eq!(weight + FontWeight(50.0), FontWeight(500.0));
1625        assert_eq!(weight - FontWeight(50.0), FontWeight(400.0));
1626    }
1627}
1628
1629#[cfg(test)]
1630mod line_wrapper_pool_tests {
1631    use super::*;
1632    use crate::{AppResourceProfile, NoopTextSystem};
1633
1634    #[test]
1635    fn pooled_line_wrappers_do_not_retain_text_system() {
1636        let platform = Arc::new(NoopTextSystem::new());
1637        let text_system = Arc::new(TextSystem::new(
1638            platform.clone(),
1639            &AppResourceProfile::desktop().text,
1640        ));
1641        let weak_text_system = Arc::downgrade(&text_system);
1642
1643        let mut wrapper = text_system.line_wrapper(font(".ZedMono"), px(16.));
1644        let fragments = [LineFragment::text("hello world")];
1645        wrapper
1646            .wrap_line(&fragments, px(40.), IndentAdjustment::default())
1647            .for_each(drop);
1648        drop(wrapper);
1649
1650        // This handle and the text system are the only remaining owners. The
1651        // pool stores width caches, not wrappers that clone the platform system.
1652        assert_eq!(Arc::strong_count(&platform), 2);
1653
1654        drop(text_system);
1655        drop(platform);
1656
1657        assert!(weak_text_system.upgrade().is_none());
1658    }
1659}
1660
1661#[cfg(test)]
1662mod missing_glyph_tests {
1663    use super::*;
1664    use crate::{AppResourceProfile, NoopTextSystem};
1665    use futures::FutureExt as _;
1666
1667    #[test]
1668    fn bounds_retained_missing_glyphs() {
1669        let (reporter, mut receiver) = missing_glyph_channel();
1670        reporter.report(
1671            (0..MAX_REPORTED_MISSING_GLYPHS)
1672                .map(|index| {
1673                    MissingGlyph::new(index.to_string().into(), FallbackFontClass::Proportional)
1674                })
1675                .collect(),
1676        );
1677        assert!(receiver.recv().now_or_never().unwrap().is_ok());
1678
1679        let newest = MissingGlyph::new("newest".into(), FallbackFontClass::Monospace);
1680        reporter.report(vec![newest.clone()]);
1681        assert!(receiver.recv().now_or_never().unwrap().is_ok());
1682
1683        let state = &receiver.state;
1684        assert_eq!(state.reported.len(), MAX_REPORTED_MISSING_GLYPHS);
1685        assert_eq!(state.reported_order.len(), MAX_REPORTED_MISSING_GLYPHS);
1686        assert!(state.reported.contains(&newest));
1687    }
1688
1689    #[test]
1690    fn dropping_receiver_closes_and_clears_reports() {
1691        let (reporter, receiver) = missing_glyph_channel();
1692        reporter.report(vec![missing_glyph("missing")]);
1693
1694        drop(receiver);
1695
1696        assert!(reporter.sender.is_closed());
1697        assert!(reporter.sender.is_empty());
1698    }
1699
1700    #[test]
1701    fn text_system_delivers_reported_missing_glyphs() {
1702        let text_system = TextSystem::new(
1703            Arc::new(NoopTextSystem::new()),
1704            &AppResourceProfile::desktop().text,
1705        );
1706        let mut receiver = text_system.take_missing_glyph_receiver().expect("receiver");
1707        text_system.report_missing_glyphs_in_test(vec![missing_glyph("界")]);
1708
1709        let received = receiver.recv().now_or_never().unwrap().unwrap();
1710        assert_eq!(received.len(), 1);
1711        assert_eq!(received[0].grapheme(), "界");
1712        assert!(text_system.take_missing_glyph_receiver().is_none());
1713    }
1714
1715    fn missing_glyph_channel() -> (MissingGlyphReporter, MissingGlyphReceiver) {
1716        let (sender, receiver) = async_channel::bounded(MAX_REPORTED_MISSING_GLYPHS);
1717        let generation = Arc::<AtomicUsize>::default();
1718        let reporter = MissingGlyphReporter {
1719            generation: generation.clone(),
1720            sender,
1721        };
1722        let receiver = MissingGlyphReceiver {
1723            state: MissingGlyphState::default(),
1724            generation,
1725            receiver,
1726        };
1727        (reporter, receiver)
1728    }
1729
1730    fn missing_glyph(grapheme: &'static str) -> MissingGlyph {
1731        MissingGlyph::new(grapheme.into(), FallbackFontClass::Proportional)
1732    }
1733}