mirror of
https://github.com/italicsjenga/agb.git
synced 2025-02-23 22:58:18 +11:00
text rendering that supports different alignments
This commit is contained in:
parent
d5d3d1a658
commit
f947d82049
4 changed files with 556 additions and 108 deletions
agb
|
@ -4,7 +4,7 @@
|
||||||
use agb::{
|
use agb::{
|
||||||
display::{
|
display::{
|
||||||
object::{
|
object::{
|
||||||
font::{BufferedWordRender, Configuration},
|
font::{BufferedRender, TextAlignment},
|
||||||
PaletteVram, Size,
|
PaletteVram, Size,
|
||||||
},
|
},
|
||||||
palette16::Palette16,
|
palette16::Palette16,
|
||||||
|
@ -15,6 +15,9 @@ use agb::{
|
||||||
};
|
};
|
||||||
use agb_fixnum::Rect;
|
use agb_fixnum::Rect;
|
||||||
|
|
||||||
|
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/pixelated.ttf", 8);
|
||||||
|
@ -32,9 +35,7 @@ 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 config = Configuration::new(Size::S16x8, palette);
|
let mut wr = BufferedRender::new(&FONT, Size::S16x8, palette);
|
||||||
|
|
||||||
let mut wr = BufferedWordRender::new(&FONT, config);
|
|
||||||
let _ = writeln!(
|
let _ = writeln!(
|
||||||
wr,
|
wr,
|
||||||
"{}",
|
"{}",
|
||||||
|
@ -54,22 +55,40 @@ fn main(mut gba: agb::Gba) -> ! {
|
||||||
let mut num_letters = 0;
|
let mut num_letters = 0;
|
||||||
let mut frame = 0;
|
let mut frame = 0;
|
||||||
|
|
||||||
|
let mut alignment = TextAlignment::Left;
|
||||||
|
|
||||||
|
let mut text = Vec::new();
|
||||||
|
|
||||||
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);
|
for (letter, slot) in text.iter().zip(oam) {
|
||||||
|
slot.set(letter);
|
||||||
|
}
|
||||||
|
|
||||||
let start = timer.value();
|
let start = timer.value();
|
||||||
wr.update(
|
|
||||||
Rect::new((WIDTH / 8, 0).into(), (80, 100).into()),
|
|
||||||
num_letters,
|
|
||||||
);
|
|
||||||
wr.process();
|
wr.process();
|
||||||
|
text = wr.layout(
|
||||||
|
Rect::new((WIDTH / 8, 0).into(), (80, 100).into()),
|
||||||
|
alignment,
|
||||||
|
num_letters,
|
||||||
|
2,
|
||||||
|
);
|
||||||
let end = timer.value();
|
let end = timer.value();
|
||||||
|
|
||||||
agb::println!("Took {} cycles", 256 * (end.wrapping_sub(start) as u32));
|
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;
|
frame += 1;
|
||||||
|
|
||||||
// if frame % 2 == 0 {
|
// if frame % 2 == 0 {
|
||||||
|
|
|
@ -1,127 +1,230 @@
|
||||||
|
use core::fmt::Write;
|
||||||
|
|
||||||
|
use agb_fixnum::{Rect, Vector2D};
|
||||||
|
use alloc::{collections::VecDeque, vec::Vec};
|
||||||
|
|
||||||
use crate::display::Font;
|
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,
|
sprite: SpriteVram,
|
||||||
// the width of the letter group
|
// the width of the letter group
|
||||||
width: u16,
|
width: u16,
|
||||||
left: i16,
|
left: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct WorkingLetter {
|
pub struct BufferedRender<'font> {
|
||||||
dynamic: DynamicSprite,
|
char_render: WordRender,
|
||||||
// the x offset of the current letter with respect to the start of the current letter group
|
preprocessor: Preprocessed,
|
||||||
x_position: i32,
|
buffered_chars: VecDeque<char>,
|
||||||
// where to render the letter from x_min to x_max
|
letters: Letters,
|
||||||
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> {
|
|
||||||
font: &'font Font,
|
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]
|
#[must_use]
|
||||||
fn new(font: &'font Font, config: Configuration) -> Self {
|
pub fn new(font: &'font Font, sprite_size: Size, palette: PaletteVram) -> Self {
|
||||||
WordRender {
|
let config = Configuration::new(sprite_size, palette);
|
||||||
|
BufferedRender {
|
||||||
|
char_render: WordRender::new(config),
|
||||||
|
preprocessor: Preprocessed::new(),
|
||||||
|
buffered_chars: VecDeque::new(),
|
||||||
|
letters: Default::default(),
|
||||||
font,
|
font,
|
||||||
working: WorkingLetter::new(config.sprite_size),
|
|
||||||
|
|
||||||
config,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WordRender<'_> {
|
impl BufferedRender<'_> {
|
||||||
#[must_use]
|
fn input_character(&mut self, character: char) {
|
||||||
fn finalise_letter(&mut self) -> Option<LetterGroup> {
|
self.preprocessor.add_character(self.font, character);
|
||||||
if self.working.x_offset == 0 {
|
self.buffered_chars.push_back(character);
|
||||||
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 fn process(&mut self) {
|
||||||
fn render_char(&mut self, c: char) -> Option<LetterGroup> {
|
let Some(c) = self.buffered_chars.pop_front() else { return };
|
||||||
let font_letter = self.font.letter(c);
|
match c {
|
||||||
|
' ' | '\n' => {
|
||||||
// uses more than the sprite can hold
|
if let Some(group) = self.char_render.finalise_letter() {
|
||||||
let group = if self.working.x_offset + font_letter.width as i32
|
self.letters.letters.push(group);
|
||||||
> self.config.sprite_size.to_width_height().0 as i32
|
self.letters.current_word_length += 1;
|
||||||
{
|
self.letters.number_of_groups += 1;
|
||||||
self.finalise_letter()
|
}
|
||||||
} else {
|
if self.letters.current_word_length != 0 {
|
||||||
None
|
self.letters.word_lengths.push(
|
||||||
};
|
self.letters
|
||||||
|
.current_word_length
|
||||||
if self.working.x_offset == 0 {
|
.try_into()
|
||||||
self.working.x_position = font_letter.xmin as i32;
|
.expect("word is too big"),
|
||||||
} 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,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
206
agb/src/display/object/font/preprocess.rs
Normal file
206
agb/src/display/object/font/preprocess.rs
Normal 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
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
120
agb/src/display/object/font/renderer.rs
Normal file
120
agb/src/display/object/font/renderer.rs
Normal 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
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue