Skip to content

Instantly share code, notes, and snippets.

@J-Cake
Last active July 15, 2023 01:10
Show Gist options
  • Save J-Cake/ddccf99d3f7d6fc947fc60204aa41e09 to your computer and use it in GitHub Desktop.
Save J-Cake/ddccf99d3f7d6fc947fc60204aa41e09 to your computer and use it in GitHub Desktop.
Render text into raqote using `rusttype`, because raqote text rendering is ... lacking

Rendering text

Easy peasy....

mod text;

let mut ctx = raqote::DrawTarget::new(720, 480);
let buffer = text::text(&text::TextProps { text: format!("Hello World"), size: 14 });

buffer.render(&mut ctx, raqote::Point::new(10., 10.));

Centering Text

mod text;

let mut ctx = raqote::DrawTarget::new(720, 480);
let buf = text::text(&text::TextProps { text: format!("Centre Me!"), size: 14 });

buf.render(&mut ctx, raqote::Point::new(ctx.width() / 2 - buf.width / 2, ctx.height() / 2 - buf.height / 2 + (buf.height - buf.baseline) / 2));

Things like caching are taken care of ... it's fiddly, but it works

Advanced text rendering

Don't. Don't go there.

Layouting

Things like paragraphs are pretty important, but equally tricky to get right. So I've left it as an exercise to the reader :P

Heck we don't even have line breaks...

#[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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment