diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index c8312760..aab2ff6e 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -4,7 +4,7 @@ use agb::{ display::{ object::{ - font::{BufferedRender, LayoutCache, TextAlignment}, + font::{ObjectTextRender, TextAlignment}, PaletteVram, Size, }, palette16::Palette16, @@ -35,7 +35,7 @@ fn main(mut gba: agb::Gba) -> ! { let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); - let mut wr = BufferedRender::new(&FONT, Size::S16x8, palette); + let mut wr = ObjectTextRender::new(&FONT, Size::S16x8, palette); let _ = writeln!( wr, "{}", @@ -52,55 +52,30 @@ fn main(mut gba: agb::Gba) -> ! { timer.set_enabled(true); timer.set_divider(agb::timer::Divider::Divider256); - let mut num_letters = 0; - - let mut alignment = TextAlignment::Left; - - let mut cache = LayoutCache::new(); + wr.set_alignment(TextAlignment::Left); + wr.set_size((WIDTH / 3, 20).into()); + wr.set_paragraph_spacing(2); + wr.layout(); loop { vblank.wait_for_vblank(); input.update(); let oam = &mut unmanaged.iter(); - cache.commit(oam); + wr.commit(oam, (WIDTH / 3, 0).into()); let start = timer.value(); - wr.process(); - cache.update( - &mut wr, - Rect::new((WIDTH / 3, 0).into(), (WIDTH / 3, 100).into()), - alignment, - 2, - num_letters, - ); + let line_done = !wr.next_letter_group(); + if line_done && input.is_just_pressed(Button::A) { + wr.pop_line(); + } + wr.layout(); 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; - } - - num_letters += 1; - - if input.is_just_pressed(Button::A) { - break; - } + agb::println!( + "Took {} cycles, line done {}", + 256 * (end.wrapping_sub(start) as u32), + line_done + ); } - let start = timer.value(); - drop(wr); - let oam = unmanaged.iter(); - drop(oam); - let end = timer.value(); - agb::println!( - "Drop took {} cycles", - 256 * (end.wrapping_sub(start) as u32) - ); } } diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 0a52804f..4fd38686 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -3,7 +3,7 @@ use core::fmt::Write; use agb_fixnum::{Rect, Vector2D}; use alloc::{collections::VecDeque, vec::Vec}; -use crate::display::Font; +use crate::display::{object::font::preprocess::Word, Font}; use self::{ preprocess::{Line, Preprocessed, PreprocessedElement}, @@ -15,7 +15,7 @@ use super::{OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; mod preprocess; mod renderer; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[non_exhaustive] pub(crate) enum WhiteSpace { NewLine, @@ -50,23 +50,14 @@ pub struct BufferedRender<'font> { #[derive(Debug, Default)] struct Letters { - letters: Vec, + letters: VecDeque, 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)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] #[non_exhaustive] pub enum TextAlignment { + #[default] Left, Right, Center, @@ -103,7 +94,7 @@ impl TextAlignment { impl<'font> BufferedRender<'font> { #[must_use] - pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { let config = Configuration::new(sprite_size, palette); BufferedRender { char_render: WordRender::new(config), @@ -115,19 +106,25 @@ impl<'font> BufferedRender<'font> { } } +fn is_private_use(c: char) -> bool { + ('\u{E000}'..'\u{F8FF}').contains(&c) +} + impl BufferedRender<'_> { fn input_character(&mut self, character: char) { - self.preprocessor - .add_character(self.font, character, self.char_render.sprite_width()); + if !is_private_use(character) { + self.preprocessor + .add_character(self.font, character, self.char_render.sprite_width()); + } self.buffered_chars.push_back(character); } - pub fn process(&mut self) { + 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.letters.push_back(group); self.letters.number_of_groups += 1; } @@ -135,7 +132,7 @@ impl BufferedRender<'_> { } letter => { if let Some(group) = self.char_render.render_char(self.font, letter) { - self.letters.letters.push(group); + self.letters.letters.push_back(group); self.letters.number_of_groups += 1; } } @@ -143,32 +140,173 @@ impl BufferedRender<'_> { } } -pub struct LayoutCache { - objects: Vec, - state: LayoutCacheState, +pub struct ObjectTextRender<'font> { + buffer: BufferedRender<'font>, + layout: LayoutCache, settings: LayoutSettings, } +impl<'font> ObjectTextRender<'font> { + #[must_use] + pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + Self { + buffer: BufferedRender::new(font, sprite_size, palette), + layout: LayoutCache::new(), + settings: Default::default(), + } + } +} + +impl Write for ObjectTextRender<'_> { + fn write_str(&mut self, s: &str) -> core::fmt::Result { + for c in s.chars() { + self.buffer.input_character(c); + } + + Ok(()) + } +} + +impl ObjectTextRender<'_> { + /// Remove a line from the render and shift everything up one line. + /// A full complete line must be rendered for this to do anything, incomplete lines won't be popped. Returns whether a line could be popped. + pub fn pop_line(&mut self) -> bool { + let width = self.layout.settings.area.x; + let space = self.buffer.font.letter(' ').advance_width as i32; + let Some(line) = self.buffer.preprocessor.lines(width, space).next() else { + return false; + }; + + let number_of_elements = line.number_of_letter_groups(); + if self.layout.state.line_depth >= 1 && self.layout.objects.len() >= number_of_elements { + for _ in 0..number_of_elements { + // self.buffer.letters.letters.pop_front(); + self.layout.objects.pop_front(); + } + self.buffer.preprocessor.pop(&line); + + self.layout.state.head_offset.y -= self.buffer.font.line_height(); + for obj in self.layout.objects.iter_mut() { + obj.offset.y -= self.buffer.font.line_height() as i16; + let object_offset = obj.offset.change_base(); + obj.object + .set_position(self.layout.position + object_offset); + } + + self.layout.state.line_depth -= 1; + + true + } else { + false + } + } + + /// On next update, the next unit of letters will be rendered. Returns whether the next element could be added. + /// Can only be called once per layout. + pub fn next_letter_group(&mut self) -> bool { + self.layout.next_letter_group(&self.buffer) + } + /// Commits work already done to screen. You can commit to multiple places in the same frame. + pub fn commit(&mut self, oam: &mut OamIterator, position: Vector2D) { + self.layout.commit(oam, position); + } + /// Updates the internal state based on the chosen render settings. Best + /// effort is made to reuse previous layouts, but a full rerender may be + /// required if certain settings are changed. + pub fn layout(&mut self) { + self.layout.update( + &mut self.buffer, + self.settings.area, + self.settings.alignment, + self.settings.paragraph_spacing, + ); + } + /// Causes a change to the area that text is rendered. This will cause a relayout. + pub fn set_size(&mut self, size: Vector2D) { + self.settings.area = size; + } + /// Causes a change to the text alignment. This will cause a relayout. + pub fn set_alignment(&mut self, alignment: TextAlignment) { + self.settings.alignment = alignment; + } + /// Sets the paragraph spacing. This will cause a relayout. + pub fn set_paragraph_spacing(&mut self, paragraph_spacing: i32) { + self.settings.paragraph_spacing = paragraph_spacing; + } +} + +struct LayoutObject { + object: ObjectUnmanaged, + offset: Vector2D, +} + +struct LayoutCache { + objects: VecDeque, + state: LayoutCacheState, + settings: LayoutSettings, + desired_number_of_groups: usize, + position: Vector2D, +} + impl LayoutCache { - fn update_cache(&mut self, number_of_groups: usize, render: &BufferedRender) { + fn next_letter_group(&mut self, buffer: &BufferedRender) -> bool { + let width = self.settings.area.x; + let space = buffer.font.letter(' ').advance_width as i32; + let line_height = buffer.font.line_height(); + + if self.state.head_offset.y + line_height > self.settings.area.y { + return false; + } + + if let Some((_line, mut line_elements)) = buffer + .preprocessor + .lines_element(width, space) + .nth(self.state.line_depth) + { + match line_elements.nth(self.state.line_element_depth) { + Some(PreprocessedElement::Word(_)) => { + self.desired_number_of_groups += 1; + } + Some(PreprocessedElement::WhiteSpace(WhiteSpace::Space)) => { + self.desired_number_of_groups += 1; + } + Some(PreprocessedElement::WhiteSpace(WhiteSpace::NewLine)) => { + self.desired_number_of_groups += 1; + } + None => { + if self.state.head_offset.y + line_height * 2 > self.settings.area.y { + return false; + } + self.desired_number_of_groups += 1; + } + } + } + + true + } + + fn update_cache(&mut self, render: &BufferedRender) { + if self.state.rendered_groups >= self.desired_number_of_groups { + return; + } + let minimum_space_width = render.font.letter(' ').advance_width as i32; let lines = render .preprocessor - .lines_element(self.settings.area.size.x, minimum_space_width); + .lines_element(self.settings.area.x, minimum_space_width); 'outer: for (line, line_elements) in lines.skip(self.state.line_depth) { - let settings = self.settings.alignment.settings( - &line, - minimum_space_width, - self.settings.area.size, - ); + let settings = + self.settings + .alignment + .settings(&line, minimum_space_width, self.settings.area); if self.state.line_element_depth == 0 { - self.state.head_position.x += settings.start_x; + self.state.head_offset.x += settings.start_x; } - for element in line_elements.iter().skip(self.state.line_element_depth) { + for element in line_elements.skip(self.state.line_element_depth) { match element { PreprocessedElement::Word(word) => { for letter in (word.sprite_index() @@ -177,14 +315,24 @@ impl LayoutCache { .map(|x| &render.letters.letters[x]) { let mut object = ObjectUnmanaged::new(letter.sprite.clone()); - self.state.head_position.x += letter.left as i32; - object.set_position(self.state.head_position); - self.state.head_position.x += letter.width as i32; - object.show(); - self.objects.push(object); + self.state.head_offset.x += letter.left as i32; + object + .set_position(self.state.head_offset + self.position) + .show(); + + let layout_object = LayoutObject { + object, + offset: ( + self.state.head_offset.x as i16, + self.state.head_offset.y as i16, + ) + .into(), + }; + self.state.head_offset.x += letter.width as i32; + self.objects.push_back(layout_object); self.state.rendered_groups += 1; self.state.word_depth += 1; - if self.state.rendered_groups >= number_of_groups { + if self.state.rendered_groups >= self.desired_number_of_groups { break 'outer; } } @@ -193,36 +341,39 @@ impl LayoutCache { self.state.line_element_depth += 1; } PreprocessedElement::WhiteSpace(space_type) => { - if *space_type == WhiteSpace::NewLine { - self.state.head_position.y += self.settings.paragraph_spacing; + if space_type == WhiteSpace::NewLine { + self.state.head_offset.y += self.settings.paragraph_spacing; } - self.state.head_position.x += settings.space_width; + self.state.head_offset.x += settings.space_width; self.state.rendered_groups += 1; self.state.line_element_depth += 1; - if self.state.rendered_groups >= number_of_groups { + if self.state.rendered_groups >= self.desired_number_of_groups { break 'outer; } } } } - self.state.head_position.y += render.font.line_height(); - self.state.head_position.x = self.settings.area.position.x; + self.state.head_offset.y += render.font.line_height(); + self.state.head_offset.x = 0; self.state.line_element_depth = 0; self.state.line_depth += 1; } } - pub fn update( + fn update( &mut self, r: &mut BufferedRender<'_>, - area: Rect, + area: Vector2D, alignment: TextAlignment, paragraph_spacing: i32, - number_of_groups: usize, ) { - while !r.buffered_chars.is_empty() && r.letters.number_of_groups <= number_of_groups { + r.process(); + + while !r.buffered_chars.is_empty() + && r.letters.number_of_groups <= self.desired_number_of_groups + { r.process(); } @@ -235,32 +386,43 @@ impl LayoutCache { self.reset(settings); } - self.update_cache(number_of_groups, r); + self.update_cache(r); } - pub fn commit(&self, oam: &mut OamIterator) { - for (object, slot) in self.objects.iter().zip(oam) { - slot.set(object); + fn commit(&mut self, oam: &mut OamIterator, position: Vector2D) { + if self.position != position { + for (object, slot) in self.objects.iter_mut().zip(oam) { + let object_offset = object.offset.change_base(); + object.object.set_position(position + object_offset); + slot.set(&object.object); + } + self.position = position; + } else { + for (object, slot) in self.objects.iter().zip(oam) { + slot.set(&object.object); + } } } #[must_use] - pub fn new() -> Self { + fn new() -> Self { Self { - objects: Vec::new(), + objects: VecDeque::new(), state: Default::default(), settings: LayoutSettings { - area: Rect::new((0, 0).into(), (0, 0).into()), + area: (0, 0).into(), alignment: TextAlignment::Right, paragraph_spacing: -100, }, + desired_number_of_groups: 0, + position: (0, 0).into(), } } fn reset(&mut self, settings: LayoutSettings) { self.objects.clear(); self.state = LayoutCacheState { - head_position: settings.area.position, + head_offset: (0, 0).into(), word_depth: 0, rendered_groups: 0, line_depth: 0, @@ -270,16 +432,16 @@ impl LayoutCache { } } -#[derive(PartialEq, Eq)] +#[derive(PartialEq, Eq, Default)] struct LayoutSettings { - area: Rect, + area: Vector2D, alignment: TextAlignment, paragraph_spacing: i32, } #[derive(Default)] struct LayoutCacheState { - head_position: Vector2D, + head_offset: Vector2D, word_depth: usize, rendered_groups: usize, line_depth: usize, diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index 2de88e81..f5ab2fbb 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -1,12 +1,12 @@ use core::num::NonZeroU8; -use alloc::vec::Vec; +use alloc::{collections::VecDeque, vec::Vec}; use crate::display::Font; use super::WhiteSpace; -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub(crate) enum PreprocessedElement { Word(Word), WhiteSpace(WhiteSpace), @@ -40,12 +40,9 @@ impl Word { } } -#[derive(Clone, Copy, PartialEq, Eq)] -pub(crate) struct PreprocessedElementStored(u8); - #[derive(Default, Debug)] pub(crate) struct Preprocessed { - widths: Vec, + widths: VecDeque, preprocessor: Preprocessor, } @@ -63,14 +60,14 @@ impl Preprocessor { font: &Font, character: char, sprite_width: i32, - widths: &mut Vec, + widths: &mut VecDeque, ) { match character { space @ (' ' | '\n') => { if self.current_word_width != 0 { self.number_of_sprites += 1; self.total_number_of_sprites += 1; - widths.push(PreprocessedElement::Word(Word { + widths.push_back(PreprocessedElement::Word(Word { pixels: self.current_word_width.try_into().expect("word too wide"), number_of_sprites: NonZeroU8::new( self.number_of_sprites.try_into().expect("word too wide"), @@ -84,7 +81,7 @@ impl Preprocessor { self.number_of_sprites = 0; self.width_in_sprite = 0; } - widths.push(PreprocessedElement::WhiteSpace(WhiteSpace::from_char( + widths.push_back(PreprocessedElement::WhiteSpace(WhiteSpace::from_char( space, ))); } @@ -108,7 +105,7 @@ impl Preprocessor { pub(crate) struct Lines<'preprocess> { minimum_space_width: i32, layout_width: i32, - data: &'preprocess [PreprocessedElement], + data: &'preprocess VecDeque, current_start_idx: usize, } @@ -117,6 +114,7 @@ pub(crate) struct Line { number_of_text_elements: usize, number_of_spaces: usize, number_of_words: usize, + number_of_letter_groups: usize, } impl Line { @@ -136,6 +134,11 @@ impl Line { pub(crate) fn number_of_words(&self) -> usize { self.number_of_words } + + #[inline(always)] + pub(crate) fn number_of_letter_groups(&self) -> usize { + self.number_of_letter_groups + } } impl<'pre> Iterator for Lines<'pre> { @@ -151,6 +154,7 @@ impl<'pre> Iterator for Lines<'pre> { let mut additional_space_count = 0; let mut number_of_spaces = 0; let mut number_of_words = 0; + let mut number_of_letter_groups = 0; while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { match next { @@ -161,6 +165,7 @@ impl<'pre> Iterator for Lines<'pre> { if width + current_line_width + additional_space_width > self.layout_width { break; } + number_of_letter_groups += word.number_of_sprites.get() as usize; number_of_words += 1; current_line_width += width + additional_space_width; number_of_spaces += additional_space_count; @@ -186,6 +191,7 @@ impl<'pre> Iterator for Lines<'pre> { number_of_text_elements: line_idx_length, number_of_spaces, number_of_words, + number_of_letter_groups, }) } } @@ -200,6 +206,13 @@ impl Preprocessed { .add_character(font, c, sprite_width, &mut self.widths); } + pub(crate) fn pop(&mut self, line: &Line) { + let elements = line.number_of_text_elements(); + for _ in 0..elements { + self.widths.pop_front(); + } + } + pub(crate) fn lines(&self, layout_width: i32, minimum_space_width: i32) -> Lines<'_> { Lines { minimum_space_width, @@ -213,11 +226,12 @@ impl Preprocessed { &self, layout_width: i32, minimum_space_width: i32, - ) -> impl Iterator { + ) -> impl Iterator + '_)> { 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)]; + + let d = self.widths.range(idx..(idx + length)).copied(); idx += length; (x, d) })