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
+    }
+}