diff --git a/parley/src/analysis.rs b/parley/src/analysis.rs index 6ba9f747a..f23a54587 100644 --- a/parley/src/analysis.rs +++ b/parley/src/analysis.rs @@ -7,11 +7,12 @@ use parley_engine::break_overrides::LineBreakOverrideFn; use parley_engine::AnalysisOptions; -use parlance::WordBreak; +use parlance::{BaseDirection, WordBreak}; pub(crate) fn analyze_text( lcx: &mut LayoutContext, text: &str, + base_direction: BaseDirection, line_break_override: Option<&LineBreakOverrideFn>, ) { let text = if text.is_empty() { " " } else { text }; @@ -26,6 +27,7 @@ pub(crate) fn analyze_text( })); let options = AnalysisOptions { + base_direction, word_break: &lcx.word_break, line_break_override, }; diff --git a/parley/src/builder.rs b/parley/src/builder.rs index 299cbbcb7..fcdc7fb46 100644 --- a/parley/src/builder.rs +++ b/parley/src/builder.rs @@ -11,28 +11,46 @@ use super::layout::Layout; use alloc::string::String; use core::ops::{Bound, Range, RangeBounds}; +use parlance::BaseDirection; use parley_engine::break_overrides::LineBreakOverrideFn; use crate::InlineBoxKind; use crate::inline_box::InlineBox; use crate::resolve::{ResolvedStyle, StyleRun, tree::ItemKind}; +#[derive(Clone, Copy)] +pub(crate) struct BuilderOptions<'a> { + scale: f32, + quantize: bool, + base_direction: BaseDirection, + line_break_override: Option<&'a LineBreakOverrideFn>, +} + +impl BuilderOptions<'_> { + pub(crate) fn new(scale: f32, quantize: bool) -> Self { + Self { + scale, + quantize, + base_direction: BaseDirection::Auto, + line_break_override: None, + } + } +} + /// Builder for constructing a text layout with ranged attributes. #[must_use] pub struct RangedBuilder<'a, B: Brush> { - pub(crate) scale: f32, - pub(crate) quantize: bool, + pub(crate) options: BuilderOptions<'a>, pub(crate) lcx: &'a mut LayoutContext, pub(crate) fcx: &'a mut FontContext, - pub(crate) line_break_override: Option<&'a LineBreakOverrideFn>, } impl<'b, B: Brush> RangedBuilder<'b, B> { pub fn push_default<'a>(&mut self, property: impl Into>) { - let resolved = self - .lcx - .rcx - .resolve_property(self.fcx, &property.into(), self.scale); + let resolved = + self.lcx + .rcx + .resolve_property(self.fcx, &property.into(), self.options.scale); self.lcx.ranged_style_builder.push_default(resolved); } @@ -41,10 +59,10 @@ impl<'b, B: Brush> RangedBuilder<'b, B> { property: impl Into>, range: impl RangeBounds, ) { - let resolved = self - .lcx - .rcx - .resolve_property(self.fcx, &property.into(), self.scale); + let resolved = + self.lcx + .rcx + .resolve_property(self.fcx, &property.into(), self.options.scale); self.lcx.ranged_style_builder.push(resolved, range); } @@ -52,11 +70,18 @@ impl<'b, B: Brush> RangedBuilder<'b, B> { self.lcx.inline_boxes.push(inline_box); } + /// Sets the paragraph's base direction. + /// + /// The default is [`BaseDirection::Auto`], which infers the direction from the text. + pub fn set_base_direction(&mut self, base_direction: BaseDirection) { + self.options.base_direction = base_direction; + } + /// Set the callback which will be called as a first provider of line breaking decisions. /// /// See [`LineBreakOverrideFn`] for more details. pub fn set_line_break_override(&mut self, overrides: Option<&'b LineBreakOverrideFn>) { - self.line_break_override = overrides; + self.options.line_break_override = overrides; } pub fn build_into(self, layout: &mut Layout, text: impl AsRef) { @@ -66,15 +91,7 @@ impl<'b, B: Brush> RangedBuilder<'b, B> { .finish(&mut self.lcx.style_table, &mut self.lcx.style_runs); // Call generic layout builder method - build_into_layout( - layout, - self.scale, - self.quantize, - text.as_ref(), - self.lcx, - self.fcx, - self.line_break_override, - ); + build_into_layout(layout, text.as_ref(), self.lcx, self.fcx, self.options); } pub fn build(self, text: impl AsRef) -> Layout { @@ -88,13 +105,11 @@ impl<'b, B: Brush> RangedBuilder<'b, B> { /// indexed style runs. #[must_use] pub struct StyleRunBuilder<'a, B: Brush> { - pub(crate) scale: f32, - pub(crate) quantize: bool, + pub(crate) options: BuilderOptions<'a>, pub(crate) len: usize, pub(crate) lcx: &'a mut LayoutContext, pub(crate) fcx: &'a mut FontContext, pub(crate) cursor: usize, - pub(crate) line_break_override: Option<&'a LineBreakOverrideFn>, } impl<'b, B: Brush> StyleRunBuilder<'b, B> { @@ -117,7 +132,7 @@ impl<'b, B: Brush> StyleRunBuilder<'b, B> { let resolved = self .lcx .rcx - .resolve_entire_style_set(self.fcx, &style, self.scale); + .resolve_entire_style_set(self.fcx, &style, self.options.scale); let style_index = self.lcx.style_table.len(); assert!(style_index <= u16::MAX as usize, "too many styles"); self.lcx.style_table.push(resolved); @@ -153,11 +168,18 @@ impl<'b, B: Brush> StyleRunBuilder<'b, B> { self.lcx.inline_boxes.push(inline_box); } + /// Sets the paragraph's base direction. + /// + /// The default is [`BaseDirection::Auto`], which infers the direction from the text. + pub fn set_base_direction(&mut self, base_direction: BaseDirection) { + self.options.base_direction = base_direction; + } + /// Set the callback which will be called as a first provider of line breaking decisions. /// /// See [`LineBreakOverrideFn`] for more details. pub fn set_line_break_override(&mut self, overrides: Option<&'b LineBreakOverrideFn>) { - self.line_break_override = overrides; + self.options.line_break_override = overrides; } pub fn build_into(self, layout: &mut Layout, text: impl AsRef) { @@ -165,15 +187,7 @@ impl<'b, B: Brush> StyleRunBuilder<'b, B> { self.cursor == self.len, "StyleRunBuilder requires runs that cover the full text" ); - build_into_layout( - layout, - self.scale, - self.quantize, - text.as_ref(), - self.lcx, - self.fcx, - self.line_break_override, - ); + build_into_layout(layout, text.as_ref(), self.lcx, self.fcx, self.options); } pub fn build(self, text: impl AsRef) -> Layout { @@ -186,11 +200,9 @@ impl<'b, B: Brush> StyleRunBuilder<'b, B> { /// Builder for constructing a text layout with a tree of attributes. #[must_use] pub struct TreeBuilder<'a, B: Brush> { - pub(crate) scale: f32, - pub(crate) quantize: bool, + pub(crate) options: BuilderOptions<'a>, pub(crate) lcx: &'a mut LayoutContext, pub(crate) fcx: &'a mut FontContext, - pub(crate) line_break_override: Option<&'a LineBreakOverrideFn>, } impl<'b, B: Brush> TreeBuilder<'b, B> { @@ -198,7 +210,7 @@ impl<'b, B: Brush> TreeBuilder<'b, B> { let resolved = self .lcx .rcx - .resolve_entire_style_set(self.fcx, &style, self.scale); + .resolve_entire_style_set(self.fcx, &style, self.options.scale); self.lcx.tree_style_builder.push_style_span(resolved); } @@ -209,11 +221,13 @@ impl<'b, B: Brush> TreeBuilder<'b, B> { 's: 'iter, B: 'iter, { - self.lcx.tree_style_builder.push_style_modification_span( - properties - .into_iter() - .map(|p| self.lcx.rcx.resolve_property(self.fcx, p, self.scale)), - ); + self.lcx + .tree_style_builder + .push_style_modification_span(properties.into_iter().map(|p| { + self.lcx + .rcx + .resolve_property(self.fcx, p, self.options.scale) + })); } pub fn pop_style_span(&mut self) { @@ -244,11 +258,18 @@ impl<'b, B: Brush> TreeBuilder<'b, B> { .set_white_space_mode(white_space_collapse); } + /// Sets the paragraph's base direction. + /// + /// The default is [`BaseDirection::Auto`], which infers the direction from the text. + pub fn set_base_direction(&mut self, base_direction: BaseDirection) { + self.options.base_direction = base_direction; + } + /// Set the callback which will be called as a first provider of line breaking decisions. /// /// See [`LineBreakOverrideFn`] for more details. pub fn set_line_break_override(&mut self, overrides: Option<&'b LineBreakOverrideFn>) { - self.line_break_override = overrides; + self.options.line_break_override = overrides; } #[inline] @@ -260,15 +281,7 @@ impl<'b, B: Brush> TreeBuilder<'b, B> { .finish(&mut self.lcx.style_table, &mut self.lcx.style_runs); // Call generic layout builder method - build_into_layout( - layout, - self.scale, - self.quantize, - &text, - self.lcx, - self.fcx, - self.line_break_override, - ); + build_into_layout(layout, &text, self.lcx, self.fcx, self.options); text } @@ -283,12 +296,10 @@ impl<'b, B: Brush> TreeBuilder<'b, B> { fn build_into_layout( layout: &mut Layout, - scale: f32, - quantize: bool, text: &str, lcx: &mut LayoutContext, fcx: &mut FontContext, - line_break_override: Option<&LineBreakOverrideFn>, + options: BuilderOptions<'_>, ) { if text.is_empty() && lcx.style_runs.is_empty() { lcx.style_table.push(ResolvedStyle::default()); @@ -302,11 +313,16 @@ fn build_into_layout( "at least one style run is required" ); - crate::analysis::analyze_text(lcx, text, line_break_override); + crate::analysis::analyze_text( + lcx, + text, + options.base_direction, + options.line_break_override, + ); layout.data.clear(); - layout.data.scale = scale; - layout.data.quantize = quantize; + layout.data.scale = options.scale; + layout.data.quantize = options.quantize; layout.data.base_level = lcx.analysis.paragraph_level(); layout.data.text_len = text.len(); diff --git a/parley/src/context.rs b/parley/src/context.rs index c3449c4e6..15531dfb6 100644 --- a/parley/src/context.rs +++ b/parley/src/context.rs @@ -11,7 +11,7 @@ use parlance::WordBreak; use parley_engine::{Analysis, AnalysisDataSources, Analyzer, Shaper}; use super::FontContext; -use super::builder::{RangedBuilder, StyleRunBuilder}; +use super::builder::{BuilderOptions, RangedBuilder, StyleRunBuilder}; use super::resolve::tree::TreeStyleBuilder; use super::resolve::{RangedStyleBuilder, ResolveContext, ResolvedStyle, StyleRun}; use super::style::{Brush, TextStyle}; @@ -108,11 +108,9 @@ impl LayoutContext { fcx.source_cache.prune(128, false); RangedBuilder { - scale, - quantize, + options: BuilderOptions::new(scale, quantize), lcx: self, fcx, - line_break_override: None, } } @@ -136,13 +134,11 @@ impl LayoutContext { fcx.source_cache.prune(128, false); StyleRunBuilder { - scale, - quantize, + options: BuilderOptions::new(scale, quantize), len: text.len(), lcx: self, fcx, cursor: 0, - line_break_override: None, } } @@ -180,11 +176,9 @@ impl LayoutContext { fcx.source_cache.prune(128, false); TreeBuilder { - scale, - quantize, + options: BuilderOptions::new(scale, quantize), lcx: self, fcx, - line_break_override: None, } } diff --git a/parley/src/lib.rs b/parley/src/lib.rs index 6304b7761..1aa7938a4 100644 --- a/parley/src/lib.rs +++ b/parley/src/lib.rs @@ -126,6 +126,7 @@ pub mod style; mod tests; pub use linebender_resource_handle::FontData; +pub use parlance::BaseDirection; pub use parley_engine::break_overrides::{ AsciiLineBreakTable, AsciiLineBreakTableBuilder, CHROMIUM_LINE_BREAK_OVERRIDE, LineBreakContext, LineBreakOverrideFn, diff --git a/parley/src/tests/test_builders.rs b/parley/src/tests/test_builders.rs index 3443419be..9c48aefdc 100644 --- a/parley/src/tests/test_builders.rs +++ b/parley/src/tests/test_builders.rs @@ -3,7 +3,7 @@ //! Test that the various builders produce the same results. -use std::{borrow::Cow, path::PathBuf, sync::Arc}; +use std::{borrow::Cow, path::PathBuf, sync::Arc, vec::Vec}; use fontique::{Collection, CollectionOptions, FontStyle, FontWeight, FontWidth, SourceCache}; use parlance::FontFamilyName; @@ -11,9 +11,9 @@ use peniko::{Blob, color::palette}; use super::utils::{ColorBrush, asserts::assert_eq_layout_data}; use crate::{ - FontContext, FontFamily, FontFeatures, FontVariations, Layout, LayoutContext, LineHeight, - OverflowWrap, RangedBuilder, StyleProperty, StyleRunBuilder, TextStyle, TextWrapMode, - TreeBuilder, WordBreak, + BaseDirection, FontContext, FontFamily, FontFeatures, FontVariations, Layout, LayoutContext, + LineHeight, OverflowWrap, RangedBuilder, StyleProperty, StyleRunBuilder, TextStyle, + TextWrapMode, TreeBuilder, WordBreak, }; // TODO: `FONT_FAMILY_LIST`, `load_fonts`, and `create_font_context` are @@ -128,6 +128,57 @@ fn build_layout_with_style_runs( layout } +#[test] +fn builders_apply_base_direction() { + let text = "123 / 456"; + let mut fcx = create_font_context(); + let mut lcx: LayoutContext = LayoutContext::new(); + + let mut ranged = lcx.ranged_builder(&mut fcx, text, 1.0, true); + ranged.push_default(FontFamily::from(FONT_FAMILY_LIST)); + ranged.set_base_direction(BaseDirection::Rtl); + let mut ranged_layout = ranged.build(text); + ranged_layout.break_all_lines(None); + assert!(ranged_layout.is_rtl()); + assert_eq!( + ranged_layout + .lines() + .flat_map(|line| line.runs()) + .map(|run| run.text_range()) + .collect::>(), + [6..9, 3..6, 0..3] + ); + + let root_style = TextStyle { + font_family: FontFamily::from(FONT_FAMILY_LIST), + ..TextStyle::default() + }; + let mut tree = lcx.tree_builder(&mut fcx, 1.0, true, &root_style); + tree.set_base_direction(BaseDirection::Rtl); + tree.push_text(text); + let (mut tree_layout, _) = tree.build(); + tree_layout.break_all_lines(None); + assert!(tree_layout.is_rtl()); + assert_eq_layout_data( + &ranged_layout.data, + &tree_layout.data, + "tree base direction", + ); + + let mut style_runs = lcx.style_run_builder(&mut fcx, text, 1.0, true); + style_runs.set_base_direction(BaseDirection::Rtl); + let style = style_runs.push_style(root_style); + style_runs.push_style_run(style, ..); + let mut style_run_layout = style_runs.build(text); + style_run_layout.break_all_lines(None); + assert!(style_run_layout.is_rtl()); + assert_eq_layout_data( + &ranged_layout.data, + &style_run_layout.data, + "style-run base direction", + ); +} + /// Computes layout in various ways to ensure they all produce the same result. /// /// ```text diff --git a/parley_engine/src/analysis.rs b/parley_engine/src/analysis.rs index 9cb33df09..24e4a93a9 100644 --- a/parley_engine/src/analysis.rs +++ b/parley_engine/src/analysis.rs @@ -22,7 +22,7 @@ use icu_segmenter::{ GraphemeClusterSegmenter, GraphemeClusterSegmenterBorrowed, LineSegmenter, LineSegmenterBorrowed, WordSegmenter, WordSegmenterBorrowed, }; -use parlance::WordBreak; +use parlance::{BaseDirection, WordBreak}; use parley_data::Properties; use crate::bidi; @@ -79,6 +79,12 @@ impl Analysis { pub fn paragraph_level(&self) -> u8 { self.paragraph_level } + + /// Whether the paragraph's resolved base direction is right-to-left. + #[inline(always)] + pub fn is_rtl(&self) -> bool { + !self.paragraph_level.is_multiple_of(2) + } } // TODO: Make `pub(crate)` once `parley_engine` owns shaping. @@ -439,6 +445,10 @@ pub(crate) fn analyze_text( } if text.is_empty() { + analyzer + .bidi + .resolve(core::iter::empty(), options.base_direction); + analysis.paragraph_level = analyzer.bidi.base_level(); return; } @@ -665,7 +675,7 @@ pub(crate) fn analyze_text( }, ); - if needs_bidi_resolution { + if needs_bidi_resolution || options.base_direction == BaseDirection::Rtl { analyzer.bidi.resolve( text.chars().zip( analysis @@ -673,7 +683,7 @@ pub(crate) fn analyze_text( .iter() .map(|info| (info.bidi_class, info.bracket)), ), - None, + options.base_direction, ); core::mem::swap(&mut analysis.levels, &mut analyzer.bidi.levels); analysis.paragraph_level = analyzer.bidi.base_level(); diff --git a/parley_engine/src/analyzer.rs b/parley_engine/src/analyzer.rs index 42d9aa7b5..810d0aaa3 100644 --- a/parley_engine/src/analyzer.rs +++ b/parley_engine/src/analyzer.rs @@ -5,7 +5,7 @@ use core::ops::Range; -use parlance::WordBreak; +use parlance::{BaseDirection, WordBreak}; use crate::{bidi::BidiResolver, break_overrides::LineBreakOverrideFn}; @@ -39,8 +39,13 @@ impl Analyzer { } /// Options controlling [`Analyzer::analyze`]. -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Default)] pub struct AnalysisOptions<'a> { + /// The paragraph's base direction. + /// + /// Defaults to [`BaseDirection::Auto`], which infers the direction from the text. + pub base_direction: BaseDirection, + /// Word break configuration for ranges of the source text. /// /// Ranges must be sorted and non-overlapping. Gaps use [`WordBreak::Normal`]. @@ -54,6 +59,86 @@ pub struct AnalysisOptions<'a> { impl core::fmt::Debug for AnalysisOptions<'_> { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.debug_struct("AnalysisOptions").finish_non_exhaustive() + f.debug_struct("AnalysisOptions") + .field("base_direction", &self.base_direction) + .field("word_break", &self.word_break) + .finish_non_exhaustive() + } +} + +#[cfg(test)] +mod tests { + use super::{AnalysisOptions, Analyzer}; + use crate::{Analysis, BaseDirection}; + + fn analyze(text: &str, base_direction: BaseDirection) -> Analysis { + let mut analyzer = Analyzer::new(); + let mut analysis = Analysis::new(); + analyzer.analyze( + text, + &AnalysisOptions { + base_direction, + ..AnalysisOptions::default() + }, + &mut analysis, + ); + analysis + } + + #[test] + fn explicit_rtl_resolves_numeric_and_neutral_text() { + let text = "123 / 456"; + let auto = analyze(text, BaseDirection::Auto); + let rtl = analyze(text, BaseDirection::Rtl); + + assert_eq!(auto.paragraph_level(), 0); + assert!(!auto.is_rtl()); + assert!(auto.bidi_levels().is_empty()); + + assert_eq!(rtl.paragraph_level(), 1); + assert!(rtl.is_rtl()); + assert_eq!(rtl.bidi_levels().len(), text.chars().count()); + for (ch, level) in text.chars().zip(rtl.bidi_levels()) { + if ch.is_ascii_digit() { + assert!(level.is_multiple_of(2)); + } + } + assert!( + rtl.bidi_levels() + .iter() + .any(|level| !level.is_multiple_of(2)) + ); + } + + #[test] + fn explicit_ltr_takes_precedence_over_first_strong_direction() { + let text = "مرحبا hello"; + let auto = analyze(text, BaseDirection::Auto); + let ltr = analyze(text, BaseDirection::Ltr); + + assert!(auto.is_rtl()); + assert!(!ltr.is_rtl()); + assert_ne!(auto.bidi_levels(), ltr.bidi_levels()); + } + + #[test] + fn explicit_rtl_preserves_ltr_run_direction() { + let analysis = analyze("hello", BaseDirection::Rtl); + + assert!(analysis.is_rtl()); + assert!( + analysis + .bidi_levels() + .iter() + .all(|level| level.is_multiple_of(2)) + ); + } + + #[test] + fn explicit_direction_applies_to_empty_text() { + let analysis = analyze("", BaseDirection::Rtl); + + assert!(analysis.is_rtl()); + assert!(analysis.bidi_levels().is_empty()); } } diff --git a/parley_engine/src/bidi.rs b/parley_engine/src/bidi.rs index e5d632ee1..85ddbf306 100644 --- a/parley_engine/src/bidi.rs +++ b/parley_engine/src/bidi.rs @@ -5,6 +5,7 @@ use alloc::vec::Vec; use icu_properties::props::{BidiClass, BidiMirroringGlyph, BidiPairedBracketType}; +use parlance::BaseDirection; /// Type alias for a bidirectional level. pub type BidiLevel = u8; @@ -75,7 +76,7 @@ impl BidiResolver { pub fn resolve( &mut self, chars: impl Iterator, - base_level: Option, + base_direction: BaseDirection, ) { self.clear(); let mut needs_bidi = false; @@ -90,9 +91,10 @@ impl BidiResolver { needs_bidi = needs_bidi || mask(t) & BIDI_MASK != 0; len += 1; } - self.base_level = match base_level { - Some(level) => level & 1, - _ => Self::default_level(&self.initial_types), + self.base_level = match base_direction { + BaseDirection::Auto => Self::default_level(&self.initial_types), + BaseDirection::Ltr => 0, + BaseDirection::Rtl => 1, }; if !needs_bidi && self.base_level == 0 { self.flags |= 1; diff --git a/parley_engine/src/break_overrides.rs b/parley_engine/src/break_overrides.rs index ef2639911..546445098 100644 --- a/parley_engine/src/break_overrides.rs +++ b/parley_engine/src/break_overrides.rs @@ -120,9 +120,9 @@ static CHROMIUM_LINE_BREAK_TABLE: AsciiLineBreakTable<5> = /// # let mut analysis = Analysis::new(); /// let text = "Hello there!"; /// let options = AnalysisOptions { -/// word_break: &[], /// // Emulate Chromium: /// line_break_override: Some(CHROMIUM_LINE_BREAK_OVERRIDE), +/// ..AnalysisOptions::default() /// }; /// analyzer.analyze(text, &options, &mut analysis); /// ``` diff --git a/parley_engine/src/itemize.rs b/parley_engine/src/itemize.rs index f5c14e7a2..0fd444e7f 100644 --- a/parley_engine/src/itemize.rs +++ b/parley_engine/src/itemize.rs @@ -237,10 +237,7 @@ mod tests { fn analyze(text: &str) -> Analysis { let mut analyzer = Analyzer::new(); let mut analysis = Analysis::new(); - let options = AnalysisOptions { - word_break: &[], - line_break_override: None, - }; + let options = AnalysisOptions::default(); analyzer.analyze(text, &options, &mut analysis); analysis } diff --git a/parley_engine/src/lib.rs b/parley_engine/src/lib.rs index b08284f19..29507036a 100644 --- a/parley_engine/src/lib.rs +++ b/parley_engine/src/lib.rs @@ -33,6 +33,7 @@ mod lru_cache; pub mod shape; pub use linebender_resource_handle::FontData; +pub use parlance::BaseDirection; pub use analysis::{Analysis, AnalysisDataSources, Boundary, CharInfo}; pub use analyzer::{AnalysisOptions, Analyzer}; diff --git a/parley_engine/src/shape/shaped_text.rs b/parley_engine/src/shape/shaped_text.rs index 1407bac2e..73c63cb11 100644 --- a/parley_engine/src/shape/shaped_text.rs +++ b/parley_engine/src/shape/shaped_text.rs @@ -692,6 +692,7 @@ mod tests { &AnalysisOptions { word_break: &[], line_break_override: None, + ..AnalysisOptions::default() }, &mut analysis, );