gba_agb_font_renderer 0.3.0

Bitmap font renderer for GBA/AGB
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 {
    /// Call this when switching scenes or menus to reuse the underlying Vec buffer.
    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
    }

    /// Clear a pixel region by zeroing the tile rows it covers.
    ///
    /// Note: zeros entire 8-pixel-wide tile rows, so pixels in tiles that extend
    /// outside the given rect are also cleared.
    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;
                }
            }
        }
    }
}

/// Perform a masked 4bpp blit.
/// This function is marked with link_section ".iwram" because 32-bit operations
/// are significantly faster in IWRAM than in EWRAM or ROM on the GBA.
#[unsafe(link_section = ".iwram")]
#[instruction_set(arm::a32)]
fn blit_pixel_row_arm(target: &mut u32, src: u32) {
    if src == 0 {
        return;
    }

    // Mask Generation Logic:
    // We want a bitmask where every 4-bit nibble in 'src' that is non-zero
    // is set to 0xF in the mask.

    let hi = src & 0x8888_8888;
    let lo = src & 0x7777_7777;

    // (lo + 0x7777_7777) will carry into the 4th bit of each nibble if lo > 0.
    // We then OR that with the original hi bit to catch all non-zero nibbles.
    let set_nybbles = (hi | ((lo.wrapping_add(0x7777_7777)) & 0x8888_8888)) >> 3;

    // Spread the 1-bit flags to 4-bit masks (0x1 -> 0xF)
    let mask = set_nybbles * 0xF;

    // Apply the mask: Clear target nibbles where src has data, then OR src.
    *target = (*target & !mask) | src;
}