// Copyright (c) 2022 Marceline Cramer // SPDX-License-Identifier: Apache-2.0 use std::collections::HashMap; use canary_script::api::{DrawContext, Font, TextLayout}; use canary_script::{Color, Vec2}; #[derive(Default)] pub struct TextCache { layouts: Vec, fonts: Vec>, } impl TextCache { pub fn insert(&mut self, font: Font, text: &str) -> (usize, f32) { let font_idx = font.get_id() as usize; if let Some(font_cache) = self.fonts.get(font_idx) { if let Some(layout_idx) = font_cache.get(text) { return (*layout_idx, self.get(*layout_idx).width); } } else { let new_size = font_idx + 1; self.fonts.resize_with(new_size, Default::default); } let index = self.layouts.len(); let layout = TextLayout::new(&font, text); let width = layout.get_bounds().width().max(0.0); self.layouts.push(OwnedText { font, layout, width, }); self.fonts[font_idx].insert(text.to_string(), index); (index, width) } pub fn get(&self, index: usize) -> Text<'_> { self.layouts[index].borrow() } } /// Processed text that can be laid out. pub struct Content { pub words: Vec, } impl Content { pub fn from_plain(cache: &mut TextCache, font: Font, text: &str) -> Self { use textwrap::word_splitters::{split_words, WordSplitter}; use textwrap::WordSeparator; let separator = WordSeparator::new(); let words = separator.find_words(text); // TODO: crate feature to enable hyphenation support? let splitter = WordSplitter::NoHyphenation; let split_words = split_words(words, &splitter); let mut words = Vec::new(); for split_word in split_words { let (word, word_width) = cache.insert(font, split_word.word); let (whitespace, whitespace_width) = cache.insert(font, "_"); let (penalty, penalty_width) = cache.insert(font, split_word.penalty); words.push(Word { word, word_width, whitespace, whitespace_width, penalty, penalty_width, }); } Self { words } } pub fn layout(&self, cache: &TextCache, width: f32) -> Layout { use textwrap::wrap_algorithms::wrap_optimal_fit; let fragments = self.words.as_slice(); let line_widths = &[width as f64]; let penalties = Default::default(); // Should never fail with reasonable input. Check [wrap_optimal_fit] docs for more info. let wrapped_lines = wrap_optimal_fit(fragments, line_widths, &penalties).unwrap(); let mut lines = Vec::new(); for line in wrapped_lines { lines.push(Line::from_word_line(cache, line)); } Layout { lines } } } /// An atomic fragment of processed text that is ready to be laid out. /// /// May or may not correspond to a single English "word". /// /// Please see [textwrap::core::Word] and [textwrap::core::Fragment] for more information. #[derive(Debug)] pub struct Word { pub word: usize, pub word_width: f32, pub whitespace: usize, pub whitespace_width: f32, pub penalty: usize, pub penalty_width: f32, } impl textwrap::core::Fragment for Word { fn width(&self) -> f64 { self.word_width as f64 } fn whitespace_width(&self) -> f64 { self.whitespace_width as f64 } fn penalty_width(&self) -> f64 { self.penalty_width as f64 } } #[derive(Debug)] pub struct OwnedText { pub font: Font, pub layout: TextLayout, pub width: f32, } impl OwnedText { pub fn borrow(&self) -> Text<'_> { Text { font: self.font, layout: &self.layout, width: self.width, } } } /// A single piece of renderable text. #[derive(Debug)] pub struct Text<'a> { /// The font that this fragment has been laid out with. pub font: Font, /// The draw-ready [TextLayout] of this fragment. pub layout: &'a TextLayout, /// The width of this text. pub width: f32, } /// A finished, wrapped text layout. pub struct Layout { pub lines: Vec, } impl Layout { pub fn draw( &self, cache: &TextCache, ctx: &DrawContext, scale: f32, line_height: f32, color: Color, ) { let mut cursor = Vec2::ZERO; for line in self.lines.iter() { let ctx = ctx.with_offset(cursor); line.draw(cache, &ctx, scale, color); cursor.y += line_height; } } } /// A finished line of a layout. pub struct Line { pub fragments: Vec, } impl Line { pub fn from_word_line(cache: &TextCache, words: &[Word]) -> Self { let last_idx = words.len() - 1; let mut fragments = Vec::new(); let mut add_word = |index: usize, hidden: bool| { let text = cache.get(index); fragments.push(Fragment { font: text.font, text: index, offset: Vec2::ZERO, advance: text.width, hidden, }); }; for (idx, word) in words.iter().enumerate() { add_word(word.word, false); if idx == last_idx { add_word(word.penalty, false); } else { add_word(word.whitespace, true); } } Self { fragments } } pub fn draw(&self, cache: &TextCache, ctx: &DrawContext, scale: f32, color: Color) { let mut cursor = Vec2::ZERO; for fragment in self.fragments.iter() { if !fragment.hidden { let text = cache.get(fragment.text); let offset = cursor + fragment.offset; ctx.draw_text_layout(text.layout, offset, scale, color); } cursor.x += fragment.advance * scale; } } } /// A finished fragment in a layout. pub struct Fragment { /// The font of this fragment. pub font: Font, /// The index into the [TextCache] of the content of this fragment. pub text: usize, /// The offset for drawing the text layout. pub offset: Vec2, /// The horizontal advance to draw the next fragment. pub advance: f32, /// Whether this fragment should be skipped while drawing. pub hidden: bool, }