text rendering that supports different alignments

This commit is contained in:
Corwin 2023-06-28 20:29:09 +01:00
parent d5d3d1a658
commit f947d82049
No known key found for this signature in database
4 changed files with 556 additions and 108 deletions

View file

@ -4,7 +4,7 @@
use agb::{
display::{
object::{
font::{BufferedWordRender, Configuration},
font::{BufferedRender, TextAlignment},
PaletteVram, Size,
},
palette16::Palette16,
@ -15,6 +15,9 @@ use agb::{
};
use agb_fixnum::Rect;
extern crate alloc;
use alloc::vec::Vec;
use core::fmt::Write;
const FONT: Font = include_font!("examples/font/pixelated.ttf", 8);
@ -32,9 +35,7 @@ fn main(mut gba: agb::Gba) -> ! {
let palette = Palette16::new(palette);
let palette = PaletteVram::new(&palette).unwrap();
let config = Configuration::new(Size::S16x8, palette);
let mut wr = BufferedWordRender::new(&FONT, config);
let mut wr = BufferedRender::new(&FONT, Size::S16x8, palette);
let _ = writeln!(
wr,
"{}",
@ -54,22 +55,40 @@ fn main(mut gba: agb::Gba) -> ! {
let mut num_letters = 0;
let mut frame = 0;
let mut alignment = TextAlignment::Left;
let mut text = Vec::new();
loop {
vblank.wait_for_vblank();
input.update();
let oam = &mut unmanaged.iter();
wr.commit(oam);
for (letter, slot) in text.iter().zip(oam) {
slot.set(letter);
}
let start = timer.value();
wr.update(
Rect::new((WIDTH / 8, 0).into(), (80, 100).into()),
num_letters,
);
wr.process();
text = wr.layout(
Rect::new((WIDTH / 8, 0).into(), (80, 100).into()),
alignment,
num_letters,
2,
);
let end = timer.value();
agb::println!("Took {} cycles", 256 * (end.wrapping_sub(start) as u32));
if input.is_just_pressed(Button::LEFT) {
alignment = TextAlignment::Left;
}
if input.is_just_pressed(Button::RIGHT) {
alignment = TextAlignment::Right;
}
if input.is_just_pressed(Button::UP | Button::DOWN) {
alignment = TextAlignment::Center;
}
frame += 1;
// if frame % 2 == 0 {

View file

@ -1,127 +1,230 @@
use core::fmt::Write;
use agb_fixnum::{Rect, Vector2D};
use alloc::{collections::VecDeque, vec::Vec};
use crate::display::Font;
use super::{DynamicSprite, PaletteVram, Size, SpriteVram};
use self::{
preprocess::{Line, Preprocessed, PreprocessedElement},
renderer::{Configuration, WordRender},
};
struct LetterGroup {
use super::{DynamicSprite, ObjectUnmanaged, PaletteVram, Size, SpriteVram};
mod preprocess;
mod renderer;
#[derive(Debug, PartialEq, Eq)]
#[non_exhaustive]
pub(crate) enum WhiteSpace {
NewLine,
Space,
}
impl WhiteSpace {
pub(crate) fn from_char(c: char) -> Self {
match c {
' ' => WhiteSpace::Space,
'\n' => WhiteSpace::NewLine,
_ => panic!("char not supported whitespace"),
}
}
}
#[derive(Debug)]
pub(crate) struct LetterGroup {
sprite: SpriteVram,
// the width of the letter group
width: u16,
left: i16,
}
struct WorkingLetter {
dynamic: DynamicSprite,
// the x offset of the current letter with respect to the start of the current letter group
x_position: i32,
// where to render the letter from x_min to x_max
x_offset: i32,
}
impl WorkingLetter {
fn new(size: Size) -> Self {
Self {
dynamic: DynamicSprite::new(size),
x_position: 0,
x_offset: 0,
}
}
fn reset(&mut self) {
self.x_position = 0;
self.x_offset = 0;
}
}
pub struct Configuration {
sprite_size: Size,
palette: PaletteVram,
}
impl Configuration {
#[must_use]
pub fn new(sprite_size: Size, palette: PaletteVram) -> Self {
Self {
sprite_size,
palette,
}
}
}
struct WordRender<'font> {
pub struct BufferedRender<'font> {
char_render: WordRender,
preprocessor: Preprocessed,
buffered_chars: VecDeque<char>,
letters: Letters,
font: &'font Font,
working: WorkingLetter,
config: Configuration,
}
impl<'font> WordRender<'font> {
#[derive(Debug, Default)]
struct Letters {
letters: Vec<LetterGroup>,
word_lengths: Vec<u8>,
current_word_length: usize,
number_of_groups: usize,
}
impl Write for BufferedRender<'_> {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
for c in s.chars() {
self.input_character(c);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum TextAlignment {
Left,
Right,
Center,
}
struct TextAlignmentSettings {
space_width: i32,
start_x: i32,
}
impl TextAlignment {
fn settings(
self,
line: &Line,
minimum_space_width: i32,
size: Vector2D<i32>,
) -> TextAlignmentSettings {
match self {
TextAlignment::Left => TextAlignmentSettings {
space_width: minimum_space_width,
start_x: 0,
},
TextAlignment::Right => TextAlignmentSettings {
space_width: minimum_space_width,
start_x: size.x - line.width(),
},
TextAlignment::Center => TextAlignmentSettings {
space_width: minimum_space_width,
start_x: (size.x - line.width()) / 2,
},
}
}
}
impl<'font> BufferedRender<'font> {
#[must_use]
fn new(font: &'font Font, config: Configuration) -> Self {
WordRender {
pub 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,
working: WorkingLetter::new(config.sprite_size),
config,
}
}
}
impl WordRender<'_> {
#[must_use]
fn finalise_letter(&mut self) -> Option<LetterGroup> {
if self.working.x_offset == 0 {
return None;
}
let mut new_sprite = DynamicSprite::new(self.config.sprite_size);
core::mem::swap(&mut self.working.dynamic, &mut new_sprite);
let sprite = new_sprite.to_vram(self.config.palette.clone());
let group = LetterGroup {
sprite,
width: self.working.x_offset as u16,
left: self.working.x_position as i16,
};
self.working.reset();
Some(group)
impl BufferedRender<'_> {
fn input_character(&mut self, character: char) {
self.preprocessor.add_character(self.font, character);
self.buffered_chars.push_back(character);
}
#[must_use]
fn render_char(&mut self, c: char) -> Option<LetterGroup> {
let font_letter = self.font.letter(c);
// 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.finalise_letter()
} else {
None
};
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;
}
let y_position = self.font.ascent() - font_letter.height as i32 - font_letter.ymin as i32;
for y in 0..font_letter.height as usize {
for x in 0..font_letter.width as usize {
let rendered = font_letter.bit_absolute(x, y);
if rendered {
self.working.dynamic.set_pixel(
x + self.working.x_offset as usize,
(y_position + y as i32) as usize,
1,
pub fn process(&mut self) {
let Some(c) = self.buffered_chars.pop_front() else { return };
match c {
' ' | '\n' => {
if let Some(group) = self.char_render.finalise_letter() {
self.letters.letters.push(group);
self.letters.current_word_length += 1;
self.letters.number_of_groups += 1;
}
if self.letters.current_word_length != 0 {
self.letters.word_lengths.push(
self.letters
.current_word_length
.try_into()
.expect("word is too big"),
);
}
self.letters.current_word_length = 0;
self.letters.number_of_groups += 1;
}
letter => {
if let Some(group) = self.char_render.render_char(self.font, letter) {
self.letters.letters.push(group);
self.letters.current_word_length += 1;
self.letters.number_of_groups += 1;
}
}
}
}
self.working.x_offset += font_letter.advance_width as i32;
#[must_use]
pub fn layout(
&mut self,
area: Rect<i32>,
alignment: TextAlignment,
number_of_groups: usize,
paragraph_spacing: i32,
) -> Vec<ObjectUnmanaged> {
let mut objects = Vec::new();
group
while !self.buffered_chars.is_empty() && self.letters.number_of_groups <= number_of_groups {
self.process();
}
let minimum_space_width = self.font.letter(' ').advance_width as i32;
let lines = self.preprocessor.lines(area.size.x, minimum_space_width);
let mut head_position = area.position;
let mut processed_depth = 0;
let mut group_depth = 0;
let mut word_depth = 0;
let mut rendered_groups = 0;
'outer: for line in lines {
let settings = alignment.settings(&line, minimum_space_width, area.size);
head_position.x += settings.start_x;
for idx in 0..line.number_of_text_elements() {
let element = self.preprocessor.get(processed_depth + idx);
match element {
PreprocessedElement::Word(_) => {
for _ in 0..self
.letters
.word_lengths
.get(word_depth)
.copied()
.unwrap_or(u8::MAX)
{
let letter_group = &self.letters.letters[group_depth];
let mut object = ObjectUnmanaged::new(letter_group.sprite.clone());
head_position.x += letter_group.left as i32;
object.set_position(head_position);
head_position.x += letter_group.width as i32;
object.show();
objects.push(object);
group_depth += 1;
rendered_groups += 1;
if rendered_groups >= number_of_groups {
break 'outer;
}
}
word_depth += 1;
}
PreprocessedElement::WhiteSpace(space_type) => {
if space_type == WhiteSpace::NewLine {
head_position.y += paragraph_spacing;
}
head_position.x += settings.space_width;
rendered_groups += 1;
if rendered_groups >= number_of_groups {
break 'outer;
}
}
}
}
processed_depth += line.number_of_text_elements();
head_position.x = area.position.x;
head_position.y += 9;
}
objects
}
}

View file

@ -0,0 +1,206 @@
use alloc::vec::Vec;
use crate::display::Font;
use super::WhiteSpace;
#[derive(Debug, PartialEq, Eq)]
pub(crate) enum PreprocessedElement {
Word(u8),
WhiteSpace(WhiteSpace),
}
#[derive(Clone, Copy, PartialEq, Eq)]
struct PreprocessedElementStored(u8);
impl core::fmt::Debug for PreprocessedElementStored {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("PreprocessedElementStored")
.field(&self.parse())
.finish()
}
}
impl PreprocessedElementStored {
fn parse(self) -> PreprocessedElement {
match self.0 {
255 => PreprocessedElement::WhiteSpace(WhiteSpace::NewLine),
254 => PreprocessedElement::WhiteSpace(WhiteSpace::Space),
length => PreprocessedElement::Word(length),
}
}
fn from_element(x: PreprocessedElement) -> Self {
match x {
PreprocessedElement::Word(length) => PreprocessedElementStored(length),
PreprocessedElement::WhiteSpace(space) => PreprocessedElementStored(match space {
WhiteSpace::NewLine => 255,
WhiteSpace::Space => 254,
}),
}
}
}
#[derive(Default, Debug)]
pub(crate) struct Preprocessed {
widths: Vec<PreprocessedElementStored>,
preprocessor: Preprocessor,
}
#[derive(Debug, Default)]
struct Preprocessor {
current_word_width: i32,
}
impl Preprocessor {
fn add_character(
&mut self,
font: &Font,
character: char,
widths: &mut Vec<PreprocessedElementStored>,
) {
match character {
space @ (' ' | '\n') => {
if self.current_word_width != 0 {
widths.push(PreprocessedElementStored::from_element(
PreprocessedElement::Word(
self.current_word_width
.try_into()
.expect("word should be small and positive"),
),
));
self.current_word_width = 0;
}
widths.push(PreprocessedElementStored::from_element(
PreprocessedElement::WhiteSpace(WhiteSpace::from_char(space)),
));
}
letter => {
let letter = font.letter(letter);
self.current_word_width += letter.advance_width as i32 + letter.xmin as i32;
}
}
}
}
pub(crate) struct Lines<'preprocess> {
minimum_space_width: i32,
layout_width: i32,
data: &'preprocess [PreprocessedElementStored],
current_start_idx: usize,
}
pub(crate) struct Line {
width: i32,
number_of_text_elements: usize,
number_of_spaces: usize,
number_of_words: 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_words(&self) -> usize {
self.number_of_words
}
}
impl<'pre> Iterator for Lines<'pre> {
type Item = Line;
fn next(&mut self) -> Option<Self::Item> {
if self.current_start_idx >= self.data.len() {
return None;
}
let mut line_idx_length = 0;
let mut current_line_width = 0;
let mut additional_space_count = 0;
let mut number_of_spaces = 0;
let mut number_of_words = 0;
while let Some(next) = self.data.get(self.current_start_idx + line_idx_length) {
match next.parse() {
PreprocessedElement::Word(pixels) => {
let additional_space_width =
additional_space_count as i32 * self.minimum_space_width;
let width = pixels as i32;
if width + current_line_width + additional_space_width > self.layout_width {
break;
}
number_of_words += 1;
current_line_width += width + additional_space_width;
number_of_spaces += additional_space_count;
}
PreprocessedElement::WhiteSpace(space) => match space {
WhiteSpace::NewLine => {
line_idx_length += 1;
break;
}
WhiteSpace::Space => {
additional_space_count += 1;
}
},
};
line_idx_length += 1;
}
self.current_start_idx += line_idx_length;
Some(Line {
width: current_line_width,
number_of_text_elements: line_idx_length,
number_of_spaces,
number_of_words,
})
}
}
impl Preprocessed {
pub(crate) fn new() -> Self {
Default::default()
}
pub(crate) fn get(&self, idx: usize) -> PreprocessedElement {
self.widths[idx].parse()
}
pub(crate) fn add_character(&mut self, font: &Font, c: char) {
self.preprocessor.add_character(font, c, &mut self.widths);
}
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,
}
}
fn lines_element(
&self,
layout_width: i32,
minimum_space_width: i32,
) -> impl Iterator<Item = &[PreprocessedElementStored]> {
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)];
idx += length;
d
})
}
}

View file

@ -0,0 +1,120 @@
use crate::display::{
object::{DynamicSprite, PaletteVram, Size},
Font,
};
use super::LetterGroup;
struct WorkingLetter {
dynamic: DynamicSprite,
// the x offset of the current letter with respect to the start of the current letter group
x_position: i32,
// where to render the letter from x_min to x_max
x_offset: i32,
}
impl WorkingLetter {
fn new(size: Size) -> Self {
Self {
dynamic: DynamicSprite::new(size),
x_position: 0,
x_offset: 0,
}
}
fn reset(&mut self) {
self.x_position = 0;
self.x_offset = 0;
}
}
pub struct Configuration {
sprite_size: Size,
palette: PaletteVram,
}
impl Configuration {
#[must_use]
pub fn new(sprite_size: Size, palette: PaletteVram) -> Self {
Self {
sprite_size,
palette,
}
}
}
pub(crate) struct WordRender {
working: WorkingLetter,
config: Configuration,
colour: usize,
}
impl WordRender {
#[must_use]
pub(crate) fn new(config: Configuration) -> Self {
WordRender {
working: WorkingLetter::new(config.sprite_size),
config,
colour: 1,
}
}
#[must_use]
pub(crate) fn finalise_letter(&mut self) -> Option<LetterGroup> {
if self.working.x_offset == 0 {
return None;
}
let mut new_sprite = DynamicSprite::new(self.config.sprite_size);
core::mem::swap(&mut self.working.dynamic, &mut new_sprite);
let sprite = new_sprite.to_vram(self.config.palette.clone());
let group = LetterGroup {
sprite,
width: self.working.x_offset as u16,
left: self.working.x_position as i16,
};
self.working.reset();
Some(group)
}
#[must_use]
pub(crate) fn render_char(&mut self, font: &Font, c: char) -> Option<LetterGroup> {
let font_letter: &crate::display::FontLetter = font.letter(c);
// 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.finalise_letter()
} else {
None
};
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;
}
let y_position = font.ascent() - font_letter.height as i32 - font_letter.ymin as i32;
for y in 0..font_letter.height as usize {
for x in 0..font_letter.width as usize {
let rendered = font_letter.bit_absolute(x, y);
if rendered {
self.working.dynamic.set_pixel(
x + self.working.x_offset as usize,
(y_position + y as i32) as usize,
self.colour,
);
}
}
}
self.working.x_offset += font_letter.advance_width as i32;
group
}
}