Skip to main content

xgpui_parley/
lib.rs

1//! Experimental Parley interop: flat LTR lines, resolved-face metrics and
2//! grayscale rasterization, with an opt-in `PlatformTextSystem` backend.
3
4use anyhow::{Result, ensure};
5use gpui::{FontId, GlyphId, LineLayout, Pixels, ShapedGlyph, ShapedRun, point, px};
6use parley::{Alignment, Layout, PositionedLayoutItem, Run};
7
8mod preview;
9pub use preview::VariationPreview;
10
11mod registry;
12pub use registry::{FaceRegistry, RegisteredFace};
13mod raster;
14pub use raster::GlyphRasterizer;
15mod backend;
16pub use backend::{LayoutErrorHandler, ParleyTextSystem};
17mod options;
18pub use options::{ParagraphOptions, TextDirection, TextLanguage};
19pub mod typeset;
20pub use typeset::{Typeset, typeset};
21
22#[cfg(test)]
23mod test_support;
24
25/// Convert a scale-1, start-aligned Parley line into GPUI's baseline-relative
26/// glyph records. The caller owns/reuses Parley's font and layout contexts.
27///
28/// `resolve_face` must register the actual `run.font()` (including collection
29/// index), normalized variation coordinates and synthesis for later metrics and
30/// rasterization. Returning the originally requested family ID is insufficient
31/// when shaping selected a fallback face.
32///
33/// This initial bridge rejects paragraphs, inline boxes, RTL and mixed sizes.
34/// It adds no layout or raster cache: those remain owned by GPUI's `TextSystem`.
35pub fn line_layout(
36    layout: &Layout<()>,
37    font_size: Pixels,
38    mut resolve_face: impl FnMut(&Run<'_, ()>) -> Result<FontId>,
39) -> Result<LineLayout> {
40    ensure!(
41        f32::from(font_size).is_finite() && font_size > px(0.0),
42        "invalid font size"
43    );
44    ensure!(layout.scale() == 1.0, "expected logical-pixel layout");
45    ensure!(layout.len() == 1, "expected one unwrapped line");
46    ensure!(
47        layout.alignment() == Some(Alignment::Start),
48        "expected start alignment"
49    );
50    validate_layout_geometry(layout)?;
51    let line = layout.lines().next().unwrap();
52    ensure!(
53        layout.text_len() == 0 || line.text_range() == (0..layout.text_len()),
54        "incomplete line: {:?} for {} source bytes",
55        line.text_range(),
56        layout.text_len()
57    );
58    ensure!(
59        line.items()
60            .all(|item| matches!(item, PositionedLayoutItem::GlyphRun(_))),
61        "inline boxes are unsupported"
62    );
63    for item in line.items() {
64        if let PositionedLayoutItem::GlyphRun(item) = item {
65            ensure!(
66                item.baseline() == line.metrics().baseline,
67                "vertical alignment is unsupported"
68            );
69            ensure!(
70                item.glyphs().count()
71                    == item
72                        .run()
73                        .visual_clusters()
74                        .map(|cluster| cluster.glyphs().count())
75                        .sum::<usize>(),
76                "split style runs are unsupported"
77            );
78        }
79    }
80    // Validate before allowing the caller to register any faces.
81    for run in line.runs() {
82        ensure!(!run.is_rtl(), "RTL interaction is not supported yet");
83        ensure!(
84            run.font_size() == f32::from(font_size),
85            "mixed font sizes are unsupported"
86        );
87    }
88
89    let metrics = line.metrics();
90    let mut result = LineLayout {
91        font_size,
92        width: px(metrics.advance),
93        ascent: px(metrics.baseline - metrics.content_block_min_coord),
94        descent: px(metrics.content_block_max_coord - metrics.baseline),
95        runs: Vec::new(),
96        len: layout.text_len(),
97    };
98    // Parley uses a synthetic 0..1 range for its empty-line strut. Keep its real
99    // baseline metrics, but never expose that synthetic character to GPUI.
100    if result.len == 0 {
101        return Ok(result);
102    }
103    let mut x = metrics.offset;
104    for run in line.runs() {
105        let font_id = resolve_face(&run)?;
106        let color = raster::color::ColorProbe::new(run.font().data.data(), run.font().index);
107        let mut glyphs = Vec::new();
108        for cluster in run.visual_clusters() {
109            for glyph in cluster.glyphs() {
110                let position = point(px(x + glyph.x), px(glyph.y));
111                ensure!(
112                    f32::from(position.x).is_finite() && f32::from(position.y).is_finite(),
113                    "nonfinite glyph position"
114                );
115                glyphs.push(ShapedGlyph {
116                    id: GlyphId(glyph.id),
117                    position,
118                    index: cluster.text_range().start,
119                    // GPUI paints glyphs with color data as emoji sprites.
120                    is_emoji: color.as_ref().is_some_and(|color| color.has(glyph.id)),
121                });
122                x += glyph.advance;
123            }
124        }
125        result.runs.push(ShapedRun { font_id, glyphs });
126    }
127    Ok(result)
128}
129
130// A finite requested font size can still overflow shaping or line breaking.
131// Reject invalid geometry before exposing it to GPUI or registering its faces.
132fn validate_layout_geometry(layout: &Layout<()>) -> Result<()> {
133    ensure!(
134        [layout.width(), layout.full_width(), layout.height()]
135            .into_iter()
136            .all(f32::is_finite),
137        "nonfinite paragraph geometry"
138    );
139    for line in layout.lines() {
140        let m = line.metrics();
141        ensure!(
142            [
143                m.line_height,
144                m.baseline,
145                m.offset,
146                m.advance,
147                m.block_min_coord,
148                m.block_max_coord,
149                m.content_block_min_coord,
150                m.content_block_max_coord,
151                m.baseline - m.content_block_min_coord,
152                m.content_block_max_coord - m.baseline
153            ]
154            .into_iter()
155            .all(f32::is_finite),
156            "nonfinite line geometry"
157        );
158    }
159    Ok(())
160}
161
162mod paragraph;
163pub use paragraph::{ParagraphLayout, ParagraphShape, TextPosition, VisualDirection};