From 4798c7ee1a396ce88f2db52a74a16064b1415a5e Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 26 Oct 2022 14:16:48 -0600 Subject: [PATCH] Add initial rich text example --- examples/rich-text/Cargo.toml | 18 + examples/rich-text/src/main.rs | 156 +++++++++ rich-text.sh | 1 + src/attrs.rs | 32 ++ src/buffer.rs | 75 ++-- src/font/matches.rs | 328 +----------------- src/font/mod.rs | 3 - src/font/shape.rs | 228 ------------- src/layout.rs | 4 +- src/lib.rs | 3 + src/shape.rs | 601 +++++++++++++++++++++++++++++++++ 11 files changed, 854 insertions(+), 595 deletions(-) create mode 100644 examples/rich-text/Cargo.toml create mode 100644 examples/rich-text/src/main.rs create mode 100755 rich-text.sh delete mode 100644 src/font/shape.rs create mode 100644 src/shape.rs diff --git a/examples/rich-text/Cargo.toml b/examples/rich-text/Cargo.toml new file mode 100644 index 0000000..b139f78 --- /dev/null +++ b/examples/rich-text/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "rich-text" +version = "0.1.0" +authors = ["Jeremy Soller "] +edition = "2021" +license = "MPL2" +publish = false + +[dependencies] +cosmic-text = { path = "../../" } +env_logger = "0.9" +fontdb = "0.9" +log = "0.4" +orbclient = "0.3.35" +unicode-segmentation = "1.7" + +[features] +mono = [] diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs new file mode 100644 index 0000000..2ea3a5e --- /dev/null +++ b/examples/rich-text/src/main.rs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use cosmic_text::{Attrs, AttrsSpan, Color, Family, FontSystem, Style, SwashCache, + TextAction, TextBuffer, TextBufferLine, TextMetrics, Weight}; +use orbclient::{EventOption, Renderer, Window, WindowFlag}; +use std::{env, fs, process, thread, time::{Duration, Instant}}; +use unicode_segmentation::UnicodeSegmentation; + +fn main() { + env_logger::init(); + + let font_system = FontSystem::new(); + + let mut window = Window::new_flags( + -1, + -1, + 1024, + 768, + &format!("COSMIC TEXT - {}", font_system.locale), + &[WindowFlag::Resizable], + ) + .unwrap(); + + let attrs = cosmic_text::Attrs::new(); + let mut buffer = TextBuffer::new( + &font_system, + attrs, + TextMetrics::new(20, 28) + ); + + buffer.set_size( + window.width() as i32, + window.height() as i32 + ); + + let serif_attrs = attrs.family(Family::Serif); + let mono_attrs = attrs.monospaced(true).family(Family::Monospace); + let comic_attrs = attrs.family(Family::Name("Comic Neue")); + + let mut line_i = 0; + for &(text, attrs) in &[ + ("Sans-Serif Normal ", attrs), + ("Sans-Serif Bold ", attrs.weight(Weight::BOLD)), + ("Sans-Serif Italic ", attrs.style(Style::Italic)), + ("Sans-Serif Bold Italic", attrs.weight(Weight::BOLD).style(Style::Italic)), + ("\n", attrs), + ("Serif Normal ", serif_attrs), + ("Serif Bold ", serif_attrs.weight(Weight::BOLD)), + ("Serif Italic ", serif_attrs.style(Style::Italic)), + ("Serif Bold Italic", serif_attrs.weight(Weight::BOLD).style(Style::Italic)), + ("\n", attrs), + ("Mono Normal ", mono_attrs), + ("Mono Bold ", mono_attrs.weight(Weight::BOLD)), + ("Mono Italic ", mono_attrs.style(Style::Italic)), + ("Mono Bold Italic", mono_attrs.weight(Weight::BOLD).style(Style::Italic)), + ("\n", attrs), + ("Comic Normal ", comic_attrs), + ("Comic Bold ", comic_attrs.weight(Weight::BOLD)), + ("Comic Italic ", comic_attrs.style(Style::Italic)), + ("Comic Bold Italic", comic_attrs.weight(Weight::BOLD).style(Style::Italic)), + ("\n", attrs), + ("R", attrs.color(Color::rgb(0xFF, 0x00, 0x00))), + ("A", attrs.color(Color::rgb(0xFF, 0x7F, 0x00))), + ("I", attrs.color(Color::rgb(0xFF, 0xFF, 0x00))), + ("N", attrs.color(Color::rgb(0x00, 0xFF, 0x00))), + ("B", attrs.color(Color::rgb(0x00, 0x00, 0xFF))), + ("O", attrs.color(Color::rgb(0x4B, 0x00, 0x82))), + ("W ", attrs.color(Color::rgb(0x94, 0x00, 0xD3))), + ("Red ", attrs.color(Color::rgb(0xFF, 0x00, 0x00))), + ("Orange ", attrs.color(Color::rgb(0xFF, 0x7F, 0x00))), + ("Yellow ", attrs.color(Color::rgb(0xFF, 0xFF, 0x00))), + ("Green ", attrs.color(Color::rgb(0x00, 0xFF, 0x00))), + ("Blue ", attrs.color(Color::rgb(0x00, 0x00, 0xFF))), + ("Indigo ", attrs.color(Color::rgb(0x4B, 0x00, 0x82))), + ("Violet ", attrs.color(Color::rgb(0x94, 0x00, 0xD3))), + ] { + if text == "\n" { + line_i += 1; + while line_i >= buffer.lines.len() { + buffer.lines.push(TextBufferLine::new(String::new(), attrs)); + } + continue; + } + + let line = &mut buffer.lines[line_i]; + let start = line.text.len(); + line.text.push_str(text); + let end = line.text.len(); + line.attrs_spans.push(AttrsSpan { + start, + end, + attrs, + }); + line.reset(); + } + + let mut swash_cache = SwashCache::new(&font_system); + + //TODO: make window not async? + let mut mouse_x = -1; + let mut mouse_y = -1; + let mut mouse_left = false; + loop { + let bg_color = orbclient::Color::rgb(0x34, 0x34, 0x34); + let font_color = orbclient::Color::rgb(0xFF, 0xFF, 0xFF); + + if buffer.cursor_moved { + buffer.shape_until_cursor(); + buffer.cursor_moved = false; + } else { + buffer.shape_until_scroll(); + } + + if buffer.redraw { + let instant = Instant::now(); + + window.set(bg_color); + + buffer.draw(&mut swash_cache, font_color.data, |x, y, w, h, color| { + window.rect(x, y, w, h, orbclient::Color { data: color }); + }); + + window.sync(); + + buffer.redraw = false; + + let duration = instant.elapsed(); + log::debug!("redraw: {:?}", duration); + } + + for event in window.events() { + match event.to_option() { + EventOption::Mouse(mouse) => { + mouse_x = mouse.x; + mouse_y = mouse.y; + if mouse_left { + buffer.action(TextAction::Drag { x: mouse_x, y: mouse_y }); + } + }, + EventOption::Button(button) => { + mouse_left = button.left; + if mouse_left { + buffer.action(TextAction::Click { x: mouse_x, y: mouse_y }); + } + }, + EventOption::Resize(resize) => { + buffer.set_size(resize.width as i32, resize.height as i32); + }, + EventOption::Quit(_) => process::exit(0), + _ => (), + } + } + + thread::sleep(Duration::from_millis(1)); + } +} diff --git a/rich-text.sh b/rich-text.sh new file mode 100755 index 0000000..5ba3ada --- /dev/null +++ b/rich-text.sh @@ -0,0 +1 @@ +RUST_LOG=cosmic_text=debug cargo run --release --package rich-text diff --git a/src/attrs.rs b/src/attrs.rs index ce1b591..1763563 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -2,8 +2,28 @@ pub use fontdb::{Family, Stretch, Style, Weight}; +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub struct Color(pub u32); + +impl Color { + pub const fn rgb(r: u8, g: u8, b: u8) -> Self { + Self::rgba(r, g, b, 0xFF) + } + + pub const fn rgba(r: u8, g: u8, b: u8, a: u8) -> Self { + Self( + ((a as u32) << 24) | + ((r as u32) << 16) | + ((g as u32) << 8) | + (b as u32) + ) + } +} + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Attrs<'a> { + //TODO: should this be an option? + pub color_opt: Option, pub family: Family<'a>, pub monospaced: bool, pub stretch: Stretch, @@ -14,6 +34,7 @@ pub struct Attrs<'a> { impl<'a> Attrs<'a> { pub fn new() -> Self { Self { + color_opt: None, family: Family::SansSerif, monospaced: false, stretch: Stretch::Normal, @@ -22,6 +43,11 @@ impl<'a> Attrs<'a> { } } + pub fn color(mut self, color: Color) -> Self { + self.color_opt = Some(color); + self + } + pub fn family(mut self, family: Family<'a>) -> Self { self.family = family; self @@ -55,3 +81,9 @@ impl<'a> Attrs<'a> { (face.monospaced == self.monospaced || face.post_script_name.contains("Emoji")) } } + +pub struct AttrsSpan<'a> { + pub start: usize, + pub end: usize, + pub attrs: Attrs<'a> +} diff --git a/src/buffer.rs b/src/buffer.rs index 06df0c1..e1524f4 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -3,12 +3,11 @@ use std::{ cmp, fmt, - sync::Arc, time::Instant, }; use unicode_segmentation::UnicodeSegmentation; -use crate::{Attrs, LayoutGlyph, LayoutLine, FontMatches, FontShapeLine, FontSystem}; +use crate::{Attrs, AttrsSpan, FontSystem, LayoutGlyph, LayoutLine, ShapeLine}; /// An action to perform on a [TextBuffer] #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -174,16 +173,23 @@ impl fmt::Display for TextMetrics { } } -pub struct TextBufferLine { - text: String, - shape_opt: Option, +pub struct TextBufferLine<'a> { + pub text: String, + pub attrs_spans: Vec>, + shape_opt: Option, layout_opt: Option>, } -impl TextBufferLine { - pub fn new(text: String) -> Self { +impl<'a> TextBufferLine<'a> { + pub fn new(text: String, attrs: Attrs<'a>) -> Self { + let attrs_spans = vec![AttrsSpan { + start: 0, + end: text.len(), + attrs + }]; Self { text, + attrs_spans, shape_opt: None, layout_opt: None, } @@ -198,18 +204,18 @@ impl TextBufferLine { self.layout_opt = None; } - pub fn shape(&mut self, font_matches: &FontMatches<'_>) -> &FontShapeLine { + pub fn shape(&mut self, font_system: &'a FontSystem<'a>) -> &ShapeLine { if self.shape_opt.is_none() { - self.shape_opt = Some(font_matches.shape_line(&self.text)); + self.shape_opt = Some(ShapeLine::new(font_system, &self.text, &self.attrs_spans)); self.layout_opt = None; } self.shape_opt.as_ref().unwrap() } - pub fn layout(&mut self, font_matches: &FontMatches<'_>, font_size: i32, width: i32) -> &[LayoutLine] { + pub fn layout(&mut self, font_system: &'a FontSystem<'a>, font_size: i32, width: i32) -> &[LayoutLine] { if self.layout_opt.is_none() { let mut layout = Vec::new(); - let shape = self.shape(font_matches); + let shape = self.shape(font_system); shape.layout( font_size, width, @@ -225,10 +231,8 @@ impl TextBufferLine { /// A buffer of text that is shaped and laid out pub struct TextBuffer<'a> { font_system: &'a FontSystem<'a>, - font_matches: Arc>, attrs: Attrs<'a>, - attr_spans: Vec<(TextCursor, usize, Attrs<'a>)>, - lines: Vec, + pub lines: Vec>, metrics: TextMetrics, width: i32, height: i32, @@ -245,12 +249,9 @@ impl<'a> TextBuffer<'a> { attrs: Attrs<'a>, metrics: TextMetrics, ) -> Self { - let font_matches = font_system.get_font_matches(attrs); let mut buffer = Self { font_system, - font_matches, attrs, - attr_spans: Vec::new(), lines: Vec::new(), metrics, width: 0, @@ -280,7 +281,7 @@ impl<'a> TextBuffer<'a> { reshaped += 1; } let layout = line.layout( - &self.font_matches, + self.font_system, self.metrics.font_size, self.width ); @@ -311,7 +312,7 @@ impl<'a> TextBuffer<'a> { reshaped += 1; } let layout = line.layout( - &self.font_matches, + self.font_system, self.metrics.font_size, self.width ); @@ -362,7 +363,7 @@ impl<'a> TextBuffer<'a> { if line.shape_opt.is_some() { line.layout_opt = None; line.layout( - &self.font_matches, + self.font_system, self.metrics.font_size, self.width ); @@ -421,7 +422,7 @@ impl<'a> TextBuffer<'a> { fn set_layout_cursor(&mut self, cursor: TextLayoutCursor) { let line = &mut self.lines[cursor.line]; let layout = line.layout( - &self.font_matches, + self.font_system, self.metrics.font_size, self.width ); @@ -498,10 +499,6 @@ impl<'a> TextBuffer<'a> { self.height / self.metrics.line_height } - pub fn font_matches(&self) -> &FontMatches<'a> { - &self.font_matches - } - pub fn attrs(&self) -> &Attrs<'a> { &self.attrs } @@ -509,30 +506,30 @@ impl<'a> TextBuffer<'a> { /// Set attributes pub fn set_attrs(&mut self, attrs: Attrs<'a>) { if attrs != self.attrs { - self.font_matches = self.font_system.get_font_matches(attrs); self.attrs = attrs; for line in self.lines.iter_mut() { line.reset(); + line.attrs_spans = vec![AttrsSpan { + start: 0, + end: line.text.len(), + attrs + }]; } self.shape_until_scroll(); } } - pub fn add_attr_span(&mut self, cursor: TextCursor, len: usize, attrs: Attrs<'a>) { - self.attr_spans.push((cursor, len, attrs)); - } - /// Set text of buffer pub fn set_text(&mut self, text: &str) { self.lines.clear(); for line in text.lines() { - self.lines.push(TextBufferLine::new(line.to_string())); + self.lines.push(TextBufferLine::new(line.to_string(), self.attrs)); } // Make sure there is always one line if self.lines.is_empty() { - self.lines.push(TextBufferLine::new(String::new())); + self.lines.push(TextBufferLine::new(String::new(), self.attrs)); } self.scroll = 0; @@ -628,7 +625,7 @@ impl<'a> TextBuffer<'a> { let layout_len = { let line = &mut self.lines[cursor.line]; let layout = line.layout( - &self.font_matches, + self.font_system, self.metrics.font_size, self.width ); @@ -688,7 +685,7 @@ impl<'a> TextBuffer<'a> { }; let next_line = self.cursor.line + 1; - self.lines.insert(next_line, TextBufferLine::new(new_line)); + self.lines.insert(next_line, TextBufferLine::new(new_line, self.attrs)); self.cursor.line = next_line; self.cursor.index = 0; @@ -851,7 +848,7 @@ impl<'a> TextBuffer<'a> { if new_cursor != self.cursor { if let Some(glyph) = run.glyphs.get(new_cursor_glyph) { - let font_opt = self.font_matches.get_font(&glyph.cache_key.font_id); + let font_opt = self.font_system.get_font(glyph.cache_key.font_id); let text_glyph = &run.text[glyph.start..glyph.end]; log::debug!( "{}, {}: '{}' ('{}'): '{}' ({:?})", @@ -1040,7 +1037,13 @@ impl<'a> TextBuffer<'a> { for glyph in run.glyphs.iter() { let (cache_key, x_int, y_int) = (glyph.cache_key, glyph.x_int, glyph.y_int); - cache.with_pixels(cache_key, color, |x, y, color| { + + let glyph_color = match glyph.color_opt { + Some(some) => some.0, + None => color, + }; + + cache.with_pixels(cache_key, glyph_color, |x, y, color| { f(x_int + x, line_y + y_int + y, 1, 1, color) }); } diff --git a/src/font/matches.rs b/src/font/matches.rs index e65fb59..dc2b3c3 100644 --- a/src/font/matches.rs +++ b/src/font/matches.rs @@ -1,10 +1,8 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use std::sync::Arc; -use unicode_script::{Script, UnicodeScript}; -use crate::{Font, FontShapeGlyph, FontShapeLine, FontShapeSpan, FontShapeWord}; -use crate::fallback::{FontFallbackIter}; +use crate::Font; /// Fonts that match a pattern pub struct FontMatches<'a> { @@ -12,327 +10,3 @@ pub struct FontMatches<'a> { pub default_family: String, pub fonts: Vec>>, } - -impl<'a> FontMatches<'a> { - fn shape_fallback( - &self, - font: &Font<'a>, - line: &str, - start_word: usize, - end_word: usize, - span_rtl: bool, - ) -> (Vec, Vec) { - let word = &line[start_word..end_word]; - - let font_scale = font.rustybuzz.units_per_em() as f32; - - let mut buffer = rustybuzz::UnicodeBuffer::new(); - buffer.set_direction(if span_rtl { - rustybuzz::Direction::RightToLeft - } else { - rustybuzz::Direction::LeftToRight - }); - buffer.push_str(word); - buffer.guess_segment_properties(); - - let rtl = match buffer.direction() { - rustybuzz::Direction::RightToLeft => true, - //TODO: other directions? - _ => false, - }; - assert_eq!(rtl, span_rtl); - - let glyph_buffer = rustybuzz::shape(&font.rustybuzz, &[], buffer); - let glyph_infos = glyph_buffer.glyph_infos(); - let glyph_positions = glyph_buffer.glyph_positions(); - - let mut missing = Vec::new(); - let mut glyphs = Vec::with_capacity(glyph_infos.len()); - for (info, pos) in glyph_infos.iter().zip(glyph_positions.iter()) { - let x_advance = pos.x_advance as f32 / font_scale; - let y_advance = pos.y_advance as f32 / font_scale; - let x_offset = pos.x_offset as f32 / font_scale; - let y_offset = pos.y_offset as f32 / font_scale; - - //println!(" {:?} {:?}", info, pos); - if info.glyph_id == 0 { - missing.push(start_word + info.cluster as usize); - } - - glyphs.push(FontShapeGlyph { - start: start_word + info.cluster as usize, - end: end_word, // Set later - x_advance, - y_advance, - x_offset, - y_offset, - font_id: font.info.id, - glyph_id: info.glyph_id.try_into().unwrap(), - }); - } - - // Adjust end of glyphs - if rtl { - for i in 1..glyphs.len() { - let next_start = glyphs[i - 1].start; - let next_end = glyphs[i - 1].end; - let prev = &mut glyphs[i]; - if prev.start == next_start { - prev.end = next_end; - } else { - prev.end = next_start; - } - } - } else { - for i in (1..glyphs.len()).rev() { - let next_start = glyphs[i].start; - let next_end = glyphs[i].end; - let prev = &mut glyphs[i - 1]; - if prev.start == next_start { - prev.end = next_end; - } else { - prev.end = next_start; - } - } - } - - (glyphs, missing) - } - - fn shape_word( - &self, - line: &str, - start_word: usize, - end_word: usize, - span_rtl: bool, - blank: bool, - ) -> FontShapeWord { - //TODO: use smallvec? - let mut scripts = Vec::new(); - for c in line[start_word..end_word].chars() { - match c.script() { - Script::Common | - Script::Inherited | - Script::Latin | - Script::Unknown => (), - script => if ! scripts.contains(&script) { - scripts.push(script); - }, - } - } - - log::trace!( - " Word {:?}{}: '{}'", - scripts, - if blank { " BLANK" } else { "" }, - &line[start_word..end_word], - ); - - let default_families = [self.default_family.as_str()]; - let mut font_iter = FontFallbackIter::new( - &self.fonts, - &default_families, - scripts, - self.locale - ); - - let (mut glyphs, mut missing) = self.shape_fallback( - font_iter.next().unwrap(), - line, - start_word, - end_word, - span_rtl - ); - - //TODO: improve performance! - while !missing.is_empty() { - let font = match font_iter.next() { - Some(some) => some, - None => break, - }; - - log::trace!("Evaluating fallback with font '{}'", font.info.family); - let (mut fb_glyphs, fb_missing) = self.shape_fallback( - font, - line, - start_word, - end_word, - span_rtl - ); - - // Insert all matching glyphs - let mut fb_i = 0; - while fb_i < fb_glyphs.len() { - let start = fb_glyphs[fb_i].start; - let end = fb_glyphs[fb_i].end; - - // Skip clusters that are not missing, or where the fallback font is missing - if !missing.contains(&start) || fb_missing.contains(&start) { - fb_i += 1; - continue; - } - - let mut missing_i = 0; - while missing_i < missing.len() { - if missing[missing_i] >= start && missing[missing_i] < end { - // println!("No longer missing {}", missing[missing_i]); - missing.remove(missing_i); - } else { - missing_i += 1; - } - } - - // Find prior glyphs - let mut i = 0; - while i < glyphs.len() { - if glyphs[i].start >= start && glyphs[i].end <= end { - break; - } else { - i += 1; - } - } - - // Remove prior glyphs - while i < glyphs.len() { - if glyphs[i].start >= start && glyphs[i].end <= end { - let _glyph = glyphs.remove(i); - // log::trace!("Removed {},{} from {}", _glyph.start, _glyph.end, i); - } else { - break; - } - } - - while fb_i < fb_glyphs.len() { - if fb_glyphs[fb_i].start >= start && fb_glyphs[fb_i].end <= end { - let fb_glyph = fb_glyphs.remove(fb_i); - // log::trace!("Insert {},{} from font {} at {}", fb_glyph.start, fb_glyph.end, font_i, i); - glyphs.insert(i, fb_glyph); - i += 1; - } else { - break; - } - } - } - } - - // Debug missing font fallbacks - font_iter.check_missing(&line[start_word..end_word]); - - /* - for glyph in glyphs.iter() { - log::trace!("'{}': {}, {}, {}, {}", &line[glyph.start..glyph.end], glyph.x_advance, glyph.y_advance, glyph.x_offset, glyph.y_offset); - } - */ - - FontShapeWord { blank, glyphs } - } - - fn shape_span( - &self, - line: &str, - start_span: usize, - end_span: usize, - line_rtl: bool, - span_rtl: bool, - ) -> FontShapeSpan { - let span = &line[start_span..end_span]; - - log::trace!( - " Span {}: '{}'", - if span_rtl { "RTL" } else { "LTR" }, - span - ); - - let mut words = Vec::new(); - - let mut start_word = 0; - for (end_lb, _) in unicode_linebreak::linebreaks(span) { - let mut start_lb = end_lb; - for (i, c) in span[start_word..end_lb].char_indices() { - if start_word + i == end_lb { - break; - } else if c.is_whitespace() { - start_lb = start_word + i; - } - } - if start_word < start_lb { - words.push(self.shape_word( - line, - start_span + start_word, - start_span + start_lb, - span_rtl, - false, - )); - } - if start_lb < end_lb { - words.push(self.shape_word( - line, - start_span + start_lb, - start_span + end_lb, - span_rtl, - true, - )); - } - start_word = end_lb; - } - - // Reverse glyphs in RTL lines - if line_rtl { - for word in words.iter_mut() { - word.glyphs.reverse(); - } - } - - // Reverse words in spans that do not match line direction - if line_rtl != span_rtl { - words.reverse(); - } - - FontShapeSpan { - rtl: span_rtl, - words, - } - } - - pub fn shape_line(&self, line: &str) -> FontShapeLine { - let mut spans = Vec::new(); - - let bidi = unicode_bidi::BidiInfo::new(line, None); - let rtl = if bidi.paragraphs.is_empty() { - false - } else { - assert_eq!(bidi.paragraphs.len(), 1); - let para_info = &bidi.paragraphs[0]; - let line_rtl = para_info.level.is_rtl(); - - log::trace!("Line {}: '{}'", if line_rtl { "RTL" } else { "LTR" }, line); - - let paragraph = unicode_bidi::Paragraph::new(&bidi, para_info); - - let mut start = 0; - let mut span_rtl = line_rtl; - 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, line_rtl, span_rtl)); - span_rtl = next_rtl; - start = i; - } - } - spans.push(self.shape_span(line, start, line.len(), line_rtl, span_rtl)); - - line_rtl - }; - - FontShapeLine { rtl, spans } - } - - pub fn get_font(&self, id: &fontdb::ID) -> Option<&Font> { - for font in self.fonts.iter() { - if &font.info.id == id { - return Some(font); - } - } - None - } -} diff --git a/src/font/mod.rs b/src/font/mod.rs index 1304a3b..ea881d7 100644 --- a/src/font/mod.rs +++ b/src/font/mod.rs @@ -8,8 +8,5 @@ mod font; pub use self::matches::*; mod matches; -pub(crate) use self::shape::*; -mod shape; - pub use self::system::*; mod system; diff --git a/src/font/shape.rs b/src/font/shape.rs deleted file mode 100644 index 7bede6e..0000000 --- a/src/font/shape.rs +++ /dev/null @@ -1,228 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 - -use crate::{CacheKey, LayoutGlyph, LayoutLine}; - -pub struct FontShapeGlyph { - pub start: usize, - pub end: usize, - pub x_advance: f32, - pub y_advance: f32, - pub x_offset: f32, - pub y_offset: f32, - pub font_id: fontdb::ID, - pub glyph_id: u16, -} - -impl FontShapeGlyph { - fn layout(&self, font_size: i32, x: f32, y: f32, rtl: bool) -> LayoutGlyph { - let x_offset = font_size as f32 * self.x_offset; - let y_offset = font_size as f32 * self.y_offset; - let x_advance = font_size as f32 * self.x_advance; - - let (cache_key, x_int, y_int) = CacheKey::new( - self.font_id, - self.glyph_id, - font_size, - (x + x_offset, y - y_offset) - ); - LayoutGlyph { - start: self.start, - end: self.end, - x, - w: x_advance, - rtl, - cache_key, - x_int, - y_int, - } - } -} - -pub struct FontShapeWord { - pub blank: bool, - pub glyphs: Vec, -} - -pub struct FontShapeSpan { - pub rtl: bool, - pub words: Vec, -} - -pub struct FontShapeLine { - pub rtl: bool, - pub spans: Vec, -} - -impl FontShapeLine { - pub fn layout( - &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 start_x = if self.rtl { line_width as f32 } else { 0.0 }; - let end_x = if self.rtl { 0.0 } else { line_width as f32 }; - let mut x = start_x; - let mut y = 0.0; - for span in self.spans.iter() { - //TODO: improve performance! - let mut word_ranges = Vec::new(); - if self.rtl != span.rtl { - let mut fit_x = x; - let mut fitting_end = span.words.len(); - for i in (0..span.words.len()).rev() { - let word = &span.words[i]; - - let mut word_size = 0.0; - for glyph in word.glyphs.iter() { - word_size += font_size as f32 * glyph.x_advance; - } - - let wrap = if self.rtl { - fit_x - word_size < end_x - } else { - fit_x + word_size > end_x - }; - - if wrap { - let mut fitting_start = i + 1; - while fitting_start < fitting_end { - if span.words[fitting_start].blank { - fitting_start += 1; - } else { - break; - } - } - word_ranges.push((fitting_start..fitting_end, true)); - fitting_end = i + 1; - - fit_x = start_x; - } - - if self.rtl { - fit_x -= word_size; - } else { - fit_x += word_size; - } - } - if !word_ranges.is_empty() { - while fitting_end > 0 { - if span.words[fitting_end - 1].blank { - fitting_end -= 1; - } else { - break; - } - } - } - word_ranges.push((0..fitting_end, false)); - } else { - let mut fit_x = x; - let mut fitting_start = 0; - for i in 0..span.words.len() { - let word = &span.words[i]; - - let mut word_size = 0.0; - for glyph in word.glyphs.iter() { - word_size += font_size as f32 * glyph.x_advance; - } - - let wrap = if self.rtl { - fit_x - word_size < end_x - } else { - fit_x + word_size > end_x - }; - - if wrap { - //TODO: skip blanks - word_ranges.push((fitting_start..i, true)); - fitting_start = i; - - fit_x = start_x; - } - - if self.rtl { - fit_x -= word_size; - } else { - fit_x += word_size; - } - } - word_ranges.push((fitting_start..span.words.len(), false)); - } - - for (range, wrap) in word_ranges { - for word in span.words[range].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 - let wrap = if self.rtl { - x - word_size < end_x - } else { - x + word_size > end_x - }; - if wrap && !glyphs.is_empty() { - let mut glyphs_swap = Vec::new(); - std::mem::swap(&mut glyphs, &mut glyphs_swap); - layout_lines.insert( - layout_i, - LayoutLine { - glyphs: glyphs_swap, - }, - ); - layout_i += 1; - - x = start_x; - 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; - - if self.rtl { - x -= x_advance - } - - glyphs.push(glyph.layout(font_size, x, y, span.rtl)); - push_line = true; - - if !self.rtl { - x += x_advance; - } - y += y_advance; - } - } - - if wrap { - let mut glyphs_swap = Vec::new(); - std::mem::swap(&mut glyphs, &mut glyphs_swap); - layout_lines.insert( - layout_i, - LayoutLine { - glyphs: glyphs_swap, - }, - ); - layout_i += 1; - - x = start_x; - y = 0.0; - } - } - } - - if push_line { - layout_lines.insert( - layout_i, - LayoutLine { - glyphs, - }, - ); - } - } -} diff --git a/src/layout.rs b/src/layout.rs index e7ddd4b..809a703 100644 --- a/src/layout.rs +++ b/src/layout.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use super::CacheKey; +use crate::{CacheKey, Color}; /// A laid out glyph pub struct LayoutGlyph { @@ -20,6 +20,8 @@ pub struct LayoutGlyph { pub x_int: i32, /// Integer component of Y offset in line pub y_int: i32, + /// Optional color override + pub color_opt: Option, } /// A line of laid out glyphs diff --git a/src/lib.rs b/src/lib.rs index cfdd039..4d57b76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,9 @@ mod font; pub use self::layout::*; mod layout; +pub use self::shape::*; +mod shape; + #[cfg(feature = "swash")] pub use self::swash::*; #[cfg(feature = "swash")] diff --git a/src/shape.rs b/src/shape.rs new file mode 100644 index 0000000..54c5ab2 --- /dev/null +++ b/src/shape.rs @@ -0,0 +1,601 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 + +use unicode_script::{Script, UnicodeScript}; + +use crate::{Attrs, AttrsSpan, CacheKey, Color, Font, FontSystem, LayoutGlyph, LayoutLine}; +use crate::fallback::FontFallbackIter; + +fn shape_fallback( + font: &Font, + line: &str, + attrs_spans: &[AttrsSpan], + start_word: usize, + end_word: usize, + span_rtl: bool, +) -> (Vec, Vec) { + let word = &line[start_word..end_word]; + + let font_scale = font.rustybuzz.units_per_em() as f32; + + let mut buffer = rustybuzz::UnicodeBuffer::new(); + buffer.set_direction(if span_rtl { + rustybuzz::Direction::RightToLeft + } else { + rustybuzz::Direction::LeftToRight + }); + buffer.push_str(word); + buffer.guess_segment_properties(); + + let rtl = match buffer.direction() { + rustybuzz::Direction::RightToLeft => true, + //TODO: other directions? + _ => false, + }; + assert_eq!(rtl, span_rtl); + + let glyph_buffer = rustybuzz::shape(&font.rustybuzz, &[], buffer); + let glyph_infos = glyph_buffer.glyph_infos(); + let glyph_positions = glyph_buffer.glyph_positions(); + + let mut missing = Vec::new(); + let mut glyphs = Vec::with_capacity(glyph_infos.len()); + for (info, pos) in glyph_infos.iter().zip(glyph_positions.iter()) { + let x_advance = pos.x_advance as f32 / font_scale; + let y_advance = pos.y_advance as f32 / font_scale; + let x_offset = pos.x_offset as f32 / font_scale; + let y_offset = pos.y_offset as f32 / font_scale; + + let start_glyph = start_word + info.cluster as usize; + + //println!(" {:?} {:?}", info, pos); + if info.glyph_id == 0 { + missing.push(start_glyph); + } + + let mut attrs = Attrs::new(); + for attrs_span in attrs_spans.iter() { + //TODO: BETTER SUPPORT ATTRIBUTES PER GLYPH + if attrs_span.start <= start_glyph && attrs_span.end > start_glyph { + attrs = attrs_span.attrs; + break; + } + } + + glyphs.push(ShapeGlyph { + start: start_glyph, + end: end_word, // Set later + x_advance, + y_advance, + x_offset, + y_offset, + font_id: font.info.id, + glyph_id: info.glyph_id.try_into().unwrap(), + color_opt: attrs.color_opt, + }); + } + + // Adjust end of glyphs + if rtl { + for i in 1..glyphs.len() { + let next_start = glyphs[i - 1].start; + let next_end = glyphs[i - 1].end; + let prev = &mut glyphs[i]; + if prev.start == next_start { + prev.end = next_end; + } else { + prev.end = next_start; + } + } + } else { + for i in (1..glyphs.len()).rev() { + let next_start = glyphs[i].start; + let next_end = glyphs[i].end; + let prev = &mut glyphs[i - 1]; + if prev.start == next_start { + prev.end = next_end; + } else { + prev.end = next_start; + } + } + } + + (glyphs, missing) +} + +pub struct ShapeGlyph { + pub start: usize, + pub end: usize, + pub x_advance: f32, + pub y_advance: f32, + pub x_offset: f32, + pub y_offset: f32, + pub font_id: fontdb::ID, + pub glyph_id: u16, + pub color_opt: Option, +} + +impl ShapeGlyph { + fn layout(&self, font_size: i32, x: f32, y: f32, rtl: bool) -> LayoutGlyph { + let x_offset = font_size as f32 * self.x_offset; + let y_offset = font_size as f32 * self.y_offset; + let x_advance = font_size as f32 * self.x_advance; + + let (cache_key, x_int, y_int) = CacheKey::new( + self.font_id, + self.glyph_id, + font_size, + (x + x_offset, y - y_offset) + ); + LayoutGlyph { + start: self.start, + end: self.end, + x, + w: x_advance, + rtl, + cache_key, + x_int, + y_int, + color_opt: self.color_opt, + } + } +} + +pub struct ShapeWord { + pub blank: bool, + pub glyphs: Vec, +} + +impl ShapeWord { + pub fn new<'a>( + font_system: &'a FontSystem<'a>, + line: &str, + attrs_spans: &[AttrsSpan<'a>], + start_word: usize, + end_word: usize, + span_rtl: bool, + blank: bool, + ) -> Self { + //TODO: use smallvec? + let mut scripts = Vec::new(); + for c in line[start_word..end_word].chars() { + match c.script() { + Script::Common | + Script::Inherited | + Script::Latin | + Script::Unknown => (), + script => if ! scripts.contains(&script) { + scripts.push(script); + }, + } + } + + log::trace!( + " Word {:?}{}: '{}'", + scripts, + if blank { " BLANK" } else { "" }, + &line[start_word..end_word], + ); + + let mut attrs = Attrs::new(); + for attrs_span in attrs_spans.iter() { + //TODO: BETTER SUPPORT ATTRIBUTES PER GLYPH + if attrs_span.start <= start_word && attrs_span.end >= end_word { + attrs = attrs_span.attrs; + break; + } + } + + let font_matches = font_system.get_font_matches(attrs); + + let default_families = [font_matches.default_family.as_str()]; + let mut font_iter = FontFallbackIter::new( + &font_matches.fonts, + &default_families, + scripts, + font_matches.locale + ); + + let (mut glyphs, mut missing) = shape_fallback( + font_iter.next().unwrap(), + line, + attrs_spans, + start_word, + end_word, + span_rtl, + ); + + //TODO: improve performance! + while !missing.is_empty() { + let font = match font_iter.next() { + Some(some) => some, + None => break, + }; + + log::trace!("Evaluating fallback with font '{}'", font.info.family); + let (mut fb_glyphs, fb_missing) = shape_fallback( + font, + line, + attrs_spans, + start_word, + end_word, + span_rtl, + ); + + // Insert all matching glyphs + let mut fb_i = 0; + while fb_i < fb_glyphs.len() { + let start = fb_glyphs[fb_i].start; + let end = fb_glyphs[fb_i].end; + + // Skip clusters that are not missing, or where the fallback font is missing + if !missing.contains(&start) || fb_missing.contains(&start) { + fb_i += 1; + continue; + } + + let mut missing_i = 0; + while missing_i < missing.len() { + if missing[missing_i] >= start && missing[missing_i] < end { + // println!("No longer missing {}", missing[missing_i]); + missing.remove(missing_i); + } else { + missing_i += 1; + } + } + + // Find prior glyphs + let mut i = 0; + while i < glyphs.len() { + if glyphs[i].start >= start && glyphs[i].end <= end { + break; + } else { + i += 1; + } + } + + // Remove prior glyphs + while i < glyphs.len() { + if glyphs[i].start >= start && glyphs[i].end <= end { + let _glyph = glyphs.remove(i); + // log::trace!("Removed {},{} from {}", _glyph.start, _glyph.end, i); + } else { + break; + } + } + + while fb_i < fb_glyphs.len() { + if fb_glyphs[fb_i].start >= start && fb_glyphs[fb_i].end <= end { + let fb_glyph = fb_glyphs.remove(fb_i); + // log::trace!("Insert {},{} from font {} at {}", fb_glyph.start, fb_glyph.end, font_i, i); + glyphs.insert(i, fb_glyph); + i += 1; + } else { + break; + } + } + } + } + + // Debug missing font fallbacks + font_iter.check_missing(&line[start_word..end_word]); + + /* + for glyph in glyphs.iter() { + log::trace!("'{}': {}, {}, {}, {}", &line[glyph.start..glyph.end], glyph.x_advance, glyph.y_advance, glyph.x_offset, glyph.y_offset); + } + */ + + Self { blank, glyphs } + } +} + +pub struct ShapeSpan { + pub rtl: bool, + pub words: Vec, +} + +impl ShapeSpan { + pub fn new<'a>( + font_system: &'a FontSystem<'a>, + line: &str, + attrs_spans: &[AttrsSpan<'a>], + start_span: usize, + end_span: usize, + line_rtl: bool, + span_rtl: bool, + ) -> Self { + let span = &line[start_span..end_span]; + + log::trace!( + " Span {}: '{}'", + if span_rtl { "RTL" } else { "LTR" }, + span + ); + + let mut words = Vec::new(); + + let mut start_word = 0; + for (end_lb, _) in unicode_linebreak::linebreaks(span) { + let mut start_lb = end_lb; + for (i, c) in span[start_word..end_lb].char_indices() { + if start_word + i == end_lb { + break; + } else if c.is_whitespace() { + start_lb = start_word + i; + } + } + if start_word < start_lb { + words.push(ShapeWord::new( + font_system, + line, + attrs_spans, + start_span + start_word, + start_span + start_lb, + span_rtl, + false, + )); + } + if start_lb < end_lb { + words.push(ShapeWord::new( + font_system, + line, + attrs_spans, + start_span + start_lb, + start_span + end_lb, + span_rtl, + true, + )); + } + start_word = end_lb; + } + + // Reverse glyphs in RTL lines + if line_rtl { + for word in words.iter_mut() { + word.glyphs.reverse(); + } + } + + // Reverse words in spans that do not match line direction + if line_rtl != span_rtl { + words.reverse(); + } + + ShapeSpan { + rtl: span_rtl, + words, + } + } +} + +pub struct ShapeLine { + pub rtl: bool, + pub spans: Vec, +} + +impl ShapeLine { + pub fn new<'a>( + font_system: &'a FontSystem<'a>, + line: &str, + attrs_spans: &[AttrsSpan<'a>] + ) -> Self { + let mut spans = Vec::new(); + + let bidi = unicode_bidi::BidiInfo::new(line, None); + let rtl = if bidi.paragraphs.is_empty() { + false + } else { + assert_eq!(bidi.paragraphs.len(), 1); + let para_info = &bidi.paragraphs[0]; + let line_rtl = para_info.level.is_rtl(); + + log::trace!("Line {}: '{}'", if line_rtl { "RTL" } else { "LTR" }, line); + + let paragraph = unicode_bidi::Paragraph::new(&bidi, para_info); + + let mut start = 0; + let mut span_rtl = line_rtl; + for i in paragraph.para.range.clone() { + let next_rtl = paragraph.info.levels[i].is_rtl(); + if span_rtl != next_rtl { + spans.push(ShapeSpan::new( + font_system, + line, + attrs_spans, + start, + i, + line_rtl, + span_rtl + )); + span_rtl = next_rtl; + start = i; + } + } + spans.push(ShapeSpan::new( + font_system, + line, + attrs_spans, + start, + line.len(), + line_rtl, + span_rtl + )); + + line_rtl + }; + + Self { rtl, spans } + } + + pub fn layout( + &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 start_x = if self.rtl { line_width as f32 } else { 0.0 }; + let end_x = if self.rtl { 0.0 } else { line_width as f32 }; + let mut x = start_x; + let mut y = 0.0; + for span in self.spans.iter() { + //TODO: improve performance! + let mut word_ranges = Vec::new(); + if self.rtl != span.rtl { + let mut fit_x = x; + let mut fitting_end = span.words.len(); + for i in (0..span.words.len()).rev() { + let word = &span.words[i]; + + let mut word_size = 0.0; + for glyph in word.glyphs.iter() { + word_size += font_size as f32 * glyph.x_advance; + } + + let wrap = if self.rtl { + fit_x - word_size < end_x + } else { + fit_x + word_size > end_x + }; + + if wrap { + let mut fitting_start = i + 1; + while fitting_start < fitting_end { + if span.words[fitting_start].blank { + fitting_start += 1; + } else { + break; + } + } + word_ranges.push((fitting_start..fitting_end, true)); + fitting_end = i + 1; + + fit_x = start_x; + } + + if self.rtl { + fit_x -= word_size; + } else { + fit_x += word_size; + } + } + if !word_ranges.is_empty() { + while fitting_end > 0 { + if span.words[fitting_end - 1].blank { + fitting_end -= 1; + } else { + break; + } + } + } + word_ranges.push((0..fitting_end, false)); + } else { + let mut fit_x = x; + let mut fitting_start = 0; + for i in 0..span.words.len() { + let word = &span.words[i]; + + let mut word_size = 0.0; + for glyph in word.glyphs.iter() { + word_size += font_size as f32 * glyph.x_advance; + } + + let wrap = if self.rtl { + fit_x - word_size < end_x + } else { + fit_x + word_size > end_x + }; + + if wrap { + //TODO: skip blanks + word_ranges.push((fitting_start..i, true)); + fitting_start = i; + + fit_x = start_x; + } + + if self.rtl { + fit_x -= word_size; + } else { + fit_x += word_size; + } + } + word_ranges.push((fitting_start..span.words.len(), false)); + } + + for (range, wrap) in word_ranges { + for word in span.words[range].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 + let wrap = if self.rtl { + x - word_size < end_x + } else { + x + word_size > end_x + }; + if wrap && !glyphs.is_empty() { + let mut glyphs_swap = Vec::new(); + std::mem::swap(&mut glyphs, &mut glyphs_swap); + layout_lines.insert( + layout_i, + LayoutLine { + glyphs: glyphs_swap, + }, + ); + layout_i += 1; + + x = start_x; + 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; + + if self.rtl { + x -= x_advance + } + + glyphs.push(glyph.layout(font_size, x, y, span.rtl)); + push_line = true; + + if !self.rtl { + x += x_advance; + } + y += y_advance; + } + } + + if wrap { + let mut glyphs_swap = Vec::new(); + std::mem::swap(&mut glyphs, &mut glyphs_swap); + layout_lines.insert( + layout_i, + LayoutLine { + glyphs: glyphs_swap, + }, + ); + layout_i += 1; + + x = start_x; + y = 0.0; + } + } + } + + if push_line { + layout_lines.insert( + layout_i, + LayoutLine { + glyphs, + }, + ); + } + } +}