|
#[derive(Clone, Eq, PartialEq)] |
|
pub(crate) struct TextRenderBuffer { |
|
pub width: i32, |
|
pub height: i32, |
|
pub baseline: i32, |
|
pub data: Vec<u32>, |
|
pub text: String, |
|
} |
|
|
|
impl TextRenderBuffer { |
|
pub fn into_image(&self) -> raqote::Image { |
|
raqote::Image { |
|
width: self.width as i32, |
|
height: self.height as i32, |
|
data: &self.data, |
|
} |
|
} |
|
|
|
pub fn render(&self, ctx: &mut raqote::DrawTarget, point: raqote::Point) -> &Self { |
|
ctx.draw_image_at(point.x, point.y, &self.into_image(), &raqote::DrawOptions { |
|
blend_mode: raqote::BlendMode::SrcAtop, |
|
alpha: 1., |
|
antialias: raqote::AntialiasMode::Gray, |
|
}); |
|
|
|
return self; |
|
} |
|
|
|
pub fn measure(&self) -> Size { |
|
return Size::new(self.width as f64, self.height as f64, self.baseline as f64); |
|
} |
|
} |
|
|
|
impl std::fmt::Debug for TextRenderBuffer { |
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
write!(f, "RenderBuffer {{ {}x{}_{} '{}' }}", self.width, self.height, self.baseline, self.text).unwrap(); |
|
Ok(()) |
|
} |
|
} |
|
|
|
pub(crate) fn get_font<'a>() -> rusttype::Font<'a> { |
|
// TODO: Replace with dynamic font-lookup |
|
return rusttype::Font::try_from_vec(std::fs::read("/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf").unwrap()).unwrap(); |
|
} |
|
|
|
#[derive(Eq, PartialEq, Debug, Clone, Hash)] |
|
pub struct TextProps { |
|
pub text: String, |
|
pub size: i32, |
|
} |
|
|
|
unsafe impl Send for TextProps {} |
|
|
|
unsafe impl Send for TextRenderBuffer {} |
|
|
|
fn render_text<'a>(props: TextProps, colour: raqote::Color) -> TextRenderBuffer { |
|
let font = get_font(); |
|
let size = rusttype::Scale::uniform(props.size as f32 * 1.333f32); |
|
let metrics = font.v_metrics(size); |
|
|
|
let glyphs: Vec<_> = font |
|
.layout(&props.text, size, rusttype::point(0., 0. + metrics.ascent)) |
|
.collect(); |
|
|
|
let mut image = { |
|
let width: i32 = { |
|
let min_x = glyphs |
|
.first() |
|
.map(|g| g.pixel_bounding_box().unwrap().min.x) |
|
.unwrap(); |
|
let max_x = glyphs |
|
.last() |
|
.map(|g| g.pixel_bounding_box().unwrap().max.x) |
|
.unwrap(); |
|
(max_x - min_x + 1) as i32 |
|
}; |
|
let height: i32 = (metrics.ascent - metrics.descent).ceil() as i32; |
|
|
|
TextRenderBuffer { |
|
width, |
|
height, |
|
text: props.text.clone(), |
|
baseline: metrics.ascent as i32, |
|
data: vec![0u32; (width * height) as usize], |
|
} |
|
}; |
|
|
|
// Loop through the glyphs in the text, positing each one on a line |
|
for glyph in glyphs { |
|
if let Some(bounding_box) = glyph.pixel_bounding_box() { |
|
glyph.draw(|x, y, v| { |
|
let index = ((y as usize + bounding_box.min.y as usize) * image.width as usize) + (x as usize + bounding_box.min.x as usize); |
|
|
|
image.data[index] = u32::from_be_bytes([ |
|
(colour.a() as f32 * v) as u8, |
|
(colour.r() as f32 * v) as u8, |
|
(colour.g() as f32 * v) as u8, |
|
(colour.b() as f32 * v) as u8, |
|
]); |
|
}); |
|
} |
|
} |
|
|
|
return image; |
|
} |
|
|
|
lazy_static::lazy_static!(static ref CACHE: std::sync::Mutex<std::collections::HashMap<TextProps, std::sync::Arc<TextRenderBuffer>>> = std::sync::Mutex::new(std::collections::HashMap::new());); |
|
pub(crate) fn text(props: &TextProps, colour: raqote::Color) -> std::sync::Arc<TextRenderBuffer> { |
|
let mut cache = CACHE.lock().unwrap(); |
|
|
|
if let Some(texture) = cache.get(props) { |
|
return texture.clone(); |
|
} |
|
|
|
let buffer = std::sync::Arc::new(render_text(props.clone(), colour)); |
|
|
|
cache.insert(props.clone(), buffer); |
|
return cache.get(props).unwrap().clone(); |
|
} |
|
|
|
pub struct Size { |
|
pub width: f64, |
|
pub height: f64, |
|
pub baseline: f64, |
|
} |
|
|
|
impl Size { |
|
pub fn new(width: f64, height: f64, baseline: f64) -> Self { |
|
Size { width, height, baseline } |
|
} |
|
} |
|
|
|
fn measure_text(props: &TextProps) -> Size { |
|
let font = get_font(); |
|
let size = rusttype::Scale::uniform(props.size as f32 * 1.333f32); |
|
let metrics = font.v_metrics(size); |
|
|
|
let glyphs: Vec<_> = font |
|
.layout(&props.text, size, rusttype::point(0., 0. + metrics.ascent)) |
|
.collect(); |
|
|
|
return Size::new({ |
|
let min_x = glyphs |
|
.first() |
|
.map(|g| g.pixel_bounding_box().unwrap().min.x) |
|
.unwrap(); |
|
let max_x = glyphs |
|
.last() |
|
.map(|g| g.pixel_bounding_box().unwrap().max.x) |
|
.unwrap(); |
|
(max_x - min_x + 1) as f64 |
|
}, (metrics.ascent - metrics.descent).ceil() as f64, metrics.ascent as f64); |
|
} |
|
|
|
pub(crate) fn measure(props: &TextProps) -> Size { |
|
let cache = CACHE.lock().unwrap(); |
|
|
|
if let Some(texture) = cache.get(props) { |
|
return Size::new(texture.width as f64, texture.height as f64, texture.baseline as f64); |
|
} |
|
|
|
return measure_text(props) |
|
} |