From 956331e1521d18a673f63900804d3ebdfae3e7ff Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 6 Oct 2022 13:47:25 -0600 Subject: [PATCH] Improvements to bidi rendering --- examples/text/res/arabic.txt | 2 +- examples/text/res/hebrew.txt | 2 +- examples/text/src/font/matches.rs | 101 +++++++++------- examples/text/src/font/shape.rs | 192 +++++++++++++++++++----------- 4 files changed, 187 insertions(+), 110 deletions(-) diff --git a/examples/text/res/arabic.txt b/examples/text/res/arabic.txt index 621dfc1b..2bdaa884 100644 --- a/examples/text/res/arabic.txt +++ b/examples/text/res/arabic.txt @@ -1 +1 @@ - عندما يريد العالم أن ‪يتكلّم ‬ ، فهو يتحدّث بلغة يونيكود. تسجّل الآن لحضور المؤتمر الدولي العاشر ليونيكود (Unicode Conference)، الذي سيعقد في 10-12 آذار 1997 بمدينة مَايِنْتْس، ألمانيا. و سيجمع المؤتمر بين خبراء من كافة قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء مناقشة سبل استخدام يونكود في النظم القائمة وفيما يخص التطبيقات الحاسوبية، الخطوط، تصميم النصوص والحوسبة متعددة اللغات. +عندما يريد العالم أن ‪يتكلّم ‬ ، فهو يتحدّث بلغة يونيكود. تسجّل الآن لحضور المؤتمر الدولي العاشر ليونيكود (Unicode Conference)، الذي سيعقد في 10-12 آذار 1997 بمدينة مَايِنْتْس، ألمانيا. و سيجمع المؤتمر بين خبراء من كافة قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء مناقشة سبل استخدام يونكود في النظم القائمة وفيما يخص التطبيقات الحاسوبية، الخطوط، تصميم النصوص والحوسبة متعددة اللغات. diff --git a/examples/text/res/hebrew.txt b/examples/text/res/hebrew.txt index 8f8f92f4..d95d6ac0 100644 --- a/examples/text/res/hebrew.txt +++ b/examples/text/res/hebrew.txt @@ -1 +1 @@ - כאשר העולם רוצה לדבר, הוא מדבר ב־Unicode. הירשמו כעת לכנס Unicode הבינלאומי העשירי, שייערך בין התאריכים 12־10 במרץ 1997, בְּמָיְינְץ שבגרמניה. בכנס ישתתפו מומחים מכל ענפי התעשייה בנושא האינטרנט העולמי וה־Unicode, בהתאמה לשוק הבינלאומי והמקומי, ביישום Unicode במערכות הפעלה וביישומים, בגופנים, בפריסת טקסט ובמחשוב רב־לשוני. +כאשר העולם רוצה לדבר, הוא מדבר ב־Unicode. הירשמו כעת לכנס Unicode הבינלאומי העשירי, שייערך בין התאריכים 12־10 במרץ 1997, בְּמָיְינְץ שבגרמניה. בכנס ישתתפו מומחים מכל ענפי התעשייה בנושא האינטרנט העולמי וה־Unicode, בהתאמה לשוק הבינלאומי והמקומי, ביישום Unicode במערכות הפעלה וביישומים, בגופנים, בפריסת טקסט ובמחשוב רב־לשוני. diff --git a/examples/text/src/font/matches.rs b/examples/text/src/font/matches.rs index 56fb1a36..9f035e91 100644 --- a/examples/text/src/font/matches.rs +++ b/examples/text/src/font/matches.rs @@ -1,23 +1,23 @@ -use super::{Font, FontLineIndex, FontShapeGlyph, FontShapeLine, FontShapeSpan}; +use super::{Font, FontLineIndex, FontShapeGlyph, FontShapeWord, FontShapeLine, FontShapeSpan}; pub struct FontMatches<'a> { pub fonts: Vec<&'a Font<'a>>, } impl<'a> FontMatches<'a> { - fn shape_span(&self, line: &str, start_span: usize, end_span: usize) -> FontShapeSpan { - let span = &line[start_span..end_span]; + fn shape_word(&self, line: &str, start_word: usize, end_word: usize) -> FontShapeWord { + let word = &line[start_word..end_word]; - let mut spans_by_font = Vec::with_capacity(self.fonts.len()); + let mut words_by_font = Vec::with_capacity(self.fonts.len()); for (font_i, font) in self.fonts.iter().enumerate() { let font_scale = font.rustybuzz.units_per_em() as f32; let mut buffer = rustybuzz::UnicodeBuffer::new(); - buffer.push_str(span); + buffer.push_str(word); buffer.guess_segment_properties(); let direction = buffer.direction(); if font_i == 0 { - //println!("{:?}, {:?}: '{}'", script, direction, span); + println!("{:?}, {:?}: '{}'", buffer.script(), direction, word); } let glyph_buffer = rustybuzz::shape(&font.rustybuzz, &[], buffer); @@ -44,8 +44,8 @@ impl<'a> FontMatches<'a> { let inner = font.rusttype.glyph(rusttype::GlyphId(info.glyph_id as u16)); glyphs.push(FontShapeGlyph { - start: start_span + info.cluster as usize, - end: end_span, // Set later + start: start_word + info.cluster as usize, + end: end_word, // Set later x_advance, y_advance, x_offset, @@ -86,20 +86,17 @@ impl<'a> FontMatches<'a> { _ => (), } - let span = FontShapeSpan { - direction, - glyphs - }; + let word = FontShapeWord { glyphs }; if misses == 0 { - return span; + return word; } else { - spans_by_font.push((misses, span)); + words_by_font.push((misses, word)); } } let mut least_misses_i = 0; let mut least_misses = usize::MAX; - for (i, (misses, _)) in spans_by_font.iter().enumerate() { + for (i, (misses, _)) in words_by_font.iter().enumerate() { if *misses < least_misses { least_misses_i = i; least_misses = *misses; @@ -110,41 +107,65 @@ impl<'a> FontMatches<'a> { //println!("MISSES {}, {}", least_misses_i, least_misses); } - spans_by_font.remove(least_misses_i).1 + words_by_font.remove(least_misses_i).1 + } + + fn shape_span(&self, line: &str, start_span: usize, end_span: usize, rtl: bool) -> FontShapeSpan { + use unicode_script::{Script, UnicodeScript}; + + let span = &line[start_span..end_span]; + + let mut words = Vec::new(); + + let mut start = 0; + /* + let mut word_script = Script::Unknown; + for (i, c) in span.char_indices() { + let next_script = c.script(); + if word_script != next_script && word_script != Script::Unknown { + // No combination, start new span + words.push(self.shape_word(line, start_span + start, start_span + i)); + start = i; + word_script = Script::Unknown; + } else { + word_script = next_script; + } + } + */ + words.push(self.shape_word(line, start_span + start, end_span)); + + FontShapeSpan { + rtl, + words, + } } pub fn shape_line(&self, line_i: FontLineIndex, line: &str) -> FontShapeLine { - use unicode_script::{Script, UnicodeScript}; - - //TODO: more special handling of characters let mut spans = Vec::new(); - let mut start = 0; - let mut prev = Script::Unknown; - for (i, c) in line.char_indices() { - if ! line.is_char_boundary(i) { - continue; - } - - let cur = c.script(); - if prev != cur && prev != Script::Unknown { - // No combination, start new span - spans.push(self.shape_span(line, start, i)); - start = i; - prev = Script::Unknown; - } else { - prev = cur; - } - } - - spans.push(self.shape_span(line, start, line.len())); - let bidi = unicode_bidi::BidiInfo::new(line, None); let rtl = if bidi.paragraphs.is_empty() { false } else { assert_eq!(bidi.paragraphs.len(), 1); - bidi.paragraphs[0].level.is_rtl() + let para_info = &bidi.paragraphs[0]; + let para_rtl = para_info.level.is_rtl(); + + let paragraph = unicode_bidi::Paragraph::new(&bidi, ¶_info); + + let mut start = 0; + let mut span_rtl = para_rtl; + for i in paragraph.para.range.clone() { + let next_rtl = paragraph.info.levels[i].is_rtl(); + if span_rtl != next_rtl { + span_rtl = next_rtl; + spans.push(self.shape_span(line, start, i, span_rtl)); + start = i; + } + } + spans.push(self.shape_span(line, start, line.len(), span_rtl)); + + para_rtl }; FontShapeLine { diff --git a/examples/text/src/font/shape.rs b/examples/text/src/font/shape.rs index 62715fad..9b7c723c 100644 --- a/examples/text/src/font/shape.rs +++ b/examples/text/src/font/shape.rs @@ -19,11 +19,15 @@ pub struct FontShapeGlyph<'a> { pub inner: rusttype::Glyph<'a>, } -pub struct FontShapeSpan<'a> { - pub direction: rustybuzz::Direction, +pub struct FontShapeWord<'a> { pub glyphs: Vec>, } +pub struct FontShapeSpan<'a> { + pub rtl: bool, + pub words: Vec>, +} + pub struct FontShapeLine<'a> { pub line_i: FontLineIndex, pub rtl: bool, @@ -31,47 +35,22 @@ pub struct FontShapeLine<'a> { } impl<'a> FontShapeLine<'a> { - pub fn layout(&self, font_size: i32, line_width: i32, layout_lines: &mut Vec>, mut layout_i: usize) { + + fn layout_ltr(&self, font_size: i32, line_width: i32, layout_lines: &mut Vec>, mut layout_i: usize) { let mut push_line = true; let mut glyphs = Vec::new(); let mut x = 0.0; let mut y = 0.0; for span in self.spans.iter() { - let mut span_width = 0.0; - for glyph in span.glyphs.iter() { - let x_advance = font_size as f32 * glyph.x_advance; - span_width += x_advance; - } + for word in span.words.iter() { + for glyph in word.glyphs.iter() { + let x_advance = font_size as f32 * glyph.x_advance; + let y_advance = font_size as f32 * glyph.y_advance; + let x_offset = font_size as f32 * glyph.x_offset; + let y_offset = font_size as f32 * glyph.y_offset; - if self.rtl { - if glyphs.is_empty() { - x = line_width as f32; - } - x -= span_width; - } - - for glyph in span.glyphs.iter() { - let x_advance = font_size as f32 * glyph.x_advance; - let y_advance = font_size as f32 * glyph.y_advance; - let x_offset = font_size as f32 * glyph.x_offset; - let y_offset = font_size as f32 * glyph.y_offset; - - //TODO: make wrapping optional - if self.rtl { - if x < 0.0 { - let mut glyphs_swap = Vec::new(); - std::mem::swap(&mut glyphs, &mut glyphs_swap); - layout_lines.push(FontLayoutLine { - line_i: self.line_i, - glyphs: glyphs_swap - }); - layout_i += 1; - - x = line_width as f32 - x_advance; - y = 0.0; - } - } else { + //TODO: make wrapping optional if x + x_advance > line_width as f32 { let mut glyphs_swap = Vec::new(); std::mem::swap(&mut glyphs, &mut glyphs_swap); @@ -84,43 +63,39 @@ impl<'a> FontShapeLine<'a> { x = 0.0; y = 0.0; } - } - #[cfg(feature = "ab_glyph")] - let inner = glyph.font.outline_glyph( - glyph.inner.with_scale_and_position( - font_size as f32, - ab_glyph::point( + #[cfg(feature = "ab_glyph")] + let inner = glyph.font.outline_glyph( + glyph.inner.with_scale_and_position( + font_size as f32, + ab_glyph::point( + x + x_offset, + y + y_offset, + ) + ) + ); + + #[cfg(feature = "rusttype")] + let inner = glyph.inner.clone() + .scaled(rusttype::Scale::uniform(font_size as f32)) + .positioned(rusttype::point( x + x_offset, y + y_offset, - ) - ) - ); + )); - #[cfg(feature = "rusttype")] - let inner = glyph.inner.clone() - .scaled(rusttype::Scale::uniform(font_size as f32)) - .positioned(rusttype::point( - x + x_offset, - y + y_offset, - )); + glyphs.push(FontLayoutGlyph { + start: glyph.start, + end: glyph.end, + x, + w: x_advance, + inner, + phantom: PhantomData, + }); + push_line = true; - glyphs.push(FontLayoutGlyph { - start: glyph.start, - end: glyph.end, - x, - w: x_advance, - inner, - phantom: PhantomData, - }); - push_line = true; - - x += x_advance; - y += y_advance; - } - - if self.rtl { - x -= span_width; + x += x_advance; + y += y_advance; + } } } @@ -131,4 +106,85 @@ impl<'a> FontShapeLine<'a> { }); } } + + fn layout_rtl(&self, font_size: i32, line_width: i32, layout_lines: &mut Vec>, mut layout_i: usize) { + let mut push_line = true; + let mut glyphs = Vec::new(); + + let mut x = line_width as f32; + let mut y = 0.0; + for span in self.spans.iter() { + for word in span.words.iter() { + for glyph in word.glyphs.iter().rev() { + let x_advance = font_size as f32 * glyph.x_advance; + let y_advance = font_size as f32 * glyph.y_advance; + let x_offset = font_size as f32 * glyph.x_offset; + let y_offset = font_size as f32 * glyph.y_offset; + + //TODO: make wrapping optional + if x - x_advance < 0.0 { + let mut glyphs_swap = Vec::new(); + std::mem::swap(&mut glyphs, &mut glyphs_swap); + layout_lines.insert(layout_i, FontLayoutLine { + line_i: self.line_i, + glyphs: glyphs_swap + }); + layout_i += 1; + + x = line_width as f32; + y = 0.0; + } + + x -= x_advance; + + #[cfg(feature = "ab_glyph")] + let inner = glyph.font.outline_glyph( + glyph.inner.with_scale_and_position( + font_size as f32, + ab_glyph::point( + x + x_offset, + y + y_offset, + ) + ) + ); + + #[cfg(feature = "rusttype")] + let inner = glyph.inner.clone() + .scaled(rusttype::Scale::uniform(font_size as f32)) + .positioned(rusttype::point( + x + x_offset, + y + y_offset, + )); + + glyphs.push(FontLayoutGlyph { + start: glyph.start, + end: glyph.end, + x, + w: x_advance, + inner, + phantom: PhantomData, + }); + push_line = true; + + y += y_advance; + } + } + } + + if push_line { + layout_lines.insert(layout_i, FontLayoutLine { + line_i: self.line_i, + glyphs + }); + } + + } + + pub fn layout(&self, font_size: i32, line_width: i32, layout_lines: &mut Vec>, layout_i: usize) { + if self.rtl { + self.layout_rtl(font_size, line_width, layout_lines, layout_i); + } else { + self.layout_ltr(font_size, line_width, layout_lines, layout_i); + } + } }