the proper nice okay working text rendering

This commit is contained in:
Corwin 2023-07-01 23:36:58 +01:00
parent ec3003c81d
commit 5f12040752
No known key found for this signature in database
5 changed files with 362 additions and 408 deletions

View file

@ -25,9 +25,9 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
let line_metrics = font.horizontal_line_metrics(pixels_per_em).unwrap(); let line_metrics = font.horizontal_line_metrics(pixels_per_em).unwrap();
let line_height = line_metrics.new_line_size as i32; let line_height = line_metrics.new_line_size as i32;
let ascent = line_metrics.ascent as i32; let mut ascent = line_metrics.ascent as i32;
let font = (0..128) let letters: Vec<_> = (0..128)
.map(|i| font.rasterize(char::from_u32(i).unwrap(), pixels_per_em)) .map(|i| font.rasterize(char::from_u32(i).unwrap(), pixels_per_em))
.map(|(metrics, bitmap)| { .map(|(metrics, bitmap)| {
let width = metrics.width; let width = metrics.width;
@ -56,7 +56,19 @@ pub fn load_font(font_data: &[u8], pixels_per_em: f32) -> TokenStream {
advance_width: metrics.advance_width, advance_width: metrics.advance_width,
} }
}) })
.map(|letter_data| { .collect();
let maximum_above_line = letters
.iter()
.map(|x| (x.height as i32 + x.ymin))
.max()
.unwrap();
if (ascent - maximum_above_line) < 0 {
ascent = maximum_above_line;
}
let font = letters.iter().map(|letter_data| {
let data_raw = ByteString(&letter_data.rendered); let data_raw = ByteString(&letter_data.rendered);
let height = letter_data.height as u8; let height = letter_data.height as u8;
let width = letter_data.width as u8; let width = letter_data.width as u8;

View file

@ -8,19 +8,17 @@ use agb::{
PaletteVram, Size, PaletteVram, Size,
}, },
palette16::Palette16, palette16::Palette16,
Font, WIDTH, Font, HEIGHT, WIDTH,
}, },
include_font, include_font,
input::Button, input::Button,
}; };
use agb_fixnum::Rect;
extern crate alloc; extern crate alloc;
use alloc::vec::Vec;
use core::fmt::Write; use core::fmt::Write;
const FONT: Font = include_font!("examples/font/pixelated.ttf", 8); const FONT: Font = include_font!("examples/font/yoster.ttf", 12);
#[agb::entry] #[agb::entry]
fn entry(gba: agb::Gba) -> ! { fn entry(gba: agb::Gba) -> ! {
main(gba); main(gba);
@ -35,12 +33,11 @@ fn main(mut gba: agb::Gba) -> ! {
let palette = Palette16::new(palette); let palette = Palette16::new(palette);
let palette = PaletteVram::new(&palette).unwrap(); let palette = PaletteVram::new(&palette).unwrap();
let mut wr = ObjectTextRender::new(&FONT, Size::S16x8, palette); let mut wr = ObjectTextRender::new(&FONT, Size::S16x16, palette);
let _ = writeln!( let _ = writeln!(
wr, wr,
"{}", "Woah! Hey there! I have a bunch of text I want to show you. However, you will find that the amount of text I can display is limited. Who'd have thought! Good thing that my text system supports scrolling! It only took around 20 jank versions to get here!"
"counts for three shoot dice for damage calculation\nmalfunctions all dice after use"
.to_ascii_uppercase()
); );
let vblank = agb::interrupt::VBlank::get(); let vblank = agb::interrupt::VBlank::get();
@ -52,25 +49,32 @@ fn main(mut gba: agb::Gba) -> ! {
timer.set_enabled(true); timer.set_enabled(true);
timer.set_divider(agb::timer::Divider::Divider256); timer.set_divider(agb::timer::Divider::Divider256);
wr.set_alignment(TextAlignment::Left); wr.layout((WIDTH, 40).into(), TextAlignment::Left, 2);
wr.set_size((WIDTH / 3, 20).into());
wr.set_paragraph_spacing(2); let mut line_done = false;
wr.layout(); let mut frame = 0;
loop { loop {
vblank.wait_for_vblank(); vblank.wait_for_vblank();
input.update(); input.update();
let oam = &mut unmanaged.iter(); let oam = &mut unmanaged.iter();
wr.commit(oam, (WIDTH / 3, 0).into()); wr.commit(oam);
let start = timer.value(); let start = timer.value();
let line_done = !wr.next_letter_group(); if frame % 4 == 0 {
if line_done && input.is_just_pressed(Button::A) { line_done = !wr.next_letter_group();
}
if line_done
&& input.is_just_pressed(Button::A)
{
line_done = false;
wr.pop_line(); wr.pop_line();
} }
wr.layout(); wr.update((0, HEIGHT - 40).into());
let end = timer.value(); let end = timer.value();
frame += 1;
agb::println!( agb::println!(
"Took {} cycles, line done {}", "Took {} cycles, line done {}",
256 * (end.wrapping_sub(start) as u32), 256 * (end.wrapping_sub(start) as u32),

View file

@ -1,9 +1,9 @@
use core::fmt::Write; use core::fmt::Write;
use agb_fixnum::{Rect, Vector2D}; use agb_fixnum::Vector2D;
use alloc::{collections::VecDeque, vec::Vec}; use alloc::{collections::VecDeque, vec::Vec};
use crate::display::{object::font::preprocess::Word, Font}; use crate::display::Font;
use self::{ use self::{
preprocess::{Line, Preprocessed, PreprocessedElement}, preprocess::{Line, Preprocessed, PreprocessedElement},
@ -32,14 +32,6 @@ impl WhiteSpace {
} }
} }
#[derive(Debug)]
pub(crate) struct LetterGroup {
sprite: SpriteVram,
// the width of the letter group
width: u16,
left: i16,
}
pub struct BufferedRender<'font> { pub struct BufferedRender<'font> {
char_render: WordRender, char_render: WordRender,
preprocessor: Preprocessed, preprocessor: Preprocessed,
@ -50,7 +42,7 @@ pub struct BufferedRender<'font> {
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Letters { struct Letters {
letters: VecDeque<LetterGroup>, letters: VecDeque<SpriteVram>,
number_of_groups: usize, number_of_groups: usize,
} }
@ -69,12 +61,7 @@ struct TextAlignmentSettings {
} }
impl TextAlignment { impl TextAlignment {
fn settings( fn settings(self, line: &Line, minimum_space_width: i32, width: i32) -> TextAlignmentSettings {
self,
line: &Line,
minimum_space_width: i32,
size: Vector2D<i32>,
) -> TextAlignmentSettings {
match self { match self {
TextAlignment::Left => TextAlignmentSettings { TextAlignment::Left => TextAlignmentSettings {
space_width: minimum_space_width, space_width: minimum_space_width,
@ -82,11 +69,11 @@ impl TextAlignment {
}, },
TextAlignment::Right => TextAlignmentSettings { TextAlignment::Right => TextAlignmentSettings {
space_width: minimum_space_width, space_width: minimum_space_width,
start_x: size.x - line.width(), start_x: width - line.width(),
}, },
TextAlignment::Center => TextAlignmentSettings { TextAlignment::Center => TextAlignmentSettings {
space_width: minimum_space_width, space_width: minimum_space_width,
start_x: (size.x - line.width()) / 2, start_x: (width - line.width()) / 2,
}, },
} }
} }
@ -143,7 +130,7 @@ impl BufferedRender<'_> {
pub struct ObjectTextRender<'font> { pub struct ObjectTextRender<'font> {
buffer: BufferedRender<'font>, buffer: BufferedRender<'font>,
layout: LayoutCache, layout: LayoutCache,
settings: LayoutSettings, number_of_objects: usize,
} }
impl<'font> ObjectTextRender<'font> { impl<'font> ObjectTextRender<'font> {
@ -151,8 +138,14 @@ impl<'font> ObjectTextRender<'font> {
pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self { pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self {
Self { Self {
buffer: BufferedRender::new(font, sprite_size, palette), buffer: BufferedRender::new(font, sprite_size, palette),
layout: LayoutCache::new(), number_of_objects: 0,
settings: Default::default(), layout: LayoutCache {
positions: VecDeque::new(),
line_capacity: VecDeque::new(),
objects: Vec::new(),
objects_are_at_origin: (0, 0).into(),
area: (0, 0).into(),
},
} }
} }
} }
@ -168,267 +161,222 @@ impl Write for ObjectTextRender<'_> {
} }
impl ObjectTextRender<'_> { impl ObjectTextRender<'_> {
/// Remove a line from the render and shift everything up one line.
/// A full complete line must be rendered for this to do anything, incomplete lines won't be popped. Returns whether a line could be popped.
pub fn pop_line(&mut self) -> bool {
let width = self.layout.settings.area.x;
let space = self.buffer.font.letter(' ').advance_width as i32;
let Some(line) = self.buffer.preprocessor.lines(width, space).next() else {
return false;
};
let number_of_elements = line.number_of_letter_groups();
if self.layout.state.line_depth >= 1 && self.layout.objects.len() >= number_of_elements {
for _ in 0..number_of_elements {
// self.buffer.letters.letters.pop_front();
self.layout.objects.pop_front();
}
self.buffer.preprocessor.pop(&line);
self.layout.state.head_offset.y -= self.buffer.font.line_height();
for obj in self.layout.objects.iter_mut() {
obj.offset.y -= self.buffer.font.line_height() as i16;
let object_offset = obj.offset.change_base();
obj.object
.set_position(self.layout.position + object_offset);
}
self.layout.state.line_depth -= 1;
true
} else {
false
}
}
/// On next update, the next unit of letters will be rendered. Returns whether the next element could be added.
/// Can only be called once per layout.
pub fn next_letter_group(&mut self) -> bool {
self.layout.next_letter_group(&self.buffer)
}
/// Commits work already done to screen. You can commit to multiple places in the same frame. /// Commits work already done to screen. You can commit to multiple places in the same frame.
pub fn commit(&mut self, oam: &mut OamIterator, position: Vector2D<i32>) { pub fn commit(&mut self, oam: &mut OamIterator) {
self.layout.commit(oam, position); for (object, slot) in self.layout.objects.iter().zip(oam) {
} slot.set(object);
/// Updates the internal state based on the chosen render settings. Best
/// effort is made to reuse previous layouts, but a full rerender may be
/// required if certain settings are changed.
pub fn layout(&mut self) {
self.layout.update(
&mut self.buffer,
self.settings.area,
self.settings.alignment,
self.settings.paragraph_spacing,
);
}
/// Causes a change to the area that text is rendered. This will cause a relayout.
pub fn set_size(&mut self, size: Vector2D<i32>) {
self.settings.area = size;
}
/// Causes a change to the text alignment. This will cause a relayout.
pub fn set_alignment(&mut self, alignment: TextAlignment) {
self.settings.alignment = alignment;
}
/// Sets the paragraph spacing. This will cause a relayout.
pub fn set_paragraph_spacing(&mut self, paragraph_spacing: i32) {
self.settings.paragraph_spacing = paragraph_spacing;
} }
} }
struct LayoutObject { /// Force a relayout, must be called after writing.
object: ObjectUnmanaged, pub fn layout(
offset: Vector2D<i16>,
}
struct LayoutCache {
objects: VecDeque<LayoutObject>,
state: LayoutCacheState,
settings: LayoutSettings,
desired_number_of_groups: usize,
position: Vector2D<i32>,
}
impl LayoutCache {
fn next_letter_group(&mut self, buffer: &BufferedRender) -> bool {
let width = self.settings.area.x;
let space = buffer.font.letter(' ').advance_width as i32;
let line_height = buffer.font.line_height();
if self.state.head_offset.y + line_height > self.settings.area.y {
return false;
}
if let Some((_line, mut line_elements)) = buffer
.preprocessor
.lines_element(width, space)
.nth(self.state.line_depth)
{
match line_elements.nth(self.state.line_element_depth) {
Some(PreprocessedElement::Word(_)) => {
self.desired_number_of_groups += 1;
}
Some(PreprocessedElement::WhiteSpace(WhiteSpace::Space)) => {
self.desired_number_of_groups += 1;
}
Some(PreprocessedElement::WhiteSpace(WhiteSpace::NewLine)) => {
self.desired_number_of_groups += 1;
}
None => {
if self.state.head_offset.y + line_height * 2 > self.settings.area.y {
return false;
}
self.desired_number_of_groups += 1;
}
}
}
true
}
fn update_cache(&mut self, render: &BufferedRender) {
if self.state.rendered_groups >= self.desired_number_of_groups {
return;
}
let minimum_space_width = render.font.letter(' ').advance_width as i32;
let lines = render
.preprocessor
.lines_element(self.settings.area.x, minimum_space_width);
'outer: for (line, line_elements) in lines.skip(self.state.line_depth) {
let settings =
self.settings
.alignment
.settings(&line, minimum_space_width, self.settings.area);
if self.state.line_element_depth == 0 {
self.state.head_offset.x += settings.start_x;
}
for element in line_elements.skip(self.state.line_element_depth) {
match element {
PreprocessedElement::Word(word) => {
for letter in (word.sprite_index()
..(word.sprite_index() + word.number_of_sprites()))
.skip(self.state.word_depth)
.map(|x| &render.letters.letters[x])
{
let mut object = ObjectUnmanaged::new(letter.sprite.clone());
self.state.head_offset.x += letter.left as i32;
object
.set_position(self.state.head_offset + self.position)
.show();
let layout_object = LayoutObject {
object,
offset: (
self.state.head_offset.x as i16,
self.state.head_offset.y as i16,
)
.into(),
};
self.state.head_offset.x += letter.width as i32;
self.objects.push_back(layout_object);
self.state.rendered_groups += 1;
self.state.word_depth += 1;
if self.state.rendered_groups >= self.desired_number_of_groups {
break 'outer;
}
}
self.state.word_depth = 0;
self.state.line_element_depth += 1;
}
PreprocessedElement::WhiteSpace(space_type) => {
if space_type == WhiteSpace::NewLine {
self.state.head_offset.y += self.settings.paragraph_spacing;
}
self.state.head_offset.x += settings.space_width;
self.state.rendered_groups += 1;
self.state.line_element_depth += 1;
if self.state.rendered_groups >= self.desired_number_of_groups {
break 'outer;
}
}
}
}
self.state.head_offset.y += render.font.line_height();
self.state.head_offset.x = 0;
self.state.line_element_depth = 0;
self.state.line_depth += 1;
}
}
fn update(
&mut self, &mut self,
r: &mut BufferedRender<'_>,
area: Vector2D<i32>, area: Vector2D<i32>,
alignment: TextAlignment, alignment: TextAlignment,
paragraph_spacing: i32, paragraph_spacing: i32,
) { ) {
r.process(); self.layout.create_positions(
self.buffer.font,
while !r.buffered_chars.is_empty() &self.buffer.preprocessor,
&& r.letters.number_of_groups <= self.desired_number_of_groups &LayoutSettings {
{
r.process();
}
let settings = LayoutSettings {
area, area,
alignment, alignment,
paragraph_spacing, paragraph_spacing,
};
if settings != self.settings {
self.reset(settings);
}
self.update_cache(r);
}
fn commit(&mut self, oam: &mut OamIterator, position: Vector2D<i32>) {
if self.position != position {
for (object, slot) in self.objects.iter_mut().zip(oam) {
let object_offset = object.offset.change_base();
object.object.set_position(position + object_offset);
slot.set(&object.object);
}
self.position = position;
} else {
for (object, slot) in self.objects.iter().zip(oam) {
slot.set(&object.object);
}
}
}
#[must_use]
fn new() -> Self {
Self {
objects: VecDeque::new(),
state: Default::default(),
settings: LayoutSettings {
area: (0, 0).into(),
alignment: TextAlignment::Right,
paragraph_spacing: -100,
}, },
desired_number_of_groups: 0, );
position: (0, 0).into(), }
/// Removes one complete line.
pub fn pop_line(&mut self) -> bool {
let width = self.layout.area.x;
let space = self.buffer.font.letter(' ').advance_width as i32;
let line_height = self.buffer.font.line_height();
if let Some(line) = self.buffer.preprocessor.lines(width, space).next() {
// there is a line
if self.layout.objects.len() >= line.number_of_letter_groups() {
// we have enough rendered letter groups to count
self.number_of_objects -= line.number_of_letter_groups();
for _ in 0..line.number_of_letter_groups() {
self.buffer.letters.letters.pop_front();
self.layout.positions.pop_front();
}
self.layout.line_capacity.pop_front();
self.layout.objects.clear();
self.buffer.preprocessor.pop(&line);
for position in self.layout.positions.iter_mut() {
position.y -= line_height as i16;
}
return true;
}
}
false
}
pub fn update(&mut self, position: Vector2D<i32>) {
if !self.buffer.buffered_chars.is_empty()
&& self.buffer.letters.letters.len() <= self.number_of_objects + 5
{
self.buffer.process();
}
self.layout.update_objects_to_display_at_position(
position,
self.buffer.letters.letters.iter(),
self.number_of_objects,
);
}
pub fn next_letter_group(&mut self) -> bool {
if !self.can_render_another_element() {
return false;
}
self.number_of_objects += 1;
self.at_least_n_letter_groups(self.number_of_objects);
true
}
fn can_render_another_element(&self) -> bool {
let max_number_of_lines = (self.layout.area.y / self.buffer.font.line_height()) as usize;
let max_number_of_objects = self
.layout
.line_capacity
.iter()
.take(max_number_of_lines)
.sum::<usize>();
max_number_of_objects > self.number_of_objects
}
pub fn next_line(&mut self) -> bool {
let max_number_of_lines = (self.layout.area.y / self.buffer.font.line_height()) as usize;
// find current line
for (start, end) in self
.layout
.line_capacity
.iter()
.scan(0, |count, line_size| {
let start = *count;
*count += line_size;
Some((start, *count))
})
.take(max_number_of_lines)
{
if self.number_of_objects >= start && self.number_of_objects < end {
self.number_of_objects = end;
self.at_least_n_letter_groups(end);
return true;
} }
} }
fn reset(&mut self, settings: LayoutSettings) { false
}
fn at_least_n_letter_groups(&mut self, n: usize) {
while !self.buffer.buffered_chars.is_empty() && self.buffer.letters.letters.len() <= n {
self.buffer.process();
}
}
}
struct LayoutCache {
positions: VecDeque<Vector2D<i16>>,
line_capacity: VecDeque<usize>,
objects: Vec<ObjectUnmanaged>,
objects_are_at_origin: Vector2D<i32>,
area: Vector2D<i32>,
}
impl LayoutCache {
fn update_objects_to_display_at_position<'a>(
&mut self,
position: Vector2D<i32>,
letters: impl Iterator<Item = &'a SpriteVram>,
number_of_objects: usize,
) {
let already_done = if position == self.objects_are_at_origin {
self.objects.len()
} else {
self.objects.clear(); self.objects.clear();
self.state = LayoutCacheState { 0
head_offset: (0, 0).into(),
word_depth: 0,
rendered_groups: 0,
line_depth: 0,
line_element_depth: 0,
}; };
self.settings = settings; self.objects.extend(
self.positions
.iter()
.zip(letters)
.take(number_of_objects)
.skip(already_done)
.map(|(offset, letter)| {
let position = offset.change_base() + position;
let mut object = ObjectUnmanaged::new(letter.clone());
object.show().set_position(position);
object
}),
);
self.objects.truncate(number_of_objects);
self.objects_are_at_origin = position;
}
fn create_positions(
&mut self,
font: &Font,
preprocessed: &Preprocessed,
settings: &LayoutSettings,
) {
self.area = settings.area;
self.line_capacity.clear();
self.positions.clear();
for (line, line_positions) in Self::create_layout(font, preprocessed, settings) {
self.line_capacity.push_back(line.number_of_letter_groups());
self.positions
.extend(line_positions.map(|x| Vector2D::new(x.x as i16, x.y as i16)));
}
}
fn create_layout<'a>(
font: &Font,
preprocessed: &'a Preprocessed,
settings: &'a LayoutSettings,
) -> impl Iterator<Item = (Line, impl Iterator<Item = Vector2D<i32>> + 'a)> + 'a {
let minimum_space_width = font.letter(' ').advance_width as i32;
let width = settings.area.x;
let line_height = font.line_height();
let mut head_position: Vector2D<i32> = (0, -line_height).into();
preprocessed
.lines_element(width, minimum_space_width)
.map(move |(line, line_elements)| {
let line_settings = settings
.alignment
.settings(&line, minimum_space_width, width);
head_position.y += line_height;
head_position.x = line_settings.start_x;
(
line,
line_elements.filter_map(move |element| match element.decode() {
PreprocessedElement::LetterGroup { width } => {
let this_position = head_position;
head_position.x += width as i32;
Some(this_position)
}
PreprocessedElement::WhiteSpace(space) => {
match space {
WhiteSpace::NewLine => {
head_position.y += settings.paragraph_spacing;
}
WhiteSpace::Space => head_position.x += line_settings.space_width,
}
None
}
}),
)
})
} }
} }
@ -438,12 +386,3 @@ struct LayoutSettings {
alignment: TextAlignment, alignment: TextAlignment,
paragraph_spacing: i32, paragraph_spacing: i32,
} }
#[derive(Default)]
struct LayoutCacheState {
head_offset: Vector2D<i32>,
word_depth: usize,
rendered_groups: usize,
line_depth: usize,
line_element_depth: usize,
}

View file

@ -6,52 +6,47 @@ use crate::display::Font;
use super::WhiteSpace; use super::WhiteSpace;
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct PreprocessedElementEncoded(u8);
impl PreprocessedElementEncoded {
pub(crate) fn decode(self) -> PreprocessedElement {
match self.0 {
255 => PreprocessedElement::WhiteSpace(WhiteSpace::NewLine),
254 => PreprocessedElement::WhiteSpace(WhiteSpace::Space),
width => PreprocessedElement::LetterGroup { width },
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum PreprocessedElement { pub(crate) enum PreprocessedElement {
Word(Word), LetterGroup { width: u8 },
WhiteSpace(WhiteSpace), WhiteSpace(WhiteSpace),
} }
#[test_case] impl PreprocessedElement {
fn check_size_of_preprocessed_element_is_correct(_: &mut crate::Gba) { fn encode(self) -> PreprocessedElementEncoded {
assert_eq!( PreprocessedElementEncoded(match self {
core::mem::size_of::<PreprocessedElement>(), PreprocessedElement::LetterGroup { width } => width,
core::mem::size_of::<Word>() PreprocessedElement::WhiteSpace(space) => match space {
); WhiteSpace::NewLine => 255,
} WhiteSpace::Space => 254,
},
#[derive(Debug, PartialEq, Eq, Clone, Copy)] })
#[repr(align(4))]
pub(crate) struct Word {
pixels: u8,
number_of_sprites: NonZeroU8,
index: u16,
}
impl Word {
pub fn pixels(self) -> i32 {
self.pixels.into()
}
pub fn number_of_sprites(self) -> usize {
self.number_of_sprites.get().into()
}
pub fn sprite_index(self) -> usize {
self.index.into()
} }
} }
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub(crate) struct Preprocessed { pub(crate) struct Preprocessed {
widths: VecDeque<PreprocessedElement>, widths: VecDeque<PreprocessedElementEncoded>,
preprocessor: Preprocessor, preprocessor: Preprocessor,
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct Preprocessor { struct Preprocessor {
current_word_width: i32,
number_of_sprites: usize,
width_in_sprite: i32, width_in_sprite: i32,
total_number_of_sprites: usize,
} }
impl Preprocessor { impl Preprocessor {
@ -60,37 +55,32 @@ impl Preprocessor {
font: &Font, font: &Font,
character: char, character: char,
sprite_width: i32, sprite_width: i32,
widths: &mut VecDeque<PreprocessedElement>, widths: &mut VecDeque<PreprocessedElementEncoded>,
) { ) {
match character { match character {
space @ (' ' | '\n') => { space @ (' ' | '\n') => {
if self.current_word_width != 0 { if self.width_in_sprite != 0 {
self.number_of_sprites += 1; widths.push_back(
self.total_number_of_sprites += 1; PreprocessedElement::LetterGroup {
widths.push_back(PreprocessedElement::Word(Word { width: self.width_in_sprite as u8,
pixels: self.current_word_width.try_into().expect("word too wide"), }
number_of_sprites: NonZeroU8::new( .encode(),
self.number_of_sprites.try_into().expect("word too wide"), );
)
.unwrap(),
index: (self.total_number_of_sprites - self.number_of_sprites)
.try_into()
.expect("out of range"),
}));
self.current_word_width = 0;
self.number_of_sprites = 0;
self.width_in_sprite = 0; self.width_in_sprite = 0;
} }
widths.push_back(PreprocessedElement::WhiteSpace(WhiteSpace::from_char( widths.push_back(
space, PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)).encode(),
))); );
} }
letter => { letter => {
let letter = font.letter(letter); let letter = font.letter(letter);
self.current_word_width += letter.advance_width as i32 + letter.xmin as i32;
if self.width_in_sprite + letter.width as i32 > sprite_width { if self.width_in_sprite + letter.width as i32 > sprite_width {
self.number_of_sprites += 1; widths.push_back(
self.total_number_of_sprites += 1; PreprocessedElement::LetterGroup {
width: self.width_in_sprite as u8,
}
.encode(),
);
self.width_in_sprite = 0; self.width_in_sprite = 0;
} }
if self.width_in_sprite != 0 { if self.width_in_sprite != 0 {
@ -105,7 +95,7 @@ impl Preprocessor {
pub(crate) struct Lines<'preprocess> { pub(crate) struct Lines<'preprocess> {
minimum_space_width: i32, minimum_space_width: i32,
layout_width: i32, layout_width: i32,
data: &'preprocess VecDeque<PreprocessedElement>, data: &'preprocess VecDeque<PreprocessedElementEncoded>,
current_start_idx: usize, current_start_idx: usize,
} }
@ -150,35 +140,58 @@ impl<'pre> Iterator for Lines<'pre> {
} }
let mut line_idx_length = 0; let mut line_idx_length = 0;
let mut current_line_width = 0; let mut current_line_width_pixels = 0;
let mut additional_space_count = 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_spaces = 0;
let mut number_of_words = 0; let mut number_of_words = 0;
let mut number_of_letter_groups = 0; let mut number_of_letter_groups = 0;
while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) { while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) {
match next { match next.decode() {
PreprocessedElement::Word(word) => { PreprocessedElement::LetterGroup { width } => {
let additional_space_width = if start_of_current_word == usize::MAX {
additional_space_count as i32 * self.minimum_space_width; start_of_current_word = line_idx_length;
let width = word.pixels(); }
if width + current_line_width + additional_space_width > self.layout_width { 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; break;
} }
number_of_letter_groups += word.number_of_sprites.get() as usize;
number_of_words += 1;
current_line_width += width + additional_space_width;
number_of_spaces += additional_space_count;
} }
PreprocessedElement::WhiteSpace(space) => match space { PreprocessedElement::WhiteSpace(space) => {
if start_of_current_word != usize::MAX {
// flush word
current_line_width_pixels += length_of_current_word_pixels
+ spaces_after_last_word_count as i32 * self.minimum_space_width;
number_of_spaces += spaces_after_last_word_count;
number_of_words += 1;
number_of_letter_groups += length_of_current_word;
// reset parser
length_of_current_word_pixels = 0;
length_of_current_word = 0;
start_of_current_word = usize::MAX;
spaces_after_last_word_count = 0;
}
match space {
WhiteSpace::NewLine => { WhiteSpace::NewLine => {
line_idx_length += 1; line_idx_length += 1;
break; break;
} }
WhiteSpace::Space => { WhiteSpace::Space => {
additional_space_count += 1; spaces_after_last_word_count += 1;
}
}
} }
},
}; };
line_idx_length += 1; line_idx_length += 1;
@ -187,7 +200,7 @@ impl<'pre> Iterator for Lines<'pre> {
self.current_start_idx += line_idx_length; self.current_start_idx += line_idx_length;
Some(Line { Some(Line {
width: current_line_width, width: current_line_width_pixels,
number_of_text_elements: line_idx_length, number_of_text_elements: line_idx_length,
number_of_spaces, number_of_spaces,
number_of_words, number_of_words,
@ -226,7 +239,7 @@ impl Preprocessed {
&self, &self,
layout_width: i32, layout_width: i32,
minimum_space_width: i32, minimum_space_width: i32,
) -> impl Iterator<Item = (Line, impl Iterator<Item = PreprocessedElement> + '_)> { ) -> impl Iterator<Item = (Line, impl Iterator<Item = PreprocessedElementEncoded> + '_)> {
let mut idx = 0; let mut idx = 0;
self.lines(layout_width, minimum_space_width).map(move |x| { self.lines(layout_width, minimum_space_width).map(move |x| {
let length = x.number_of_text_elements; let length = x.number_of_text_elements;

View file

@ -1,14 +1,10 @@
use crate::display::{ use crate::display::{
object::{DynamicSprite, PaletteVram, Size}, object::{DynamicSprite, PaletteVram, Size, SpriteVram},
Font, Font,
}; };
use super::LetterGroup;
struct WorkingLetter { struct WorkingLetter {
dynamic: DynamicSprite, dynamic: DynamicSprite,
// the x offset of the current letter with respect to the start of the current letter group
x_position: i32,
// where to render the letter from x_min to x_max // where to render the letter from x_min to x_max
x_offset: i32, x_offset: i32,
} }
@ -17,13 +13,11 @@ impl WorkingLetter {
fn new(size: Size) -> Self { fn new(size: Size) -> Self {
Self { Self {
dynamic: DynamicSprite::new(size), dynamic: DynamicSprite::new(size),
x_position: 0,
x_offset: 0, x_offset: 0,
} }
} }
fn reset(&mut self) { fn reset(&mut self) {
self.x_position = 0;
self.x_offset = 0; self.x_offset = 0;
} }
} }
@ -64,7 +58,7 @@ impl WordRender {
} }
#[must_use] #[must_use]
pub(crate) fn finalise_letter(&mut self) -> Option<LetterGroup> { pub(crate) fn finalise_letter(&mut self) -> Option<SpriteVram> {
if self.working.x_offset == 0 { if self.working.x_offset == 0 {
return None; return None;
} }
@ -72,19 +66,13 @@ impl WordRender {
let mut new_sprite = DynamicSprite::new(self.config.sprite_size); let mut new_sprite = DynamicSprite::new(self.config.sprite_size);
core::mem::swap(&mut self.working.dynamic, &mut new_sprite); core::mem::swap(&mut self.working.dynamic, &mut new_sprite);
let sprite = new_sprite.to_vram(self.config.palette.clone()); let sprite = new_sprite.to_vram(self.config.palette.clone());
let group = LetterGroup {
sprite,
width: self.working.x_offset as u16,
left: self.working.x_position as i16,
};
self.working.reset(); self.working.reset();
Some(group) Some(sprite)
} }
#[must_use] #[must_use]
pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option<LetterGroup> { pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option<SpriteVram> {
let font_letter: &crate::display::FontLetter = font.letter(c); let font_letter: &crate::display::FontLetter = font.letter(c);
// uses more than the sprite can hold // uses more than the sprite can hold
@ -96,9 +84,7 @@ impl WordRender {
None None
}; };
if self.working.x_offset == 0 { if self.working.x_offset != 0 {
self.working.x_position = font_letter.xmin as i32;
} else {
self.working.x_offset += font_letter.xmin as i32; self.working.x_offset += font_letter.xmin as i32;
} }