From ea64291abb07cd2acf868dfb7d999db58d30339d Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 26 Oct 2022 15:16:06 -0600 Subject: [PATCH] Make it possible to set attributes per glyph --- examples/rich-text/src/main.rs | 20 +- src/attrs.rs | 50 ++++- src/buffer.rs | 19 +- src/shape.rs | 333 +++++++++++++++++++-------------- 4 files changed, 254 insertions(+), 168 deletions(-) diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index 2ea3a5e..73b0266 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use cosmic_text::{Attrs, AttrsSpan, Color, Family, FontSystem, Style, SwashCache, +use cosmic_text::{Attrs, 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}}; @@ -39,6 +39,11 @@ fn main() { let mut line_i = 0; for &(text, attrs) in &[ + ("B", attrs.weight(Weight::BOLD)), + ("old ", attrs), + ("I", attrs.style(Style::Italic)), + ("talic", attrs), + ("\n", attrs), ("Sans-Serif Normal ", attrs), ("Sans-Serif Bold ", attrs.weight(Weight::BOLD)), ("Sans-Serif Italic ", attrs.style(Style::Italic)), @@ -73,6 +78,13 @@ fn main() { ("Blue ", attrs.color(Color::rgb(0x00, 0x00, 0xFF))), ("Indigo ", attrs.color(Color::rgb(0x4B, 0x00, 0x82))), ("Violet ", attrs.color(Color::rgb(0x94, 0x00, 0xD3))), + ("U", attrs.color(Color::rgb(0x94, 0x00, 0xD3))), + ("N", attrs.color(Color::rgb(0x4B, 0x00, 0x82))), + ("I", attrs.color(Color::rgb(0x00, 0x00, 0xFF))), + ("C", attrs.color(Color::rgb(0x00, 0xFF, 0x00))), + ("O", attrs.color(Color::rgb(0xFF, 0xFF, 0x00))), + ("R", attrs.color(Color::rgb(0xFF, 0x7F, 0x00))), + ("N", attrs.color(Color::rgb(0xFF, 0x00, 0x00))), ] { if text == "\n" { line_i += 1; @@ -86,11 +98,7 @@ fn main() { let start = line.text.len(); line.text.push_str(text); let end = line.text.len(); - line.attrs_spans.push(AttrsSpan { - start, - end, - attrs, - }); + line.attrs_list.add_span(start, end, attrs); line.reset(); } diff --git a/src/attrs.rs b/src/attrs.rs index 1763563..e9b2c59 100644 --- a/src/attrs.rs +++ b/src/attrs.rs @@ -80,10 +80,52 @@ impl<'a> Attrs<'a> { //TODO: smarter way of including emoji (face.monospaced == self.monospaced || face.post_script_name.contains("Emoji")) } + + pub fn compatible(&self, other: &Self) -> bool { + self.family == other.family + && self.monospaced == other.monospaced + && self.stretch == other.stretch + && self.style == other.style + && self.weight == other.weight + } } -pub struct AttrsSpan<'a> { - pub start: usize, - pub end: usize, - pub attrs: Attrs<'a> +pub struct AttrsList<'a> { + defaults: Attrs<'a>, + spans: Vec<(usize, usize, Attrs<'a>)>, +} + +impl<'a> AttrsList<'a> { + pub fn new(defaults: Attrs<'a>) -> Self { + Self { + defaults, + spans: Vec::new(), + } + } + + pub fn defaults(&self) -> Attrs<'a> { + self.defaults + } + + pub fn spans(&self) -> &Vec<(usize, usize, Attrs<'a>)> { + &self.spans + } + + pub fn clear_spans(&mut self) { + self.spans.clear(); + } + + pub fn add_span(&mut self, start: usize, end: usize, attrs: Attrs<'a>) { + self.spans.push((start, end, attrs)); + } + + pub fn get_span(&self, start: usize, end: usize) -> Attrs<'a> { + let mut attrs = self.defaults; + for span in self.spans.iter() { + if start >= span.0 && end <= span.1 { + attrs = span.2; + } + } + attrs + } } diff --git a/src/buffer.rs b/src/buffer.rs index e1524f4..6d73869 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -7,7 +7,7 @@ use std::{ }; use unicode_segmentation::UnicodeSegmentation; -use crate::{Attrs, AttrsSpan, FontSystem, LayoutGlyph, LayoutLine, ShapeLine}; +use crate::{Attrs, AttrsList, FontSystem, LayoutGlyph, LayoutLine, ShapeLine}; /// An action to perform on a [TextBuffer] #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -175,21 +175,16 @@ impl fmt::Display for TextMetrics { pub struct TextBufferLine<'a> { pub text: String, - pub attrs_spans: Vec>, + pub attrs_list: AttrsList<'a>, shape_opt: Option, layout_opt: Option>, } 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, + attrs_list: AttrsList::new(attrs), shape_opt: None, layout_opt: None, } @@ -206,7 +201,7 @@ impl<'a> TextBufferLine<'a> { pub fn shape(&mut self, font_system: &'a FontSystem<'a>) -> &ShapeLine { if self.shape_opt.is_none() { - self.shape_opt = Some(ShapeLine::new(font_system, &self.text, &self.attrs_spans)); + self.shape_opt = Some(ShapeLine::new(font_system, &self.text, &self.attrs_list)); self.layout_opt = None; } self.shape_opt.as_ref().unwrap() @@ -509,12 +504,8 @@ impl<'a> TextBuffer<'a> { self.attrs = attrs; for line in self.lines.iter_mut() { + line.attrs_list = AttrsList::new(attrs); line.reset(); - line.attrs_spans = vec![AttrsSpan { - start: 0, - end: line.text.len(), - attrs - }]; } self.shape_until_scroll(); diff --git a/src/shape.rs b/src/shape.rs index 54c5ab2..5e5c877 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -1,19 +1,20 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 use unicode_script::{Script, UnicodeScript}; +use unicode_segmentation::UnicodeSegmentation; -use crate::{Attrs, AttrsSpan, CacheKey, Color, Font, FontSystem, LayoutGlyph, LayoutLine}; +use crate::{AttrsList, 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, + attrs_list: &AttrsList, + start_run: usize, + end_run: usize, span_rtl: bool, ) -> (Vec, Vec) { - let word = &line[start_word..end_word]; + let run = &line[start_run..end_run]; let font_scale = font.rustybuzz.units_per_em() as f32; @@ -23,7 +24,7 @@ fn shape_fallback( } else { rustybuzz::Direction::LeftToRight }); - buffer.push_str(word); + buffer.push_str(run); buffer.guess_segment_properties(); let rtl = match buffer.direction() { @@ -45,32 +46,23 @@ fn shape_fallback( 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; + let start_glyph = start_run + 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 + end: end_run, // 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, + color_opt: None, }); } @@ -99,9 +91,149 @@ fn shape_fallback( } } + // Set color + //TODO: these attributes should not be related to shaping + for glyph in glyphs.iter_mut() { + let attrs = attrs_list.get_span(glyph.start, glyph.end); + glyph.color_opt = attrs.color_opt; + } + (glyphs, missing) } +fn shape_run<'a>( + font_system: &'a FontSystem<'a>, + line: &str, + attrs_list: &AttrsList<'a>, + start_run: usize, + end_run: usize, + span_rtl: bool, +) -> Vec { + //TODO: use smallvec? + let mut scripts = Vec::new(); + for c in line[start_run..end_run].chars() { + match c.script() { + Script::Common | + Script::Inherited | + Script::Latin | + Script::Unknown => (), + script => if ! scripts.contains(&script) { + scripts.push(script); + }, + } + } + + log::trace!( + " Run {:?}: '{}'", + scripts, + &line[start_run..end_run], + ); + + let attrs = attrs_list.get_span(start_run, end_run); + + 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_list, + start_run, + end_run, + 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_list, + start_run, + end_run, + 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_run..end_run]); + + /* + for glyph in glyphs.iter() { + log::trace!("'{}': {}, {}, {}, {}", &line[glyph.start..glyph.end], glyph.x_advance, glyph.y_advance, glyph.x_offset, glyph.y_offset); + } + */ + + glyphs +} + pub struct ShapeGlyph { pub start: usize, pub end: usize, @@ -149,142 +281,55 @@ impl ShapeWord { pub fn new<'a>( font_system: &'a FontSystem<'a>, line: &str, - attrs_spans: &[AttrsSpan<'a>], + attrs_list: &AttrsList<'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); - }, - } - } + let word = &line[start_word..end_word]; log::trace!( - " Word {:?}{}: '{}'", - scripts, + " Word{}: '{}'", if blank { " BLANK" } else { "" }, - &line[start_word..end_word], + 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 mut glyphs = Vec::new(); + + let mut start_run = start_word; + let mut attrs = attrs_list.defaults(); + for (egc_i, egc) in word.grapheme_indices(true) { + let start_egc = start_word + egc_i; + let end_egc = start_egc + egc.len(); + let attrs_egc = attrs_list.get_span(start_egc, end_egc); + if ! attrs.compatible(&attrs_egc) { + //TODO: more efficient + glyphs.append(&mut shape_run( + font_system, + line, + attrs_list, + start_run, + start_egc, + span_rtl + )); + + start_run = start_egc; + attrs = attrs_egc; } } - - 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, + if start_run < end_word { + //TODO: more efficient + glyphs.append(&mut shape_run( + font_system, line, - attrs_spans, - start_word, + attrs_list, + start_run, 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; - } - } - } + span_rtl + )); } - // 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 } } } @@ -298,7 +343,7 @@ impl ShapeSpan { pub fn new<'a>( font_system: &'a FontSystem<'a>, line: &str, - attrs_spans: &[AttrsSpan<'a>], + attrs_list: &AttrsList<'a>, start_span: usize, end_span: usize, line_rtl: bool, @@ -328,7 +373,7 @@ impl ShapeSpan { words.push(ShapeWord::new( font_system, line, - attrs_spans, + attrs_list, start_span + start_word, start_span + start_lb, span_rtl, @@ -339,7 +384,7 @@ impl ShapeSpan { words.push(ShapeWord::new( font_system, line, - attrs_spans, + attrs_list, start_span + start_lb, start_span + end_lb, span_rtl, @@ -377,7 +422,7 @@ impl ShapeLine { pub fn new<'a>( font_system: &'a FontSystem<'a>, line: &str, - attrs_spans: &[AttrsSpan<'a>] + attrs_list: &AttrsList<'a> ) -> Self { let mut spans = Vec::new(); @@ -401,7 +446,7 @@ impl ShapeLine { spans.push(ShapeSpan::new( font_system, line, - attrs_spans, + attrs_list, start, i, line_rtl, @@ -414,7 +459,7 @@ impl ShapeLine { spans.push(ShapeSpan::new( font_system, line, - attrs_spans, + attrs_list, start, line.len(), line_rtl,