diff --git a/CHANGELOG.md b/CHANGELOG.md index cd56e8205..a842f4482 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ Subheadings to categorize changes are `added, changed, deprecated, removed, fixe This release has an [MSRV] of 1.88. +### Added + +#### Parlance + +- `BidiLevel` to encode bidirectional text embedding levels. ([#710][] by [@tomcur][]) + ### Changed #### Parley @@ -704,6 +710,7 @@ This release has an [MSRV][] of 1.70. [#661]: https://github.com/linebender/parley/pull/661 [#671]: https://github.com/linebender/parley/pull/671 [#697]: https://github.com/linebender/parley/pull/697 +[#710]: https://github.com/linebender/parley/pull/710 [Unreleased]: https://github.com/linebender/parley/compare/v0.11.0...HEAD [0.11.0]: https://github.com/linebender/parley/compare/v0.10.0...v0.11.0 diff --git a/Cargo.lock b/Cargo.lock index 068c73682..b8d245942 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2894,6 +2894,7 @@ name = "parley" version = "0.11.0" dependencies = [ "accesskit", + "bytemuck", "core_maths", "fontique", "hashbrown 0.17.1", diff --git a/parlance/src/bidi.rs b/parlance/src/bidi.rs index fe1a8ac05..27210dca6 100644 --- a/parlance/src/bidi.rs +++ b/parlance/src/bidi.rs @@ -38,3 +38,76 @@ pub enum BidiOverride { /// Force right-to-left. Rtl, } + +/// Bidirectional text embedding level. +/// +/// These are numbers indicating how deeply bidirectional embeddings are nested in the text, and the +/// default direction of text on that level. Even levels are left-to-right, odd levels are +/// right-to-left. Normally, the minimum level is 0 (left-to-right), and the maximum level, +/// according to [UAX #9 § 3.1.1 BD2][uax-bd2], is 125. +/// +/// See [UAX #9 § 3.1][uax-definitions] for more information. +/// +/// [uax-definitions]: https://unicode.org/reports/tr9/#Definitions +/// [uax-bd2]: https://unicode.org/reports/tr9/#BD2 +/// +// NOTICE: If the representation changes, be sure to check the `bytemuck` marker trait +// implementations. +// +// TODO: it would be quite nice for this to implement +// , once stabilized. +#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)] +#[repr(transparent)] +pub struct BidiLevel(u8); + +impl BidiLevel { + /// The maximum bidirectional text embedding level, according to [UAX #9 § 3.1.1 BD2][uax-bd2]. + /// + /// It is possible for `BidiLevel` to encode greater values; in particular, `unsafe` code **must + /// not** rely on `BidiLevel` never being greater than this. + /// + /// [uax-bd2]: https://unicode.org/reports/tr9/#BD2 + pub const MAX: Self = Self(125); + + /// Construct a new bidi level. + #[inline(always)] + pub const fn new(level: u8) -> Self { + Self(level) + } + + /// Get the numeric bidi level. + #[inline(always)] + pub const fn to_u8(self) -> u8 { + self.0 + } + + /// Whether this level is left-to-right. + #[inline(always)] + pub const fn is_ltr(self) -> bool { + self.0.is_multiple_of(2) + } + + /// Whether this level is right-to-left. + #[inline(always)] + pub const fn is_rtl(self) -> bool { + !self.is_ltr() + } + + /// Get the next odd bidi level. + /// + /// When the return value overflows (`self.to_u8() >= 255`) this panics when overflow checks are + /// enabled. Otherwise, the return value wraps. + #[inline(always)] + pub const fn next_odd(self) -> Self { + Self::new((self.to_u8() + 1) | 1) + } + + /// Get the next even bidi level. + /// + /// When the return value overflows (`self.to_u8() >= 254`) this panics when overflow checks are + /// enabled. Otherwise, the return value wraps. + #[inline(always)] + pub const fn next_even(self) -> Self { + Self::new((self.to_u8() + 2) & !1) + } +} diff --git a/parlance/src/impl_bytemuck.rs b/parlance/src/impl_bytemuck.rs index 318cf8e07..b23ceca32 100644 --- a/parlance/src/impl_bytemuck.rs +++ b/parlance/src/impl_bytemuck.rs @@ -8,8 +8,8 @@ reason = "The `bytemuck` marker traits are `unsafe` and require `unsafe impl`." )] -use crate::GenericFamily; -use bytemuck::{Contiguous, NoUninit, Zeroable, checked::CheckedBitPattern}; +use crate::{BidiLevel, GenericFamily}; +use bytemuck::{Contiguous, NoUninit, Pod, Zeroable, checked::CheckedBitPattern}; // Safety: The enum is `repr(u8)` and has only fieldless variants. unsafe impl NoUninit for GenericFamily {} @@ -39,12 +39,21 @@ unsafe impl Contiguous for GenericFamily { const MAX_VALUE: u8 = GenericFamily::MAX_VALUE; } +// Safety: The struct is `repr(transparent)`, wrapping a `u8`. +// +// While generally BidiLevels have a maximum of 125, no value is unsound. +unsafe impl Pod for BidiLevel {} + +// Safety: The struct is `repr(transparent)`, wrapping a `u8`. +unsafe impl Zeroable for BidiLevel {} + #[cfg(test)] mod tests { - use super::GenericFamily; use bytemuck::{Contiguous, Zeroable, checked::try_from_bytes}; use core::ptr; + use super::{BidiLevel, GenericFamily}; + #[test] fn checked_bit_pattern() { let valid = bytemuck::bytes_of(&2_u8); @@ -86,6 +95,16 @@ mod tests { value += 1; } }; + + /// Tests that [`BidiLevel`] is one byte. + /// + /// That may catch its representation changing, in which case the implementations here + /// definitely need revisiting. + const _: () = { + if size_of::() != 1 { + panic!("`BidiLevel` is not one byte"); + } + }; } #[cfg(doctest)] diff --git a/parlance/src/lib.rs b/parlance/src/lib.rs index 5a66fb41b..fc001d237 100644 --- a/parlance/src/lib.rs +++ b/parlance/src/lib.rs @@ -49,7 +49,7 @@ mod script; mod tag; mod text; -pub use bidi::{BidiControl, BidiDirection, BidiOverride}; +pub use bidi::{BidiControl, BidiDirection, BidiLevel, BidiOverride}; pub use font::{FontStyle, FontWeight, FontWidth}; pub use font_family::{FontFamily, FontFamilyName, ParseFontFamilyError, ParseFontFamilyErrorKind}; pub use generic_family::GenericFamily; diff --git a/parley/Cargo.toml b/parley/Cargo.toml index f538392e3..0397a5e0f 100644 --- a/parley/Cargo.toml +++ b/parley/Cargo.toml @@ -38,9 +38,11 @@ accesskit = { workspace = true, optional = true } hashbrown = { workspace = true } [dev-dependencies] +parlance = { workspace = true, features = ["bytemuck"] } parley_dev = { workspace = true } peniko = { workspace = true } +bytemuck = { workspace = true } icu_properties = { workspace = true, features = ["compiled_data"] } # We special-case android targets because oxipng doesn't build in Android CI. diff --git a/parley/src/layout/alignment.rs b/parley/src/layout/alignment.rs index b1a0ff50f..392b99c8b 100644 --- a/parley/src/layout/alignment.rs +++ b/parley/src/layout/alignment.rs @@ -96,8 +96,7 @@ fn align_impl( alignment: Alignment, options: AlignmentOptions, ) { - // Whether the text base direction is right-to-left. - let is_rtl = layout.base_level & 1 == 1; + let is_rtl = layout.base_level.is_rtl(); // Apply alignment to line items for line in &mut layout.lines { @@ -170,9 +169,8 @@ fn align_impl( .for_each(|line_item| { let clusters = &mut layout.shaped_text.clusters_mut()[line_item.cluster_range.clone()]; - let line_item_is_rtl = line_item.bidi_level & 1 != 0; let clusters: &mut dyn Iterator = - if line_item_is_rtl { + if line_item.bidi_level.is_rtl() { &mut clusters.iter_mut().rev() } else { &mut clusters.iter_mut() diff --git a/parley/src/layout/data.rs b/parley/src/layout/data.rs index c939ca9f8..c3bda5c93 100644 --- a/parley/src/layout/data.rs +++ b/parley/src/layout/data.rs @@ -10,6 +10,7 @@ use crate::{IndentOptions, InlineBoxKind, LineHeight, OverflowWrap, TextWrapMode use core::ops::Range; use alloc::vec::Vec; +use parlance::BidiLevel; use parley_engine::shape::ClusterData; use parley_engine::{Boundary, ShapedText}; @@ -68,7 +69,7 @@ pub(crate) struct LineItemData { /// The index of the run or inline box in the runs or `inline_boxes` vec pub(crate) index: usize, /// Bidi level for the item (used for reordering) - pub(crate) bidi_level: u8, + pub(crate) bidi_level: BidiLevel, /// Advance (size in direction of text flow) for the run. pub(crate) advance: f32, @@ -91,7 +92,7 @@ impl LineItemData { #[inline(always)] pub(crate) fn is_rtl(&self) -> bool { - self.bidi_level & 1 != 0 + self.bidi_level.is_rtl() } /// If the item is a text run @@ -143,7 +144,7 @@ pub(crate) struct LayoutItem { /// The index of the run or inline box in the runs or `inline_boxes` vec pub(crate) index: usize, /// Bidi level for the item (used for reordering) - pub(crate) bidi_level: u8, + pub(crate) bidi_level: BidiLevel, } #[derive(Clone, Debug, PartialEq)] @@ -154,7 +155,7 @@ pub(crate) struct LayoutData { /// Whether metrics should be quantized to pixel boundaries pub(crate) quantize: bool, /// The `BiDi` base level - pub(crate) base_level: u8, + pub(crate) base_level: BidiLevel, /// The length of the text in the layout pub(crate) text_len: usize, @@ -199,7 +200,7 @@ impl Default for LayoutData { Self { scale: 1., quantize: true, - base_level: 0, + base_level: BidiLevel::new(0), text_len: 0, width: 0., full_width: 0., @@ -225,7 +226,7 @@ impl LayoutData { pub(crate) fn clear(&mut self) { self.scale = 1.; self.quantize = true; - self.base_level = 0; + self.base_level = BidiLevel::new(0); self.text_len = 0; self.width = 0.; self.full_width = 0.; @@ -248,7 +249,7 @@ impl LayoutData { .runs() .last() .map(|r| r.bidi_level) - .unwrap_or(0); + .unwrap_or(BidiLevel::new(0)); self.items.push(LayoutItem { kind: LayoutItemKind::InlineBox, @@ -353,7 +354,7 @@ impl LayoutData { let mut running_max_width = 0.0; let mut text_wrap_mode = TextWrapMode::Wrap; let mut prev_cluster: Option<&ClusterData> = None; - let is_rtl = self.base_level & 1 == 1; + let is_rtl = self.base_level.is_rtl(); for item in &self.items { match item.kind { LayoutItemKind::TextRun => { diff --git a/parley/src/layout/layout.rs b/parley/src/layout/layout.rs index 7e4836198..db0c73684 100644 --- a/parley/src/layout/layout.rs +++ b/parley/src/layout/layout.rs @@ -124,7 +124,7 @@ impl Layout { /// Returns `true` if the dominant direction of the layout is right-to-left. pub fn is_rtl(&self) -> bool { - self.data.base_level & 1 != 0 + self.data.base_level.is_rtl() } pub fn inline_boxes(&self) -> &[InlineBox] { diff --git a/parley/src/layout/line_break.rs b/parley/src/layout/line_break.rs index e33fc1026..9f636d7c4 100644 --- a/parley/src/layout/line_break.rs +++ b/parley/src/layout/line_break.rs @@ -8,6 +8,7 @@ use alloc::vec::Vec; #[cfg(feature = "libm")] #[allow(unused_imports)] use core_maths::CoreFloat; +use parlance::BidiLevel; use crate::layout::{ BreakReason, Layout, LayoutData, LayoutItem, LayoutItemKind, LineData, LineItemData, @@ -1164,7 +1165,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { // Mark line as needing bidi re-ordering if it contains any runs with non-zero bidi level // (zero is the default level, so this is equivalent to marking lines that have multiple levels) - if line_item.bidi_level != 0 { + if line_item.bidi_level != BidiLevel::new(0) { needs_reorder = true; } @@ -1260,7 +1261,7 @@ impl<'a, B: Brush> BreakLines<'a, B> { self.lines.line_items.push(LineItemData { kind: LayoutItemKind::TextRun, index, - bidi_level: 0, + bidi_level: BidiLevel::new(0), advance: 0., is_whitespace: false, has_trailing_whitespace: false, @@ -1512,16 +1513,16 @@ fn reorder_line_items(runs: &mut [LineItemData]) { let mut lowest_odd_level = 255; for run in runs.iter() { let level = run.bidi_level; - let is_odd = level & 1 != 0; + let is_odd = level.to_u8() & 1 != 0; // Update max level - if level > max_level { - max_level = level; + if level.to_u8() > max_level { + max_level = level.to_u8(); } // Update min odd level - if is_odd && level < lowest_odd_level { - lowest_odd_level = level; + if is_odd && level.to_u8() < lowest_odd_level { + lowest_odd_level = level.to_u8(); } } @@ -1530,9 +1531,9 @@ fn reorder_line_items(runs: &mut [LineItemData]) { // Iterate over text runs let mut i = 0; while i < run_count { - if runs[i].bidi_level >= level { + if runs[i].bidi_level.to_u8() >= level { let mut end = i + 1; - while end < run_count && runs[end].bidi_level >= level { + while end < run_count && runs[end].bidi_level.to_u8() >= level { end += 1; } diff --git a/parley/src/layout/run.rs b/parley/src/layout/run.rs index 72638c684..f20d92968 100644 --- a/parley/src/layout/run.rs +++ b/parley/src/layout/run.rs @@ -110,7 +110,7 @@ impl<'a, B: Brush> Run<'a, B> { /// Returns `true` if the run has right-to-left directionality. pub fn is_rtl(&self) -> bool { - self.shaped.bidi_level & 1 != 0 + self.shaped.bidi_level.is_rtl() } /// Returns the cluster range for the run. diff --git a/parley/src/tests/test_analysis.rs b/parley/src/tests/test_analysis.rs index 7084d9c3a..77db69c6c 100644 --- a/parley/src/tests/test_analysis.rs +++ b/parley/src/tests/test_analysis.rs @@ -26,9 +26,13 @@ impl TestContext { self } - fn expect_bidi_embed_level_list(self, expected: Vec) -> Self { + fn expect_bidi_embed_level_list(self, expected: &[u8]) -> Self { let actual = self.layout_context.analysis.bidi_levels(); - assert_eq!(actual, expected, "Bidi embed level list mismatch"); + assert_eq!( + bytemuck::cast_slice::<_, u8>(actual), + expected, + "Bidi embed level list mismatch" + ); self } @@ -231,7 +235,7 @@ fn test_latin_mixed_keep_all_last() { builder.push(StyleProperty::WordBreak(WordBreak::KeepAll), 1..2); }) .expect_boundary_list(vec![Boundary::Word, Boundary::None]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![Script::Latin, Script::Latin]) .expect_grapheme_cluster_break_list(vec![ GraphemeClusterBreak::Other, @@ -256,7 +260,7 @@ fn test_mandatory_break_in_text() { Boundary::Word, Boundary::Mandatory, ]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![ Script::Latin, Script::Latin, @@ -308,7 +312,7 @@ fn test_paragraph_separator_is_hard_break() { fn test_blank() { verify_analysis("", |_| {}) .expect_boundary_list(vec![Boundary::Word]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![Script::Common]) .expect_grapheme_cluster_break_list(vec![GraphemeClusterBreak::Other]) .expect_is_control_list(vec![false]) @@ -372,7 +376,7 @@ fn test_latin_trailing_space_mixed() { builder.push(StyleProperty::WordBreak(WordBreak::Normal), 1..3); }) .expect_boundary_list(vec![Boundary::Word, Boundary::None, Boundary::Word]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![Script::Latin, Script::Latin, Script::Common]); } @@ -383,7 +387,7 @@ fn test_latin_leading_space_mixed() { builder.push(StyleProperty::WordBreak(WordBreak::Normal), 1..3); }) .expect_boundary_list(vec![Boundary::Word, Boundary::Line, Boundary::None]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![Script::Common, Script::Latin, Script::Latin]); } @@ -409,7 +413,7 @@ fn test_latin_mixed_break_all_first() { fn test_all_whitespace() { verify_analysis(" ", |_| {}) .expect_boundary_list(vec![Boundary::Word, Boundary::None, Boundary::None]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![Script::Common, Script::Common, Script::Common]); } @@ -549,7 +553,7 @@ fn test_mixed_ltr_rtl() { Boundary::None, Boundary::None, ]) - .expect_bidi_embed_level_list(vec![0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) + .expect_bidi_embed_level_list(&[0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) .expect_script_list(vec![ Script::Latin, Script::Latin, @@ -682,7 +686,7 @@ fn test_mixed_ltr_rtl_nested_embedding() { Boundary::Word, Boundary::Word, ]) - .expect_bidi_embed_level_list(vec![ + .expect_bidi_embed_level_list(&[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1, 0, 0, 0, ]) .expect_script_list(vec![ @@ -760,7 +764,7 @@ fn test_multi_char_grapheme_mixed_break_all() { Boundary::Word, Boundary::Line, ]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![ Script::Latin, Script::Common, @@ -849,7 +853,7 @@ fn test_mixed_ltr_rtl_multiple_segments() { Boundary::None, Boundary::None, ]) - .expect_bidi_embed_level_list(vec![ + .expect_bidi_embed_level_list(&[ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, ]) @@ -944,7 +948,7 @@ fn test_multi_char_grapheme_mixed_break_and_keep_all() { Boundary::Word, Boundary::Line, ]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![ Script::Latin, Script::Common, @@ -983,7 +987,7 @@ fn test_multi_char_grapheme_mixed_keep_all() { Boundary::Word, Boundary::Line, ]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![ Script::Latin, Script::Common, @@ -1034,7 +1038,7 @@ fn test_multi_paragraph_bidi() { Boundary::None, Boundary::None, ]) - .expect_bidi_embed_level_list(vec![ + .expect_bidi_embed_level_list(&[ 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, ]) .expect_script_list(vec![ @@ -1120,7 +1124,7 @@ fn test_rtl_paragraph_with_non_authoritative_logical_first_char_two_paragraphs() Boundary::None, Boundary::Word, ]) - .expect_bidi_embed_level_list(vec![1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) + .expect_bidi_embed_level_list(&[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]) .expect_script_list(vec![ Script::Arabic, Script::Arabic, @@ -1172,7 +1176,7 @@ fn test_single_char_multi_byte() { builder.push(StyleProperty::WordBreak(WordBreak::KeepAll), 0..3); }) .expect_boundary_list(vec![Boundary::Word]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![Script::Common]) .expect_grapheme_cluster_break_list(vec![GraphemeClusterBreak::Other]); } @@ -1187,7 +1191,7 @@ fn test_rtl_paragraph_with_non_authoritative_logical_first_character() { Boundary::None, Boundary::Word, ]) - .expect_bidi_embed_level_list(vec![1, 1, 1, 1, 1]) + .expect_bidi_embed_level_list(&[1, 1, 1, 1, 1]) .expect_script_list(vec![ Script::Arabic, Script::Arabic, @@ -1211,7 +1215,7 @@ fn test_rtl_paragraph_with_non_authoritative_logical_first_character() { fn test_two_newlines() { verify_analysis("\n\n", |_| {}) .expect_boundary_list(vec![Boundary::Word, Boundary::Mandatory]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![Script::Common, Script::Common]) .expect_grapheme_cluster_break_list(vec![ GraphemeClusterBreak::LF, @@ -1223,7 +1227,7 @@ fn test_two_newlines() { fn test_newline() { verify_analysis("\n", |_| {}) .expect_boundary_list(vec![Boundary::Word]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![Script::Common]) .expect_grapheme_cluster_break_list(vec![GraphemeClusterBreak::LF]); } @@ -1234,7 +1238,7 @@ fn test_two_chars_keep_all() { builder.push(StyleProperty::WordBreak(WordBreak::KeepAll), 0..2); }) .expect_boundary_list(vec![Boundary::Word, Boundary::None]) - .expect_bidi_embed_level_list(vec![]) + .expect_bidi_embed_level_list(&[]) .expect_script_list(vec![Script::Latin, Script::Latin]); } diff --git a/parley_engine/src/analysis.rs b/parley_engine/src/analysis.rs index 24e4a93a9..00b155eeb 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::{BaseDirection, WordBreak}; +use parlance::{BaseDirection, BidiLevel, WordBreak}; use parley_data::Properties; use crate::bidi; @@ -38,10 +38,10 @@ pub struct Analysis { /// Bidi level for each character, parallel to `info`. /// /// Empty if the text is all LTR. - pub(crate) levels: Vec, + pub(crate) levels: Vec, /// The base bidi level of the paragraph of text. - pub(crate) paragraph_level: u8, + pub(crate) paragraph_level: BidiLevel, } impl Analysis { @@ -57,7 +57,7 @@ impl Analysis { pub(crate) fn clear(&mut self) { self.info.clear(); self.levels.clear(); - self.paragraph_level = 0; + self.paragraph_level = BidiLevel::new(0); } /// The per-character info in source order. @@ -70,21 +70,15 @@ impl Analysis { /// /// Empty when the whole paragraph is left-to-right. #[inline(always)] - pub fn bidi_levels(&self) -> &[u8] { + pub fn bidi_levels(&self) -> &[BidiLevel] { &self.levels } /// The base bidi level of the paragraph of text. #[inline(always)] - pub fn paragraph_level(&self) -> u8 { + pub fn paragraph_level(&self) -> BidiLevel { 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. diff --git a/parley_engine/src/analyzer.rs b/parley_engine/src/analyzer.rs index 810d0aaa3..e646ed4b6 100644 --- a/parley_engine/src/analyzer.rs +++ b/parley_engine/src/analyzer.rs @@ -68,6 +68,8 @@ impl core::fmt::Debug for AnalysisOptions<'_> { #[cfg(test)] mod tests { + use parlance::BidiLevel; + use super::{AnalysisOptions, Analyzer}; use crate::{Analysis, BaseDirection}; @@ -91,23 +93,19 @@ mod tests { let auto = analyze(text, BaseDirection::Auto); let rtl = analyze(text, BaseDirection::Rtl); - assert_eq!(auto.paragraph_level(), 0); - assert!(!auto.is_rtl()); + assert_eq!(auto.paragraph_level(), BidiLevel::new(0)); + assert!(auto.paragraph_level().is_ltr()); assert!(auto.bidi_levels().is_empty()); - assert_eq!(rtl.paragraph_level(), 1); - assert!(rtl.is_rtl()); + assert_eq!(rtl.paragraph_level(), BidiLevel::new(1)); + assert!(rtl.paragraph_level().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!(level.is_ltr()); } } - assert!( - rtl.bidi_levels() - .iter() - .any(|level| !level.is_multiple_of(2)) - ); + assert!(rtl.bidi_levels().iter().any(|level| level.is_rtl())); } #[test] @@ -116,8 +114,8 @@ mod tests { let auto = analyze(text, BaseDirection::Auto); let ltr = analyze(text, BaseDirection::Ltr); - assert!(auto.is_rtl()); - assert!(!ltr.is_rtl()); + assert!(auto.paragraph_level().is_rtl()); + assert!(ltr.paragraph_level().is_ltr()); assert_ne!(auto.bidi_levels(), ltr.bidi_levels()); } @@ -125,20 +123,15 @@ mod tests { 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)) - ); + assert!(analysis.paragraph_level().is_rtl()); + assert!(analysis.bidi_levels().iter().all(|level| level.is_ltr())); } #[test] fn explicit_direction_applies_to_empty_text() { let analysis = analyze("", BaseDirection::Rtl); - assert!(analysis.is_rtl()); + assert!(analysis.paragraph_level().is_rtl()); assert!(analysis.bidi_levels().is_empty()); } } diff --git a/parley_engine/src/bidi.rs b/parley_engine/src/bidi.rs index 85ddbf306..5d0884e51 100644 --- a/parley_engine/src/bidi.rs +++ b/parley_engine/src/bidi.rs @@ -5,10 +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; +use parlance::{BaseDirection, BidiLevel}; /// Resolver for the Unicode bidirectional algorithm. #[derive(Clone, Default)] @@ -37,7 +34,7 @@ impl BidiResolver { /// Creates a new resolver. pub fn new() -> Self { Self { - base_level: 0, + base_level: BidiLevel::new(0), levels: Vec::new(), initial_types: Vec::new(), types: Vec::new(), @@ -50,7 +47,7 @@ impl BidiResolver { } /// Returns the base level of the text. - pub fn base_level(&self) -> u8 { + pub fn base_level(&self) -> BidiLevel { self.base_level } @@ -68,7 +65,7 @@ impl BidiResolver { self.brackets.clear(); self.bracket_pairs.clear(); self.flags = 0; - self.base_level = 0; + self.base_level = BidiLevel::new(0); } /// Resolves a paragraph with the specified base direction and @@ -93,10 +90,10 @@ impl BidiResolver { } self.base_level = match base_direction { BaseDirection::Auto => Self::default_level(&self.initial_types), - BaseDirection::Ltr => 0, - BaseDirection::Rtl => 1, + BaseDirection::Ltr => BidiLevel::new(0), + BaseDirection::Rtl => BidiLevel::new(1), }; - if !needs_bidi && self.base_level == 0 { + if !needs_bidi && self.base_level == BidiLevel::new(0) { self.flags |= 1; self.levels.resize(len, self.base_level); return; @@ -130,7 +127,7 @@ impl BidiResolver { None => break, }; } - self.resolve_sequence(level, sos, eos, self.indices.len()); + self.resolve_sequence(level.to_u8(), sos, eos, self.indices.len()); } for i in 0..len { let t = self.initial_types[i]; @@ -173,7 +170,7 @@ impl BidiResolver { } } - fn default_level(types: &[BidiClass]) -> u8 { + fn default_level(types: &[BidiClass]) -> BidiLevel { let mut isolates = 0; for ty in types { let ty = *ty; @@ -185,12 +182,16 @@ impl BidiResolver { BidiClass::LeftToRight | BidiClass::RightToLeft | BidiClass::ArabicLetter if isolates == 0 => { - return if ty == BidiClass::LeftToRight { 0 } else { 1 }; + return if ty == BidiClass::LeftToRight { + BidiLevel::new(0) + } else { + BidiLevel::new(1) + }; } _ => {} } } - 0 + BidiLevel::new(0) } fn default_level_until_pdi(types: &[BidiClass]) -> u8 { @@ -223,7 +224,7 @@ impl BidiResolver { let base = self.base_level; let len = self.types.len(); self.levels.clear(); - self.levels.resize(len, 0); + self.levels.resize(len, BidiLevel::new(0)); let mut stack = Stack::new(); let mut overflow_isolates = 0; let mut overflow_embedding = 0; @@ -247,11 +248,12 @@ impl BidiResolver { } } let new_level = if is_rtl { - (stack.embedding_level() + 1) | 1 + stack.embedding_level().next_odd() } else { - (stack.embedding_level() + 2) & !1 + stack.embedding_level().next_even() }; - if new_level <= MAX_STACK && overflow_isolates == 0 && overflow_embedding == 0 { + if new_level <= BidiLevel::MAX && overflow_isolates == 0 && overflow_embedding == 0 + { if is_isolate { valid_isolates += 1; } @@ -655,11 +657,11 @@ impl BidiResolver { let index = self.indices[i]; let t = types[i]; if t == BidiClass::RightToLeft { - self.levels[index] = level + 1; + self.levels[index] = BidiLevel::new(level + 1); } else if t != BidiClass::LeftToRight { - self.levels[index] = level + 2; + self.levels[index] = BidiLevel::new(level + 2); } else { - self.levels[index] = level; + self.levels[index] = BidiLevel::new(level); } } } else { @@ -668,9 +670,9 @@ impl BidiResolver { let index = self.indices[i]; let t = types[i]; if t != BidiClass::RightToLeft { - self.levels[index] = level + 1; + self.levels[index] = BidiLevel::new(level + 1); } else { - self.levels[index] = level; + self.levels[index] = BidiLevel::new(level); } } } @@ -679,7 +681,7 @@ impl BidiResolver { /// Returns a default bidi type for a level. pub(crate) fn type_from_level(level: BidiLevel) -> BidiClass { - if level & 1 == 0 { + if level.is_ltr() { BidiClass::LeftToRight } else { BidiClass::RightToLeft @@ -689,7 +691,7 @@ pub(crate) fn type_from_level(level: BidiLevel) -> BidiClass { /// Computes an ordering for a sequence of bidi runs based on levels. pub(crate) fn _reorder(order: &mut [usize], levels: F) where - F: Fn(usize) -> BidiLevel, + F: Fn(usize) -> u8, { let mut max_level = 0; let mut lowest_odd_level = 255; @@ -788,7 +790,7 @@ fn find_limit_by_mask(types: &[BidiClass], offset: usize, mask: u32) -> usize { #[derive(Clone)] struct Run { - level: u8, + level: BidiLevel, ends_with_isolate: bool, starts_with_pdi: bool, sos: BidiClass, @@ -800,7 +802,7 @@ struct Run { } impl Run { - fn new(level: u8, start: usize, end: usize) -> Self { + fn new(level: BidiLevel, start: usize, end: usize) -> Self { Self { level, ends_with_isolate: false, @@ -815,12 +817,12 @@ impl Run { } } -const MAX_STACK: u8 = 125; +const MAX_STACK: usize = BidiLevel::MAX.to_u8() as usize; struct Stack { - embedding_level: [u8; MAX_STACK as usize + 1], - override_status: [BidiClass; MAX_STACK as usize + 1], - isolate_status: [bool; MAX_STACK as usize + 1], + embedding_level: [BidiLevel; MAX_STACK + 1], + override_status: [BidiClass; MAX_STACK + 1], + isolate_status: [bool; MAX_STACK + 1], depth: usize, } @@ -828,13 +830,13 @@ impl Stack { fn new() -> Self { Self { depth: 0, - embedding_level: [0; MAX_STACK as usize + 1], - override_status: [BidiClass::OtherNeutral; MAX_STACK as usize + 1], - isolate_status: [false; MAX_STACK as usize + 1], + embedding_level: [BidiLevel::new(0); MAX_STACK + 1], + override_status: [BidiClass::OtherNeutral; MAX_STACK + 1], + isolate_status: [false; MAX_STACK + 1], } } - fn push(&mut self, level: u8, override_status: BidiClass, isolate_status: bool) { + fn push(&mut self, level: BidiLevel, override_status: BidiClass, isolate_status: bool) { let d = self.depth; self.embedding_level[d] = level; self.override_status[d] = override_status; @@ -848,7 +850,7 @@ impl Stack { } } - fn embedding_level(&self) -> u8 { + fn embedding_level(&self) -> BidiLevel { self.embedding_level[self.depth - 1] } diff --git a/parley_engine/src/itemize.rs b/parley_engine/src/itemize.rs index 0fd444e7f..6ebab63fd 100644 --- a/parley_engine/src/itemize.rs +++ b/parley_engine/src/itemize.rs @@ -6,9 +6,9 @@ use core::{ops::Range, str::CharIndices}; use icu_properties::props::Script as IcuScript; -use parlance::Script; +use parlance::{BidiLevel, Script}; -use crate::{Analysis, CharInfo, bidi::BidiLevel}; +use crate::{Analysis, CharInfo}; /// A range of text. #[derive(Clone, Debug, PartialEq, Eq)] @@ -134,7 +134,7 @@ impl bool> Iterator for Itemizer<'_, F> { return None; } - let mut item_bidi_level = 0; // Initialized in the loop. + let mut item_bidi_level = BidiLevel::new(0); // Initialized in the loop. let start_byte_offset = self.char_indices.offset(); let mut item_char_len = 0; @@ -257,11 +257,11 @@ mod tests { let items = items(text); assert!(items.len() >= 2); assert_eq!(items[0].script, LATN); - assert!(items[0].bidi_level.is_multiple_of(2)); + assert!(items[0].bidi_level.is_ltr()); assert!( items .iter() - .any(|item| item.script == ARAB && !item.bidi_level.is_multiple_of(2)) + .any(|item| item.script == ARAB && item.bidi_level.is_rtl()) ); // Items tile the text contiguously. diff --git a/parley_engine/src/shape/shaped_text.rs b/parley_engine/src/shape/shaped_text.rs index 73c63cb11..cc6b5e944 100644 --- a/parley_engine/src/shape/shaped_text.rs +++ b/parley_engine/src/shape/shaped_text.rs @@ -6,6 +6,7 @@ use core::ops::Range; use alloc::vec::Vec; +use parlance::BidiLevel; use crate::{ CharInfo, FontInstance, Glyph, ShapeOptions, @@ -262,10 +263,9 @@ impl ShapedText { let glyph_positions = glyph_buffer.glyph_positions(); let scale_factor = options.font_size / units_per_em; let clusters_start = self.clusters.len(); - let is_rtl = !item.bidi_level.is_multiple_of(2); let glyphs_start = self.glyphs.len(); - if !is_rtl { + if item.bidi_level.is_ltr() { process_clusters( Direction::Ltr, &mut self.clusters, @@ -333,7 +333,7 @@ pub struct ShapedRun { /// The normalized variation coords of this run, as a range into [`ShapedText::normalized_coords`]. pub normalized_coords_range: Range, /// The bidi level of the run. - pub bidi_level: u8, + pub bidi_level: BidiLevel, /// Total advance of the run. pub advance: f32, /// The font metrics of this run. diff --git a/parley_engine/src/shape/shaper.rs b/parley_engine/src/shape/shaper.rs index 19620e1d1..e5e2bc10f 100644 --- a/parley_engine/src/shape/shaper.rs +++ b/parley_engine/src/shape/shaper.rs @@ -238,7 +238,7 @@ fn shape_item( }, ); - let direction = if item.bidi_level & 1 != 0 { + let direction = if item.bidi_level.is_rtl() { harfrust::Direction::RightToLeft } else { harfrust::Direction::LeftToRight