use crate::widget::Color;
use alloc::vec::Vec;
#[cfg(not(target_os = "none"))]
use blake3;
use fontdue::{Font, FontResult, FontSettings};
pub use fontdue::{LineMetrics, Metrics};
#[cfg(not(target_os = "none"))]
use once_cell::sync::OnceCell;
#[cfg(not(target_os = "none"))]
use std::collections::HashMap;
#[cfg(not(target_os = "none"))]
use std::sync::Mutex;
#[cfg(not(target_os = "none"))]
static FONT_CACHE: OnceCell<Mutex<HashMap<u64, Font>>> = OnceCell::new();
#[cfg(not(target_os = "none"))]
fn hash_font_data(font_data: &[u8]) -> u64 {
let key = blake3::hash(font_data);
u64::from_le_bytes(key.as_bytes()[..8].try_into().unwrap())
}
#[cfg(not(target_os = "none"))]
fn get_cached_font(font_data: &[u8]) -> Font {
let key = hash_font_data(font_data);
let cache = FONT_CACHE.get_or_init(|| Mutex::new(HashMap::new()));
let mut map = cache.lock().unwrap();
map.entry(key)
.or_insert_with(|| {
Font::from_bytes(font_data, FontSettings::default()).expect("valid font")
})
.clone()
}
#[cfg(target_os = "none")]
fn get_cached_font(font_data: &[u8]) -> Font {
Font::from_bytes(font_data, FontSettings::default()).expect("valid font")
}
fn round_to_i32(value: f32) -> i32 {
if value.is_nan() {
0
} else if value >= 0.0 {
(value + 0.5) as i32
} else {
(value - 0.5) as i32
}
}
pub fn rasterize_glyph(font_data: &[u8], ch: char, px: f32) -> FontResult<(Metrics, Vec<u8>)> {
let font = get_cached_font(font_data);
Ok(font.rasterize(ch, px))
}
pub fn line_metrics(font_data: &[u8], px: f32) -> FontResult<LineMetrics> {
let font = Font::from_bytes(font_data, FontSettings::default())?;
font.horizontal_line_metrics(px)
.ok_or("missing horizontal metrics")
}
pub trait FontdueRenderTarget {
fn dimensions(&self) -> (usize, usize);
fn blend_pixel(&mut self, x: i32, y: i32, color: Color, alpha: u8);
}
pub fn render_text<R: FontdueRenderTarget>(
target: &mut R,
font_data: &[u8],
position: (i32, i32),
text: &str,
color: Color,
px: f32,
) -> FontResult<()> {
let vm = line_metrics(font_data, px)?;
let ascent = round_to_i32(vm.ascent);
let baseline = position.1 + ascent;
let (width, height) = target.dimensions();
let mut x_cursor = position.0;
for ch in text.chars() {
if let Ok((metrics, bitmap)) = rasterize_glyph(font_data, ch, px) {
let w = metrics.width as i32;
let h = metrics.height as i32;
let draw_y = baseline - ascent - metrics.ymin;
for y in 0..h {
let py = draw_y - y;
if py < 0 || (py as usize) >= height {
continue;
}
for x in 0..w {
let px_coord = x_cursor + metrics.xmin + x;
if px_coord < 0 || (px_coord as usize) >= width {
continue;
}
let alpha = bitmap[(h - 1 - y) as usize * metrics.width + x as usize];
if alpha > 0 {
target.blend_pixel(px_coord, py, color, alpha);
}
}
}
x_cursor += round_to_i32(metrics.advance_width);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
const FONT_DATA: &[u8] = include_bytes!("../../../assets/fonts/DejaVuSans.ttf");
#[test]
fn rasterize_a() {
let (metrics, bitmap) = rasterize_glyph(FONT_DATA, 'A', 16.0).unwrap();
assert_eq!(bitmap.len(), metrics.width * metrics.height);
assert!(metrics.width > 0 && metrics.height > 0);
}
#[test]
fn line_metrics_present() {
let vm = line_metrics(FONT_DATA, 16.0).unwrap();
assert!(vm.ascent > 0.0 && vm.descent < 0.0);
}
struct Surface {
buf: [u8; 32 * 32 * 4],
}
impl Surface {
fn new() -> Self {
Self {
buf: [0; 32 * 32 * 4],
}
}
}
impl FontdueRenderTarget for Surface {
fn dimensions(&self) -> (usize, usize) {
(32, 32)
}
fn blend_pixel(&mut self, x: i32, y: i32, color: Color, alpha: u8) {
if x >= 0 && y >= 0 && x < 32 && y < 32 {
let idx = ((y as usize) * 32 + x as usize) * 4;
let r = (color.0 as u16 * alpha as u16 / 255) as u8;
let g = (color.1 as u16 * alpha as u16 / 255) as u8;
let b = (color.2 as u16 * alpha as u16 / 255) as u8;
self.buf[idx] = r;
self.buf[idx + 1] = g;
self.buf[idx + 2] = b;
self.buf[idx + 3] = 0xff;
}
}
}
#[test]
fn render_text_draws_pixels() {
let mut surf = Surface::new();
render_text(
&mut surf,
FONT_DATA,
(0, 0),
"A",
Color(255, 255, 255, 255),
16.0,
)
.unwrap();
assert!(surf.buf.iter().any(|&p| p != 0));
}
}