use crate::{TextAlign, TextFormat, TextOverflow};
use agb::display::tiled::{DynamicTile16, RegularBackground, TileEffect};
use agb::fixnum::{Vector2D, vec2};
use alloc::vec::Vec;
use gba_agb_font_eb::AgbFont;
#[derive(Debug)]
pub struct TextRenderer {
pub tiles: Vec<(i32, i32, DynamicTile16)>,
pub last_idx_cache: Option<(i32, i32, usize)>,
pub palette_id: u8,
}
impl Default for TextRenderer {
fn default() -> Self {
Self {
tiles: Vec::with_capacity(64),
last_idx_cache: None,
palette_id: 15,
}
}
}
fn x_offset(line_w: u32, alignment: TextAlign) -> i32 {
match alignment {
TextAlign::Left => 0,
TextAlign::Center(column_w) => {
let gap = (column_w as u32).saturating_sub(line_w);
(gap >> 1) as i32
}
TextAlign::Right(column_w) => (column_w as u32).saturating_sub(line_w) as i32,
}
}
impl TextRenderer {
pub fn reset(&mut self, drop_tiles: bool) {
if drop_tiles {
self.tiles.clear();
} else {
for (_, _, tile) in &mut self.tiles {
tile.data_mut().fill(0);
}
}
self.last_idx_cache = None;
}
#[inline(always)]
pub fn draw_text<T: AgbFont>(
&mut self,
text: &[u8],
font: &T,
background: &mut RegularBackground,
pos: Vector2D<i32>,
format: &TextFormat,
) -> (i32, i32, i32) {
let (wrap_px, word_wrap) = match format.overflow {
TextOverflow::Wrap(w, ww) => (Some(w as u32), ww),
_ => (None, false),
};
let cutoff_x = match format.overflow {
TextOverflow::Cutoff(w) => Some(pos.x + w as i32),
_ => None,
};
let mut cursor_y = pos.y;
let mut longest: i32 = 0;
let mut last_cx = pos.x;
let mut staged: Vec<(i32, i32, [u32; 8])> = Vec::with_capacity(48);
let mut left_cache: Option<(i32, i32, usize)> = None;
let mut right_cache: Option<(i32, i32, usize)> = None;
let mut first = true;
for (line, line_w) in font.lines(text, wrap_px, word_wrap) {
if !first {
cursor_y += font.glyph_height() as i32;
}
first = false;
let mut cursor_x = pos.x + x_offset(line_w, format.align);
for &c in line {
if cutoff_x.is_none_or(|cx| cursor_x < cx) {
Self::blit_glyph_staged(
font.glyph(c),
font,
&mut staged,
cursor_x,
cursor_y,
&mut left_cache,
&mut right_cache,
);
}
cursor_x += font.char_width(c) as i32;
}
longest = longest.max(line_w as i32);
last_cx = cursor_x;
}
let clear_size = (format.clear.0 as i32, format.clear.1 as i32);
if clear_size.0 > 0 && clear_size.1 > 0 {
let tile_x_start = pos.x >> 3;
let tile_x_end = (pos.x + clear_size.0 - 1) >> 3;
let tile_y_start = pos.y >> 3;
let tile_y_end = (pos.y + clear_size.1 - 1) >> 3;
let py_end = pos.y + clear_size.1 - 1;
let px_end = pos.x + clear_size.0 - 1;
for (tx, ty, tile) in &mut self.tiles {
if *tx < tile_x_start || *tx > tile_x_end || *ty < tile_y_start || *ty > tile_y_end
{
continue;
}
let tile_py0 = *ty << 3;
let row_start = (pos.y - tile_py0).max(0) as usize;
let row_end = (py_end - tile_py0).min(7) as usize;
let tile_px0 = *tx << 3;
let n_start = (pos.x - tile_px0).max(0) as u32;
let n_end = (px_end - tile_px0).min(7) as u32;
let n_count = n_end - n_start + 1;
let mask = if n_count >= 8 {
u32::MAX
} else {
((1u32 << (n_count << 2)) - 1) << (n_start << 2)
};
for row in tile.data_mut().iter_mut().take(row_end + 1).skip(row_start) {
*row &= !mask;
}
}
}
for (tx, ty, data) in &staged {
let idx = self.ensure_tile_idx(*tx, *ty, background, self.palette_id);
let tile_data = self.tiles[idx].2.data_mut();
for (dst, &src) in tile_data.iter_mut().zip(data.iter()) {
blit_pixel_row_arm(dst, src);
}
}
(last_cx - pos.x, cursor_y - pos.y, longest)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn int_draw_text<T: AgbFont>(
&mut self,
text: &[u8],
font: &T,
background: &mut RegularBackground,
pos: Vector2D<i32>,
overflow: TextOverflow,
alignment: TextAlign,
palette_id: u8,
clear_size: (i32, i32),
initial_line_w: i32,
left_margin_x: i32,
) -> (i32, i32, i32) {
let wrap_px = match overflow {
TextOverflow::Wrap(w, _) => Some(w),
_ => None,
};
let cutoff_x = match overflow {
TextOverflow::Cutoff(w) => Some(left_margin_x + w as i32),
_ => None,
};
let mut cursor_y = pos.y;
let mut line_w: i32 = initial_line_w;
let mut longest: i32 = 0;
let mut staged: Vec<(i32, i32, [u32; 8])> = Vec::with_capacity(48);
let (first_w, _) = font.measure_line(text, wrap_px.map(|n| n as u32));
let mut cursor_x = pos.x + x_offset(first_w, alignment);
let mut left_cache: Option<(i32, i32, usize)> = None;
let mut right_cache: Option<(i32, i32, usize)> = None;
for (i, &c) in text.iter().enumerate() {
if c == b'\n' {
longest = longest.max(line_w);
cursor_y += font.glyph_height() as i32;
line_w = 0;
let (next_w, _) = font.measure_line(&text[i + 1..], wrap_px.map(|n| n as u32));
cursor_x = left_margin_x + x_offset(next_w, alignment);
} else {
let char_w = font.char_width(c) as i32;
if let Some(wa) = wrap_px
&& line_w + char_w > wa as i32
{
longest = longest.max(line_w);
cursor_y += font.glyph_height() as i32;
line_w = 0;
let (next_w, _) = font.measure_line(&text[i..], wrap_px.map(|n| n as u32));
cursor_x = left_margin_x + x_offset(next_w, alignment);
}
if cutoff_x.is_none_or(|cx| cursor_x < cx) {
Self::blit_glyph_staged(
font.glyph(c),
font,
&mut staged,
cursor_x,
cursor_y,
&mut left_cache,
&mut right_cache,
);
}
cursor_x += char_w;
line_w += char_w;
}
}
longest = longest.max(line_w);
if clear_size.0 > 0 && clear_size.1 > 0 {
let tile_x_start = pos.x >> 3;
let tile_x_end = (pos.x + clear_size.0 - 1) >> 3;
let tile_y_start = pos.y >> 3;
let tile_y_end = (pos.y + clear_size.1 - 1) >> 3;
let py_end = pos.y + clear_size.1 - 1;
let px_end = pos.x + clear_size.0 - 1;
for (tx, ty, tile) in &mut self.tiles {
if *tx < tile_x_start || *tx > tile_x_end || *ty < tile_y_start || *ty > tile_y_end
{
continue;
}
let tile_py0 = *ty << 3;
let row_start = (pos.y - tile_py0).max(0) as usize;
let row_end = (py_end - tile_py0).min(7) as usize;
let tile_px0 = *tx << 3;
let n_start = (pos.x - tile_px0).max(0) as u32;
let n_end = (px_end - tile_px0).min(7) as u32;
let n_count = n_end - n_start + 1;
let mask = if n_count >= 8 {
u32::MAX
} else {
((1u32 << (n_count << 2)) - 1) << (n_start << 2)
};
for row in tile.data_mut().iter_mut().take(row_end + 1).skip(row_start) {
*row &= !mask;
}
}
}
for (tx, ty, data) in &staged {
let idx = self.ensure_tile_idx(*tx, *ty, background, palette_id);
let tile_data = self.tiles[idx].2.data_mut();
for (dst, &src) in tile_data.iter_mut().zip(data.iter()) {
blit_pixel_row_arm(dst, src);
}
}
(cursor_x - pos.x, cursor_y - pos.y, longest)
}
fn blit_glyph_staged<T: AgbFont>(
glyph: &[u32],
font: &T,
staged: &mut Vec<(i32, i32, [u32; 8])>,
pixel_x: i32,
pixel_y: i32,
left_cache: &mut Option<(i32, i32, usize)>,
right_cache: &mut Option<(i32, i32, usize)>,
) {
let row_u32s = font.row_u32s();
let height = font.glyph_height() as i32;
for chunk in 0..row_u32s {
let px_left = pixel_x + ((chunk as i32) << 3);
let tile_x = px_left >> 3;
let x_shift = (px_left & 7) as u32;
let shift_bits = x_shift << 2;
let mut last_tile_y = -1i32;
let mut left_idx = 0;
let mut right_idx = None;
for row in 0..height {
let abs_y = pixel_y + row;
let current_tile_y = abs_y >> 3;
let row_in_tile = (abs_y & 7) as usize;
if current_tile_y != last_tile_y {
left_idx = Self::ensure_staged_idx(staged, tile_x, current_tile_y, left_cache);
right_idx = if x_shift > 0 {
Some(Self::ensure_staged_idx(
staged,
tile_x + 1,
current_tile_y,
right_cache,
))
} else {
None
};
last_tile_y = current_tile_y;
}
let pixel_data = glyph[(row as usize * row_u32s) + chunk];
blit_pixel_row_arm(
&mut staged[left_idx].2[row_in_tile],
pixel_data << shift_bits,
);
if let Some(r_idx) = right_idx {
blit_pixel_row_arm(
&mut staged[r_idx].2[row_in_tile],
pixel_data >> (32 - shift_bits),
);
}
}
}
}
fn ensure_staged_idx(
staged: &mut Vec<(i32, i32, [u32; 8])>,
tx: i32,
ty: i32,
cache: &mut Option<(i32, i32, usize)>,
) -> usize {
if let Some((cx, cy, cidx)) = *cache
&& cx == tx
&& cy == ty
{
return cidx;
}
if let Some(pos) = staged.iter().rposition(|(x, y, _)| *x == tx && *y == ty) {
*cache = Some((tx, ty, pos));
return pos;
}
staged.push((tx, ty, [0u32; 8]));
let pos = staged.len() - 1;
*cache = Some((tx, ty, pos));
pos
}
fn ensure_tile_idx(&mut self, tx: i32, ty: i32, bg: &mut RegularBackground, pal: u8) -> usize {
if let Some((cx, cy, cidx)) = self.last_idx_cache
&& cx == tx
&& cy == ty
{
return cidx;
}
if let Some(pos) = self
.tiles
.iter()
.rposition(|(x, y, _)| *x == tx && *y == ty)
{
self.last_idx_cache = Some((tx, ty, pos));
return pos;
}
let tile = DynamicTile16::new().fill_with(0);
bg.set_tile_dynamic16(vec2(tx, ty), &tile, TileEffect::default().palette(pal));
self.tiles.push((tx, ty, tile));
let pos = self.tiles.len() - 1;
self.last_idx_cache = Some((tx, ty, pos));
pos
}
pub fn clear_pixel_rect(&mut self, pos: Vector2D<i32>, width: i32, height: i32) {
let tile_x_start = pos.x >> 3;
let tile_x_end = (pos.x + width - 1) >> 3;
for row in 0..height {
let abs_y = pos.y + row;
let tile_y = abs_y >> 3;
let row_in_tile = (abs_y & 7) as usize;
for tile_x in tile_x_start..=tile_x_end {
let tile_px_start = tile_x << 3;
let n_start = (pos.x.max(tile_px_start) - tile_px_start) as u32;
let n_end = ((pos.x + width - 1).min(tile_px_start + 7) - tile_px_start) as u32;
let n_count = n_end - n_start + 1;
let mask = if n_count >= 8 {
u32::MAX
} else {
((1u32 << (n_count << 2)) - 1) << (n_start << 2)
};
if let Some(idx) = self
.tiles
.iter()
.position(|(x, y, _)| *x == tile_x && *y == tile_y)
{
self.tiles[idx].2.data_mut()[row_in_tile] &= !mask;
}
}
}
}
}
#[unsafe(link_section = ".iwram")]
#[instruction_set(arm::a32)]
fn blit_pixel_row_arm(target: &mut u32, src: u32) {
if src == 0 {
return;
}
let hi = src & 0x8888_8888;
let lo = src & 0x7777_7777;
let set_nybbles = (hi | ((lo.wrapping_add(0x7777_7777)) & 0x8888_8888)) >> 3;
let mask = set_nybbles * 0xF;
*target = (*target & !mask) | src;
}