2022-12-06 03:09:03 +00:00
|
|
|
// Copyright (c) 2022 Marceline Cramer
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
2022-12-06 03:00:13 +00:00
|
|
|
use std::collections::HashMap;
|
|
|
|
|
|
|
|
use canary_script::api::{DrawContext, Font, TextLayout};
|
|
|
|
use canary_script::{Color, Vec2};
|
|
|
|
|
|
|
|
#[derive(Default)]
|
|
|
|
pub struct TextCache {
|
|
|
|
layouts: Vec<OwnedText>,
|
|
|
|
fonts: Vec<HashMap<String, usize>>,
|
|
|
|
}
|
|
|
|
|
|
|
|
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<Word>,
|
|
|
|
}
|
|
|
|
|
|
|
|
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<Line>,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Layout {
|
2022-12-06 03:26:12 +00:00
|
|
|
pub fn draw(
|
|
|
|
&self,
|
|
|
|
cache: &TextCache,
|
|
|
|
ctx: &DrawContext,
|
|
|
|
scale: f32,
|
|
|
|
line_height: f32,
|
|
|
|
color: Color,
|
|
|
|
) {
|
2022-12-06 03:00:13 +00:00
|
|
|
let mut cursor = Vec2::ZERO;
|
|
|
|
for line in self.lines.iter() {
|
|
|
|
let ctx = ctx.with_offset(cursor);
|
2022-12-06 03:26:12 +00:00
|
|
|
line.draw(cache, &ctx, scale, color);
|
2022-12-06 03:00:13 +00:00
|
|
|
cursor.y += line_height;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A finished line of a layout.
|
|
|
|
pub struct Line {
|
|
|
|
pub fragments: Vec<Fragment>,
|
2022-12-05 21:44:44 +00:00
|
|
|
}
|
|
|
|
|
2022-12-06 03:00:13 +00:00
|
|
|
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 }
|
|
|
|
}
|
|
|
|
|
2022-12-06 03:26:12 +00:00
|
|
|
pub fn draw(&self, cache: &TextCache, ctx: &DrawContext, scale: f32, color: Color) {
|
2022-12-06 03:00:13 +00:00
|
|
|
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);
|
|
|
|
}
|
2022-12-05 21:44:44 +00:00
|
|
|
|
2022-12-06 03:00:13 +00:00
|
|
|
cursor.x += fragment.advance * scale;
|
|
|
|
}
|
2022-12-05 21:44:44 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-06 03:00:13 +00:00
|
|
|
|
|
|
|
/// 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,
|
|
|
|
}
|