diff --git a/agb/examples/object_text_render.rs b/agb/examples/object_text_render.rs index fda39a890..b339abc12 100644 --- a/agb/examples/object_text_render.rs +++ b/agb/examples/object_text_render.rs @@ -3,17 +3,21 @@ use agb::{ display::{ - object::{ChangeColour, ObjectTextRender, PaletteVram, Size, TextAlignment}, + object::{ + ChangeColour, LeftAlignLayout, ObjectUnmanaged, PaletteVram, SimpleTextRender, Size, + }, palette16::Palette16, Font, HEIGHT, WIDTH, }, include_font, input::Button, }; +use agb_fixnum::Vector2D; -extern crate alloc; +use alloc::borrow::Cow; +use core::num::NonZeroU32; -use core::fmt::Write; +extern crate alloc; static FONT: Font = include_font!("examples/font/ark-pixel-10px-proportional-ja.ttf", 10); @@ -36,17 +40,22 @@ 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 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!( @@ -57,42 +66,64 @@ 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, 40), TextAlignment::Justify, 2); - let end = timer.value(); - - agb::println!( - "Layout took {} cycles", - 256 * (end.wrapping_sub(start) as u32) - ); - - let mut line_done = false; 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); + wr.at_least_n_letter_groups(groups_to_show + 2); let start = timer.value(); - 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.update((0, HEIGHT - 40)); - let end = timer.value(); - frame += 1; + 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) { + 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); + } + + 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; + } + false + } else { + true + } + } else { + false + } + }; + let end = timer.value(); agb::println!( - "Took {} cycles, line done {}", - 256 * (end.wrapping_sub(start) as u32), - line_done + "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/examples/rotating_text.rs b/agb/examples/rotating_text.rs new file mode 100644 index 000000000..0389e0630 --- /dev/null +++ b/agb/examples/rotating_text.rs @@ -0,0 +1,107 @@ +#![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; + +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, + width: i32, + paragraph_spacing: i32, + arguments: core::fmt::Arguments, +) -> Vec<(SpriteVram, Vector2D)> { + let text = alloc::format!("{}\n", arguments); + let mut wr = ObjectTextRender::new(text, font, sprite_size, palette, None); + + wr.layout(width, 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, + 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); + } + } +} 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.rs b/agb/src/display/object.rs index 85df2a554..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, 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 47ad0adfd..1d54dbaa5 100644 --- a/agb/src/display/object/font.rs +++ b/agb/src/display/object/font.rs @@ -1,511 +1,497 @@ -use core::fmt::{Display, Write}; +use core::{fmt::Display, num::NonZeroU32}; -use agb_fixnum::{Num, Vector2D}; -use alloc::{collections::VecDeque, 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, +#[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 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 } } + + fn to_char(self) -> char { + char::from_u32(self.0 as u32 + 0xE000).unwrap() + } } -struct BufferedRender<'font> { - char_render: WordRender, - preprocessor: Preprocessed, - buffered_chars: VecDeque, - letters: Letters, - font: &'font Font, +fn is_private_use(c: char) -> bool { + ('\u{E000}'..'\u{F8FF}').contains(&c) } -#[derive(Debug, Default)] -struct Letters { - letters: VecDeque, - number_of_groups: usize, +struct RenderConfig<'string> { + string: Cow<'string, str>, + font: &'static Font, } -#[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 { - 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(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { - let config = Configuration::new(sprite_size, palette); - BufferedRender { - char_render: WordRender::new(config), - preprocessor: Preprocessed::new(), - buffered_chars: VecDeque::new(), - letters: Default::default(), - font, - } + fn width(&self) -> i32 { + self.width + } + + fn sprite(&self) -> &SpriteVram { + &self.sprite } } -fn is_private_use(c: char) -> bool { - ('\u{E000}'..'\u{F8FF}').contains(&c) +pub 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 input_character(&mut self, character: char) { - if !is_private_use(character) { - self.preprocessor - .add_character(self.font, character, self.char_render.sprite_width()); +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, + } + } + + 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 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)) + } + + 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(); } - self.buffered_chars.push_back(character); + + total_letters_to_pop } - fn process(&mut self) { - let Some(c) = self.buffered_chars.pop_front() else { + pub fn update(&mut self) { + 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); - 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(group) = self.char_render.render_char(self.font, letter) { - self.letters.letters.push_back(group); - self.letters.number_of_groups += 1; + if let Some((start_index, group, width)) = + self.inner_renderer + .render_char(self.config.font, letter, idx) + { + 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(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { + pub fn new( + string: Cow<'string, str>, + 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 { - buffer: BufferedRender::new(font, sprite_size, palette), - 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(), - }, + config: RenderConfig { string, font }, + rendered_sprite_window: VecDeque::new(), + word_lengths, + render_index: 0, + inner_renderer: renderer::WordRender::new( + Configuration::new(sprite_size, palette), + explicit_break_on, + ), } } + + fn string(&self) -> &str { + &self.config.string + } } -impl Write for ObjectTextRender<'_> { - fn write_str(&mut self, s: &str) -> core::fmt::Result { - for c in s.chars() { - self.buffer.input_character(c); - } +pub struct LeftAlignLayout<'string> { + simple: SimpleTextRender<'string>, + data: LeftAlignLayoutData, +} - Ok(()) - } +struct LeftAlignLayoutData { + width: Option, + string_index: usize, + words_per_line: VecDeque, + current_line_width: i32, } -impl ObjectTextRender<'_> { - /// 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); - } +struct PreparedLetterGroupPosition { + x: i32, + line: i32, +} + +fn length_of_next_word(current_index: &mut usize, s: &str, font: &Font) -> Option<(bool, i32)> { + let s = &s[*current_index..]; + if s.is_empty() { + return None; } - /// Force a relayout, must be called after writing. - pub fn layout( - &mut self, - area: impl Into>, - alignment: TextAlignment, - paragraph_spacing: i32, - ) { - self.layout.create_positions( - self.buffer.font, - &self.buffer.preprocessor, - &LayoutSettings { - area: area.into(), - alignment, - paragraph_spacing, - }, - ); - } - - /// 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.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; + let mut width = 0; + let mut previous_character = None; + for (idx, chr) in s.char_indices() { + match chr { + '\n' | ' ' => { + *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); } - return true; + + // width += letter.xmin as i32; + width += letter.advance_width as i32; } } - false + previous_character = Some(chr); } + *current_index += s.len(); + Some((false, width)) +} - /// 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>) { - if !self.buffer.buffered_chars.is_empty() - && self.buffer.letters.letters.len() <= self.number_of_objects + 5 - { - self.buffer.process(); - } +pub struct LaidOutLetter<'text_render> { + line: usize, + x: i32, + sprite: &'text_render SpriteVram, + string: &'text_render str, +} - self.layout.update_objects_to_display_at_position( - position.into(), - self.buffer.letters.letters.iter(), - self.number_of_objects, - ); +impl LaidOutLetter<'_> { + pub fn line(&self) -> usize { + self.line } - /// 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 + pub fn x(&self) -> i32 { + self.x } - 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 sprite(&self) -> &SpriteVram { + self.sprite } - /// 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; + pub fn string(&self) -> &str { + self.string + } +} - // find current line +impl<'string> LeftAlignLayout<'string> { + pub fn new(simple: SimpleTextRender<'string>, width: Option) -> Self { + let mut words_per_line = VecDeque::new(); + words_per_line.push_back(0); - 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; - } + Self { + simple, + data: LeftAlignLayoutData { + string_index: 0, + words_per_line, + current_line_width: 0, + width, + }, } + } - false + 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) } - 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(); + 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(); } } -} -struct LayoutCache { - positions: VecDeque>, - line_capacity: VecDeque, - objects: Vec, - objects_are_at_origin: Vector2D, - area: Vector2D, + pub fn layout(&mut self) -> impl Iterator { + self.data.layout( + self.simple.string(), + self.simple.config.font, + self.simple.words(), + ) + } } -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))); - } +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) } - 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(); + 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("Should have more in the line to extend into"); - 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); + if self.current_line_width + length_of_next_word + > self.width.map_or(i32::MAX, |x| x.get() as i32) + { + 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 + .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 + } + } - head_position.y += line_height; - head_position.x = 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] + && (!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; + } - ( - 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()) - } - PreprocessedElement::WhiteSpace(space) => { - match space { - WhiteSpace::NewLine => { - head_position.y += settings.paragraph_spacing; - } - WhiteSpace::Space => head_position.x += 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 { - area: Vector2D, - 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 e07356a2b..000000000 --- a/agb/src/display/object/font/preprocess.rs +++ /dev/null @@ -1,248 +0,0 @@ -use alloc::collections::VecDeque; - -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: VecDeque, - 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 VecDeque, - ) { - match character { - space @ (' ' | '\n') => { - 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)).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 { - widths.push_back( - 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 VecDeque, - current_start_idx: usize, -} - -pub(crate) struct Line { - width: i32, - number_of_text_elements: usize, - number_of_spaces: usize, - number_of_letter_groups: usize, -} - -impl Line { - #[inline(always)] - pub(crate) fn width(&self) -> i32 { - self.width - } - #[inline(always)] - pub(crate) fn number_of_text_elements(&self) -> usize { - self.number_of_text_elements - } - #[inline(always)] - pub(crate) fn number_of_spaces(&self) -> usize { - self.number_of_spaces - } - #[inline(always)] - pub(crate) fn number_of_letter_groups(&self) -> usize { - self.number_of_letter_groups - } -} - -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; - - 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; - 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, - }) - } -} - -impl Preprocessed { - pub(crate) fn new() -> Self { - Default::default() - } - - pub(crate) fn add_character(&mut self, font: &Font, c: char, sprite_width: i32) { - self.preprocessor - .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, - 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.range(idx..(idx + length)).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..ab641e5d2 100644 --- a/agb/src/display/object/font/renderer.rs +++ b/agb/src/display/object/font/renderer.rs @@ -43,27 +43,30 @@ pub(crate) struct WordRender { working: WorkingLetter, config: Configuration, colour: usize, + start_index_of_letter: usize, previous_character: Option, + explicit_break_on: Option bool>, } 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 { + 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, } } #[must_use] - pub(crate) fn finalise_letter(&mut self) -> Option { + pub(crate) fn finalise_letter( + &mut self, + index_of_character: usize, + ) -> Option<(usize, SpriteVram, i32)> { if self.working.x_offset == 0 { return None; } @@ -71,13 +74,21 @@ 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; + let width = self.working.x_offset; self.working.reset(); + self.start_index_of_letter = index_of_character; - Some(sprite) + Some((start_index, sprite, width)) } #[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, i32)> { if let Some(next_colour) = ChangeColour::try_from_char(c) { self.colour = next_colour.0 as usize; return None; @@ -93,16 +104,13 @@ 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() + self.finalise_letter(index_of_character) } else { 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 { 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();