From 07779fa179f6244088b63bd93a6dcb692ca47a0b Mon Sep 17 00:00:00 2001 From: Corwin Date: Fri, 8 Mar 2024 14:30:37 +0000 Subject: [PATCH 1/9] expose letter groups to let the user handle the sprites for font rendering if they want to --- agb/examples/object_text_render.rs | 4 +- agb/src/display/object.rs | 2 +- agb/src/display/object/font.rs | 158 +++++++++++++++++++---------- 3 files changed, 108 insertions(+), 56 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index fda39a890..b875f8be4 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -74,7 +74,7 @@ fn main(mut gba: agb::Gba) -> ! { vblank.wait_for_vblank(); input.update(); let oam = &mut unmanaged.iter(); - wr.commit(oam); + wr.commit(oam, (0, HEIGHT - 40)); let start = timer.value(); if frame % 4 == 0 { @@ -84,7 +84,7 @@ fn main(mut gba: agb::Gba) -> ! { line_done = false; wr.pop_line(); } - wr.update((0, HEIGHT - 40)); + wr.update(); let end = timer.value(); frame += 1; diff --git a/agb/src/display/object.rs b/agb/src/display/object.rs index 85df2a554..4cf5c71db 100644 --- a/agb/src/display/object.rs +++ b/agb/src/display/object.rs @@ -23,7 +23,7 @@ pub use affine::AffineMatrixInstance; pub use managed::{OamManaged, Object}; pub use unmanaged::{AffineMode, OamIterator, OamSlot, OamUnmanaged, ObjectUnmanaged}; -pub use font::{ChangeColour, ObjectTextRender, TextAlignment}; +pub use font::{ChangeColour, LetterGroup, LetterGroupIter, ObjectTextRender, TextAlignment}; use super::DISPLAY_CONTROL; diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 47ad0adfd..36382de13 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,7 +1,10 @@ -use core::fmt::{Display, Write}; +use core::{ + fmt::{Display, Write}, + ops::Add, +}; use agb_fixnum::{Num, Vector2D}; -use alloc::{collections::VecDeque, vec::Vec}; +use alloc::collections::VecDeque; use crate::display::Font; @@ -256,10 +259,9 @@ impl<'font> ObjectTextRender<'font> { buffer: BufferedRender::new(font, sprite_size, palette), number_of_objects: 0, layout: LayoutCache { - positions: VecDeque::new(), + relative_positions: VecDeque::new(), line_capacity: VecDeque::new(), - objects: Vec::new(), - objects_are_at_origin: (0, 0).into(), + area: (0, 0).into(), }, } @@ -276,12 +278,100 @@ impl Write for ObjectTextRender<'_> { } } +/// A borrowed letter group and its relative position. +pub struct LetterGroup<'a> { + sprite: &'a SpriteVram, + relative_position: Vector2D, +} + +impl<'a> LetterGroup<'a> { + /// The sprite in vram for this group of letters. + #[must_use] + pub fn sprite(&self) -> &'a SpriteVram { + self.sprite + } + + /// The relative position of the letter group. For example a left aligned + /// text would start at (0,0) but a right aligned would have the last group + /// of a line be at (0,0). + #[must_use] + pub fn relative_position(&self) -> Vector2D { + self.relative_position + } +} + +impl<'a> Add> for &LetterGroup<'a> { + type Output = LetterGroup<'a>; + + fn add(self, rhs: Vector2D) -> Self::Output { + LetterGroup { + sprite: self.sprite, + relative_position: self.relative_position + rhs, + } + } +} + +impl<'a> From> for ObjectUnmanaged { + fn from(value: LetterGroup<'a>) -> Self { + let mut object = ObjectUnmanaged::new(value.sprite.clone()); + object.set_position(value.relative_position); + object.show(); + + object + } +} + +/// An iterator over the currently displaying letter groups +pub struct LetterGroupIter<'a> { + sprite_iter: alloc::collections::vec_deque::Iter<'a, SpriteVram>, + position_iter: alloc::collections::vec_deque::Iter<'a, Vector2D>, + remaining_letter_groups: usize, +} + +impl<'a> Iterator for LetterGroupIter<'a> { + type Item = LetterGroup<'a>; + + fn next(&mut self) -> Option { + if self.remaining_letter_groups == 0 { + return None; + } + self.remaining_letter_groups -= 1; + + match (self.sprite_iter.next(), self.position_iter.next()) { + (Some(sprite), Some(position)) => Some(LetterGroup { + sprite, + relative_position: position.change_base(), + }), + _ => None, + } + } +} + impl ObjectTextRender<'_> { + #[must_use] + /// An iterator over the letter groups that make up the text + pub fn letter_groups(&self) -> LetterGroupIter<'_> { + LetterGroupIter { + sprite_iter: self.buffer.letters.letters.iter(), + position_iter: self.layout.relative_positions.iter(), + remaining_letter_groups: self.number_of_objects, + } + } + /// Commits work already done to screen. You can commit to multiple places in the same frame. - pub fn commit(&mut self, oam: &mut OamIterator) { - for (object, slot) in self.layout.objects.iter().zip(oam) { - slot.set(object); + pub fn commit>>(&mut self, oam: &mut OamIterator, position: V) { + fn inner(this: &mut ObjectTextRender, oam: &mut OamIterator, position: Vector2D) { + let objects = this + .letter_groups() + .map(|x| &x + position) + .map(ObjectUnmanaged::from); + + for (object, slot) in objects.zip(oam) { + slot.set(&object); + } } + + inner(self, oam, position.into()); } /// Force a relayout, must be called after writing. @@ -309,17 +399,16 @@ impl ObjectTextRender<'_> { 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() { + if self.number_of_objects >= 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.relative_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() { + for position in self.layout.relative_positions.iter_mut() { position.y -= line_height as i16; } return true; @@ -331,18 +420,12 @@ impl ObjectTextRender<'_> { /// Updates the internal state of the number of letters to write and popped /// line. Should be called in the same frame as and after /// [`next_letter_group`][ObjectTextRender::next_letter_group], [`next_line`][ObjectTextRender::next_line], and [`pop_line`][ObjectTextRender::pop_line]. - pub fn update(&mut self, position: impl Into>) { + pub fn update(&mut self) { 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.into(), - self.buffer.letters.letters.iter(), - self.number_of_objects, - ); } /// Causes the next letter group to be shown on the next update. Returns @@ -406,43 +489,12 @@ impl ObjectTextRender<'_> { } struct LayoutCache { - positions: VecDeque>, + relative_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, @@ -451,10 +503,10 @@ impl LayoutCache { ) { self.area = settings.area; self.line_capacity.clear(); - self.positions.clear(); + self.relative_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 + self.relative_positions .extend(line_positions.map(|x| Vector2D::new(x.x as i16, x.y as i16))); } } From 9b7d97194b86f4450804130001b621fa1cffaf75 Mon Sep 17 00:00:00 2001 From: Corwin Date: Fri, 8 Mar 2024 14:36:35 +0000 Subject: [PATCH 2/9] fix dpl --- .../the-dungeon-puzzlers-lament/src/game.rs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/examples/the-dungeon-puzzlers-lament/src/game.rs b/examples/the-dungeon-puzzlers-lament/src/game.rs index caa8a4c32..533e7046f 100644 --- a/examples/the-dungeon-puzzlers-lament/src/game.rs +++ b/examples/the-dungeon-puzzlers-lament/src/game.rs @@ -81,7 +81,7 @@ impl<'a, 'b> Lament<'a, 'b> { { let mut writer = self.writer.borrow_mut(); writer.next_letter_group(); - writer.update(Vector2D::new(16, HEIGHT / 4)); + writer.update(); } if input.is_just_pressed(Button::A) { GamePhase::Construction(Construction::new(self.level, self.background, vram_manager)) @@ -91,7 +91,9 @@ impl<'a, 'b> Lament<'a, 'b> { } fn render(&self, oam: &mut OamIterator) { - self.writer.borrow_mut().commit(oam); + self.writer + .borrow_mut() + .commit(oam, Vector2D::new(16, HEIGHT / 4)); } } @@ -287,27 +289,21 @@ struct PauseMenu { } impl PauseMenu { - fn text_at_position( - text: core::fmt::Arguments, - position: Vector2D, - ) -> ObjectTextRender<'static> { + fn text_at_position(text: core::fmt::Arguments) -> ObjectTextRender<'static> { let mut t = ObjectTextRender::new(&FONT, Size::S32x16, generate_text_palette()); let _ = writeln!(t, "{}", text); t.layout(Vector2D::new(i32::MAX, i32::MAX), TextAlignment::Left, 0); t.next_line(); - t.update(position); + t.update(); t } fn new(loader: &mut SpriteLoader, maximum_level: usize, current_level: usize) -> Self { PauseMenu { option_text: RefCell::new([ - Self::text_at_position(format_args!("Restart"), Vector2D::new(32, HEIGHT / 4)), - Self::text_at_position( - format_args!("Go to level: {}", current_level + 1), - Vector2D::new(32, HEIGHT / 4 + 20), - ), + Self::text_at_position(format_args!("Restart")), + Self::text_at_position(format_args!("Go to level: {}", current_level + 1)), ]), selection: PauseSelectionInner::Restart, indicator_sprite: loader.get_vram_sprite(ARROW_RIGHT.sprite(0)), @@ -333,10 +329,8 @@ impl PauseMenu { let selected_level = (selected_level + lr as i32).rem_euclid(self.maximum_level as i32 + 1); self.selected_level = selected_level as usize; - self.option_text.borrow_mut()[1] = Self::text_at_position( - format_args!("Go to level: {}", selected_level + 1), - Vector2D::new(32, HEIGHT / 4 + 20), - ) + self.option_text.borrow_mut()[1] = + Self::text_at_position(format_args!("Go to level: {}", selected_level + 1)) } if input.is_just_pressed(Button::A) | input.is_just_pressed(Button::START) { @@ -352,8 +346,8 @@ impl PauseMenu { } fn render(&self, oam: &mut OamIterator) { - for text in self.option_text.borrow_mut().iter_mut() { - text.commit(oam); + for (idx, text) in self.option_text.borrow_mut().iter_mut().enumerate() { + text.commit(oam, Vector2D::new(32, HEIGHT / 4 + 20 * idx as i32)); } let mut indicator = ObjectUnmanaged::new(self.indicator_sprite.clone()); indicator.show(); From 79668f1fcf6ef6038cd86c8e2e9995ba22c36e4c Mon Sep 17 00:00:00 2001 From: Corwin Date: Fri, 8 Mar 2024 22:23:45 +0000 Subject: [PATCH 3/9] add convenience to font render --- agb/src/display/object/font.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 36382de13..36e89134a 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -481,6 +481,14 @@ impl ObjectTextRender<'_> { false } + /// Immediately renders all the completed letter groups in the buffer. + pub fn render_all(&mut self) { + while !self.buffer.buffered_chars.is_empty() { + self.buffer.process(); + } + self.number_of_objects = self.buffer.letters.letters.len(); + } + 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(); From d43e7461c23bca6adfe34600080cbe34cfc3f8af Mon Sep 17 00:00:00 2001 From: Corwin Date: Fri, 8 Mar 2024 22:23:51 +0000 Subject: [PATCH 4/9] rotating text example --- agb/examples/rotating_text.rs | 109 ++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 agb/examples/rotating_text.rs diff --git a/agb/examples/rotating_text.rs b/agb/examples/rotating_text.rs new file mode 100644 index 000000000..b8ef2713f --- /dev/null +++ b/agb/examples/rotating_text.rs @@ -0,0 +1,109 @@ +#![no_std] +#![no_main] + +use agb::{ + display::{ + affine::AffineMatrix, + object::{ + AffineMatrixInstance, AffineMode, ObjectTextRender, ObjectUnmanaged, PaletteVram, Size, + SpriteVram, TextAlignment, + }, + palette16::Palette16, + Font, HEIGHT, WIDTH, + }, + include_font, +}; +use agb_fixnum::{num, Num, Vector2D}; +use alloc::vec::Vec; + +extern crate alloc; + +use core::fmt::Write; + +const FONT: Font = include_font!("examples/font/yoster.ttf", 12); +#[agb::entry] +fn entry(gba: agb::Gba) -> ! { + main(gba); +} + +fn text_objects( + font: &Font, + sprite_size: Size, + palette: PaletteVram, + text_alignment: TextAlignment, + area: Vector2D, + paragraph_spacing: i32, + arguments: core::fmt::Arguments, +) -> Vec<(SpriteVram, Vector2D)> { + let mut wr = ObjectTextRender::new(font, sprite_size, palette); + let _ = writeln!(wr, "{}", arguments); + + wr.layout(area, text_alignment, paragraph_spacing); + wr.render_all(); + + wr.letter_groups() + .map(|x| (x.sprite().clone(), x.relative_position())) + .collect() +} + +fn main(mut gba: agb::Gba) -> ! { + let (mut unmanaged, _sprites) = gba.display.object.get_unmanaged(); + + let mut palette = [0x0; 16]; + palette[1] = 0xFF_FF; + palette[2] = 0x00_FF; + let palette = Palette16::new(palette); + let palette = PaletteVram::new(&palette).unwrap(); + + let groups: Vec<_> = text_objects( + &FONT, + Size::S16x16, + palette, + TextAlignment::Center, + (WIDTH, i32::MAX).into(), + 0, + format_args!("Woah, ROTATION!"), + ) + .into_iter() + .map(|x| (x.0, x.1 - (WIDTH / 2, 0).into() + (8, 4).into())) + .collect(); + + let vblank = agb::interrupt::VBlank::get(); + let mut angle: Num = num!(0.); + + loop { + angle += num!(0.01); + if angle >= num!(1.) { + angle -= num!(1.); + } + + let rotation_matrix = AffineMatrix::from_rotation(angle); + + let letter_group_rotation_matrix_instance = + AffineMatrixInstance::new(AffineMatrix::from_rotation(-angle).to_object_wrapping()); + + let frame_positions: Vec<_> = groups + .iter() + .map(|x| { + let mat = AffineMatrix::from_translation(x.1.change_base()); + + let mat = rotation_matrix * mat; + + let position = mat.position() + (WIDTH / 2, HEIGHT / 2).into() - (16, 16).into(); + + let mut object = ObjectUnmanaged::new(x.0.clone()); + object.set_affine_matrix(letter_group_rotation_matrix_instance.clone()); + object.show_affine(AffineMode::AffineDouble); + object.set_position(position.floor()); + + object + }) + .collect(); + + vblank.wait_for_vblank(); + let mut oam = unmanaged.iter(); + for (object, oam_slot) in frame_positions.into_iter().zip(&mut oam) { + oam_slot.set(&object); + } + } +} From 4522a97f9c20cefd7361db3df0a17cda1a097cb1 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sat, 13 Apr 2024 23:38:09 +0100 Subject: [PATCH 5/9] remove line popping --- agb/src/display/object/font.rs | 25 ----------------------- agb/src/display/object/font/preprocess.rs | 11 ---------- 2 files changed, 36 deletions(-) diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 36e89134a..fab2d7b21 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -392,31 +392,6 @@ impl ObjectTextRender<'_> { ); } - /// Removes one complete line. Returns whether a line could be removed. You must call [`update`][ObjectTextRender::update] after this - 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.number_of_objects >= 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.relative_positions.pop_front(); - } - self.layout.line_capacity.pop_front(); - self.buffer.preprocessor.pop(&line); - for position in self.layout.relative_positions.iter_mut() { - position.y -= line_height as i16; - } - return true; - } - } - false - } - /// Updates the internal state of the number of letters to write and popped /// line. Should be called in the same frame as and after /// [`next_letter_group`][ObjectTextRender::next_letter_group], [`next_line`][ObjectTextRender::next_line], and [`pop_line`][ObjectTextRender::pop_line]. diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index e07356a2b..8adfe17ed 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -117,10 +117,6 @@ impl Line { 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 } @@ -215,13 +211,6 @@ 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, From 804a823f3364c3b130c1104b7abbf2b528a286de Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 14 Apr 2024 03:39:18 +0100 Subject: [PATCH 6/9] able to refer to the string --- agb/src/display/object/font.rs | 113 ++++++++++++++-------- agb/src/display/object/font/preprocess.rs | 18 ++-- agb/src/display/object/font/renderer.rs | 24 +++-- 3 files changed, 97 insertions(+), 58 deletions(-) diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index fab2d7b21..798c8021a 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -4,7 +4,7 @@ use core::{ }; use agb_fixnum::{Num, Vector2D}; -use alloc::collections::VecDeque; +use alloc::{string::String, vec::Vec}; use crate::display::Font; @@ -38,14 +38,22 @@ impl WhiteSpace { struct BufferedRender<'font> { char_render: WordRender, preprocessor: Preprocessed, - buffered_chars: VecDeque, + string: String, + cursor_position: usize, letters: Letters, font: &'font Font, } +#[derive(Debug)] +struct LettersIndexed { + start_index: usize, + end_index: usize, + sprite: SpriteVram, +} + #[derive(Debug, Default)] struct Letters { - letters: VecDeque, + letters: Vec, number_of_groups: usize, } @@ -103,13 +111,23 @@ impl TextAlignment { impl<'font> BufferedRender<'font> { #[must_use] - fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + fn new(string: String, font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { let config = Configuration::new(sprite_size, palette); + + let mut pre = Preprocessed::new(); + + for character in string.chars() { + if !is_private_use(character) { + pre.add_character(font, character, sprite_size.to_width_height().0 as i32); + } + } + BufferedRender { char_render: WordRender::new(config), - preprocessor: Preprocessed::new(), - buffered_chars: VecDeque::new(), + preprocessor: pre, + string, letters: Default::default(), + cursor_position: 0, font, } } @@ -175,30 +193,44 @@ impl Display for ChangeColour { } impl BufferedRender<'_> { - fn input_character(&mut self, character: char) { - if !is_private_use(character) { - self.preprocessor - .add_character(self.font, character, self.char_render.sprite_width()); - } - self.buffered_chars.push_back(character); + fn next_character(&mut self) -> Option<(usize, char)> { + let next = self.string.get(self.cursor_position..)?.chars().next()?; + let idx = self.cursor_position; + + self.cursor_position += next.len_utf8(); + Some((idx, next)) + } + + fn completed_character(&self) -> bool { + self.string.len() == self.cursor_position } fn process(&mut self) { - let Some(c) = self.buffered_chars.pop_front() else { + let Some((idx, c)) = self.next_character() else { return; }; match c { ' ' | '\n' => { - if let Some(group) = self.char_render.finalise_letter() { - self.letters.letters.push_back(group); + if let Some((start_index, group)) = self.char_render.finalise_letter(idx) { + self.letters.letters.push(LettersIndexed { + start_index, + end_index: idx, + sprite: group, + }); self.letters.number_of_groups += 1; } self.letters.number_of_groups += 1; } letter => { - if let Some(group) = self.char_render.render_char(self.font, letter) { - self.letters.letters.push_back(group); + if let Some((start_index, group)) = + self.char_render.render_char(self.font, letter, idx) + { + self.letters.letters.push(LettersIndexed { + start_index, + end_index: idx, + sprite: group, + }); self.letters.number_of_groups += 1; } } @@ -254,13 +286,13 @@ impl<'font> ObjectTextRender<'font> { /// Creates a new text renderer with a given font, sprite size, and palette. /// You must ensure that the sprite size can accomodate the letters from the /// font otherwise it will panic at render time. - pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + pub fn new(text: String, font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { Self { - buffer: BufferedRender::new(font, sprite_size, palette), + buffer: BufferedRender::new(text, font, sprite_size, palette), number_of_objects: 0, layout: LayoutCache { - relative_positions: VecDeque::new(), - line_capacity: VecDeque::new(), + relative_positions: Vec::new(), + line_capacity: Vec::new(), area: (0, 0).into(), }, @@ -268,19 +300,10 @@ impl<'font> ObjectTextRender<'font> { } } -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(()) - } -} - /// A borrowed letter group and its relative position. pub struct LetterGroup<'a> { sprite: &'a SpriteVram, + contained_string: &'a str, relative_position: Vector2D, } @@ -298,6 +321,12 @@ impl<'a> LetterGroup<'a> { pub fn relative_position(&self) -> Vector2D { self.relative_position } + + #[must_use] + /// A reference to the string that are represented by this sprite + pub fn letters(&self) -> &'a str { + self.contained_string + } } impl<'a> Add> for &LetterGroup<'a> { @@ -306,6 +335,7 @@ impl<'a> Add> for &LetterGroup<'a> { fn add(self, rhs: Vector2D) -> Self::Output { LetterGroup { sprite: self.sprite, + contained_string: self.contained_string, relative_position: self.relative_position + rhs, } } @@ -323,8 +353,9 @@ impl<'a> From> for ObjectUnmanaged { /// An iterator over the currently displaying letter groups pub struct LetterGroupIter<'a> { - sprite_iter: alloc::collections::vec_deque::Iter<'a, SpriteVram>, - position_iter: alloc::collections::vec_deque::Iter<'a, Vector2D>, + sprite_iter: core::slice::Iter<'a, LettersIndexed>, + position_iter: core::slice::Iter<'a, Vector2D>, + string: &'a str, remaining_letter_groups: usize, } @@ -339,7 +370,8 @@ impl<'a> Iterator for LetterGroupIter<'a> { match (self.sprite_iter.next(), self.position_iter.next()) { (Some(sprite), Some(position)) => Some(LetterGroup { - sprite, + sprite: &sprite.sprite, + contained_string: &self.string[sprite.start_index..sprite.end_index], relative_position: position.change_base(), }), _ => None, @@ -355,6 +387,7 @@ impl ObjectTextRender<'_> { sprite_iter: self.buffer.letters.letters.iter(), position_iter: self.layout.relative_positions.iter(), remaining_letter_groups: self.number_of_objects, + string: &self.buffer.string, } } @@ -396,7 +429,7 @@ impl ObjectTextRender<'_> { /// line. Should be called in the same frame as and after /// [`next_letter_group`][ObjectTextRender::next_letter_group], [`next_line`][ObjectTextRender::next_line], and [`pop_line`][ObjectTextRender::pop_line]. pub fn update(&mut self) { - if !self.buffer.buffered_chars.is_empty() + if !self.buffer.completed_character() && self.buffer.letters.letters.len() <= self.number_of_objects + 5 { self.buffer.process(); @@ -458,22 +491,22 @@ impl ObjectTextRender<'_> { /// Immediately renders all the completed letter groups in the buffer. pub fn render_all(&mut self) { - while !self.buffer.buffered_chars.is_empty() { + while !self.buffer.completed_character() { self.buffer.process(); } self.number_of_objects = self.buffer.letters.letters.len(); } fn at_least_n_letter_groups(&mut self, n: usize) { - while !self.buffer.buffered_chars.is_empty() && self.buffer.letters.letters.len() <= n { + while !self.buffer.completed_character() && self.buffer.letters.letters.len() <= n { self.buffer.process(); } } } struct LayoutCache { - relative_positions: VecDeque>, - line_capacity: VecDeque, + relative_positions: Vec>, + line_capacity: Vec, area: Vector2D, } @@ -488,7 +521,7 @@ impl LayoutCache { self.line_capacity.clear(); self.relative_positions.clear(); for (line, line_positions) in Self::create_layout(font, preprocessed, settings) { - self.line_capacity.push_back(line.number_of_letter_groups()); + self.line_capacity.push(line.number_of_letter_groups()); self.relative_positions .extend(line_positions.map(|x| Vector2D::new(x.x as i16, x.y as i16))); } diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index 8adfe17ed..ee61e9973 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -1,4 +1,4 @@ -use alloc::collections::VecDeque; +use alloc::vec::Vec; use crate::display::Font; @@ -38,7 +38,7 @@ impl PreprocessedElement { #[derive(Default, Debug)] pub(crate) struct Preprocessed { - widths: VecDeque, + widths: Vec, preprocessor: Preprocessor, } @@ -54,12 +54,12 @@ impl Preprocessor { font: &Font, character: char, sprite_width: i32, - widths: &mut VecDeque, + widths: &mut Vec, ) { match character { space @ (' ' | '\n') => { if self.width_in_sprite != 0 { - widths.push_back( + widths.push( PreprocessedElement::LetterGroup { width: self.width_in_sprite as u8, } @@ -67,9 +67,7 @@ impl Preprocessor { ); self.width_in_sprite = 0; } - widths.push_back( - PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)).encode(), - ); + widths.push(PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)).encode()); } letter => { let letter = font.letter(letter); @@ -78,7 +76,7 @@ impl Preprocessor { } if self.width_in_sprite + letter.width as i32 > sprite_width { - widths.push_back( + widths.push( PreprocessedElement::LetterGroup { width: self.width_in_sprite as u8, } @@ -100,7 +98,7 @@ impl Preprocessor { pub(crate) struct Lines<'preprocess> { minimum_space_width: i32, layout_width: i32, - data: &'preprocess VecDeque, + data: &'preprocess [PreprocessedElementEncoded], current_start_idx: usize, } @@ -229,7 +227,7 @@ impl Preprocessed { self.lines(layout_width, minimum_space_width).map(move |x| { let length = x.number_of_text_elements; - let d = self.widths.range(idx..(idx + length)).copied(); + let d = self.widths[idx..(idx + length)].iter().copied(); idx += length; (x, d) }) diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs index bbd0628b3..285e1416e 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -43,15 +43,12 @@ pub(crate) struct WordRender { working: WorkingLetter, config: Configuration, colour: usize, + start_index_of_letter: usize, previous_character: Option, } impl WordRender { - pub(crate) fn sprite_width(&self) -> i32 { - self.config.sprite_size.to_width_height().0 as i32 - } - #[must_use] pub(crate) fn new(config: Configuration) -> Self { WordRender { @@ -59,11 +56,15 @@ impl WordRender { config, colour: 1, previous_character: None, + start_index_of_letter: 0, } } #[must_use] - pub(crate) fn finalise_letter(&mut self) -> Option { + pub(crate) fn finalise_letter( + &mut self, + index_of_character: usize, + ) -> Option<(usize, SpriteVram)> { if self.working.x_offset == 0 { return None; } @@ -71,13 +72,20 @@ 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 start_index = self.start_index_of_letter; self.working.reset(); + self.start_index_of_letter = index_of_character; - Some(sprite) + Some((start_index, 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, + index_of_character: usize, + ) -> Option<(usize, SpriteVram)> { if let Some(next_colour) = ChangeColour::try_from_char(c) { self.colour = next_colour.0 as usize; return None; @@ -94,7 +102,7 @@ impl WordRender { 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() + self.finalise_letter(index_of_character) } else { None }; From bc08c5d1a83546f0ea4874144cf42fa5c176e3a8 Mon Sep 17 00:00:00 2001 From: Corwin Date: Sun, 14 Apr 2024 04:30:26 +0100 Subject: [PATCH 7/9] lots more features around lines --- agb/examples/object_text_render.rs | 68 +++++++----- agb/examples/rotating_text.rs | 12 +-- agb/src/display/font.rs | 3 +- agb/src/display/object/font.rs | 126 ++++++++++++++-------- agb/src/display/object/font/preprocess.rs | 23 +++- agb/src/display/object/font/renderer.rs | 5 +- 6 files changed, 153 insertions(+), 84 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index b875f8be4..12dc2a0fc 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -3,18 +3,19 @@ use agb::{ display::{ - object::{ChangeColour, ObjectTextRender, PaletteVram, Size, TextAlignment}, + object::{ + ChangeColour, ObjectTextRender, ObjectUnmanaged, PaletteVram, Size, TextAlignment, + }, palette16::Palette16, Font, HEIGHT, WIDTH, }, include_font, input::Button, }; +use agb_fixnum::Vector2D; extern crate alloc; -use core::fmt::Write; - static FONT: Font = include_font!("examples/font/ark-pixel-10px-proportional-ja.ttf", 10); #[agb::entry] @@ -36,17 +37,15 @@ fn main(mut gba: agb::Gba) -> ! { timer.set_enabled(true); timer.set_divider(agb::timer::Divider::Divider256); + let player_name = "You"; + let text = alloc::format!( + "Woah!{change2} {player_name}! {change1}こんにちは! 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!\n", + change2 = ChangeColour::new(2), + change1 = ChangeColour::new(1), + ); - let mut wr = ObjectTextRender::new(&FONT, Size::S16x16, palette); let start = timer.value(); - - let player_name = "You"; - let _ = writeln!( - wr, - "Woah!{change2} {player_name}! {change1}こんにちは! 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!", - change2 = ChangeColour::new(2), - change1 = ChangeColour::new(1), - ); + let mut wr = ObjectTextRender::new(text, &FONT, Size::S16x16, palette, Some(|c| c == '.')); let end = timer.value(); agb::println!( @@ -59,7 +58,7 @@ fn main(mut gba: agb::Gba) -> ! { let start = timer.value(); - wr.layout((WIDTH, 40), TextAlignment::Justify, 2); + wr.layout(WIDTH, TextAlignment::Justify, 2); let end = timer.value(); agb::println!( @@ -67,32 +66,45 @@ fn main(mut gba: agb::Gba) -> ! { 256 * (end.wrapping_sub(start) as u32) ); - let mut line_done = false; + let mut line = 0; let mut frame = 0; + let mut groups_to_show = 0; loop { vblank.wait_for_vblank(); input.update(); let oam = &mut unmanaged.iter(); - wr.commit(oam, (0, HEIGHT - 40)); - let start = timer.value(); - if frame % 4 == 0 { - line_done = !wr.next_letter_group(); + let done_rendering = !wr.next_letter_group(); + + let mut letters = wr.letter_groups(); + let displayed_letters = letters + .by_ref() + .take(groups_to_show) + .filter(|x| x.line() >= line); + + for (letter, slot) in displayed_letters.zip(oam) { + slot.set(&ObjectUnmanaged::from( + &letter + Vector2D::new(0, HEIGHT - 40 - line * FONT.line_height()), + )) } - if line_done && input.is_just_pressed(Button::A) { - line_done = false; - wr.pop_line(); + + if let Some(next_letter) = letters.next() { + if next_letter.line() < line + 2 { + if next_letter.letters() == "." { + if frame % 16 == 0 { + groups_to_show += 1; + } + } else if frame % 4 == 0 { + groups_to_show += 1; + } + } else if input.is_just_pressed(Button::A) { + line += 1; + } } + wr.update(); - let end = timer.value(); frame += 1; - - agb::println!( - "Took {} cycles, line done {}", - 256 * (end.wrapping_sub(start) as u32), - line_done - ); } } diff --git a/agb/examples/rotating_text.rs b/agb/examples/rotating_text.rs index b8ef2713f..0389e0630 100644 --- a/agb/examples/rotating_text.rs +++ b/agb/examples/rotating_text.rs @@ -18,8 +18,6 @@ use alloc::vec::Vec; extern crate alloc; -use core::fmt::Write; - const FONT: Font = include_font!("examples/font/yoster.ttf", 12); #[agb::entry] fn entry(gba: agb::Gba) -> ! { @@ -31,14 +29,14 @@ fn text_objects( sprite_size: Size, palette: PaletteVram, text_alignment: TextAlignment, - area: Vector2D, + width: i32, paragraph_spacing: i32, arguments: core::fmt::Arguments, ) -> Vec<(SpriteVram, Vector2D)> { - let mut wr = ObjectTextRender::new(font, sprite_size, palette); - let _ = writeln!(wr, "{}", arguments); + let text = alloc::format!("{}\n", arguments); + let mut wr = ObjectTextRender::new(text, font, sprite_size, palette, None); - wr.layout(area, text_alignment, paragraph_spacing); + wr.layout(width, text_alignment, paragraph_spacing); wr.render_all(); wr.letter_groups() @@ -60,7 +58,7 @@ fn main(mut gba: agb::Gba) -> ! { Size::S16x16, palette, TextAlignment::Center, - (WIDTH, i32::MAX).into(), + WIDTH, 0, format_args!("Woah, ROTATION!"), ) diff --git a/agb/src/display/font.rs b/agb/src/display/font.rs index ae3bde95d..5894542e3 100644 --- a/agb/src/display/font.rs +++ b/agb/src/display/font.rs @@ -95,7 +95,8 @@ impl Font { self.ascent } - pub(crate) fn line_height(&self) -> i32 { + #[must_use] + pub fn line_height(&self) -> i32 { self.line_height } } diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 798c8021a..8325636f2 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -93,7 +93,9 @@ impl TextAlignment { start_x: (width - line.width()) / 2, }, TextAlignment::Justify => { - let space_width = if line.number_of_spaces() != 0 { + let space_width = if line.number_of_spaces() != 0 + && !line.ended_on_explicit_newline() + { Num::new( width - line.width() + line.number_of_spaces() as i32 * minimum_space_width, ) / line.number_of_spaces() as i32 @@ -111,19 +113,30 @@ impl TextAlignment { impl<'font> BufferedRender<'font> { #[must_use] - fn new(string: String, font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + fn new( + string: String, + font: &'font Font, + sprite_size: Size, + palette: PaletteVram, + explicit_break_on: Option bool>, + ) -> Self { let config = Configuration::new(sprite_size, palette); let mut pre = Preprocessed::new(); for character in string.chars() { if !is_private_use(character) { - pre.add_character(font, character, sprite_size.to_width_height().0 as i32); + pre.add_character( + font, + character, + sprite_size.to_width_height().0 as i32, + explicit_break_on, + ); } } BufferedRender { - char_render: WordRender::new(config), + char_render: WordRender::new(config, explicit_break_on), preprocessor: pre, string, letters: Default::default(), @@ -286,25 +299,34 @@ impl<'font> ObjectTextRender<'font> { /// Creates a new text renderer with a given font, sprite size, and palette. /// You must ensure that the sprite size can accomodate the letters from the /// font otherwise it will panic at render time. - pub fn new(text: String, font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + pub fn new( + text: String, + font: &'font Font, + sprite_size: Size, + palette: PaletteVram, + explicit_break_on: Option bool>, + ) -> Self { Self { - buffer: BufferedRender::new(text, font, sprite_size, palette), + buffer: BufferedRender::new(text, font, sprite_size, palette, explicit_break_on), number_of_objects: 0, layout: LayoutCache { relative_positions: Vec::new(), line_capacity: Vec::new(), - - area: (0, 0).into(), + paragraph_spacing: 0, + width: 0, }, } } } /// A borrowed letter group and its relative position. +#[derive(Debug)] pub struct LetterGroup<'a> { sprite: &'a SpriteVram, contained_string: &'a str, relative_position: Vector2D, + line: u8, + paragraph: u8, } impl<'a> LetterGroup<'a> { @@ -322,6 +344,11 @@ impl<'a> LetterGroup<'a> { self.relative_position } + #[must_use] + pub fn line(&self) -> i32 { + self.line as i32 + } + #[must_use] /// A reference to the string that are represented by this sprite pub fn letters(&self) -> &'a str { @@ -337,6 +364,8 @@ impl<'a> Add> for &LetterGroup<'a> { sprite: self.sprite, contained_string: self.contained_string, relative_position: self.relative_position + rhs, + line: self.line, + paragraph: self.paragraph, } } } @@ -352,11 +381,14 @@ impl<'a> From> for ObjectUnmanaged { } /// An iterator over the currently displaying letter groups +#[derive(Debug)] pub struct LetterGroupIter<'a> { sprite_iter: core::slice::Iter<'a, LettersIndexed>, - position_iter: core::slice::Iter<'a, Vector2D>, + position_iter: core::slice::Iter<'a, LayoutPosition>, string: &'a str, remaining_letter_groups: usize, + line_height: i32, + paragraph_spacing: i32, } impl<'a> Iterator for LetterGroupIter<'a> { @@ -372,7 +404,13 @@ impl<'a> Iterator for LetterGroupIter<'a> { (Some(sprite), Some(position)) => Some(LetterGroup { sprite: &sprite.sprite, contained_string: &self.string[sprite.start_index..sprite.end_index], - relative_position: position.change_base(), + relative_position: Vector2D::new( + position.x, + position.line as i32 * self.line_height + + position.paragraph as i32 * self.paragraph_spacing, + ), + line: position.line, + paragraph: position.paragraph, }), _ => None, } @@ -388,6 +426,8 @@ impl ObjectTextRender<'_> { position_iter: self.layout.relative_positions.iter(), remaining_letter_groups: self.number_of_objects, string: &self.buffer.string, + line_height: self.buffer.font.line_height(), + paragraph_spacing: self.layout.paragraph_spacing, } } @@ -408,17 +448,12 @@ impl ObjectTextRender<'_> { } /// Force a relayout, must be called after writing. - pub fn layout( - &mut self, - area: impl Into>, - alignment: TextAlignment, - paragraph_spacing: i32, - ) { + pub fn layout(&mut self, width: i32, alignment: TextAlignment, paragraph_spacing: i32) { self.layout.create_positions( self.buffer.font, &self.buffer.preprocessor, &LayoutSettings { - area: area.into(), + width, alignment, paragraph_spacing, }, @@ -449,14 +484,7 @@ impl ObjectTextRender<'_> { } 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::(); + let max_number_of_objects = self.layout.line_capacity.iter().sum::(); max_number_of_objects > self.number_of_objects } @@ -464,8 +492,6 @@ impl ObjectTextRender<'_> { /// Causes the next line to be shown on the next update. Returns /// whether another line could be added in the space given. 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 @@ -477,7 +503,6 @@ impl ObjectTextRender<'_> { *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; @@ -505,9 +530,17 @@ impl ObjectTextRender<'_> { } struct LayoutCache { - relative_positions: Vec>, + relative_positions: Vec, line_capacity: Vec, - area: Vector2D, + width: i32, + paragraph_spacing: i32, +} + +#[derive(Debug)] +struct LayoutPosition { + line: u8, + paragraph: u8, + x: i32, } impl LayoutCache { @@ -517,13 +550,20 @@ impl LayoutCache { preprocessed: &Preprocessed, settings: &LayoutSettings, ) { - self.area = settings.area; + self.width = settings.width; + self.paragraph_spacing = settings.paragraph_spacing; self.line_capacity.clear(); self.relative_positions.clear(); - for (line, line_positions) in Self::create_layout(font, preprocessed, settings) { + for (line_count, (line, line_positions)) in + Self::create_layout(font, preprocessed, settings).enumerate() + { self.line_capacity.push(line.number_of_letter_groups()); self.relative_positions - .extend(line_positions.map(|x| Vector2D::new(x.x as i16, x.y as i16))); + .extend(line_positions.map(|(x, paragraph)| LayoutPosition { + line: line_count as u8, + paragraph, + x, + })); } } @@ -531,12 +571,11 @@ impl LayoutCache { font: &Font, preprocessed: &'a Preprocessed, settings: &'a LayoutSettings, - ) -> impl Iterator> + 'a)> + 'a { + ) -> 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 width = settings.width; - let mut head_position: Vector2D> = (0, -line_height).into(); + let mut paragraph_counter = 0; preprocessed .lines_element(width, minimum_space_width) @@ -545,23 +584,22 @@ impl LayoutCache { .alignment .settings(&line, minimum_space_width, width); - head_position.y += line_height; - head_position.x = line_settings.start_x.into(); + let mut head_position: Num = line_settings.start_x.into(); ( 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.floor()) + head_position += width as i32; + Some((this_position.floor(), paragraph_counter)) } PreprocessedElement::WhiteSpace(space) => { match space { WhiteSpace::NewLine => { - head_position.y += settings.paragraph_spacing; + paragraph_counter += 1; } - WhiteSpace::Space => head_position.x += line_settings.space_width, + WhiteSpace::Space => head_position += line_settings.space_width, } None } @@ -573,7 +611,7 @@ impl LayoutCache { #[derive(PartialEq, Eq, Default)] struct LayoutSettings { - area: Vector2D, + width: i32, alignment: TextAlignment, paragraph_spacing: i32, } diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs index ee61e9973..b1dae8854 100644 --- a/agb/src/display/object/font/preprocess.rs +++ b/agb/src/display/object/font/preprocess.rs @@ -55,6 +55,7 @@ impl Preprocessor { character: char, sprite_width: i32, widths: &mut Vec, + explicit_break_on: Option bool>, ) { match character { space @ (' ' | '\n') => { @@ -75,7 +76,9 @@ impl Preprocessor { self.width_in_sprite += letter.kerning_amount(previous_character); } - if self.width_in_sprite + letter.width as i32 > sprite_width { + if self.width_in_sprite + letter.width as i32 > sprite_width + || explicit_break_on.map(|x| x(character)).unwrap_or_default() + { widths.push( PreprocessedElement::LetterGroup { width: self.width_in_sprite as u8, @@ -107,6 +110,7 @@ pub(crate) struct Line { number_of_text_elements: usize, number_of_spaces: usize, number_of_letter_groups: usize, + ended_on_explicit_newline: bool, } impl Line { @@ -122,6 +126,10 @@ impl Line { pub(crate) fn number_of_letter_groups(&self) -> usize { self.number_of_letter_groups } + #[inline(always)] + pub(crate) fn ended_on_explicit_newline(&self) -> bool { + self.ended_on_explicit_newline + } } impl<'pre> Iterator for Lines<'pre> { @@ -140,6 +148,7 @@ impl<'pre> Iterator for Lines<'pre> { let mut length_of_current_word = 0; let mut number_of_spaces = 0; let mut number_of_letter_groups = 0; + let mut ended_on_explicit_newline = false; while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { match next.decode() { @@ -176,6 +185,7 @@ impl<'pre> Iterator for Lines<'pre> { match space { WhiteSpace::NewLine => { line_idx_length += 1; + ended_on_explicit_newline = true; break; } WhiteSpace::Space => { @@ -195,6 +205,7 @@ impl<'pre> Iterator for Lines<'pre> { number_of_text_elements: line_idx_length, number_of_spaces, number_of_letter_groups, + ended_on_explicit_newline, }) } } @@ -204,9 +215,15 @@ impl Preprocessed { Default::default() } - pub(crate) fn add_character(&mut self, font: &Font, c: char, sprite_width: i32) { + pub(crate) fn add_character( + &mut self, + font: &Font, + c: char, + sprite_width: i32, + explicit_break_on: Option bool>, + ) { self.preprocessor - .add_character(font, c, sprite_width, &mut self.widths); + .add_character(font, c, sprite_width, &mut self.widths, explicit_break_on); } pub(crate) fn lines(&self, layout_width: i32, minimum_space_width: i32) -> Lines<'_> { diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs index 285e1416e..3e65c820a 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -46,17 +46,19 @@ pub(crate) struct WordRender { start_index_of_letter: usize, previous_character: Option, + explicit_break_on: Option bool>, } impl WordRender { #[must_use] - pub(crate) fn new(config: Configuration) -> Self { + pub(crate) fn new(config: Configuration, explicit_break_on: Option bool>) -> Self { WordRender { working: WorkingLetter::new(config.sprite_size), config, colour: 1, previous_character: None, start_index_of_letter: 0, + explicit_break_on, } } @@ -101,6 +103,7 @@ impl WordRender { // 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.explicit_break_on.map(|x| x(c)).unwrap_or_default() { self.finalise_letter(index_of_character) } else { From c27e9a68feee53f1420c32631146e66ab8725193 Mon Sep 17 00:00:00 2001 From: Corwin Date: Tue, 16 Apr 2024 20:32:42 +0100 Subject: [PATCH 8/9] tmp --- agb/src/display/object/font.rs | 843 +++++++++------------- agb/src/display/object/font/preprocess.rs | 252 ------- agb/src/display/object/font/renderer.rs | 7 +- 3 files changed, 338 insertions(+), 764 deletions(-) delete mode 100644 agb/src/display/object/font/preprocess.rs diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 8325636f2..5216c8f58 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,617 +1,442 @@ -use core::{ - fmt::{Display, Write}, - ops::Add, -}; +use core::num::NonZeroU32; -use agb_fixnum::{Num, Vector2D}; -use alloc::{string::String, vec::Vec}; +use alloc::{borrow::Cow, collections::VecDeque, vec::Vec}; use crate::display::Font; -use self::{ - preprocess::{Line, Preprocessed, PreprocessedElement}, - renderer::{Configuration, WordRender}, -}; +use self::renderer::Configuration; -use super::{OamIterator, ObjectUnmanaged, PaletteVram, Size, SpriteVram}; +use super::{PaletteVram, Size, SpriteVram}; -mod preprocess; mod renderer; -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -#[non_exhaustive] -pub(crate) enum WhiteSpace { - NewLine, - Space, -} +pub struct ChangeColour(u8); -impl WhiteSpace { - pub(crate) fn from_char(c: char) -> Self { - match c { - ' ' => WhiteSpace::Space, - '\n' => WhiteSpace::NewLine, - _ => panic!("char not supported whitespace"), +impl ChangeColour { + #[must_use] + /// Creates the colour changer. Colour is a palette index and must be in the range 0..16. + pub fn new(colour: usize) -> Self { + assert!(colour < 16, "paletted colour must be valid (0..=15)"); + + Self(colour as u8) + } + + fn try_from_char(c: char) -> Option { + let c = c as u32 as usize; + if (0xE000..0xE000 + 16).contains(&c) { + Some(ChangeColour::new(c - 0xE000)) + } else { + None } } -} -struct BufferedRender<'font> { - char_render: WordRender, - preprocessor: Preprocessed, - string: String, - cursor_position: usize, - letters: Letters, - font: &'font Font, + fn to_char(self) -> char { + char::from_u32(self.0 as u32 + 0xE000).unwrap() + } } -#[derive(Debug)] -struct LettersIndexed { - start_index: usize, - end_index: usize, - sprite: SpriteVram, +fn is_private_use(c: char) -> bool { + ('\u{E000}'..'\u{F8FF}').contains(&c) } -#[derive(Debug, Default)] -struct Letters { - letters: Vec, - number_of_groups: usize, +struct RenderConfig<'string> { + string: Cow<'string, str>, + font: &'static Font, + explicit_end_on: Option bool>, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] -#[non_exhaustive] -/// The text alignment of the layout -pub enum TextAlignment { - #[default] - /// Left aligned, the left edge of the text lines up - Left, - /// Right aligned, the right edge of the text lines up - Right, - /// Center aligned, the center of the text lines up - Center, - /// Justified, both the left and right edges line up with space width adapted to make it so. - Justify, +struct RenderedSpriteInternal { + start: usize, + end: usize, + width: i32, + sprite: SpriteVram, } -struct TextAlignmentSettings { - space_width: Num, - start_x: i32, +struct RenderedSprite<'text_render> { + string: &'text_render str, + width: i32, + sprite: &'text_render SpriteVram, } -impl TextAlignment { - fn settings(self, line: &Line, minimum_space_width: i32, width: i32) -> TextAlignmentSettings { - match self { - TextAlignment::Left => TextAlignmentSettings { - space_width: minimum_space_width.into(), - start_x: 0, - }, - TextAlignment::Right => TextAlignmentSettings { - space_width: minimum_space_width.into(), - start_x: width - line.width(), - }, - TextAlignment::Center => TextAlignmentSettings { - space_width: minimum_space_width.into(), - start_x: (width - line.width()) / 2, - }, - TextAlignment::Justify => { - let space_width = if line.number_of_spaces() != 0 - && !line.ended_on_explicit_newline() - { - Num::new( - width - line.width() + line.number_of_spaces() as i32 * minimum_space_width, - ) / line.number_of_spaces() as i32 - } else { - minimum_space_width.into() - }; - TextAlignmentSettings { - space_width, - start_x: 0, - } - } - } +impl RenderedSprite<'_> { + fn text(&self) -> &str { + self.string } -} -impl<'font> BufferedRender<'font> { - #[must_use] - fn new( - string: String, - font: &'font Font, - sprite_size: Size, - palette: PaletteVram, - explicit_break_on: Option bool>, - ) -> Self { - let config = Configuration::new(sprite_size, palette); - - let mut pre = Preprocessed::new(); - - for character in string.chars() { - if !is_private_use(character) { - pre.add_character( - font, - character, - sprite_size.to_width_height().0 as i32, - explicit_break_on, - ); - } - } + fn width(&self) -> i32 { + self.width + } - BufferedRender { - char_render: WordRender::new(config, explicit_break_on), - preprocessor: pre, - string, - letters: Default::default(), - cursor_position: 0, - font, - } + fn sprite(&self) -> &SpriteVram { + &self.sprite } } -fn is_private_use(c: char) -> bool { - ('\u{E000}'..'\u{F8FF}').contains(&c) +struct SimpleTextRender<'string> { + config: RenderConfig<'string>, + render_index: usize, + inner_renderer: renderer::WordRender, + rendered_sprite_window: VecDeque, + word_lengths: VecDeque, } -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -/// Changes the palette to use to draw characters. -/// ```rust,no_run -/// # #![no_std] -/// # #![no_main] -/// use agb::display::object::{ObjectTextRender, PaletteVram, ChangeColour, Size}; -/// use agb::display::palette16::Palette16; -/// use agb::display::Font; -/// -/// use core::fmt::Write; -/// -/// static EXAMPLE_FONT: Font = agb::include_font!("examples/font/yoster.ttf", 12); -/// -/// # fn foo() { -/// let mut palette = [0x0; 16]; -/// palette[1] = 0xFF_FF; -/// palette[2] = 0x00_FF; -/// let palette = Palette16::new(palette); -/// let palette = PaletteVram::new(&palette).unwrap(); -/// let mut writer = ObjectTextRender::new(&EXAMPLE_FONT, Size::S16x16, palette); -/// -/// let _ = writeln!(writer, "Hello, {}World{}!", ChangeColour::new(2), ChangeColour::new(1)); -/// # } -/// ``` -pub struct ChangeColour(u8); +#[derive(Debug, Copy, Clone, Default)] +struct WordLength { + letter_groups: usize, + pixels: i32, +} -impl ChangeColour { - #[must_use] - /// Creates the colour changer. Colour is a palette index and must be in the range 0..16. - pub fn new(colour: usize) -> Self { - assert!(colour < 16, "paletted colour must be valid (0..=15)"); +struct SimpleLayoutItem<'text_render> { + string: &'text_render str, + sprite: &'text_render SpriteVram, + x: i32, +} - Self(colour as u8) +impl<'text_render> SimpleLayoutItem<'text_render> { + fn displayed_string(&self) -> &str { + &self.string } - fn try_from_char(c: char) -> Option { - let c = c as u32 as usize; - if (0xE000..0xE000 + 16).contains(&c) { - Some(ChangeColour::new(c - 0xE000)) - } else { - None - } + fn sprite(&self) -> &SpriteVram { + &self.sprite } - fn to_char(self) -> char { - char::from_u32(self.0 as u32 + 0xE000).unwrap() + fn x_offset(&self) -> i32 { + self.x } } -impl Display for ChangeColour { - fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { - f.write_char(self.to_char()) +struct SimpleLayoutIterator<'text_render> { + string: &'text_render str, + vec_iter: alloc::collections::vec_deque::Iter<'text_render, RenderedSpriteInternal>, + word_lengths_iter: alloc::collections::vec_deque::Iter<'text_render, WordLength>, + space_width: i32, + current_word_length: usize, + x_offset: i32, +} + +impl<'text_render> Iterator for SimpleLayoutIterator<'text_render> { + type Item = SimpleLayoutItem<'text_render>; + + fn next(&mut self) -> Option { + while self.current_word_length == 0 { + self.x_offset += self.space_width; + self.current_word_length = self.word_lengths_iter.next()?.letter_groups; + } + + let rendered = self.vec_iter.next()?; + let my_x_offset = self.x_offset; + self.x_offset += rendered.width; + + self.current_word_length -= 1; + + Some(SimpleLayoutItem { + string: &self.string[rendered.start..rendered.end], + sprite: &rendered.sprite, + x: my_x_offset, + }) } } -impl BufferedRender<'_> { - fn next_character(&mut self) -> Option<(usize, char)> { - let next = self.string.get(self.cursor_position..)?.chars().next()?; - let idx = self.cursor_position; +impl<'string> SimpleTextRender<'string> { + /// Lays out text in one line with a space between each word, note that + /// newlines are just treated as word breaks. + /// + /// If you want to treat layout fully use one of the layouts + /// [`LeftAlignLayout`], [`RightAlignLayout`], [`CenterAlignLayout`], or + /// [`JustifyAlignLayout`]. + pub fn simple_layout(&self) -> SimpleLayoutIterator<'_> { + SimpleLayoutIterator { + string: &self.config.string, + word_lengths_iter: self.word_lengths.iter(), + vec_iter: self.rendered_sprite_window.iter(), + space_width: self.config.font.letter(' ').advance_width as i32, + current_word_length: 0, + x_offset: 0, + } + } - self.cursor_position += next.len_utf8(); - Some((idx, next)) + fn words(&self) -> impl Iterator, impl Iterator)> { + let mut start = 0; + self.word_lengths + .iter() + .copied() + .enumerate() + .map(move |(idx, length)| { + let potentially_incomplete = self.word_lengths.len() == idx + 1; + let definitely_complete = !potentially_incomplete; + + let end = start + length.letter_groups; + let this_start = start; + start = end; + + ( + definitely_complete.then_some(length.pixels), + self.rendered_sprite_window + .range(this_start..end) + .map(|x| RenderedSprite { + string: &self.config.string[x.start..x.end], + width: x.width, + sprite: &x.sprite, + }), + ) + }) } - fn completed_character(&self) -> bool { - self.string.len() == self.cursor_position + fn next_character(&mut self) -> Option<(usize, char)> { + let next = self + .config + .string + .get(self.render_index..)? + .chars() + .next()?; + let idx = self.render_index; + + self.render_index += next.len_utf8(); + Some((idx, next)) } - fn process(&mut self) { + pub fn update(&mut self) { let Some((idx, c)) = self.next_character() else { return; }; match c { ' ' | '\n' => { - if let Some((start_index, group)) = self.char_render.finalise_letter(idx) { - self.letters.letters.push(LettersIndexed { - start_index, - end_index: idx, - sprite: group, - }); - self.letters.number_of_groups += 1; + let length = self + .word_lengths + .back_mut() + .expect("There should always be at least one word length"); + if let Some((start_index, group, width)) = self.inner_renderer.finalise_letter(idx) + { + self.rendered_sprite_window + .push_back(RenderedSpriteInternal { + start: start_index, + end: idx, + sprite: group, + width, + }); + + length.letter_groups += 1; + length.pixels += width; } - self.letters.number_of_groups += 1; + self.word_lengths.push_back(WordLength::default()); } letter => { - if let Some((start_index, group)) = - self.char_render.render_char(self.font, letter, idx) + if let Some((start_index, group, width)) = + self.inner_renderer + .render_char(self.config.font, letter, idx) { - self.letters.letters.push(LettersIndexed { - start_index, - end_index: idx, - sprite: group, - }); - self.letters.number_of_groups += 1; + self.rendered_sprite_window + .push_back(RenderedSpriteInternal { + start: start_index, + end: idx, + sprite: group, + width, + }); + let length = self + .word_lengths + .back_mut() + .expect("There should always be at least one word length"); + length.letter_groups += 1; + length.pixels += width; } } } } -} - -/// The object text renderer. Uses objects to render and layout text. It's use is non trivial. -/// Changes the palette to use to draw characters. -/// ```rust,no_run -/// #![no_std] -/// #![no_main] -/// use agb::display::object::{ObjectTextRender, PaletteVram, TextAlignment, Size}; -/// use agb::display::palette16::Palette16; -/// use agb::display::{Font, WIDTH}; -/// -/// use core::fmt::Write; -/// -/// static EXAMPLE_FONT: Font = agb::include_font!("examples/font/yoster.ttf", 12); -/// -/// #[agb::entry] -/// fn main(gba: &mut agb::Gba) -> ! { -/// let (mut unmanaged, _) = gba.display.object.get_unmanaged(); -/// let vblank = agb::interrupt::VBlank::get(); -/// -/// let mut palette = [0x0; 16]; -/// palette[1] = 0xFF_FF; -/// let palette = Palette16::new(palette); -/// let palette = PaletteVram::new(&palette).unwrap(); -/// -/// let mut writer = ObjectTextRender::new(&EXAMPLE_FONT, Size::S16x16, palette); -/// -/// let _ = writeln!(writer, "Hello, World!"); -/// writer.layout((WIDTH, 40), TextAlignment::Left, 2); -/// -/// loop { -/// writer.next_letter_group(); -/// writer.update((0, 0)); -/// vblank.wait_for_vblank(); -/// let oam = &mut unmanaged.iter(); -/// writer.commit(oam); -/// } -/// } -/// ``` -pub struct ObjectTextRender<'font> { - buffer: BufferedRender<'font>, - layout: LayoutCache, - number_of_objects: usize, -} -impl<'font> ObjectTextRender<'font> { - #[must_use] - /// Creates a new text renderer with a given font, sprite size, and palette. - /// You must ensure that the sprite size can accomodate the letters from the - /// font otherwise it will panic at render time. pub fn new( - text: String, - font: &'font Font, - sprite_size: Size, + string: Cow<'string, str>, + font: &'static Font, palette: PaletteVram, - explicit_break_on: Option bool>, + sprite_size: Size, ) -> Self { + let mut word_lengths = VecDeque::new(); + word_lengths.push_back(WordLength::default()); Self { - buffer: BufferedRender::new(text, font, sprite_size, palette, explicit_break_on), - number_of_objects: 0, - layout: LayoutCache { - relative_positions: Vec::new(), - line_capacity: Vec::new(), - paragraph_spacing: 0, - width: 0, + config: RenderConfig { + string, + font, + explicit_end_on: None, }, + rendered_sprite_window: VecDeque::new(), + word_lengths, + render_index: 0, + inner_renderer: renderer::WordRender::new( + Configuration::new(sprite_size, palette), + None, + ), } } -} - -/// A borrowed letter group and its relative position. -#[derive(Debug)] -pub struct LetterGroup<'a> { - sprite: &'a SpriteVram, - contained_string: &'a str, - relative_position: Vector2D, - line: u8, - paragraph: u8, -} - -impl<'a> LetterGroup<'a> { - /// The sprite in vram for this group of letters. - #[must_use] - pub fn sprite(&self) -> &'a SpriteVram { - self.sprite - } - - /// The relative position of the letter group. For example a left aligned - /// text would start at (0,0) but a right aligned would have the last group - /// of a line be at (0,0). - #[must_use] - pub fn relative_position(&self) -> Vector2D { - self.relative_position - } - #[must_use] - pub fn line(&self) -> i32 { - self.line as i32 - } - - #[must_use] - /// A reference to the string that are represented by this sprite - pub fn letters(&self) -> &'a str { - self.contained_string + fn string(&self) -> &str { + &self.config.string } } -impl<'a> Add> for &LetterGroup<'a> { - type Output = LetterGroup<'a>; - - fn add(self, rhs: Vector2D) -> Self::Output { - LetterGroup { - sprite: self.sprite, - contained_string: self.contained_string, - relative_position: self.relative_position + rhs, - line: self.line, - paragraph: self.paragraph, - } - } +struct LineInformation { + start_x: i32, + words: usize, + space_width: i32, } -impl<'a> From> for ObjectUnmanaged { - fn from(value: LetterGroup<'a>) -> Self { - let mut object = ObjectUnmanaged::new(value.sprite.clone()); - object.set_position(value.relative_position); - object.show(); - - object - } +struct LeftAlignLayout<'string> { + simple: SimpleTextRender<'string>, + data: LeftAlignLayoutData, } -/// An iterator over the currently displaying letter groups -#[derive(Debug)] -pub struct LetterGroupIter<'a> { - sprite_iter: core::slice::Iter<'a, LettersIndexed>, - position_iter: core::slice::Iter<'a, LayoutPosition>, - string: &'a str, - remaining_letter_groups: usize, - line_height: i32, - paragraph_spacing: i32, +struct LeftAlignLayoutData { + width: Option, + string_index: usize, + words_per_line: Vec, + current_line_width: i32, } -impl<'a> Iterator for LetterGroupIter<'a> { - type Item = LetterGroup<'a>; - - fn next(&mut self) -> Option { - if self.remaining_letter_groups == 0 { - return None; - } - self.remaining_letter_groups -= 1; - - match (self.sprite_iter.next(), self.position_iter.next()) { - (Some(sprite), Some(position)) => Some(LetterGroup { - sprite: &sprite.sprite, - contained_string: &self.string[sprite.start_index..sprite.end_index], - relative_position: Vector2D::new( - position.x, - position.line as i32 * self.line_height - + position.paragraph as i32 * self.paragraph_spacing, - ), - line: position.line, - paragraph: position.paragraph, - }), - _ => None, - } - } +struct PreparedLetterGroupPosition { + x: i32, + line: i32, } -impl ObjectTextRender<'_> { - #[must_use] - /// An iterator over the letter groups that make up the text - pub fn letter_groups(&self) -> LetterGroupIter<'_> { - LetterGroupIter { - sprite_iter: self.buffer.letters.letters.iter(), - position_iter: self.layout.relative_positions.iter(), - remaining_letter_groups: self.number_of_objects, - string: &self.buffer.string, - line_height: self.buffer.font.line_height(), - paragraph_spacing: self.layout.paragraph_spacing, - } +fn length_of_next_word(current_index: &mut usize, s: &str, font: &Font) -> Option<(bool, i32)> { + let s = &s[*current_index..]; + if s.len() == 0 { + return None; } - /// 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: V) { - fn inner(this: &mut ObjectTextRender, oam: &mut OamIterator, position: Vector2D) { - let objects = this - .letter_groups() - .map(|x| &x + position) - .map(ObjectUnmanaged::from); + let mut width = 0; + let mut previous_character = None; + for (idx, chr) in s.char_indices() { + match chr { + '\n' | ' ' => { + if idx != 0 { + return Some((chr == '\n', width)); + } + } + letter => { + let letter = font.letter(letter); + if let Some(previous_character) = previous_character { + width += letter.kerning_amount(previous_character); + } - for (object, slot) in objects.zip(oam) { - slot.set(&object); + width += letter.xmin as i32; + width += letter.advance_width as i32; } } - - inner(self, oam, position.into()); + previous_character = Some(chr); } + *current_index += s.len(); + Some((false, width)) +} + +struct LaidOutLetter<'text_render> { + line: usize, + x: i32, + sprite: &'text_render SpriteVram, + string: &'text_render str, +} - /// Force a relayout, must be called after writing. - pub fn layout(&mut self, width: i32, alignment: TextAlignment, paragraph_spacing: i32) { - self.layout.create_positions( - self.buffer.font, - &self.buffer.preprocessor, - &LayoutSettings { +impl<'string> LeftAlignLayout<'string> { + fn new(simple: SimpleTextRender<'string>, width: Option) -> Self { + let words_per_line = alloc::vec![0]; + + Self { + simple, + data: LeftAlignLayoutData { + string_index: 0, + words_per_line, + current_line_width: 0, width, - alignment, - paragraph_spacing, }, - ); - } - - /// Updates the internal state of the number of letters to write and popped - /// line. Should be called in the same frame as and after - /// [`next_letter_group`][ObjectTextRender::next_letter_group], [`next_line`][ObjectTextRender::next_line], and [`pop_line`][ObjectTextRender::pop_line]. - pub fn update(&mut self) { - if !self.buffer.completed_character() - && self.buffer.letters.letters.len() <= self.number_of_objects + 5 - { - self.buffer.process(); } } - /// Causes the next letter group to be shown on the next update. Returns - /// whether another letter could be added in the space given. - 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 layout(&mut self) -> impl Iterator { + self.data.layout( + self.simple.string(), + self.simple.config.font, + self.simple.words(), + ) } +} - fn can_render_another_element(&self) -> bool { - let max_number_of_objects = self.layout.line_capacity.iter().sum::(); - - max_number_of_objects > self.number_of_objects +impl LeftAlignLayoutData { + fn length_of_next_word(&mut self, string: &str, font: &Font) -> Option<(bool, i32)> { + length_of_next_word(&mut self.string_index, string, font) } - /// Causes the next line to be shown on the next update. Returns - /// whether another line could be added in the space given. - pub fn next_line(&mut self) -> bool { - // find current line + fn try_extend_line(&mut self, string: &str, font: &Font) -> bool { + let (force_new_line, length_of_next_word) = self + .length_of_next_word(string, font) + .expect("there should be a word for us to process"); - for (start, end) in self - .layout - .line_capacity - .iter() - .scan(0, |count, line_size| { - let start = *count; - *count += line_size; - Some((start, *count)) - }) + if force_new_line + || self.current_line_width + length_of_next_word + >= self.width.map_or(i32::MAX, |x| x.get() as i32) { - 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 - } - - /// Immediately renders all the completed letter groups in the buffer. - pub fn render_all(&mut self) { - while !self.buffer.completed_character() { - self.buffer.process(); - } - self.number_of_objects = self.buffer.letters.letters.len(); - } - - fn at_least_n_letter_groups(&mut self, n: usize) { - while !self.buffer.completed_character() && self.buffer.letters.letters.len() <= n { - self.buffer.process(); - } - } -} - -struct LayoutCache { - relative_positions: Vec, - line_capacity: Vec, - width: i32, - paragraph_spacing: i32, -} - -#[derive(Debug)] -struct LayoutPosition { - line: u8, - paragraph: u8, - x: i32, -} + self.current_line_width = 0; + self.words_per_line.push(0); + true + } else { + let current_line = self + .words_per_line + .last_mut() + .expect("should always have a line"); -impl LayoutCache { - fn create_positions( - &mut self, - font: &Font, - preprocessed: &Preprocessed, - settings: &LayoutSettings, - ) { - self.width = settings.width; - self.paragraph_spacing = settings.paragraph_spacing; - self.line_capacity.clear(); - self.relative_positions.clear(); - for (line_count, (line, line_positions)) in - Self::create_layout(font, preprocessed, settings).enumerate() - { - self.line_capacity.push(line.number_of_letter_groups()); - self.relative_positions - .extend(line_positions.map(|(x, paragraph)| LayoutPosition { - line: line_count as u8, - paragraph, - x, - })); + *current_line += 1; + false } } - 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.width; - - let mut paragraph_counter = 0; - - preprocessed - .lines_element(width, minimum_space_width) - .map(move |(line, line_elements)| { - let line_settings = settings - .alignment - .settings(&line, minimum_space_width, width); - - let mut head_position: Num = line_settings.start_x.into(); + fn layout<'a, 'text_render>( + &'a mut self, + string: &'a str, + font: &'static Font, + simple: impl Iterator< + Item = ( + Option, + impl Iterator> + 'a, + ), + > + 'a, + ) -> impl Iterator> + 'a { + let mut words_in_current_line = 0; + let mut current_line = 0; + let mut current_line_x_offset = 0; + let space_width = font.letter(' ').advance_width as i32; + + simple.flat_map(move |(pixels, letters)| { + let this_line_is_the_last_processed = current_line + 1 == self.words_per_line.len(); + + words_in_current_line += 1; + if words_in_current_line > self.words_per_line[current_line] { + if this_line_is_the_last_processed { + if self.try_extend_line(string, font) { + current_line += 1; + current_line_x_offset = 0; + } + } else { + current_line += 1; + current_line_x_offset = 0; + } + } - ( - line, - line_elements.filter_map(move |element| match element.decode() { - PreprocessedElement::LetterGroup { width } => { - let this_position = head_position; - head_position += width as i32; - Some((this_position.floor(), paragraph_counter)) - } - PreprocessedElement::WhiteSpace(space) => { - match space { - WhiteSpace::NewLine => { - paragraph_counter += 1; - } - WhiteSpace::Space => head_position += line_settings.space_width, - } - None - } - }), - ) + let current_line = current_line; + let mut letter_x_offset = current_line_x_offset; + current_line_x_offset += pixels.unwrap_or(0); + current_line_x_offset += space_width; + letters.map(move |x| { + let my_offset = letter_x_offset; + letter_x_offset += x.width; + LaidOutLetter { + line: current_line, + x: my_offset, + sprite: x.sprite, + string: x.string, + } }) + }) } } -#[derive(PartialEq, Eq, Default)] -struct LayoutSettings { - width: i32, - alignment: TextAlignment, - paragraph_spacing: i32, -} +struct RightAlignLayout {} +struct CenterAlignLayout {} +struct JustifyAlignLayout {} diff --git a/agb/src/display/object/font/preprocess.rs b/agb/src/display/object/font/preprocess.rs deleted file mode 100644 index b1dae8854..000000000 --- a/agb/src/display/object/font/preprocess.rs +++ /dev/null @@ -1,252 +0,0 @@ -use alloc::vec::Vec; - -use crate::display::Font; - -use super::WhiteSpace; - -#[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 { - LetterGroup { width: u8 }, - WhiteSpace(WhiteSpace), -} - -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: Vec, - preprocessor: Preprocessor, -} - -#[derive(Debug, Default)] -struct Preprocessor { - previous_character: Option, - width_in_sprite: i32, -} - -impl Preprocessor { - fn add_character( - &mut self, - font: &Font, - character: char, - sprite_width: i32, - widths: &mut Vec, - explicit_break_on: Option bool>, - ) { - match character { - space @ (' ' | '\n') => { - if self.width_in_sprite != 0 { - widths.push( - PreprocessedElement::LetterGroup { - width: self.width_in_sprite as u8, - } - .encode(), - ); - self.width_in_sprite = 0; - } - widths.push(PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)).encode()); - } - letter => { - let letter = font.letter(letter); - if let Some(previous_character) = self.previous_character { - self.width_in_sprite += letter.kerning_amount(previous_character); - } - - if self.width_in_sprite + letter.width as i32 > sprite_width - || explicit_break_on.map(|x| x(character)).unwrap_or_default() - { - widths.push( - PreprocessedElement::LetterGroup { - width: self.width_in_sprite as u8, - } - .encode(), - ); - self.width_in_sprite = 0; - } - if self.width_in_sprite != 0 { - self.width_in_sprite += letter.xmin as i32; - } - self.width_in_sprite += letter.advance_width as i32; - } - } - - self.previous_character = Some(character); - } -} - -pub(crate) struct Lines<'preprocess> { - minimum_space_width: i32, - layout_width: i32, - data: &'preprocess [PreprocessedElementEncoded], - current_start_idx: usize, -} - -pub(crate) struct Line { - width: i32, - number_of_text_elements: usize, - number_of_spaces: usize, - number_of_letter_groups: usize, - ended_on_explicit_newline: bool, -} - -impl Line { - #[inline(always)] - pub(crate) fn width(&self) -> i32 { - self.width - } - #[inline(always)] - pub(crate) fn number_of_spaces(&self) -> usize { - self.number_of_spaces - } - #[inline(always)] - pub(crate) fn number_of_letter_groups(&self) -> usize { - self.number_of_letter_groups - } - #[inline(always)] - pub(crate) fn ended_on_explicit_newline(&self) -> bool { - self.ended_on_explicit_newline - } -} - -impl<'pre> Iterator for Lines<'pre> { - type Item = Line; - - fn next(&mut self) -> Option { - if self.current_start_idx >= self.data.len() { - return None; - } - - let mut line_idx_length = 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_letter_groups = 0; - let mut ended_on_explicit_newline = false; - - while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { - 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; - } - } - 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_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; - } - - match space { - WhiteSpace::NewLine => { - line_idx_length += 1; - ended_on_explicit_newline = true; - break; - } - WhiteSpace::Space => { - spaces_after_last_word_count += 1; - } - } - } - }; - - line_idx_length += 1; - } - - self.current_start_idx += line_idx_length; - - Some(Line { - width: current_line_width_pixels, - number_of_text_elements: line_idx_length, - number_of_spaces, - number_of_letter_groups, - ended_on_explicit_newline, - }) - } -} - -impl Preprocessed { - pub(crate) fn new() -> Self { - Default::default() - } - - pub(crate) fn add_character( - &mut self, - font: &Font, - c: char, - sprite_width: i32, - explicit_break_on: Option bool>, - ) { - self.preprocessor - .add_character(font, c, sprite_width, &mut self.widths, explicit_break_on); - } - - 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, - } - } - - pub(crate) fn lines_element( - &self, - layout_width: i32, - minimum_space_width: i32, - ) -> 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)].iter().copied(); - idx += length; - (x, d) - }) - } -} diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs index 3e65c820a..6342b0a9b 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -66,7 +66,7 @@ impl WordRender { pub(crate) fn finalise_letter( &mut self, index_of_character: usize, - ) -> Option<(usize, SpriteVram)> { + ) -> Option<(usize, SpriteVram, i32)> { if self.working.x_offset == 0 { return None; } @@ -75,10 +75,11 @@ impl WordRender { core::mem::swap(&mut self.working.dynamic, &mut new_sprite); let sprite = new_sprite.to_vram(self.config.palette.clone()); let start_index = self.start_index_of_letter; + let width = self.working.x_offset; self.working.reset(); self.start_index_of_letter = index_of_character; - Some((start_index, sprite)) + Some((start_index, sprite, width)) } #[must_use] @@ -87,7 +88,7 @@ impl WordRender { font: &Font, c: char, index_of_character: usize, - ) -> Option<(usize, SpriteVram)> { + ) -> Option<(usize, SpriteVram, i32)> { if let Some(next_colour) = ChangeColour::try_from_char(c) { self.colour = next_colour.0 as usize; return None; From d36aeb3b65f2bc316f8936071073708cc104f946 Mon Sep 17 00:00:00 2001 From: Corwin Date: Wed, 17 Apr 2024 01:28:16 +0100 Subject: [PATCH 9/9] a complete layout with the new system, per frame slow --- agb/examples/object_text_render.rs | 87 ++++++++------ agb/src/display/object.rs | 2 +- agb/src/display/object/font.rs | 145 ++++++++++++++++-------- agb/src/display/object/font/renderer.rs | 4 - 4 files changed, 154 insertions(+), 84 deletions(-) diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index 12dc2a0fc..b339abc12 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -4,7 +4,7 @@ use agb::{ display::{ object::{ - ChangeColour, ObjectTextRender, ObjectUnmanaged, PaletteVram, Size, TextAlignment, + ChangeColour, LeftAlignLayout, ObjectUnmanaged, PaletteVram, SimpleTextRender, Size, }, palette16::Palette16, Font, HEIGHT, WIDTH, @@ -14,6 +14,9 @@ use agb::{ }; use agb_fixnum::Vector2D; +use alloc::borrow::Cow; +use core::num::NonZeroU32; + extern crate alloc; static FONT: Font = include_font!("examples/font/ark-pixel-10px-proportional-ja.ttf", 10); @@ -45,7 +48,14 @@ fn main(mut gba: agb::Gba) -> ! { ); let start = timer.value(); - let mut wr = ObjectTextRender::new(text, &FONT, Size::S16x16, palette, Some(|c| c == '.')); + let simple = SimpleTextRender::new( + Cow::Owned(text), + &FONT, + palette, + Size::S16x16, + Some(|c| c == '.'), + ); + let mut wr = LeftAlignLayout::new(simple, NonZeroU32::new(WIDTH as u32)); let end = timer.value(); agb::println!( @@ -56,17 +66,6 @@ fn main(mut gba: agb::Gba) -> ! { let vblank = agb::interrupt::VBlank::get(); let mut input = agb::input::ButtonController::new(); - let start = timer.value(); - - wr.layout(WIDTH, TextAlignment::Justify, 2); - let end = timer.value(); - - agb::println!( - "Layout took {} cycles", - 256 * (end.wrapping_sub(start) as u32) - ); - - let mut line = 0; let mut frame = 0; let mut groups_to_show = 0; @@ -75,35 +74,55 @@ fn main(mut gba: agb::Gba) -> ! { input.update(); let oam = &mut unmanaged.iter(); - let done_rendering = !wr.next_letter_group(); + wr.at_least_n_letter_groups(groups_to_show + 2); + let start = timer.value(); - let mut letters = wr.letter_groups(); - let displayed_letters = letters - .by_ref() - .take(groups_to_show) - .filter(|x| x.line() >= line); + let can_pop_line = { + let mut letters = wr.layout(); + let displayed_letters = letters.by_ref().take(groups_to_show); - for (letter, slot) in displayed_letters.zip(oam) { - slot.set(&ObjectUnmanaged::from( - &letter + Vector2D::new(0, HEIGHT - 40 - line * FONT.line_height()), - )) - } + for (letter, slot) in displayed_letters.zip(oam) { + let mut obj = ObjectUnmanaged::new(letter.sprite().clone()); + obj.show(); + let y = HEIGHT - 40 + letter.line() as i32 * FONT.line_height(); + obj.set_position(Vector2D::new(letter.x(), y)); + + slot.set(&obj); + } - if let Some(next_letter) = letters.next() { - if next_letter.line() < line + 2 { - if next_letter.letters() == "." { - if frame % 16 == 0 { + let speed_up = if input.is_pressed(Button::A | Button::B) { + 4 + } else { + 1 + }; + + if let Some(next_letter) = letters.next() { + if next_letter.line() < 2 { + if next_letter.string() == "." { + if frame % (16 / speed_up) == 0 { + groups_to_show += 1; + } + } else if frame % (4 / speed_up) == 0 { groups_to_show += 1; } - } else if frame % 4 == 0 { - groups_to_show += 1; + false + } else { + true } - } else if input.is_just_pressed(Button::A) { - line += 1; + } else { + false } - } + }; - wr.update(); + let end = timer.value(); + agb::println!( + "Layout took {} cycles", + 256 * (end.wrapping_sub(start) as u32) + ); + + if can_pop_line && input.is_just_pressed(Button::A) { + groups_to_show -= wr.pop_line(); + } frame += 1; } diff --git a/agb/src/display/object.rs b/agb/src/display/object.rs index 4cf5c71db..ae1403639 100644 --- a/agb/src/display/object.rs +++ b/agb/src/display/object.rs @@ -23,7 +23,7 @@ pub use affine::AffineMatrixInstance; pub use managed::{OamManaged, Object}; pub use unmanaged::{AffineMode, OamIterator, OamSlot, OamUnmanaged, ObjectUnmanaged}; -pub use font::{ChangeColour, LetterGroup, LetterGroupIter, ObjectTextRender, TextAlignment}; +pub use font::{ChangeColour, LeftAlignLayout, SimpleTextRender}; use super::DISPLAY_CONTROL; diff --git a/agb/src/display/object/font.rs b/agb/src/display/object/font.rs index 5216c8f58..1d54dbaa5 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,4 +1,4 @@ -use core::num::NonZeroU32; +use core::{fmt::Display, num::NonZeroU32}; use alloc::{borrow::Cow, collections::VecDeque, vec::Vec}; @@ -10,8 +10,16 @@ use super::{PaletteVram, Size, SpriteVram}; mod renderer; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct ChangeColour(u8); +impl Display for ChangeColour { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + use core::fmt::Write; + f.write_char(self.to_char()) + } +} + impl ChangeColour { #[must_use] /// Creates the colour changer. Colour is a palette index and must be in the range 0..16. @@ -42,7 +50,6 @@ fn is_private_use(c: char) -> bool { struct RenderConfig<'string> { string: Cow<'string, str>, font: &'static Font, - explicit_end_on: Option bool>, } struct RenderedSpriteInternal { @@ -72,7 +79,7 @@ impl RenderedSprite<'_> { } } -struct SimpleTextRender<'string> { +pub struct SimpleTextRender<'string> { config: RenderConfig<'string>, render_index: usize, inner_renderer: renderer::WordRender, @@ -196,6 +203,30 @@ impl<'string> SimpleTextRender<'string> { Some((idx, next)) } + pub fn is_done(&self) -> bool { + self.string().len() == self.render_index + } + + pub fn number_of_letter_groups(&self) -> usize { + self.rendered_sprite_window.len() + } + + pub fn pop_words(&mut self, words: usize) -> usize { + assert!(self.word_lengths.len() > words); + + let mut total_letters_to_pop = 0; + for _ in 0..words { + let number_of_letters_to_pop = self.word_lengths.pop_front().unwrap(); + total_letters_to_pop += number_of_letters_to_pop.letter_groups; + } + + for _ in 0..total_letters_to_pop { + self.rendered_sprite_window.pop_front(); + } + + total_letters_to_pop + } + pub fn update(&mut self) { let Some((idx, c)) = self.next_character() else { return; @@ -250,21 +281,18 @@ impl<'string> SimpleTextRender<'string> { font: &'static Font, palette: PaletteVram, sprite_size: Size, + explicit_break_on: Option bool>, ) -> Self { let mut word_lengths = VecDeque::new(); word_lengths.push_back(WordLength::default()); Self { - config: RenderConfig { - string, - font, - explicit_end_on: None, - }, + config: RenderConfig { string, font }, rendered_sprite_window: VecDeque::new(), word_lengths, render_index: 0, inner_renderer: renderer::WordRender::new( Configuration::new(sprite_size, palette), - None, + explicit_break_on, ), } } @@ -274,13 +302,7 @@ impl<'string> SimpleTextRender<'string> { } } -struct LineInformation { - start_x: i32, - words: usize, - space_width: i32, -} - -struct LeftAlignLayout<'string> { +pub struct LeftAlignLayout<'string> { simple: SimpleTextRender<'string>, data: LeftAlignLayoutData, } @@ -288,7 +310,7 @@ struct LeftAlignLayout<'string> { struct LeftAlignLayoutData { width: Option, string_index: usize, - words_per_line: Vec, + words_per_line: VecDeque, current_line_width: i32, } @@ -299,7 +321,7 @@ struct PreparedLetterGroupPosition { fn length_of_next_word(current_index: &mut usize, s: &str, font: &Font) -> Option<(bool, i32)> { let s = &s[*current_index..]; - if s.len() == 0 { + if s.is_empty() { return None; } @@ -308,17 +330,17 @@ fn length_of_next_word(current_index: &mut usize, s: &str, font: &Font) -> Optio for (idx, chr) in s.char_indices() { match chr { '\n' | ' ' => { - if idx != 0 { - return Some((chr == '\n', width)); - } + *current_index += idx + 1; + return Some((chr == '\n', width)); } + _ if is_private_use(chr) => {} letter => { let letter = font.letter(letter); if let Some(previous_character) = previous_character { width += letter.kerning_amount(previous_character); } - width += letter.xmin as i32; + // width += letter.xmin as i32; width += letter.advance_width as i32; } } @@ -328,16 +350,35 @@ fn length_of_next_word(current_index: &mut usize, s: &str, font: &Font) -> Optio Some((false, width)) } -struct LaidOutLetter<'text_render> { +pub struct LaidOutLetter<'text_render> { line: usize, x: i32, sprite: &'text_render SpriteVram, string: &'text_render str, } +impl LaidOutLetter<'_> { + pub fn line(&self) -> usize { + self.line + } + + pub fn x(&self) -> i32 { + self.x + } + + pub fn sprite(&self) -> &SpriteVram { + self.sprite + } + + pub fn string(&self) -> &str { + self.string + } +} + impl<'string> LeftAlignLayout<'string> { - fn new(simple: SimpleTextRender<'string>, width: Option) -> Self { - let words_per_line = alloc::vec![0]; + pub fn new(simple: SimpleTextRender<'string>, width: Option) -> Self { + let mut words_per_line = VecDeque::new(); + words_per_line.push_back(0); Self { simple, @@ -350,7 +391,19 @@ impl<'string> LeftAlignLayout<'string> { } } - fn layout(&mut self) -> impl Iterator { + pub fn pop_line(&mut self) -> usize { + assert!(self.data.words_per_line.len() > 1, "line not complete"); + let words = self.data.words_per_line.pop_front().unwrap(); + self.simple.pop_words(words) + } + + pub fn at_least_n_letter_groups(&mut self, desired: usize) { + while self.simple.number_of_letter_groups() < desired && !self.simple.is_done() { + self.simple.update(); + } + } + + pub fn layout(&mut self) -> impl Iterator { self.data.layout( self.simple.string(), self.simple.config.font, @@ -364,25 +417,29 @@ impl LeftAlignLayoutData { length_of_next_word(&mut self.string_index, string, font) } - fn try_extend_line(&mut self, string: &str, font: &Font) -> bool { + fn try_extend_line(&mut self, string: &str, font: &Font, space_width: i32) -> bool { let (force_new_line, length_of_next_word) = self .length_of_next_word(string, font) - .expect("there should be a word for us to process"); + .expect("Should have more in the line to extend into"); - if force_new_line - || self.current_line_width + length_of_next_word - >= self.width.map_or(i32::MAX, |x| x.get() as i32) + if self.current_line_width + length_of_next_word + > self.width.map_or(i32::MAX, |x| x.get() as i32) { - self.current_line_width = 0; - self.words_per_line.push(0); + self.current_line_width = length_of_next_word + space_width; + self.words_per_line.push_back(1); true } else { let current_line = self .words_per_line - .last_mut() + .back_mut() .expect("should always have a line"); + self.current_line_width += length_of_next_word + space_width; *current_line += 1; + if force_new_line { + self.current_line_width = 0; + self.words_per_line.push_back(0); + } false } } @@ -405,24 +462,22 @@ impl LeftAlignLayoutData { simple.flat_map(move |(pixels, letters)| { let this_line_is_the_last_processed = current_line + 1 == self.words_per_line.len(); - words_in_current_line += 1; - if words_in_current_line > self.words_per_line[current_line] { - if this_line_is_the_last_processed { - if self.try_extend_line(string, font) { - current_line += 1; - current_line_x_offset = 0; - } - } else { - current_line += 1; - current_line_x_offset = 0; - } + + if words_in_current_line > self.words_per_line[current_line] + && (!this_line_is_the_last_processed + || self.try_extend_line(string, font, space_width)) + { + current_line += 1; + current_line_x_offset = 0; + words_in_current_line = 1; } let current_line = current_line; let mut letter_x_offset = current_line_x_offset; current_line_x_offset += pixels.unwrap_or(0); current_line_x_offset += space_width; + letters.map(move |x| { let my_offset = letter_x_offset; letter_x_offset += x.width; diff --git a/agb/src/display/object/font/renderer.rs b/agb/src/display/object/font/renderer.rs index 6342b0a9b..ab641e5d2 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -111,10 +111,6 @@ impl WordRender { None }; - if self.working.x_offset != 0 { - 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 {