Skip to main content

xgpui_parley/
backend.rs

1use crate::{FaceRegistry, GlyphRasterizer, line_layout};
2use anyhow::{Context, Result, ensure};
3use gpui::{
4    Bounds, DevicePixels, Font, FontId, FontMetrics, FontRun, GlyphId, LineLayout, Pixels,
5    PlatformTextSystem, RenderGlyphParams, ShapedGlyph, ShapedRun, Size, TextRenderingMode, point,
6    px,
7};
8use parley::{
9    Alignment, AlignmentOptions, FontContext, FontFamily, FontFamilyName, Layout, LayoutContext,
10    StyleProperty,
11};
12use std::{
13    borrow::Cow,
14    collections::{HashMap, HashSet},
15    ops::Range,
16    sync::{Arc, Mutex, RwLock},
17};
18
19/// Experimental flat-LTR backend with an opt-in paragraph path. Select fonts before constructing GPUI;
20/// GPUI owns the font-metric, line-layout, wrapping and final raster caches.
21pub struct ParleyTextSystem {
22    state: Mutex<State>,
23    faces: RwLock<FaceRegistry>,
24    raster: Mutex<GlyphRasterizer>,
25    #[cfg(test)]
26    raster_calls: std::sync::atomic::AtomicUsize,
27    rejected: RejectedFace,
28}
29
30// Permanent .notdef instance; never aliases a requested descriptor.
31struct RejectedFace {
32    id: FontId,
33    metrics: FontMetrics,
34    advance: f32,
35}
36
37/// Called on an uncached rejected layout, outside the backend lock. No source
38/// text is included in the diagnostic. Install before constructing GPUI caches.
39pub type LayoutErrorHandler = Arc<dyn Fn(&anyhow::Error) + Send + Sync>;
40
41struct State {
42    fonts: FontContext,
43    layout: LayoutContext<()>,
44    default_family: String,
45    /// Registered color emoji families, appended as last-resort fallbacks.
46    emoji_families: Vec<String>,
47    // Identity bindings, not a second layout/raster cache. Old bindings remain
48    // valid for in-flight layouts after GPUI invalidates its font generation.
49    bindings: HashMap<FontId, Font>,
50    // Reuse historical aliases when a descriptor resolves to the same face.
51    aliases: HashMap<Font, HashMap<FontId, FontId>>,
52    // Only current-generation descriptors; old ID bindings above remain valid.
53    descriptors: HashMap<Font, FontId>,
54    generation: u64,
55    missing_sink: Option<Arc<dyn gpui::MissingGlyphSink>>,
56    layout_error_handler: LayoutErrorHandler,
57    #[cfg(test)]
58    shape_calls: usize,
59}
60
61impl ParleyTextSystem {
62    /// Use an existing Parley font context and an explicit application default.
63    /// `.SystemUIFont` maps to this family; native system-family discovery is not
64    /// implied. FontContext system discovery requires the `system-fonts` feature.
65    pub fn new(mut fonts: FontContext, default_family: impl Into<String>) -> Result<Self> {
66        let default_family = default_family.into();
67        ensure!(
68            fonts.collection.family_by_name(&default_family).is_some(),
69            "default family is unavailable"
70        );
71        let mut state = State {
72            fonts,
73            default_family,
74            emoji_families: Vec::new(),
75            layout: LayoutContext::new(),
76            bindings: HashMap::new(),
77            aliases: HashMap::new(),
78            descriptors: HashMap::new(),
79            generation: 0,
80            missing_sink: None,
81            layout_error_handler: Arc::new(|error| eprintln!("Parley layout rejected: {error:#}")),
82            #[cfg(test)]
83            shape_calls: 0,
84        };
85        state.refresh_emoji_families();
86        // Resolve once, in the same registry as every shaped/rasterized face.
87        // Construction fails if the explicit default cannot provide real metrics.
88        let layout = state.shape(" ", px(16.0), &[])?;
89        let run = layout
90            .lines()
91            .next()
92            .context("missing default line")?
93            .runs()
94            .next()
95            .context("missing default face")?;
96        let mut faces = FaceRegistry::default();
97        let id = faces.register(&run)?;
98        let face = faces.get(id)?;
99        let metrics = face.metrics()?;
100        let advance = face.advance(GlyphId(0))?.width;
101        ensure!(
102            advance.is_finite() && advance > 0.0,
103            "invalid default .notdef advance"
104        );
105        let rejected = RejectedFace {
106            id,
107            metrics,
108            advance,
109        };
110        Ok(Self {
111            state: Mutex::new(state),
112            faces: RwLock::new(faces),
113            raster: Mutex::new(GlyphRasterizer::default()),
114            #[cfg(test)]
115            raster_calls: std::sync::atomic::AtomicUsize::new(0),
116            rejected,
117        })
118    }
119
120    // Clone the immutable face handle under a short read lock; font parsing and
121    // raster work must never retain the registry lock or acquire shaping state.
122    fn face(&self, id: FontId) -> Result<Arc<crate::RegisteredFace>> {
123        self.faces.read().unwrap().shared(id)
124    }
125
126    /// Replace the rejection diagnostic (default: stderr). Rejections remain
127    /// fallible through `try_layout_line`; only GPUI's infallible hook substitutes.
128    pub fn set_layout_error_handler(&self, handler: LayoutErrorHandler) {
129        self.state.lock().unwrap().layout_error_handler = handler;
130    }
131
132    /// Number of Parley layout builds, for cache tests.
133    #[cfg(test)]
134    pub(crate) fn shape_calls(&self) -> usize {
135        self.state.lock().unwrap().shape_calls
136    }
137
138    /// Font collection generation; it changes whenever `add_fonts` succeeds, so
139    /// shapes cached under an older generation may use stale fallback faces.
140    pub(crate) fn font_generation(&self) -> u64 {
141        self.state.lock().unwrap().generation
142    }
143
144    /// Report a rejection through the installed handler, outside the state lock.
145    pub(crate) fn report_layout_error(&self, error: &anyhow::Error) {
146        let handler = self.state.lock().unwrap().layout_error_handler.clone();
147        handler(error);
148    }
149
150    fn rejected_line(&self, text: &str, size: Pixels) -> LineLayout {
151        let size = if f32::from(size).is_finite() && size > px(0.0) {
152            size
153        } else {
154            px(16.0)
155        };
156        let count = text.chars().count();
157        let mut size = size;
158        let mut scale = f32::from(size) / self.rejected.metrics.units_per_em as f32;
159        let max_units = (self.rejected.advance * count as f32)
160            .max(self.rejected.metrics.ascent.abs())
161            .max(self.rejected.metrics.descent.abs());
162        if !(max_units * scale).is_finite() {
163            size = px(16.0);
164            scale = f32::from(size) / self.rejected.metrics.units_per_em as f32;
165        }
166        let advance = px(self.rejected.advance * scale);
167        let glyphs = text
168            .char_indices()
169            .enumerate()
170            .map(|(offset, (index, _))| ShapedGlyph {
171                id: GlyphId(0),
172                position: point(advance * offset as f32, px(0.0)),
173                index,
174                is_emoji: false,
175            })
176            .collect();
177        LineLayout {
178            font_size: size,
179            width: advance * count as f32,
180            ascent: px(self.rejected.metrics.ascent * scale),
181            descent: px(self.rejected.metrics.descent.abs() * scale),
182            runs: vec![ShapedRun {
183                font_id: self.rejected.id,
184                glyphs,
185            }],
186            len: text.len(),
187        }
188    }
189
190    /// Fallible equivalent of the infallible GPUI layout hook. Validate unsupported
191    /// input here before using the backend through GPUI (RTL, synthetic styles,
192    /// paragraph breaks and invalid style ranges return errors).
193    pub fn try_layout_line(
194        &self,
195        text: &str,
196        size: Pixels,
197        runs: &[FontRun],
198    ) -> Result<LineLayout> {
199        let mut state = self.state.lock().unwrap();
200        let styles = state.styles(text, runs)?;
201        let layout = state.shape(text, size, &styles)?;
202        let result = line_layout(&layout, size, |run| {
203            self.faces.write().unwrap().register(run)
204        })?;
205        let sink = state.missing_sink.clone();
206        let mut missing = Vec::new();
207        if sink.is_some() && !text.is_empty() {
208            use skrifa::MetadataProvider;
209            for line in layout.lines() {
210                for run in line.runs() {
211                    let font =
212                        skrifa::FontRef::from_index(run.font().data.data(), run.font().index)?;
213                    let class = if font
214                        .metrics(
215                            skrifa::instance::Size::unscaled(),
216                            skrifa::instance::LocationRef::default(),
217                        )
218                        .is_monospace
219                    {
220                        gpui::FallbackFontClass::Monospace
221                    } else {
222                        gpui::FallbackFontClass::Proportional
223                    };
224                    for cluster in run.visual_clusters() {
225                        if cluster.glyphs().any(|glyph| glyph.id == 0) {
226                            missing.push(gpui::MissingGlyph::new(
227                                text[cluster.text_range()].to_owned().into(),
228                                class,
229                            ));
230                        }
231                    }
232                }
233            }
234        }
235        drop(state); // Never call an external sink while holding backend state.
236        if let Some(sink) = sink
237            && !missing.is_empty()
238        {
239            sink.report(missing);
240        }
241        Ok(result)
242    }
243    /// Extension-only prototype, automatic base direction, one logical paragraph.
244    /// U+2028 is a hard line break; document/paragraph separators are rejected.
245    /// Reuses backend contexts/face registry. GPUI owns paragraph caching through
246    /// the optional PlatformTextSystem hook; this method itself adds no cache.
247    pub fn prepare_paragraph(
248        &self,
249        text: &str,
250        size: Pixels,
251        runs: &[FontRun],
252    ) -> Result<crate::ParagraphShape> {
253        self.prepare_paragraph_with_options(text, size, runs, Default::default())
254    }
255
256    /// Like [`Self::prepare_paragraph`], with an explicit base direction and
257    /// content language. The language applies to the whole paragraph and selects
258    /// language-specific shaping and line breaking where fonts support it.
259    pub fn prepare_paragraph_with_options(
260        &self,
261        text: &str,
262        size: Pixels,
263        runs: &[FontRun],
264        options: crate::ParagraphOptions,
265    ) -> Result<crate::ParagraphShape> {
266        ensure!(
267            !text.contains(['\n', '\r', '\u{2029}']),
268            "expected one logical paragraph"
269        );
270        let mut state = self.state.lock().unwrap();
271        let styles = state.styles(text, runs)?;
272        let mut layout = state.build(text, size, &styles, options)?;
273        layout.break_all_lines(None);
274        layout.align(Alignment::Start, AlignmentOptions::default());
275        crate::validate_layout_geometry(&layout)?;
276        let mut faces = Vec::new();
277        let mut seen_faces = HashSet::new();
278        let mut face_runs = Vec::new();
279        let mut registry = self.faces.write().unwrap();
280        for line in layout.lines() {
281            for run in line.runs() {
282                let id = registry.register(&run)?;
283                face_runs.push((run.text_range(), id));
284                if seen_faces.insert(id) {
285                    faces.push(id);
286                }
287            }
288        }
289        drop(registry);
290        face_runs.sort_by_key(|(range, _)| range.start);
291        Ok(crate::ParagraphShape::new(
292            Arc::from(text),
293            layout,
294            faces,
295            face_runs,
296            size,
297        ))
298    }
299}
300
301/// The primary family, explicit fallbacks, then any emoji families not listed.
302fn family_list<'a>(
303    primary: &'a str,
304    fallbacks: &'a [String],
305    emoji: &'a [String],
306) -> Vec<FontFamilyName<'a>> {
307    let mut families = Vec::with_capacity(1 + fallbacks.len() + emoji.len());
308    families.push(FontFamilyName::named(primary));
309    families.extend(fallbacks.iter().map(|name| FontFamilyName::named(name)));
310    for name in emoji {
311        let listed = name == primary || fallbacks.contains(name);
312        if !listed {
313            families.push(FontFamilyName::named(name));
314        }
315    }
316    families
317}
318
319impl State {
320    /// Parley's script fallback does not reach color emoji fonts, so registered
321    /// emoji families (named "…Emoji…" and carrying COLR or color bitmaps) are
322    /// appended to every family list. Other color fonts never become fallbacks.
323    fn refresh_emoji_families(&mut self) {
324        let names: Vec<String> = self
325            .fonts
326            .collection
327            .family_names()
328            .filter(|name| name.to_lowercase().contains("emoji"))
329            .map(str::to_owned)
330            .collect();
331        let mut emoji: Vec<String> = names
332            .into_iter()
333            .filter(|name| {
334                self.fonts
335                    .collection
336                    .family_by_name(name)
337                    .is_some_and(|family| {
338                        family.fonts().iter().any(|font| {
339                            font.load(None).is_some_and(|blob| {
340                                crate::raster::color::ColorProbe::new(blob.data(), font.index())
341                                    .is_some()
342                            })
343                        })
344                    })
345            })
346            .collect();
347        emoji.sort();
348        self.emoji_families = emoji;
349    }
350
351    fn styles(&self, text: &str, runs: &[FontRun]) -> Result<Vec<(Font, Range<usize>)>> {
352        let mut start: usize = 0;
353        let mut styles = Vec::with_capacity(runs.len());
354        for run in runs {
355            let end = start.checked_add(run.len).context("font run overflow")?;
356            ensure!(
357                end <= text.len() && text.is_char_boundary(start) && text.is_char_boundary(end),
358                "invalid font run range"
359            );
360            let font = self
361                .bindings
362                .get(&run.font_id)
363                .context("unknown requested font ID")?;
364            styles.push((font.clone(), start..end));
365            start = end;
366        }
367        ensure!(
368            runs.is_empty() || start == text.len(),
369            "font runs must cover the line"
370        );
371        Ok(styles)
372    }
373
374    fn build(
375        &mut self,
376        text: &str,
377        size: Pixels,
378        styles: &[(Font, Range<usize>)],
379        options: crate::ParagraphOptions,
380    ) -> Result<Layout<()>> {
381        ensure!(
382            f32::from(size).is_finite() && size > px(0.0),
383            "invalid font size"
384        );
385        #[cfg(test)]
386        {
387            self.shape_calls += 1;
388        }
389        let mut builder = self.layout.ranged_builder(&mut self.fonts, text, 1.0, true);
390        builder.set_base_direction(options.direction.into());
391        if let Some(language) = options.language {
392            builder.push_default(StyleProperty::Locale(Some(language.parley())));
393        }
394        if let Some(line_height) = options.line_height {
395            ensure!(
396                f32::from(line_height).is_finite() && line_height > px(0.0),
397                "invalid line height"
398            );
399            builder.push_default(StyleProperty::LineHeight(parley::LineHeight::Absolute(
400                f32::from(line_height),
401            )));
402        }
403        builder.push_default(StyleProperty::FontFamily(FontFamily::List(Cow::Owned(
404            family_list(&self.default_family, &[], &self.emoji_families),
405        ))));
406        builder.push_default(StyleProperty::FontSize(f32::from(size)));
407        for (font, range) in styles {
408            let primary = if font.family.as_ref() == ".SystemUIFont" {
409                &self.default_family
410            } else {
411                font.family.as_ref()
412            };
413            let fallbacks = font
414                .fallbacks
415                .as_ref()
416                .map(|f| f.fallback_list())
417                .unwrap_or(&[]);
418            let family = FontFamily::List(Cow::Owned(family_list(
419                primary,
420                fallbacks,
421                &self.emoji_families,
422            )));
423            builder.push(StyleProperty::FontFamily(family), range.clone());
424            builder.push(
425                StyleProperty::FontWeight(parley::FontWeight::new(font.weight.0)),
426                range.clone(),
427            );
428            let style = match font.style {
429                gpui::FontStyle::Normal => parley::FontStyle::Normal,
430                gpui::FontStyle::Italic => parley::FontStyle::Italic,
431                gpui::FontStyle::Oblique => parley::FontStyle::Oblique(None),
432            };
433            builder.push(StyleProperty::FontStyle(style), range.clone());
434            let variations = font
435                .variations
436                .settings()
437                .iter()
438                .map(|(tag, value)| {
439                    Ok(parley::FontVariation::new(
440                        parley::setting::Tag::parse(&tag.to_string())
441                            .context("invalid axis tag")?,
442                        *value,
443                    ))
444                })
445                .collect::<Result<Vec<_>>>()?;
446            builder.push(
447                StyleProperty::FontVariations(parley::FontVariations::List(Cow::Owned(variations))),
448                range.clone(),
449            );
450            let features = font
451                .features
452                .tag_value_list()
453                .iter()
454                .map(|(tag, value)| {
455                    Ok(parley::FontFeature::new(
456                        parley::setting::Tag::parse(tag).context("invalid feature tag")?,
457                        (*value).try_into()?,
458                    ))
459                })
460                .collect::<Result<Vec<_>>>()?;
461            builder.push(
462                StyleProperty::FontFeatures(parley::FontFeatures::List(Cow::Owned(features))),
463                range.clone(),
464            );
465        }
466        Ok(builder.build(text))
467    }
468
469    fn shape(
470        &mut self,
471        text: &str,
472        size: Pixels,
473        styles: &[(Font, Range<usize>)],
474    ) -> Result<Layout<()>> {
475        ensure!(
476            !text.contains(['\n', '\r', '\u{2028}', '\u{2029}']),
477            "expected a single logical line"
478        );
479        let mut layout = self.build(text, size, styles, crate::ParagraphOptions::default())?;
480        layout.break_all_lines(None);
481        layout.align(Alignment::Start, AlignmentOptions::default());
482        Ok(layout)
483    }
484}
485
486impl PlatformTextSystem for ParleyTextSystem {
487    fn add_fonts(&self, fonts: Vec<Cow<'static, [u8]>>) -> Result<()> {
488        // Validate the entire batch before changing the collection or generation.
489        let mut validation =
490            parley::fontique::Collection::new(parley::fontique::CollectionOptions {
491                shared: false,
492                system_fonts: false,
493            });
494        let blobs = fonts
495            .into_iter()
496            .map(|bytes| {
497                let blob = parley::fontique::Blob::new(Arc::new(bytes.into_owned()));
498                let registered = validation.register_fonts(blob.clone(), None);
499                ensure!(!registered.is_empty(), "invalid or unnamed font data");
500                let families = registered
501                    .into_iter()
502                    .map(|(id, fonts)| {
503                        let name = validation
504                            .family_name(id)
505                            .context("missing validated family")?
506                            .to_owned();
507                        let indices = fonts.iter().map(|font| font.index()).collect::<Vec<_>>();
508                        Ok((name, indices))
509                    })
510                    .collect::<Result<Vec<_>>>()?;
511                Ok((blob, families))
512            })
513            .collect::<Result<Vec<_>>>()?;
514        let mut state = self.state.lock().unwrap();
515        for (blob, families) in blobs {
516            // Fontique appends even identical memory fonts. Check every face of
517            // the blob against its target family, including collection indices.
518            // Do not keep a separate index that would itself retain font bytes.
519            let duplicate = families.iter().all(|(name, indices)| {
520                state.fonts.collection.family_by_name(name).is_some_and(|family| {
521                    indices.iter().all(|index| family.fonts().iter().any(|font| {
522                        font.index() == *index && matches!(font.source().kind(),
523                            parley::fontique::SourceKind::Memory(data) if data.data() == blob.data())
524                    }))
525                })
526            });
527            if !duplicate {
528                state.fonts.collection.register_fonts(blob, None);
529            }
530        }
531        state.refresh_emoji_families();
532        state.generation += 1;
533        state.descriptors.clear();
534        Ok(())
535    }
536
537    fn set_missing_glyph_sink(&self, sink: Option<Arc<dyn gpui::MissingGlyphSink>>) {
538        self.state.lock().unwrap().missing_sink = sink;
539    }
540
541    fn all_font_names(&self) -> Vec<String> {
542        self.state
543            .lock()
544            .unwrap()
545            .fonts
546            .collection
547            .family_names()
548            .map(str::to_owned)
549            .collect()
550    }
551
552    fn font_id(&self, descriptor: &Font) -> Result<FontId> {
553        let mut state = self.state.lock().unwrap();
554        if let Some(&id) = state.descriptors.get(descriptor) {
555            return Ok(id);
556        }
557        let family = if descriptor.family.as_ref() == ".SystemUIFont" {
558            state.default_family.clone()
559        } else {
560            descriptor.family.to_string()
561        };
562        ensure!(
563            state.fonts.collection.family_by_name(&family).is_some(),
564            "font family is unavailable: {family}"
565        );
566        let layout = state.shape(" ", px(16.0), &[(descriptor.clone(), 0..1)])?;
567        let line = layout
568            .lines()
569            .next()
570            .context("font probe produced no line")?;
571        let run = line.runs().next().context("font probe produced no face")?;
572        let resolved = self.faces.write().unwrap().register(&run)?;
573        let id = if let Some(&id) = state
574            .aliases
575            .get(descriptor)
576            .and_then(|ids| ids.get(&resolved))
577        {
578            id
579        } else {
580            let id = self.faces.write().unwrap().alias(resolved)?;
581            state.bindings.insert(id, descriptor.clone());
582            state
583                .aliases
584                .entry(descriptor.clone())
585                .or_default()
586                .insert(resolved, id);
587            id
588        };
589        state.descriptors.insert(descriptor.clone(), id);
590        Ok(id)
591    }
592
593    fn font_capabilities(&self) -> gpui::FontCapabilities {
594        gpui::FontCapabilities {
595            feature_introspection: true,
596            axis_introspection: true,
597            static_variations: true,
598        }
599    }
600
601    fn font_table(
602        &self,
603        id: FontId,
604        tag: gpui::FontTag,
605    ) -> Result<gpui::FontQuery<Option<Vec<u8>>>> {
606        let face = self.face(id)?;
607        let data = face.data();
608        let font = skrifa::FontRef::from_index(data.data.data(), data.index)?;
609        let tag = skrifa::Tag::new(tag.as_bytes());
610        let table = if font
611            .table_directory()
612            .table_records()
613            .iter()
614            .any(|record| record.tag() == tag)
615        {
616            Some(
617                font.table_data(tag)
618                    .context("unreadable font table")?
619                    .as_bytes()
620                    .to_vec(),
621            )
622        } else {
623            None
624        };
625        Ok(gpui::FontQuery::Supported(table))
626    }
627
628    fn font_metrics(&self, id: FontId) -> FontMetrics {
629        self.face(id)
630            .and_then(|face| face.metrics())
631            .expect("invalid Parley font metrics")
632    }
633    fn typographic_bounds(&self, id: FontId, glyph: GlyphId) -> Result<Bounds<f32>> {
634        self.face(id)?.typographic_bounds(glyph)
635    }
636    fn advance(&self, id: FontId, glyph: GlyphId) -> Result<Size<f32>> {
637        self.face(id)?.advance(glyph)
638    }
639    fn glyph_for_char(&self, id: FontId, ch: char) -> Option<GlyphId> {
640        self.face(id).ok()?.glyph_for_char(ch)
641    }
642    fn glyph_raster_bounds(&self, params: &RenderGlyphParams) -> Result<Bounds<DevicePixels>> {
643        let face = self.face(params.font_id)?;
644        self.raster
645            .lock()
646            .unwrap()
647            .raster_bounds_for_face(&face, params)
648    }
649    fn rasterize_glyph(
650        &self,
651        params: &RenderGlyphParams,
652        bounds: Bounds<DevicePixels>,
653    ) -> Result<(Size<DevicePixels>, Vec<u8>)> {
654        let face = self.face(params.font_id)?;
655        #[cfg(test)]
656        self.raster_calls
657            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
658        self.raster
659            .lock()
660            .unwrap()
661            .rasterize_face(&face, params, bounds)
662    }
663    fn layout_line(&self, text: &str, size: Pixels, runs: &[FontRun]) -> LineLayout {
664        match self.try_layout_line(text, size, runs) {
665            Ok(line) => line,
666            Err(error) => {
667                let handler = self.state.lock().unwrap().layout_error_handler.clone();
668                handler(&error);
669                self.rejected_line(text, size)
670            }
671        }
672    }
673    fn shape_paragraph(
674        &self,
675        request: gpui::ParagraphRequest<'_>,
676    ) -> Result<Arc<dyn gpui::PlatformParagraphShape>> {
677        // GPUI's request carries no options; explicit options use the extension API.
678        Ok(Arc::new(self.prepare_paragraph(
679            request.text,
680            request.font_size,
681            request.runs,
682        )?))
683    }
684    fn recommended_rendering_mode(&self, _: FontId, _: Pixels) -> TextRenderingMode {
685        TextRenderingMode::Grayscale
686    }
687}
688
689#[cfg(test)]
690mod tests;
691
692#[cfg(test)]
693#[path = "paragraph/tests.rs"]
694mod paragraph_tests;