use crate::TextAlign;
use crate::font::AgbFont;
use agb::display::tiled::{DynamicTile16, RegularBackground, TileEffect};
use agb::fixnum::{Vector2D, vec2};
use alloc::vec::Vec;
pub struct TextRenderer {
tiles: Vec<(i32, i32, DynamicTile16)>,
last_idx_cache: Option<(i32, i32, usize)>,
}
impl Default for TextRenderer {
fn default() -> Self {
Self {
tiles: Vec::with_capacity(64),
last_idx_cache: None,
}
}
}
fn x_offset(line_w: u32, wrap_at: Option<u32>, alignment: TextAlign) -> i32 {
match alignment {
TextAlign::Left => 0,
TextAlign::Center | TextAlign::Right => {
let Some(wrap) = wrap_at else { return 0 };
let gap = wrap.saturating_sub(line_w);
if alignment == TextAlign::Center {
(gap >> 1) as i32
} else {
gap as i32
}
}
}
}
impl TextRenderer {
pub fn reset(&mut self) {
self.tiles.clear();
self.last_idx_cache = None;
}
#[allow(clippy::too_many_arguments)]
pub fn draw_text<T: AgbFont>(
&mut self,
text: &[u8],
font: &T,
background: &mut RegularBackground,
pos: Vector2D<i32>,
wrap_at: Option<u8>,
alignment: TextAlign,
palette_id: u8,
clear_size: (i32, i32),
) -> (i32, i32, i32) {
let wrap_px = wrap_at.map(|n| n as u32);
let mut cursor_y = pos.y;
let mut line_w: i32 = 0;
let mut longest: i32 = 0;
let mut staged: Vec<(i32, i32, [u32; 8])> = Vec::with_capacity(48);
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;
for ty in tile_y_start..=tile_y_end {
for tx in tile_x_start..=tile_x_end {
Self::ensure_staged_idx(&mut staged, tx, ty, &mut None);
}
}
}
let align_width = if clear_size.0 > 0 {
Some(clear_size.0 as u32)
} else {
wrap_px
};
let (first_w, _) = font.measure_line(text, wrap_px);
let mut cursor_x = pos.x + x_offset(first_w, align_width, 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);
cursor_x = pos.x + x_offset(next_w, align_width, 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);
cursor_x = pos.x + x_offset(next_w, align_width, alignment);
}
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);
let (clear_tx0, clear_tx1, clear_ty0, clear_ty1) = if clear_size.0 > 0 && clear_size.1 > 0 {
(
pos.x >> 3,
(pos.x + clear_size.0 - 1) >> 3,
pos.y >> 3,
(pos.y + clear_size.1 - 1) >> 3,
)
} else {
(0, -1, 0, -1)
};
for (tx, ty, data) in &staged {
let idx = self.ensure_tile_idx(*tx, *ty, background, palette_id);
if *tx >= clear_tx0 && *tx <= clear_tx1 && *ty >= clear_ty0 && *ty <= clear_ty1 {
self.tiles[idx].2.data_mut().copy_from_slice(data);
} else {
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,
background: &mut RegularBackground,
pos: Vector2D<i32>,
width: i32,
height: i32,
palette_id: u8,
) {
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 x_off in (0..width).step_by(8) {
let abs_x = pos.x + x_off;
let tile_x = abs_x >> 3;
let x_shift = (abs_x & 7) as u32;
let left_idx = self.ensure_tile_idx(tile_x, tile_y, background, palette_id);
self.tiles[left_idx].2.data_mut()[row_in_tile] = 0;
if x_shift > 0 {
let right_idx =
self.ensure_tile_idx(tile_x + 1, tile_y, background, palette_id);
self.tiles[right_idx].2.data_mut()[row_in_tile] = 0;
}
}
}
}
}
#[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;
}