From 5f120407529f7efef495ac35b7829a28a2afadd7 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 1 Jul 2023 23:36:58 +0100 Subject: [PATCH] the proper nice okay working text rendering --- agb-image-converter/src/font_loader.rs | 50 ++- agb/examples/object_text_render.rs | 36 +- agb/src/display/object/font.rs | 497 ++++++++++------------ agb/src/display/object/font/preprocess.rs | 163 +++---- agb/src/display/object/font/renderer.rs | 24 +- 5 files changed, 362 insertions(+), 408 deletions(-) diff --git a/agb-image-converter/src/font_loader.rs b/agb-image-converter/src/font_loader.rs index 2c61f664..2e59c83f 100644 --- a/agb-image-converter/src/font_loader.rs +++ b/agb-image-converter/src/font_loader.rs @@ -25,9 +25,9 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { let line_metrics = font.horizontal_line_metrics(pixels_per_em).unwrap(); let line_height = line_metrics.new_line_size as i32; - let ascent = line_metrics.ascent as i32; + let mut ascent = line_metrics.ascent as i32; - let font = (0..128) + let letters: Vec<_> = (0..128) .map(|i| font.rasterize(char::from_u32(i).unwrap(), pixels_per_em)) .map(|(metrics, bitmap)| { let width = metrics.width; @@ -56,25 +56,37 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream { advance_width: metrics.advance_width, } }) - .map(|letter_data| { - let data_raw = ByteString(&letter_data.rendered); - let height = letter_data.height as u8; - let width = letter_data.width as u8; - let xmin = letter_data.xmin as i8; - let ymin = letter_data.ymin as i8; - let advance_width = letter_data.advance_width.ceil() as u8; + .collect(); - quote!( - display::FontLetter::new( - #width, - #height, - #data_raw, - #xmin, - #ymin, - #advance_width, - ) + let maximum_above_line = letters + .iter() + .map(|x| (x.height as i32 + x.ymin)) + .max() + .unwrap(); + + if (ascent - maximum_above_line) < 0 { + ascent = maximum_above_line; + } + + let font = letters.iter().map(|letter_data| { + let data_raw = ByteString(&letter_data.rendered); + let height = letter_data.height as u8; + let width = letter_data.width as u8; + let xmin = letter_data.xmin as i8; + let ymin = letter_data.ymin as i8; + let advance_width = letter_data.advance_width.ceil() as u8; + + quote!( + display::FontLetter::new( + #width, + #height, + #data_raw, + #xmin, + #ymin, + #advance_width, ) - }); + ) + }); quote![ display::Font::new(&[#(#font),*], #line_height, #ascent) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index aab2ff6e..8bc1729f 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -8,19 +8,17 @@ use agb::{ PaletteVram, Size, }, palette16::Palette16, - Font, WIDTH, + Font, HEIGHT, WIDTH, }, include_font, input::Button, }; -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); +const FONT: Font = include_font!("examples/font/yoster.ttf", 12); #[agb::entry] fn entry(gba: agb::Gba) -> ! { main(gba); @@ -35,12 +33,11 @@ fn main(mut gba: agb::Gba) -> ! { let palette = Palette16::new(palette); let palette = PaletteVram::new(&palette).unwrap(); - let mut wr = ObjectTextRender::new(&FONT, Size::S16x8, palette); + let mut wr = ObjectTextRender::new(&FONT, Size::S16x16, palette); let _ = writeln!( wr, - "{}", - "counts for three shoot dice for damage calculation\nmalfunctions all dice after use" - .to_ascii_uppercase() + "Woah! Hey there! I have a bunch of text I want to show you. However, you will find that the amount of text I can display is limited. Who'd have thought! Good thing that my text system supports scrolling! It only took around 20 jank versions to get here!" + ); let vblank = agb::interrupt::VBlank::get(); @@ -52,25 +49,32 @@ fn main(mut gba: agb::Gba) -> ! { timer.set_enabled(true); timer.set_divider(agb::timer::Divider::Divider256); - wr.set_alignment(TextAlignment::Left); - wr.set_size((WIDTH / 3, 20).into()); - wr.set_paragraph_spacing(2); - wr.layout(); + wr.layout((WIDTH, 40).into(), TextAlignment::Left, 2); + + let mut line_done = false; + let mut frame = 0; loop { vblank.wait_for_vblank(); input.update(); let oam = &mut unmanaged.iter(); - wr.commit(oam, (WIDTH / 3, 0).into()); + wr.commit(oam); let start = timer.value(); - let line_done = !wr.next_letter_group(); - if line_done && input.is_just_pressed(Button::A) { + if frame % 4 == 0 { + line_done = !wr.next_letter_group(); + } + if line_done + && input.is_just_pressed(Button::A) + { + line_done = false; wr.pop_line(); } - wr.layout(); + wr.update((0, HEIGHT - 40).into()); let end = timer.value(); + frame += 1; + agb::println!( "Took {} cycles, line done {}", 256 * (end.wrapping_sub(start) as u32), diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 4fd38686..9cea9054 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,9 +1,9 @@ use core::fmt::Write; -use agb_fixnum::{Rect, Vector2D}; +use agb_fixnum::Vector2D; use alloc::{collections::VecDeque, vec::Vec}; -use crate::display::{object::font::preprocess::Word, Font}; +use crate::display::Font; use self::{ preprocess::{Line, Preprocessed, PreprocessedElement}, @@ -32,14 +32,6 @@ impl WhiteSpace { } } -#[derive(Debug)] -pub(crate) struct LetterGroup { - sprite: SpriteVram, - // the width of the letter group - width: u16, - left: i16, -} - pub struct BufferedRender<'font> { char_render: WordRender, preprocessor: Preprocessed, @@ -50,7 +42,7 @@ pub struct BufferedRender<'font> { #[derive(Debug, Default)] struct Letters { - letters: VecDeque, + letters: VecDeque, number_of_groups: usize, } @@ -69,12 +61,7 @@ struct TextAlignmentSettings { } impl TextAlignment { - fn settings( - self, - line: &Line, - minimum_space_width: i32, - size: Vector2D, - ) -> TextAlignmentSettings { + fn settings(self, line: &Line, minimum_space_width: i32, width: i32) -> TextAlignmentSettings { match self { TextAlignment::Left => TextAlignmentSettings { space_width: minimum_space_width, @@ -82,11 +69,11 @@ impl TextAlignment { }, TextAlignment::Right => TextAlignmentSettings { space_width: minimum_space_width, - start_x: size.x - line.width(), + start_x: width - line.width(), }, TextAlignment::Center => TextAlignmentSettings { space_width: minimum_space_width, - start_x: (size.x - line.width()) / 2, + start_x: (width - line.width()) / 2, }, } } @@ -143,7 +130,7 @@ impl BufferedRender<'_> { pub struct ObjectTextRender<'font> { buffer: BufferedRender<'font>, layout: LayoutCache, - settings: LayoutSettings, + number_of_objects: usize, } impl<'font> ObjectTextRender<'font> { @@ -151,8 +138,14 @@ impl<'font> ObjectTextRender<'font> { 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(), + number_of_objects: 0, + layout: LayoutCache { + positions: VecDeque::new(), + line_capacity: VecDeque::new(), + objects: Vec::new(), + objects_are_at_origin: (0, 0).into(), + area: (0, 0).into(), + }, } } } @@ -168,267 +161,222 @@ impl Write for ObjectTextRender<'_> { } 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 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.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); - - if self.state.line_element_depth == 0 { - self.state.head_offset.x += settings.start_x; - } - - for element in line_elements.skip(self.state.line_element_depth) { - match element { - PreprocessedElement::Word(word) => { - for letter in (word.sprite_index() - ..(word.sprite_index() + word.number_of_sprites())) - .skip(self.state.word_depth) - .map(|x| &render.letters.letters[x]) - { - let mut object = ObjectUnmanaged::new(letter.sprite.clone()); - 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 >= self.desired_number_of_groups { - break 'outer; - } - } - - self.state.word_depth = 0; - self.state.line_element_depth += 1; - } - PreprocessedElement::WhiteSpace(space_type) => { - if space_type == WhiteSpace::NewLine { - self.state.head_offset.y += self.settings.paragraph_spacing; - } - self.state.head_offset.x += settings.space_width; - self.state.rendered_groups += 1; - self.state.line_element_depth += 1; - if self.state.rendered_groups >= self.desired_number_of_groups { - break 'outer; - } - } - } - } - - 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 commit(&mut self, oam: &mut OamIterator) { + for (object, slot) in self.layout.objects.iter().zip(oam) { + slot.set(object); } } - fn update( + /// Force a relayout, must be called after writing. + pub fn layout( &mut self, - r: &mut BufferedRender<'_>, area: Vector2D, alignment: TextAlignment, paragraph_spacing: i32, ) { - r.process(); - - while !r.buffered_chars.is_empty() - && r.letters.number_of_groups <= self.desired_number_of_groups - { - r.process(); - } - - let settings = LayoutSettings { - area, - alignment, - paragraph_spacing, - }; - if settings != self.settings { - self.reset(settings); - } - - self.update_cache(r); - } - - 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] - fn new() -> Self { - Self { - objects: VecDeque::new(), - state: Default::default(), - settings: LayoutSettings { - area: (0, 0).into(), - alignment: TextAlignment::Right, - paragraph_spacing: -100, + self.layout.create_positions( + self.buffer.font, + &self.buffer.preprocessor, + &LayoutSettings { + area, + alignment, + paragraph_spacing, }, - desired_number_of_groups: 0, - position: (0, 0).into(), + ); + } + + /// Removes one complete line. + pub fn pop_line(&mut self) -> bool { + let width = self.layout.area.x; + let space = self.buffer.font.letter(' ').advance_width as i32; + let line_height = self.buffer.font.line_height(); + if let Some(line) = self.buffer.preprocessor.lines(width, space).next() { + // there is a line + if self.layout.objects.len() >= line.number_of_letter_groups() { + // we have enough rendered letter groups to count + self.number_of_objects -= line.number_of_letter_groups(); + for _ in 0..line.number_of_letter_groups() { + self.buffer.letters.letters.pop_front(); + self.layout.positions.pop_front(); + } + self.layout.line_capacity.pop_front(); + self.layout.objects.clear(); + self.buffer.preprocessor.pop(&line); + for position in self.layout.positions.iter_mut() { + position.y -= line_height as i16; + } + return true; + } + } + false + } + + pub fn update(&mut self, position: Vector2D) { + if !self.buffer.buffered_chars.is_empty() + && self.buffer.letters.letters.len() <= self.number_of_objects + 5 + { + self.buffer.process(); + } + + self.layout.update_objects_to_display_at_position( + position, + self.buffer.letters.letters.iter(), + self.number_of_objects, + ); + } + + pub fn next_letter_group(&mut self) -> bool { + if !self.can_render_another_element() { + return false; + } + self.number_of_objects += 1; + self.at_least_n_letter_groups(self.number_of_objects); + + true + } + + fn can_render_another_element(&self) -> bool { + let max_number_of_lines = (self.layout.area.y / self.buffer.font.line_height()) as usize; + + let max_number_of_objects = self + .layout + .line_capacity + .iter() + .take(max_number_of_lines) + .sum::(); + + max_number_of_objects > self.number_of_objects + } + + pub fn next_line(&mut self) -> bool { + let max_number_of_lines = (self.layout.area.y / self.buffer.font.line_height()) as usize; + + // find current line + + for (start, end) in self + .layout + .line_capacity + .iter() + .scan(0, |count, line_size| { + let start = *count; + *count += line_size; + Some((start, *count)) + }) + .take(max_number_of_lines) + { + if self.number_of_objects >= start && self.number_of_objects < end { + self.number_of_objects = end; + self.at_least_n_letter_groups(end); + return true; + } + } + + false + } + + fn at_least_n_letter_groups(&mut self, n: usize) { + while !self.buffer.buffered_chars.is_empty() && self.buffer.letters.letters.len() <= n { + self.buffer.process(); + } + } +} + +struct LayoutCache { + positions: VecDeque>, + line_capacity: VecDeque, + objects: Vec, + objects_are_at_origin: Vector2D, + area: Vector2D, +} + +impl LayoutCache { + fn update_objects_to_display_at_position<'a>( + &mut self, + position: Vector2D, + letters: impl Iterator, + number_of_objects: usize, + ) { + let already_done = if position == self.objects_are_at_origin { + self.objects.len() + } else { + self.objects.clear(); + 0 + }; + self.objects.extend( + self.positions + .iter() + .zip(letters) + .take(number_of_objects) + .skip(already_done) + .map(|(offset, letter)| { + let position = offset.change_base() + position; + let mut object = ObjectUnmanaged::new(letter.clone()); + object.show().set_position(position); + object + }), + ); + self.objects.truncate(number_of_objects); + self.objects_are_at_origin = position; + } + + fn create_positions( + &mut self, + font: &Font, + preprocessed: &Preprocessed, + settings: &LayoutSettings, + ) { + self.area = settings.area; + self.line_capacity.clear(); + self.positions.clear(); + for (line, line_positions) in Self::create_layout(font, preprocessed, settings) { + self.line_capacity.push_back(line.number_of_letter_groups()); + self.positions + .extend(line_positions.map(|x| Vector2D::new(x.x as i16, x.y as i16))); } } - fn reset(&mut self, settings: LayoutSettings) { - self.objects.clear(); - self.state = LayoutCacheState { - head_offset: (0, 0).into(), - word_depth: 0, - rendered_groups: 0, - line_depth: 0, - line_element_depth: 0, - }; - self.settings = settings; + fn create_layout<'a>( + font: &Font, + preprocessed: &'a Preprocessed, + settings: &'a LayoutSettings, + ) -> impl Iterator> + 'a)> + 'a { + let minimum_space_width = font.letter(' ').advance_width as i32; + let width = settings.area.x; + let line_height = font.line_height(); + + let mut head_position: Vector2D = (0, -line_height).into(); + + preprocessed + .lines_element(width, minimum_space_width) + .map(move |(line, line_elements)| { + let line_settings = settings + .alignment + .settings(&line, minimum_space_width, width); + + head_position.y += line_height; + head_position.x = line_settings.start_x; + + ( + line, + line_elements.filter_map(move |element| match element.decode() { + PreprocessedElement::LetterGroup { width } => { + let this_position = head_position; + head_position.x += width as i32; + Some(this_position) + } + PreprocessedElement::WhiteSpace(space) => { + match space { + WhiteSpace::NewLine => { + head_position.y += settings.paragraph_spacing; + } + WhiteSpace::Space => head_position.x += line_settings.space_width, + } + None + } + }), + ) + }) } } @@ -438,12 +386,3 @@ struct LayoutSettings { alignment: TextAlignment, paragraph_spacing: i32, } - -#[derive(Default)] -struct LayoutCacheState { - head_offset: Vector2D, - word_depth: usize, - rendered_groups: usize, - line_depth: usize, - line_element_depth: usize, -} diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index f5ab2fbb..ca1e95e5 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -6,52 +6,47 @@ use crate::display::Font; use super::WhiteSpace; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub(crate) struct PreprocessedElementEncoded(u8); + +impl PreprocessedElementEncoded { + pub(crate) fn decode(self) -> PreprocessedElement { + match self.0 { + 255 => PreprocessedElement::WhiteSpace(WhiteSpace::NewLine), + 254 => PreprocessedElement::WhiteSpace(WhiteSpace::Space), + width => PreprocessedElement::LetterGroup { width }, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] + pub(crate) enum PreprocessedElement { - Word(Word), + LetterGroup { width: u8 }, WhiteSpace(WhiteSpace), } -#[test_case] -fn check_size_of_preprocessed_element_is_correct(_: &mut crate::Gba) { - assert_eq!( - core::mem::size_of::(), - core::mem::size_of::() - ); -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -#[repr(align(4))] -pub(crate) struct Word { - pixels: u8, - number_of_sprites: NonZeroU8, - index: u16, -} - -impl Word { - pub fn pixels(self) -> i32 { - self.pixels.into() - } - pub fn number_of_sprites(self) -> usize { - self.number_of_sprites.get().into() - } - pub fn sprite_index(self) -> usize { - self.index.into() +impl PreprocessedElement { + fn encode(self) -> PreprocessedElementEncoded { + PreprocessedElementEncoded(match self { + PreprocessedElement::LetterGroup { width } => width, + PreprocessedElement::WhiteSpace(space) => match space { + WhiteSpace::NewLine => 255, + WhiteSpace::Space => 254, + }, + }) } } #[derive(Default, Debug)] pub(crate) struct Preprocessed { - widths: VecDeque, + widths: VecDeque, preprocessor: Preprocessor, } #[derive(Debug, Default)] struct Preprocessor { - current_word_width: i32, - number_of_sprites: usize, width_in_sprite: i32, - total_number_of_sprites: usize, } impl Preprocessor { @@ -60,37 +55,32 @@ impl Preprocessor { font: &Font, character: char, sprite_width: i32, - widths: &mut VecDeque, + 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_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"), - ) - .unwrap(), - index: (self.total_number_of_sprites - self.number_of_sprites) - .try_into() - .expect("out of range"), - })); - self.current_word_width = 0; - self.number_of_sprites = 0; + if self.width_in_sprite != 0 { + widths.push_back( + PreprocessedElement::LetterGroup { + width: self.width_in_sprite as u8, + } + .encode(), + ); self.width_in_sprite = 0; } - widths.push_back(PreprocessedElement::WhiteSpace(WhiteSpace::from_char( - space, - ))); + widths.push_back( + PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)).encode(), + ); } letter => { let letter = font.letter(letter); - self.current_word_width += letter.advance_width as i32 + letter.xmin as i32; if self.width_in_sprite + letter.width as i32 > sprite_width { - self.number_of_sprites += 1; - self.total_number_of_sprites += 1; + widths.push_back( + PreprocessedElement::LetterGroup { + width: self.width_in_sprite as u8, + } + .encode(), + ); self.width_in_sprite = 0; } if self.width_in_sprite != 0 { @@ -105,7 +95,7 @@ impl Preprocessor { pub(crate) struct Lines<'preprocess> { minimum_space_width: i32, layout_width: i32, - data: &'preprocess VecDeque, + data: &'preprocess VecDeque, current_start_idx: usize, } @@ -150,35 +140,58 @@ impl<'pre> Iterator for Lines<'pre> { } let mut line_idx_length = 0; - let mut current_line_width = 0; - let mut additional_space_count = 0; + let mut current_line_width_pixels = 0; + let mut spaces_after_last_word_count = 0usize; + let mut start_of_current_word = usize::MAX; + let mut length_of_current_word_pixels = 0; + let mut length_of_current_word = 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 { - PreprocessedElement::Word(word) => { - let additional_space_width = - additional_space_count as i32 * self.minimum_space_width; - let width = word.pixels(); - if width + current_line_width + additional_space_width > self.layout_width { + match next.decode() { + PreprocessedElement::LetterGroup { width } => { + if start_of_current_word == usize::MAX { + start_of_current_word = line_idx_length; + } + length_of_current_word_pixels += width as i32; + length_of_current_word += 1; + if current_line_width_pixels + + length_of_current_word_pixels + + spaces_after_last_word_count as i32 * self.minimum_space_width + >= self.layout_width + { + line_idx_length = start_of_current_word; 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; } - PreprocessedElement::WhiteSpace(space) => match space { - WhiteSpace::NewLine => { - line_idx_length += 1; - break; + PreprocessedElement::WhiteSpace(space) => { + if start_of_current_word != usize::MAX { + // flush word + current_line_width_pixels += length_of_current_word_pixels + + spaces_after_last_word_count as i32 * self.minimum_space_width; + number_of_spaces += spaces_after_last_word_count; + number_of_words += 1; + number_of_letter_groups += length_of_current_word; + + // reset parser + length_of_current_word_pixels = 0; + length_of_current_word = 0; + start_of_current_word = usize::MAX; + spaces_after_last_word_count = 0; } - WhiteSpace::Space => { - additional_space_count += 1; + + match space { + WhiteSpace::NewLine => { + line_idx_length += 1; + break; + } + WhiteSpace::Space => { + spaces_after_last_word_count += 1; + } } - }, + } }; line_idx_length += 1; @@ -187,7 +200,7 @@ impl<'pre> Iterator for Lines<'pre> { self.current_start_idx += line_idx_length; Some(Line { - width: current_line_width, + width: current_line_width_pixels, number_of_text_elements: line_idx_length, number_of_spaces, number_of_words, @@ -226,7 +239,7 @@ 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; diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs index f364d07c..d08ee8e0 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -1,14 +1,10 @@ use crate::display::{ - object::{DynamicSprite, PaletteVram, Size}, + object::{DynamicSprite, PaletteVram, Size, SpriteVram}, 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, } @@ -17,13 +13,11 @@ 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; } } @@ -64,7 +58,7 @@ impl WordRender { } #[must_use] - pub(crate) fn finalise_letter(&mut self) -> Option { + pub(crate) fn finalise_letter(&mut self) -> Option { if self.working.x_offset == 0 { return None; } @@ -72,19 +66,13 @@ impl WordRender { 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) + Some(sprite) } #[must_use] - pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option { + pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option { let font_letter: &crate::display::FontLetter = font.letter(c); // uses more than the sprite can hold @@ -96,9 +84,7 @@ impl WordRender { None }; - if self.working.x_offset == 0 { - self.working.x_position = font_letter.xmin as i32; - } else { + if self.working.x_offset != 0 { self.working.x_offset += font_letter.xmin as i32; }