canary-rs/crates/textwrap/src/lib.rs

249 lines
6.4 KiB
Rust

// 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<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 {
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<Fragment>,
}
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,
}