From feec4de6575731c1a656312c18a8c11617862014 Mon Sep 17 00:00:00 2001 From: mars Date: Mon, 5 Dec 2022 14:44:44 -0700 Subject: [PATCH 1/6] Add initial canary-textwrap crate --- Cargo.toml | 1 + crates/textwrap/Cargo.toml | 8 ++++++++ crates/textwrap/src/lib.rs | 14 ++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 crates/textwrap/Cargo.toml create mode 100644 crates/textwrap/src/lib.rs diff --git a/Cargo.toml b/Cargo.toml index 287df87..354eb52 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,6 +4,7 @@ members = [ "apps/music-player", "apps/sandbox", "crates/script", + "crates/textwrap", "scripts/music-player", "scripts/sao-ui", ] diff --git a/crates/textwrap/Cargo.toml b/crates/textwrap/Cargo.toml new file mode 100644 index 0000000..73212a9 --- /dev/null +++ b/crates/textwrap/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "canary-textwrap" +version = "0.1.0" +edition = "2021" + +[dependencies] +canary-script = { path = "../script" } +textwrap = "0.16" diff --git a/crates/textwrap/src/lib.rs b/crates/textwrap/src/lib.rs new file mode 100644 index 0000000..7d12d9a --- /dev/null +++ b/crates/textwrap/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: usize, right: usize) -> usize { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} From 9920fea90013fab8bca7226f0382b15ccda21a0f Mon Sep 17 00:00:00 2001 From: mars Date: Mon, 5 Dec 2022 19:59:46 -0700 Subject: [PATCH 2/6] Better ID handling in Font + TextLayout --- crates/script/src/api/mod.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/script/src/api/mod.rs b/crates/script/src/api/mod.rs index 0df721e..47944cd 100644 --- a/crates/script/src/api/mod.rs +++ b/crates/script/src/api/mod.rs @@ -101,16 +101,22 @@ impl Panel { } #[repr(transparent)] -#[derive(Copy, Clone)] +#[derive(Copy, Clone, Debug)] pub struct Font(u32); impl Font { pub fn new(family: &str) -> Self { unsafe { Self(font_load(family.as_ptr() as u32, family.len() as u32)) } } + + /// Retrieves the script-local identifier of this font. + pub fn get_id(&self) -> u32 { + self.0 + } } #[repr(transparent)] +#[derive(Debug)] pub struct TextLayout(u32); impl TextLayout { From e4a279c230375e7dd943ef88fb6ac252c3c3e6a8 Mon Sep 17 00:00:00 2001 From: mars Date: Mon, 5 Dec 2022 20:00:13 -0700 Subject: [PATCH 3/6] Barely-functional Canary text wrapping --- crates/textwrap/src/lib.rs | 245 +++++++++++++++++++++++++++++++++++-- 1 file changed, 236 insertions(+), 9 deletions(-) diff --git a/crates/textwrap/src/lib.rs b/crates/textwrap/src/lib.rs index 7d12d9a..02ebd0b 100644 --- a/crates/textwrap/src/lib.rs +++ b/crates/textwrap/src/lib.rs @@ -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, + fonts: Vec>, } -#[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, +} + +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) { + 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, +} + +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, +} From c7981b50640ba218c6a68fe5888d159d09cc1f2a Mon Sep 17 00:00:00 2001 From: mars Date: Mon, 5 Dec 2022 20:09:03 -0700 Subject: [PATCH 4/6] Add Apache 2.0 license to canary-textwrap --- crates/textwrap/Cargo.toml | 1 + crates/textwrap/src/lib.rs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/crates/textwrap/Cargo.toml b/crates/textwrap/Cargo.toml index 73212a9..1462ef6 100644 --- a/crates/textwrap/Cargo.toml +++ b/crates/textwrap/Cargo.toml @@ -2,6 +2,7 @@ name = "canary-textwrap" version = "0.1.0" edition = "2021" +license = "Apache-2.0" [dependencies] canary-script = { path = "../script" } diff --git a/crates/textwrap/src/lib.rs b/crates/textwrap/src/lib.rs index 02ebd0b..28ab6fe 100644 --- a/crates/textwrap/src/lib.rs +++ b/crates/textwrap/src/lib.rs @@ -1,3 +1,6 @@ +// Copyright (c) 2022 Marceline Cramer +// SPDX-License-Identifier: Apache-2.0 + use std::collections::HashMap; use canary_script::api::{DrawContext, Font, TextLayout}; From 70f9ca44057ce87e7d84a39f46a4877fc5f3d095 Mon Sep 17 00:00:00 2001 From: mars Date: Mon, 5 Dec 2022 20:20:33 -0700 Subject: [PATCH 5/6] Add configurable textwrap scale and line height --- crates/textwrap/src/lib.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/textwrap/src/lib.rs b/crates/textwrap/src/lib.rs index 28ab6fe..6e602a2 100644 --- a/crates/textwrap/src/lib.rs +++ b/crates/textwrap/src/lib.rs @@ -163,12 +163,11 @@ pub struct Layout { } impl Layout { - pub fn draw(&self, cache: &TextCache, ctx: &DrawContext) { - let line_height = 12.0; + pub fn draw(&self, cache: &TextCache, ctx: &DrawContext, scale: f32, line_height: f32) { let mut cursor = Vec2::ZERO; for line in self.lines.iter() { let ctx = ctx.with_offset(cursor); - line.draw(cache, &ctx); + line.draw(cache, &ctx, scale); cursor.y += line_height; } } @@ -209,8 +208,7 @@ impl Line { Self { fragments } } - pub fn draw(&self, cache: &TextCache, ctx: &DrawContext) { - let scale = 10.0; + pub fn draw(&self, cache: &TextCache, ctx: &DrawContext, scale: f32) { let mut cursor = Vec2::ZERO; for fragment in self.fragments.iter() { if !fragment.hidden { From 38674c25808c00007a45100f76870a7fc0fb032d Mon Sep 17 00:00:00 2001 From: mars Date: Mon, 5 Dec 2022 20:26:12 -0700 Subject: [PATCH 6/6] Add configurable text color to textwrap --- crates/textwrap/src/lib.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/crates/textwrap/src/lib.rs b/crates/textwrap/src/lib.rs index 6e602a2..d6feb28 100644 --- a/crates/textwrap/src/lib.rs +++ b/crates/textwrap/src/lib.rs @@ -163,11 +163,18 @@ pub struct Layout { } impl Layout { - pub fn draw(&self, cache: &TextCache, ctx: &DrawContext, scale: f32, line_height: f32) { + 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); + line.draw(cache, &ctx, scale, color); cursor.y += line_height; } } @@ -208,13 +215,12 @@ impl Line { Self { fragments } } - pub fn draw(&self, cache: &TextCache, ctx: &DrawContext, scale: f32) { + 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; - let color = Color::WHITE; ctx.draw_text_layout(text.layout, offset, scale, color); }