Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion parley/src/shape/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ pub(crate) fn shape_text<'a, B: Brush>(
font_size: item_style.font_size,
features: &style_features[item_style_index as usize],
variations: rcx.variations(item_style.font_variations).unwrap_or(&[]),
char_style_indices,
},
})
})
Expand All @@ -184,6 +183,7 @@ pub(crate) fn shape_text<'a, B: Brush>(
scx.shape_text(
text,
analysis,
char_style_indices,
items,
font_selector,
&mut layout.data.shaped_text,
Expand Down
65 changes: 64 additions & 1 deletion parley_engine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,75 @@ See https://linebender.org/blog/doc-include/ for related discussion. -->

<!-- cargo-rdme start -->

Parley Engine provides low level APIs for implementing text layout.
Parley Engine provides low level APIs for shaping paragraphs of text.

## Usage

Use [`Analyzer`], [`Analysis`], [`Shaper`] and [`ShapedText`] to shape a paragraph of text into
glyphs. Correct reshaping of lines is in progress; in the meantime you can break text at
[`Atom`] or [`ShapedCluster`][crate::shape::ShapedCluster] boundaries.

Text analysis is performed before shaping, and the same source string must be passed to each
stage.

Higher-level users may prefer using [`parley`][parley], which uses this crate and implements
layout and styling.

```rust
let mut analysis = Analysis::default();
let mut analyzer = Analyzer::default();
let mut shaped_text = ShapedText::default();
let mut shaper = Shaper::default();

let text = "The quick brown ثعلب jumps over the lazy dog.";
let char_count = text.chars().count();
let char_style_indices = vec![0; char_count];

analyzer.analyze(text, &AnalysisOptions::default(), &mut analysis);
shaper.shape_text(
text,
&analysis,
&char_style_indices,
[Item {
char_end: char_count.try_into().unwrap(),
options: ShapeOptions {
font_size: 16.0,
language: None,
features: &[],
variations: &[],
},
}],
select_font, // Selects fonts covering each cluster.
&mut shaped_text,
);

for (run_idx, run) in shaped_text.runs().iter().enumerate() {
let slice = shaped_text.run_slice(run_idx as u32);
// You can, for example, measure grapheme advances for hit-testing or
// placing carets.
for atom in slice.atoms_start() {
for grapheme in atom.graphemes_start() {
std::dbg!(grapheme);
}
}

// Or get glyphs for rendering (for simplicity, this iterates clusters in
// logical order, but for rendering you'd want to reorder runs and clusters
// according to their `run.bidi_level`).
for cluster in slice.shaped_clusters_range() {
for glyph in slice.shaped_cluster_glyphs(cluster) {
std::dbg!(glyph);
}
}
}
```

## Features

- `std` (enabled by default): This is currently unused and is provided for forward compatibility.

[parley]: https://docs.rs/parley

<!-- cargo-rdme end -->

## Minimum supported Rust Version (MSRV)
Expand Down
83 changes: 82 additions & 1 deletion parley_engine/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,92 @@
// Copyright 2025 the Parley Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

//! Parley Engine provides low level APIs for implementing text layout.
//! Parley Engine provides low level APIs for shaping paragraphs of text.
//!
//! ## Usage
//!
//! Use [`Analyzer`], [`Analysis`], [`Shaper`] and [`ShapedText`] to shape a paragraph of text into
//! glyphs. Correct reshaping of lines is in progress; in the meantime you can break text at
//! [`Atom`] or [`ShapedCluster`][crate::shape::ShapedCluster] boundaries.
//!
//! Text analysis is performed before shaping, and the same source string must be passed to each
//! stage.
//!
//! Higher-level users may prefer using [`parley`][parley], which uses this crate and implements
//! layout and styling.
//!
//! ```rust,no_run
//! # // We only compile this doctest because we don't have a font available.
//! # use parley_engine::{Analysis, AnalysisOptions, Analyzer, FontInstance, FontSelector, ShapedText, ShapeOptions, Shaper};
//! # use parley_engine::shape::CharCluster;
//! # use parley_engine::itemize::{Item, Segment};
//! #
//! # struct NoFont;
//! # impl FontSelector for NoFont {
//! # fn select_font(
//! # &mut self,
//! # _segment: &Segment,
//! # _options: &ShapeOptions<'_>,
//! # _cluster: &mut CharCluster,
//! # ) -> Option<FontInstance> {
//! # unimplemented!()
//! # }
//! # }
//! #
//! # let select_font = NoFont;
//! let mut analysis = Analysis::default();
//! let mut analyzer = Analyzer::default();
//! let mut shaped_text = ShapedText::default();
//! let mut shaper = Shaper::default();
//!
//! let text = "The quick brown ثعلب jumps over the lazy dog.";
//! let char_count = text.chars().count();
//! let char_style_indices = vec![0; char_count];
//!
//! analyzer.analyze(text, &AnalysisOptions::default(), &mut analysis);
//! shaper.shape_text(
//! text,
//! &analysis,
//! &char_style_indices,
//! [Item {
//! char_end: char_count.try_into().unwrap(),
//! options: ShapeOptions {
//! font_size: 16.0,
//! language: None,
//! features: &[],
//! variations: &[],
//! },
//! }],
//! select_font, // Selects fonts covering each cluster.
//! &mut shaped_text,
//! );
//!
//! for (run_idx, run) in shaped_text.runs().iter().enumerate() {
//! let slice = shaped_text.run_slice(run_idx as u32);
//! // You can, for example, measure grapheme advances for hit-testing or
//! // placing carets.
//! for atom in slice.atoms_start() {
//! for grapheme in atom.graphemes_start() {
//! std::dbg!(grapheme);
//! }
//! }
//!
//! // Or get glyphs for rendering (for simplicity, this iterates clusters in
//! // logical order, but for rendering you'd want to reorder runs and clusters
//! // according to their `run.bidi_level`).
//! for cluster in slice.shaped_clusters_range() {
//! for glyph in slice.shaped_cluster_glyphs(cluster) {
//! std::dbg!(glyph);
//! }
//! }
//! }
//! ```
//!
//! ## Features
//!
//! - `std` (enabled by default): This is currently unused and is provided for forward compatibility.
//!
//! [parley]: https://docs.rs/parley

// LINEBENDER LINT SET - lib.rs - v3
// See https://linebender.org/wiki/canonical-lints/
Expand Down
10 changes: 8 additions & 2 deletions parley_engine/src/shape/atom.rs
Original file line number Diff line number Diff line change
Expand Up @@ -858,10 +858,16 @@ mod tests {
language: None,
features: &[],
variations: &[],
char_style_indices: &char_style_indices,
},
}];
shaper.shape_text(text, &analysis, items, SingleFont(font), &mut shaped);
shaper.shape_text(
text,
&analysis,
&char_style_indices,
items,
SingleFont(font),
&mut shaped,
);
shaped
}

