diff --git a/examples/rich-text/src/main.rs b/examples/rich-text/src/main.rs index f569884e44..b5d1ab4629 100644 --- a/examples/rich-text/src/main.rs +++ b/examples/rich-text/src/main.rs @@ -2,7 +2,7 @@ use cosmic_text::{ Action, Attrs, AttrsList, Buffer, BufferLine, Color, Edit, Editor, Family, FontSystem, Metrics, - Style, SwashCache, Weight, + Shaping, Style, SwashCache, Weight, }; use orbclient::{EventOption, Renderer, Window, WindowFlag}; use std::{ @@ -143,7 +143,7 @@ fn main() { editor .buffer_mut() .lines - .push(BufferLine::new(line_text, attrs_list)); + .push(BufferLine::new(line_text, attrs_list, Shaping::Advanced)); } let mut swash_cache = SwashCache::new(); diff --git a/examples/terminal/src/main.rs b/examples/terminal/src/main.rs index 852207f9ab..d5fbeda7c7 100644 --- a/examples/terminal/src/main.rs +++ b/examples/terminal/src/main.rs @@ -1,6 +1,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 -use cosmic_text::{Attrs, Buffer, Color, FontSystem, Metrics, SwashCache}; +use cosmic_text::{Attrs, Buffer, Color, FontSystem, Metrics, Shaping, SwashCache}; use std::cmp::{self, Ordering}; use termion::{color, cursor}; @@ -28,7 +28,7 @@ fn main() { let attrs = Attrs::new(); // Add some text! - buffer.set_text(" Hi, Rust! 🦀", attrs); + buffer.set_text(" Hi, Rust! 🦀", attrs, Shaping::Advanced); // Perform shaping as desired buffer.shape_until_scroll(); diff --git a/src/buffer.rs b/src/buffer.rs index 123ce384b9..e459e723e4 100644 --- a/src/buffer.rs +++ b/src/buffer.rs @@ -12,7 +12,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::Color; use crate::{ Attrs, AttrsList, BorrowedWithFontSystem, BufferLine, FontSystem, LayoutGlyph, LayoutLine, - ShapeLine, Wrap, + ShapeLine, Shaping, Wrap, }; /// Current cursor location @@ -331,7 +331,7 @@ impl Buffer { redraw: false, wrap: Wrap::Word, }; - buffer.set_text(font_system, "", Attrs::new()); + buffer.set_text(font_system, "", Attrs::new(), Shaping::Advanced); buffer } @@ -562,16 +562,28 @@ impl Buffer { } /// Set text of buffer, using provided attributes for each line by default - pub fn set_text(&mut self, font_system: &mut FontSystem, text: &str, attrs: Attrs) { + pub fn set_text( + &mut self, + font_system: &mut FontSystem, + text: &str, + attrs: Attrs, + shaping: Shaping, + ) { self.lines.clear(); for line in text.lines() { - self.lines - .push(BufferLine::new(line.to_string(), AttrsList::new(attrs))); + self.lines.push(BufferLine::new( + line.to_string(), + AttrsList::new(attrs), + shaping, + )); } // Make sure there is always one line if self.lines.is_empty() { - self.lines - .push(BufferLine::new(String::new(), AttrsList::new(attrs))); + self.lines.push(BufferLine::new( + String::new(), + AttrsList::new(attrs), + shaping, + )); } self.scroll = 0; @@ -769,8 +781,8 @@ impl<'a> BorrowedWithFontSystem<'a, Buffer> { } /// Set text of buffer, using provided attributes for each line by default - pub fn set_text(&mut self, text: &str, attrs: Attrs) { - self.inner.set_text(self.font_system, text, attrs); + pub fn set_text(&mut self, text: &str, attrs: Attrs, shaping: Shaping) { + self.inner.set_text(self.font_system, text, attrs, shaping); } /// Draw the buffer diff --git a/src/buffer_line.rs b/src/buffer_line.rs index fb808f0551..4ada0f95f3 100644 --- a/src/buffer_line.rs +++ b/src/buffer_line.rs @@ -1,7 +1,7 @@ #[cfg(not(feature = "std"))] use alloc::{string::String, vec::Vec}; -use crate::{Align, AttrsList, FontSystem, LayoutLine, ShapeLine, Wrap}; +use crate::{Align, AttrsList, FontSystem, LayoutLine, ShapeLine, Shaping, Wrap}; /// A line (or paragraph) of text that is shaped and laid out pub struct BufferLine { @@ -12,13 +12,14 @@ pub struct BufferLine { align: Option, shape_opt: Option, layout_opt: Option>, + shaping: Shaping, } impl BufferLine { /// Create a new line with the given text and attributes list /// Cached shaping and layout can be done using the [`Self::shape`] and /// [`Self::layout`] functions - pub fn new>(text: T, attrs_list: AttrsList) -> Self { + pub fn new>(text: T, attrs_list: AttrsList, shaping: Shaping) -> Self { Self { text: text.into(), attrs_list, @@ -26,6 +27,7 @@ impl BufferLine { align: None, shape_opt: None, layout_opt: None, + shaping, } } @@ -142,7 +144,7 @@ impl BufferLine { let attrs_list = self.attrs_list.split_off(index); self.reset(); - let mut new = Self::new(text, attrs_list); + let mut new = Self::new(text, attrs_list, self.shaping); new.wrap = self.wrap; new } @@ -167,7 +169,12 @@ impl BufferLine { /// Shape line, will cache results pub fn shape(&mut self, font_system: &mut FontSystem) -> &ShapeLine { if self.shape_opt.is_none() { - self.shape_opt = Some(ShapeLine::new(font_system, &self.text, &self.attrs_list)); + self.shape_opt = Some(ShapeLine::new( + font_system, + &self.text, + &self.attrs_list, + self.shaping, + )); self.layout_opt = None; } self.shape_opt.as_ref().expect("shape not found") diff --git a/src/edit/editor.rs b/src/edit/editor.rs index 1b82018d52..ff2f00b4d3 100644 --- a/src/edit/editor.rs +++ b/src/edit/editor.rs @@ -12,6 +12,7 @@ use unicode_segmentation::UnicodeSegmentation; use crate::Color; use crate::{ Action, Affinity, AttrsList, Buffer, BufferLine, Cursor, Edit, FontSystem, LayoutCursor, + Shaping, }; /// A wrapper of [`Buffer`] for easy editing @@ -245,6 +246,7 @@ impl Edit for Editor { .strip_suffix(char::is_control) .unwrap_or(data_line), these_attrs, + Shaping::Advanced, )); } else { panic!("str::lines() did not yield any elements"); @@ -256,6 +258,7 @@ impl Edit for Editor { .strip_suffix(char::is_control) .unwrap_or(data_line), final_attrs.split_off(remaining_split_len), + Shaping::Advanced, ); tmp.append(after); self.buffer.lines.insert(insert_line, tmp); @@ -270,6 +273,7 @@ impl Edit for Editor { .strip_suffix(char::is_control) .unwrap_or(data_line), final_attrs.split_off(remaining_split_len), + Shaping::Advanced, ); self.buffer.lines.insert(insert_line, tmp); self.cursor.line += 1; diff --git a/src/edit/syntect.rs b/src/edit/syntect.rs index f454285699..2c57ca5a20 100644 --- a/src/edit/syntect.rs +++ b/src/edit/syntect.rs @@ -9,7 +9,7 @@ use syntect::parsing::{ParseState, ScopeStack, SyntaxReference, SyntaxSet}; use crate::{ Action, AttrsList, BorrowedWithFontSystem, Buffer, Color, Cursor, Edit, Editor, FontSystem, - Style, Weight, Wrap, + Shaping, Style, Weight, Wrap, }; pub struct SyntaxSystem { @@ -75,7 +75,9 @@ impl<'a> SyntaxEditor<'a> { let path = path.as_ref(); let text = fs::read_to_string(path)?; - self.editor.buffer_mut().set_text(font_system, &text, attrs); + self.editor + .buffer_mut() + .set_text(font_system, &text, attrs, Shaping::Advanced); //TODO: re-use text self.syntax = match self.syntax_system.syntax_set.find_syntax_for_file(path) { diff --git a/src/lib.rs b/src/lib.rs index 7c98dcb381..f4b700e11e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,7 @@ //! point, you can use the `SwashCache` to rasterize glyphs into either images or pixels. //! //! ``` -//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics}; +//! use cosmic_text::{Attrs, Color, FontSystem, SwashCache, Buffer, Metrics, Shaping}; //! //! // A FontSystem provides access to detected system fonts, create one per application //! let mut font_system = FontSystem::new(); @@ -36,7 +36,7 @@ //! let attrs = Attrs::new(); //! //! // Add some text! -//! buffer.set_text("Hello, Rust! 🦀\n", attrs); +//! buffer.set_text("Hello, Rust! 🦀\n", attrs, Shaping::Advanced); //! //! // Perform shaping as desired //! buffer.shape_until_scroll(); diff --git a/src/shape.rs b/src/shape.rs index cbcf910339..e050d585f2 100644 --- a/src/shape.rs +++ b/src/shape.rs @@ -11,6 +11,46 @@ use unicode_segmentation::UnicodeSegmentation; use crate::fallback::FontFallbackIter; use crate::{Align, AttrsList, CacheKey, Color, Font, FontSystem, LayoutGlyph, LayoutLine, Wrap}; +/// The shaping strategy of some text. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Shaping { + /// Basic shaping with no font fallback. + /// + /// This shaping strategy is very cheap, but it will not display complex + /// scripts properly nor try to find missing glyphs in your system fonts. + /// + /// You should use this strategy when you have complete control of the text + /// and the font you are displaying in your application. + #[cfg(feature = "swash")] + Basic, + /// Advanced text shaping and font fallback. + /// + /// You will need to enable this strategy if the text contains a complex + /// script, the font used needs it, and/or multiple fonts in your system + /// may be needed to display all of the glyphs. + Advanced, +} + +impl Shaping { + fn run( + self, + font_system: &mut FontSystem, + line: &str, + attrs_list: &AttrsList, + start_run: usize, + end_run: usize, + span_rtl: bool, + ) -> Vec { + match self { + #[cfg(feature = "swash")] + Self::Basic => shape_skip(font_system, line, attrs_list, start_run, end_run), + Self::Advanced => { + shape_run(font_system, line, attrs_list, start_run, end_run, span_rtl) + } + } + } +} + fn shape_fallback( font: &Font, line: &str, @@ -213,6 +253,50 @@ fn shape_run( glyphs } +#[cfg(feature = "swash")] +fn shape_skip( + font_system: &mut FontSystem, + line: &str, + attrs_list: &AttrsList, + start_run: usize, + end_run: usize, +) -> Vec { + let attrs = attrs_list.get_span(start_run); + let fonts = font_system.get_font_matches(attrs); + + let default_families = [&attrs.family]; + let mut font_iter = FontFallbackIter::new(font_system, &fonts, &default_families, Vec::new()); + + let font = font_iter.next().expect("no default font found"); + let font_id = font.id(); + let font = font.as_swash(); + + let charmap = font.charmap(); + let glyph_metrics = font.glyph_metrics(&[]).scale(1.0); + + line[start_run..end_run] + .chars() + .enumerate() + .map(|(i, codepoint)| { + let glyph_id = charmap.map(codepoint); + let x_advance = glyph_metrics.advance_width(glyph_id); + + ShapeGlyph { + start: i, + end: i + 1, + x_advance, + y_advance: 0.0, + x_offset: 0.0, + y_offset: 0.0, + font_id, + glyph_id, + color_opt: attrs.color_opt, + metadata: attrs.metadata, + } + }) + .collect() +} + /// A shaped glyph pub struct ShapeGlyph { pub start: usize, @@ -278,6 +362,7 @@ impl ShapeWord { word_range: Range, level: unicode_bidi::Level, blank: bool, + shaping: Shaping, ) -> Self { let word = &line[word_range.clone()]; @@ -297,7 +382,7 @@ impl ShapeWord { let attrs_egc = attrs_list.get_span(start_egc); if !attrs.compatible(&attrs_egc) { //TODO: more efficient - glyphs.append(&mut shape_run( + glyphs.append(&mut shaping.run( font_system, line, attrs_list, @@ -312,7 +397,7 @@ impl ShapeWord { } if start_run < word_range.end { //TODO: more efficient - glyphs.append(&mut shape_run( + glyphs.append(&mut shaping.run( font_system, line, attrs_list, @@ -352,6 +437,7 @@ impl ShapeSpan { span_range: Range, line_rtl: bool, level: unicode_bidi::Level, + shaping: Shaping, ) -> Self { let span = &line[span_range.start..span_range.end]; @@ -382,6 +468,7 @@ impl ShapeSpan { (span_range.start + start_word)..(span_range.start + start_lb), level, false, + shaping, )); } if start_lb < end_lb { @@ -395,6 +482,7 @@ impl ShapeSpan { ..(span_range.start + start_lb + i + c.len_utf8()), level, true, + shaping, )); } } @@ -437,7 +525,12 @@ impl ShapeLine { /// # Panics /// /// Will panic if `line` contains more than one paragraph. - pub fn new(font_system: &mut FontSystem, line: &str, attrs_list: &AttrsList) -> Self { + pub fn new( + font_system: &mut FontSystem, + line: &str, + attrs_list: &AttrsList, + shaping: Shaping, + ) -> Self { let mut spans = Vec::new(); let bidi = unicode_bidi::BidiInfo::new(line, None); @@ -473,6 +566,7 @@ impl ShapeLine { start..i, line_rtl, run_level, + shaping, )); start = i; run_level = new_level; @@ -485,6 +579,7 @@ impl ShapeLine { start..line_range.end, line_rtl, run_level, + shaping, )); line_rtl };