From f947d82049712580f54850d61f57649a89dca83b Mon Sep 17 00:00:00 2001 From: Corwin <corwin@kuiper.dev> Date: Wed, 28 Jun 2023 20:29:09 +0100 Subject: [PATCH] text rendering that supports different alignments --- agb/examples/object_text_render.rs | 37 ++- agb/src/display/object/font.rs | 301 +++++++++++++++------- agb/src/display/object/font/preprocess.rs | 206 +++++++++++++++ agb/src/display/object/font/renderer.rs | 120 +++++++++ 4 files changed, 556 insertions(+), 108 deletions(-) create mode 100644 agb/src/display/object/font/preprocess.rs create mode 100644 agb/src/display/object/font/renderer.rs diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index a5f8f5bf..bd2397de 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -4,7 +4,7 @@ use agb::{ display::{ object::{ - font::{BufferedWordRender, Configuration}, + font::{BufferedRender, TextAlignment}, PaletteVram, Size, }, palette16::Palette16, @@ -15,6 +15,9 @@ use agb::{ }; use agb_fixnum::Rect; +extern crate alloc; +use alloc::vec::Vec; + use core::fmt::Write; const FONT: Font = include_font!("examples/font/pixelated.ttf", 8); @@ -32,9 +35,7 @@ fn main(mut gba: agb::Gba) -> ! { let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); - let config = Configuration::new(Size::S16x8, palette); - - let mut wr = BufferedWordRender::new(&FONT, config); + let mut wr = BufferedRender::new(&FONT, Size::S16x8, palette); let _ = writeln!( wr, "{}", @@ -54,22 +55,40 @@ fn main(mut gba: agb::Gba) -> ! { let mut num_letters = 0; let mut frame = 0; + let mut alignment = TextAlignment::Left; + + let mut text = Vec::new(); + loop { vblank.wait_for_vblank(); input.update(); let oam = &mut unmanaged.iter(); - wr.commit(oam); + for (letter, slot) in text.iter().zip(oam) { + slot.set(letter); + } let start = timer.value(); - wr.update( - Rect::new((WIDTH / 8, 0).into(), (80, 100).into()), - num_letters, - ); wr.process(); + text = wr.layout( + Rect::new((WIDTH / 8, 0).into(), (80, 100).into()), + alignment, + num_letters, + 2, + ); let end = timer.value(); agb::println!("Took {} cycles", 256 * (end.wrapping_sub(start) as u32)); + if input.is_just_pressed(Button::LEFT) { + alignment = TextAlignment::Left; + } + if input.is_just_pressed(Button::RIGHT) { + alignment = TextAlignment::Right; + } + if input.is_just_pressed(Button::UP | Button::DOWN) { + alignment = TextAlignment::Center; + } + frame += 1; // if frame % 2 == 0 { diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 4eea710e..75149d1b 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,127 +1,230 @@ +use core::fmt::Write; + +use agb_fixnum::{Rect, Vector2D}; +use alloc::{collections::VecDeque, vec::Vec}; + use crate::display::Font; -use super::{DynamicSprite, PaletteVram, Size, SpriteVram}; +use self::{ + preprocess::{Line, Preprocessed, PreprocessedElement}, + renderer::{Configuration, WordRender}, +}; -struct LetterGroup { +use super::{DynamicSprite, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; + +mod preprocess; +mod renderer; + +#[derive(Debug, PartialEq, Eq)] +#[non_exhaustive] +pub(crate) enum WhiteSpace { + NewLine, + Space, +} + +impl WhiteSpace { + pub(crate) fn from_char(c: char) -> Self { + match c { + ' ' => WhiteSpace::Space, + '\n' => WhiteSpace::NewLine, + _ => panic!("char not supported whitespace"), + } + } +} + +#[derive(Debug)] +pub(crate) struct LetterGroup { sprite: SpriteVram, // the width of the letter group width: u16, left: i16, } -struct WorkingLetter { - dynamic: DynamicSprite, - // the x offset of the current letter with respect to the start of the current letter group - x_position: i32, - // where to render the letter from x_min to x_max - x_offset: i32, -} - -impl WorkingLetter { - fn new(size: Size) -> Self { - Self { - dynamic: DynamicSprite::new(size), - x_position: 0, - x_offset: 0, - } - } - - fn reset(&mut self) { - self.x_position = 0; - self.x_offset = 0; - } -} - -pub struct Configuration { - sprite_size: Size, - palette: PaletteVram, -} - -impl Configuration { - #[must_use] - pub fn new(sprite_size: Size, palette: PaletteVram) -> Self { - Self { - sprite_size, - palette, - } - } -} - -struct WordRender<'font> { +pub struct BufferedRender<'font> { + char_render: WordRender, + preprocessor: Preprocessed, + buffered_chars: VecDeque<char>, + letters: Letters, font: &'font Font, - working: WorkingLetter, - config: Configuration, } -impl<'font> WordRender<'font> { +#[derive(Debug, Default)] +struct Letters { + letters: Vec<LetterGroup>, + word_lengths: Vec<u8>, + current_word_length: usize, + number_of_groups: usize, +} + +impl Write for BufferedRender<'_> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for c in s.chars() { + self.input_character(c); + } + + Ok(()) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum TextAlignment { + Left, + Right, + Center, +} + +struct TextAlignmentSettings { + space_width: i32, + start_x: i32, +} + +impl TextAlignment { + fn settings( + self, + line: &Line, + minimum_space_width: i32, + size: Vector2D<i32>, + ) -> TextAlignmentSettings { + match self { + TextAlignment::Left => TextAlignmentSettings { + space_width: minimum_space_width, + start_x: 0, + }, + TextAlignment::Right => TextAlignmentSettings { + space_width: minimum_space_width, + start_x: size.x - line.width(), + }, + TextAlignment::Center => TextAlignmentSettings { + space_width: minimum_space_width, + start_x: (size.x - line.width()) / 2, + }, + } + } +} + +impl<'font> BufferedRender<'font> { #[must_use] - fn new(font: &'font Font, config: Configuration) -> Self { - WordRender { + pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + let config = Configuration::new(sprite_size, palette); + BufferedRender { + char_render: WordRender::new(config), + preprocessor: Preprocessed::new(), + buffered_chars: VecDeque::new(), + letters: Default::default(), font, - working: WorkingLetter::new(config.sprite_size), - - config, } } } -impl WordRender<'_> { - #[must_use] - fn finalise_letter(&mut self) -> Option<LetterGroup> { - if self.working.x_offset == 0 { - return None; - } - - let mut new_sprite = DynamicSprite::new(self.config.sprite_size); - core::mem::swap(&mut self.working.dynamic, &mut new_sprite); - let sprite = new_sprite.to_vram(self.config.palette.clone()); - - let group = LetterGroup { - sprite, - width: self.working.x_offset as u16, - left: self.working.x_position as i16, - }; - self.working.reset(); - - Some(group) +impl BufferedRender<'_> { + fn input_character(&mut self, character: char) { + self.preprocessor.add_character(self.font, character); + self.buffered_chars.push_back(character); } - #[must_use] - fn render_char(&mut self, c: char) -> Option<LetterGroup> { - let font_letter = self.font.letter(c); - - // uses more than the sprite can hold - let group = if self.working.x_offset + font_letter.width as i32 - > self.config.sprite_size.to_width_height().0 as i32 - { - self.finalise_letter() - } else { - None - }; - - if self.working.x_offset == 0 { - self.working.x_position = font_letter.xmin as i32; - } else { - self.working.x_offset += font_letter.xmin as i32; - } - - let y_position = self.font.ascent() - font_letter.height as i32 - font_letter.ymin as i32; - - for y in 0..font_letter.height as usize { - for x in 0..font_letter.width as usize { - let rendered = font_letter.bit_absolute(x, y); - if rendered { - self.working.dynamic.set_pixel( - x + self.working.x_offset as usize, - (y_position + y as i32) as usize, - 1, + pub fn process(&mut self) { + let Some(c) = self.buffered_chars.pop_front() else { return }; + match c { + ' ' | '\n' => { + if let Some(group) = self.char_render.finalise_letter() { + self.letters.letters.push(group); + self.letters.current_word_length += 1; + self.letters.number_of_groups += 1; + } + if self.letters.current_word_length != 0 { + self.letters.word_lengths.push( + self.letters + .current_word_length + .try_into() + .expect("word is too big"), ); } + self.letters.current_word_length = 0; + self.letters.number_of_groups += 1; + } + letter => { + if let Some(group) = self.char_render.render_char(self.font, letter) { + self.letters.letters.push(group); + self.letters.current_word_length += 1; + self.letters.number_of_groups += 1; + } } } + } - self.working.x_offset += font_letter.advance_width as i32; + #[must_use] + pub fn layout( + &mut self, + area: Rect<i32>, + alignment: TextAlignment, + number_of_groups: usize, + paragraph_spacing: i32, + ) -> Vec<ObjectUnmanaged> { + let mut objects = Vec::new(); - group + while !self.buffered_chars.is_empty() && self.letters.number_of_groups <= number_of_groups { + self.process(); + } + + let minimum_space_width = self.font.letter(' ').advance_width as i32; + + let lines = self.preprocessor.lines(area.size.x, minimum_space_width); + let mut head_position = area.position; + + let mut processed_depth = 0; + let mut group_depth = 0; + let mut word_depth = 0; + let mut rendered_groups = 0; + + 'outer: for line in lines { + let settings = alignment.settings(&line, minimum_space_width, area.size); + head_position.x += settings.start_x; + + for idx in 0..line.number_of_text_elements() { + let element = self.preprocessor.get(processed_depth + idx); + match element { + PreprocessedElement::Word(_) => { + for _ in 0..self + .letters + .word_lengths + .get(word_depth) + .copied() + .unwrap_or(u8::MAX) + { + let letter_group = &self.letters.letters[group_depth]; + let mut object = ObjectUnmanaged::new(letter_group.sprite.clone()); + head_position.x += letter_group.left as i32; + object.set_position(head_position); + head_position.x += letter_group.width as i32; + object.show(); + objects.push(object); + group_depth += 1; + rendered_groups += 1; + if rendered_groups >= number_of_groups { + break 'outer; + } + } + word_depth += 1; + } + PreprocessedElement::WhiteSpace(space_type) => { + if space_type == WhiteSpace::NewLine { + head_position.y += paragraph_spacing; + } + head_position.x += settings.space_width; + rendered_groups += 1; + if rendered_groups >= number_of_groups { + break 'outer; + } + } + } + } + + processed_depth += line.number_of_text_elements(); + head_position.x = area.position.x; + head_position.y += 9; + } + + objects } } diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs new file mode 100644 index 00000000..26023608 --- /dev/null +++ b/agb/src/display/object/font/preprocess.rs @@ -0,0 +1,206 @@ +use alloc::vec::Vec; + +use crate::display::Font; + +use super::WhiteSpace; + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum PreprocessedElement { + Word(u8), + WhiteSpace(WhiteSpace), +} + +#[derive(Clone, Copy, PartialEq, Eq)] +struct PreprocessedElementStored(u8); + +impl core::fmt::Debug for PreprocessedElementStored { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + f.debug_tuple("PreprocessedElementStored") + .field(&self.parse()) + .finish() + } +} + +impl PreprocessedElementStored { + fn parse(self) -> PreprocessedElement { + match self.0 { + 255 => PreprocessedElement::WhiteSpace(WhiteSpace::NewLine), + 254 => PreprocessedElement::WhiteSpace(WhiteSpace::Space), + length => PreprocessedElement::Word(length), + } + } + + fn from_element(x: PreprocessedElement) -> Self { + match x { + PreprocessedElement::Word(length) => PreprocessedElementStored(length), + PreprocessedElement::WhiteSpace(space) => PreprocessedElementStored(match space { + WhiteSpace::NewLine => 255, + WhiteSpace::Space => 254, + }), + } + } +} + +#[derive(Default, Debug)] +pub(crate) struct Preprocessed { + widths: Vec<PreprocessedElementStored>, + preprocessor: Preprocessor, +} + +#[derive(Debug, Default)] +struct Preprocessor { + current_word_width: i32, +} + +impl Preprocessor { + fn add_character( + &mut self, + font: &Font, + character: char, + widths: &mut Vec<PreprocessedElementStored>, + ) { + match character { + space @ (' ' | '\n') => { + if self.current_word_width != 0 { + widths.push(PreprocessedElementStored::from_element( + PreprocessedElement::Word( + self.current_word_width + .try_into() + .expect("word should be small and positive"), + ), + )); + self.current_word_width = 0; + } + widths.push(PreprocessedElementStored::from_element( + PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)), + )); + } + letter => { + let letter = font.letter(letter); + self.current_word_width += letter.advance_width as i32 + letter.xmin as i32; + } + } + } +} + +pub(crate) struct Lines<'preprocess> { + minimum_space_width: i32, + layout_width: i32, + data: &'preprocess [PreprocessedElementStored], + current_start_idx: usize, +} + +pub(crate) struct Line { + width: i32, + number_of_text_elements: usize, + number_of_spaces: usize, + number_of_words: usize, +} + +impl Line { + #[inline(always)] + pub(crate) fn width(&self) -> i32 { + self.width + } + #[inline(always)] + pub(crate) fn number_of_text_elements(&self) -> usize { + self.number_of_text_elements + } + #[inline(always)] + pub(crate) fn number_of_spaces(&self) -> usize { + self.number_of_spaces + } + #[inline(always)] + pub(crate) fn number_of_words(&self) -> usize { + self.number_of_words + } +} + +impl<'pre> Iterator for Lines<'pre> { + type Item = Line; + + fn next(&mut self) -> Option<Self::Item> { + if self.current_start_idx >= self.data.len() { + return None; + } + + let mut line_idx_length = 0; + let mut current_line_width = 0; + let mut additional_space_count = 0; + let mut number_of_spaces = 0; + let mut number_of_words = 0; + + while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { + match next.parse() { + PreprocessedElement::Word(pixels) => { + let additional_space_width = + additional_space_count as i32 * self.minimum_space_width; + let width = pixels as i32; + if width + current_line_width + additional_space_width > self.layout_width { + break; + } + number_of_words += 1; + current_line_width += width + additional_space_width; + number_of_spaces += additional_space_count; + } + PreprocessedElement::WhiteSpace(space) => match space { + WhiteSpace::NewLine => { + line_idx_length += 1; + break; + } + WhiteSpace::Space => { + additional_space_count += 1; + } + }, + }; + + line_idx_length += 1; + } + + self.current_start_idx += line_idx_length; + + Some(Line { + width: current_line_width, + number_of_text_elements: line_idx_length, + number_of_spaces, + number_of_words, + }) + } +} + +impl Preprocessed { + pub(crate) fn new() -> Self { + Default::default() + } + + pub(crate) fn get(&self, idx: usize) -> PreprocessedElement { + self.widths[idx].parse() + } + + pub(crate) fn add_character(&mut self, font: &Font, c: char) { + self.preprocessor.add_character(font, c, &mut self.widths); + } + + pub(crate) fn lines(&self, layout_width: i32, minimum_space_width: i32) -> Lines<'_> { + Lines { + minimum_space_width, + layout_width, + data: &self.widths, + current_start_idx: 0, + } + } + + fn lines_element( + &self, + layout_width: i32, + minimum_space_width: i32, + ) -> impl Iterator<Item = &[PreprocessedElementStored]> { + let mut idx = 0; + self.lines(layout_width, minimum_space_width).map(move |x| { + let length = x.number_of_text_elements; + let d = &self.widths[idx..(idx + length)]; + idx += length; + d + }) + } +} diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs new file mode 100644 index 00000000..1aa3b0e0 --- /dev/null +++ b/agb/src/display/object/font/renderer.rs @@ -0,0 +1,120 @@ +use crate::display::{ + object::{DynamicSprite, PaletteVram, Size}, + Font, +}; + +use super::LetterGroup; + +struct WorkingLetter { + dynamic: DynamicSprite, + // the x offset of the current letter with respect to the start of the current letter group + x_position: i32, + // where to render the letter from x_min to x_max + x_offset: i32, +} + +impl WorkingLetter { + fn new(size: Size) -> Self { + Self { + dynamic: DynamicSprite::new(size), + x_position: 0, + x_offset: 0, + } + } + + fn reset(&mut self) { + self.x_position = 0; + self.x_offset = 0; + } +} + +pub struct Configuration { + sprite_size: Size, + palette: PaletteVram, +} + +impl Configuration { + #[must_use] + pub fn new(sprite_size: Size, palette: PaletteVram) -> Self { + Self { + sprite_size, + palette, + } + } +} + +pub(crate) struct WordRender { + working: WorkingLetter, + config: Configuration, + colour: usize, +} + +impl WordRender { + #[must_use] + pub(crate) fn new(config: Configuration) -> Self { + WordRender { + working: WorkingLetter::new(config.sprite_size), + config, + colour: 1, + } + } + + #[must_use] + pub(crate) fn finalise_letter(&mut self) -> Option<LetterGroup> { + if self.working.x_offset == 0 { + return None; + } + + let mut new_sprite = DynamicSprite::new(self.config.sprite_size); + core::mem::swap(&mut self.working.dynamic, &mut new_sprite); + let sprite = new_sprite.to_vram(self.config.palette.clone()); + + let group = LetterGroup { + sprite, + width: self.working.x_offset as u16, + left: self.working.x_position as i16, + }; + self.working.reset(); + + Some(group) + } + + #[must_use] + pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option<LetterGroup> { + let font_letter: &crate::display::FontLetter = font.letter(c); + + // uses more than the sprite can hold + let group = if self.working.x_offset + font_letter.width as i32 + > self.config.sprite_size.to_width_height().0 as i32 + { + self.finalise_letter() + } else { + None + }; + + if self.working.x_offset == 0 { + self.working.x_position = font_letter.xmin as i32; + } else { + self.working.x_offset += font_letter.xmin as i32; + } + + let y_position = font.ascent() - font_letter.height as i32 - font_letter.ymin as i32; + + for y in 0..font_letter.height as usize { + for x in 0..font_letter.width as usize { + let rendered = font_letter.bit_absolute(x, y); + if rendered { + self.working.dynamic.set_pixel( + x + self.working.x_offset as usize, + (y_position + y as i32) as usize, + self.colour, + ); + } + } + } + + self.working.x_offset += font_letter.advance_width as i32; + + group + } +}