Expand Down
49 changes: 21 additions & 28 deletions parley_engine/src/shape/shaped_text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,7 @@ impl ShapedText {
item: &Segment,
options: &ShapeOptions<'_>,
char_info: &[CharInfo],
char_style_indices: &[u16],
font: &FontInstance,
glyph_buffer: &harfrust::GlyphBuffer,
normalized_coords: &[harfrust::NormalizedCoord],
Expand Down Expand Up @@ -318,7 +319,7 @@ impl ShapedText {
for (((byte_offset, ch), info), style_index) in text[range.byte_range.clone()]
.char_indices()
.zip(&char_info[range.char_range.clone()])
.zip(&options.char_style_indices[range.char_range.clone()])
.zip(&char_style_indices[range.char_range.clone()])
{
self.characters.push(Character {
text_byte_start: (range.byte_range.start + byte_offset) as u32,
Expand All @@ -340,7 +341,6 @@ impl ShapedText {
scale_factor,
glyph_infos.iter(),
glyph_positions.iter(),
&options.char_style_indices[range.char_range.clone()],
&self.characters,
characters_start,
);
Expand All @@ -351,7 +351,6 @@ impl ShapedText {
scale_factor,
glyph_infos.iter().rev(),
glyph_positions.iter().rev(),
&options.char_style_indices[range.char_range.clone()],
&self.characters,
characters_start,
);
Expand Down Expand Up @@ -429,7 +428,6 @@ pub struct ShapedRun {
/// * `glyph_infos` - `HarfRust` glyph information in logical order (i.e., reversed for RTL runs).
/// * `glyph_positions` - `HarfRust` glyph positioning data in logical order (i.e., reversed for RTL
/// runs).
/// * `char_style_indices` - The run's slice of per-character style indices, indexed by cluster ID.
/// * `characters` must contain the shaped characters whose clusters we're now processing, starting at
/// index `characters_start`.
/// * `characters_start` - See `characters`.
Expand All @@ -439,7 +437,6 @@ fn process_shaped_clusters<'a>(
scale_factor: f32,
glyph_infos: impl Iterator<Item = &'a harfrust::GlyphInfo>,
glyph_positions: impl Iterator<Item = &'a harfrust::GlyphPosition>,
char_style_indices: &[u16],
characters: &[Character],
characters_start: usize,
) {
Expand All @@ -457,7 +454,6 @@ fn process_shaped_clusters<'a>(
fn flush(
cluster: &mut Cluster,
char_end: usize,
style_index: u16,
characters: &[Character],
shaped_clusters: &mut Vec<ShapedCluster>,
) {
Expand All @@ -479,7 +475,7 @@ fn process_shaped_clusters<'a>(

shaped_clusters.push(ShapedCluster {
chars_range: (cluster.characters_start as u32, char_end as u32),
style_index,
style_index: first_character.style_index,
flags: ShapedClusterFlags::new(glyph_len)
.with_grapheme_start(first_character.grapheme_start)
// TODO: fill with actual shaping data (`parley` currently just ignores this)
Expand All @@ -502,14 +498,7 @@ fn process_shaped_clusters<'a>(
for (glyph_info, glyph_pos) in glyph_infos.zip(glyph_positions) {
if glyph_info.cluster != cluster.id {
let char_end = characters_start + glyph_info.cluster as usize;
let style_index = char_style_indices[cluster.id as usize];
flush(
&mut cluster,
char_end,
style_index,
characters,
shaped_clusters,
);
flush(&mut cluster, char_end, characters, shaped_clusters);

cluster = Cluster {
id: glyph_info.cluster,
Expand Down Expand Up @@ -542,14 +531,7 @@ fn process_shaped_clusters<'a>(
}

// Flush the final cluster.
let style_index = char_style_indices[cluster.id as usize];
flush(
&mut cluster,
characters.len(),
style_index,
characters,
shaped_clusters,
);
flush(&mut cluster, characters.len(), characters, shaped_clusters);
}

#[cfg(test)]
Expand Down Expand Up @@ -621,10 +603,16 @@ mod tests {
language: None,
features: &[],
variations: &[],
char_style_indices: &char_style_indices,
},
}];
shaper.shape_text(text, &analysis, items, SingleFont(font), &mut shaped);
shaper.shape_text(
text,
&analysis,
&char_style_indices,
items,
SingleFont(font),
&mut shaped,
);
shaped
}

Expand Down Expand Up @@ -732,7 +720,6 @@ mod tests {
language: None,
features: &[],
variations: &[],
char_style_indices: &char_style_indices,
},
},
Item {
Expand All @@ -742,11 +729,17 @@ mod tests {
language: None,
features: &[],
variations: &[],
char_style_indices: &char_style_indices,
},
},
];
shaper.shape_text(text, &analysis, items, SingleFont(font), &mut shaped);
shaper.shape_text(
text,
&analysis,
&char_style_indices,
items,
SingleFont(font),
&mut shaped,
);

let grapheme_starts: Vec<bool> =
shaped.characters.iter().map(|c| c.grapheme_start).collect();
Expand Down
Loading
Loading