Barely-functional Canary text wrapping
This commit is contained in:
parent
9920fea900
commit
e4a279c230
|
@ -1,14 +1,241 @@
|
|||
pub fn add(left: usize, right: usize) -> usize {
|
||||
left + right
|
||||
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>>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
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);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let result = add(2, 2);
|
||||
assert_eq!(result, 4);
|
||||
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) {
|
||||
let line_height = 12.0;
|
||||
let mut cursor = Vec2::ZERO;
|
||||
for line in self.lines.iter() {
|
||||
let ctx = ctx.with_offset(cursor);
|
||||
line.draw(cache, &ctx);
|
||||
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) {
|
||||
let scale = 10.0;
|
||||
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;
|
||||
let color = Color::WHITE;
|
||||
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,
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue