From fc6904dc3bb84b8d0b58c9c0905d5bd26dd0b0cd Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 6 Oct 2022 16:09:43 -0600 Subject: [PATCH] Word wrapping with bi-directional text --- examples/text/Cargo.toml | 1 + examples/text/res/arabic.txt | 2 + examples/text/res/hebrew.txt | 2 + examples/text/src/font/matches.rs | 67 ++++++++++++++++++++++++++----- examples/text/src/font/shape.rs | 66 +++++++++++++++++------------- examples/text/src/main.rs | 2 +- 6 files changed, 100 insertions(+), 40 deletions(-) diff --git a/examples/text/Cargo.toml b/examples/text/Cargo.toml index dab59686..aa9879d4 100644 --- a/examples/text/Cargo.toml +++ b/examples/text/Cargo.toml @@ -11,6 +11,7 @@ orbclient = "0.3" rusttype = { version = "0.9", optional = true } rustybuzz = "0.5" unicode-bidi = "0.3" +unicode-linebreak = "0.1" unicode-script = "0.5" [features] diff --git a/examples/text/res/arabic.txt b/examples/text/res/arabic.txt index 2bdaa884..087fb768 100644 --- a/examples/text/res/arabic.txt +++ b/examples/text/res/arabic.txt @@ -1 +1,3 @@ +العربية + عندما يريد العالم أن ‪يتكلّم ‬ ، فهو يتحدّث بلغة يونيكود. تسجّل الآن لحضور المؤتمر الدولي العاشر ليونيكود (Unicode Conference)، الذي سيعقد في 10-12 آذار 1997 بمدينة مَايِنْتْس، ألمانيا. و سيجمع المؤتمر بين خبراء من كافة قطاعات الصناعة على الشبكة العالمية انترنيت ويونيكود، حيث ستتم، على الصعيدين الدولي والمحلي على حد سواء مناقشة سبل استخدام يونكود في النظم القائمة وفيما يخص التطبيقات الحاسوبية، الخطوط، تصميم النصوص والحوسبة متعددة اللغات. diff --git a/examples/text/res/hebrew.txt b/examples/text/res/hebrew.txt index d95d6ac0..f79921bd 100644 --- a/examples/text/res/hebrew.txt +++ b/examples/text/res/hebrew.txt @@ -1 +1,3 @@ כאשר העולם רוצה לדבר, הוא מדבר ב־Unicode. הירשמו כעת לכנס Unicode הבינלאומי העשירי, שייערך בין התאריכים 12־10 במרץ 1997, בְּמָיְינְץ שבגרמניה. בכנס ישתתפו מומחים מכל ענפי התעשייה בנושא האינטרנט העולמי וה־Unicode, בהתאמה לשוק הבינלאומי והמקומי, ביישום Unicode במערכות הפעלה וביישומים, בגופנים, בפריסת טקסט ובמחשוב רב־לשוני. + +Many computer programs fail to display bidirectional text correctly. For example, this page is mostly LTR English script, and here is the RTL Hebrew name Sarah: שרה, spelled sin (ש) on the right, resh (ר) in the middle, and heh (ה) on the left. diff --git a/examples/text/src/font/matches.rs b/examples/text/src/font/matches.rs index 9f035e91..de105ead 100644 --- a/examples/text/src/font/matches.rs +++ b/examples/text/src/font/matches.rs @@ -9,16 +9,13 @@ impl<'a> FontMatches<'a> { let word = &line[start_word..end_word]; let mut words_by_font = Vec::with_capacity(self.fonts.len()); - for (font_i, font) in self.fonts.iter().enumerate() { + for font in self.fonts.iter() { let font_scale = font.rustybuzz.units_per_em() as f32; let mut buffer = rustybuzz::UnicodeBuffer::new(); buffer.push_str(word); buffer.guess_segment_properties(); let direction = buffer.direction(); - if font_i == 0 { - println!("{:?}, {:?}: '{}'", buffer.script(), direction, word); - } let glyph_buffer = rustybuzz::shape(&font.rustybuzz, &[], buffer); let glyph_infos = glyph_buffer.glyph_infos(); @@ -86,6 +83,12 @@ impl<'a> FontMatches<'a> { _ => (), } + /* + for glyph in glyphs.iter() { + println!("'{}': {}, {}, {}, {}", &line[glyph.start..glyph.end], glyph.x_advance, glyph.y_advance, glyph.x_offset, glyph.y_offset); + } + */ + let word = FontShapeWord { glyphs }; if misses == 0 { return word; @@ -110,14 +113,56 @@ impl<'a> FontMatches<'a> { words_by_font.remove(least_misses_i).1 } - fn shape_span(&self, line: &str, start_span: usize, end_span: usize, rtl: bool) -> FontShapeSpan { + fn shape_span(&self, line: &str, start_span: usize, end_span: usize, para_rtl: bool, span_rtl: bool) -> FontShapeSpan { use unicode_script::{Script, UnicodeScript}; let span = &line[start_span..end_span]; - let mut words = Vec::new(); + println!("Span {}: '{}'", if span_rtl { "RTL" } else { "LTR" }, span); + + let mut words = vec![ + self.shape_word(line, start_span, end_span), + ]; + + if span_rtl { + for word in words.iter_mut() { + word.glyphs.reverse(); + } + } + + //TODO: improve performance + for (linebreak, _) in unicode_linebreak::linebreaks(span) { + println!("linebreak {}", linebreak); + let mut glyphs_opt = None; + 'words: for word_i in 0..words.len() { + for glyph_i in 0..words[word_i].glyphs.len() { + if words[word_i].glyphs[glyph_i].start == start_span + linebreak { + println!("glyph {}", words[word_i].glyphs[glyph_i].start); + println!("word '{}'", &line[ + words[word_i].glyphs[0].start + .. + words[word_i].glyphs[glyph_i].start + ]); + glyphs_opt = Some(words[word_i].glyphs.split_off(glyph_i)); + break 'words; + } + } + } + if let Some(glyphs) = glyphs_opt { + words.push(FontShapeWord { glyphs }); + } + } + + if span_rtl { + for word in words.iter_mut() { + word.glyphs.reverse(); + } + } + + if para_rtl != span_rtl { + words.reverse(); + } - let mut start = 0; /* let mut word_script = Script::Unknown; for (i, c) in span.char_indices() { @@ -131,11 +176,11 @@ impl<'a> FontMatches<'a> { word_script = next_script; } } - */ words.push(self.shape_word(line, start_span + start, end_span)); + */ FontShapeSpan { - rtl, + rtl: span_rtl, words, } } @@ -158,12 +203,12 @@ impl<'a> FontMatches<'a> { for i in paragraph.para.range.clone() { let next_rtl = paragraph.info.levels[i].is_rtl(); if span_rtl != next_rtl { + spans.push(self.shape_span(line, start, i, para_rtl, span_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)); + spans.push(self.shape_span(line, start, line.len(), para_rtl, span_rtl)); para_rtl }; diff --git a/examples/text/src/font/shape.rs b/examples/text/src/font/shape.rs index 9b7c723c..f4efb547 100644 --- a/examples/text/src/font/shape.rs +++ b/examples/text/src/font/shape.rs @@ -44,26 +44,31 @@ impl<'a> FontShapeLine<'a> { let mut y = 0.0; for span in self.spans.iter() { for word in span.words.iter() { + let mut word_size = 0.0; + for glyph in word.glyphs.iter() { + word_size += font_size as f32 * glyph.x_advance; + } + + //TODO: make wrapping optional + if x + word_size > line_width as f32 && ! glyphs.is_empty() { + 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 = 0.0; + y = 0.0; + } + 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; - //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); - layout_lines.insert(layout_i, FontLayoutLine { - line_i: self.line_i, - glyphs: glyphs_swap - }); - layout_i += 1; - - x = 0.0; - y = 0.0; - } - #[cfg(feature = "ab_glyph")] let inner = glyph.font.outline_glyph( glyph.inner.with_scale_and_position( @@ -115,26 +120,31 @@ impl<'a> FontShapeLine<'a> { let mut y = 0.0; for span in self.spans.iter() { for word in span.words.iter() { + let mut word_size = 0.0; + for glyph in word.glyphs.iter() { + word_size += font_size as f32 * glyph.x_advance; + } + + //TODO: make wrapping optional + if x - word_size < 0.0 && ! glyphs.is_empty() { + 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; + } + 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")] diff --git a/examples/text/src/main.rs b/examples/text/src/main.rs index 89fc040d..8e61e939 100644 --- a/examples/text/src/main.rs +++ b/examples/text/src/main.rs @@ -181,7 +181,7 @@ fn main() { (28, 36), // Title 2 (32, 44), // Title 1 ]; - let font_size_default = 2; // Title 4 + let font_size_default = 5; // Title 1 let mut font_size_i = font_size_default; let text = if let Some(arg) = env::args().nth(1) {