use std::collections::HashMap;
use crate::mesh::Vertex;
pub use crate::text_label::{TextLabel, TextLabelBuilder, TextLabelHandle, HorizontalAlignment, VerticalAlignment};
#[cfg(feature = "default-fonts")]
pub use crate::text_label::DefaultFont;
use crate::text_label::rasterize_text;
pub struct TextOverlay {
pub labels: HashMap<usize, TextLabel>,
pub(crate) next_id: usize,
pub fonts: Vec<(String, fontdue::Font)>,
}
impl Default for TextOverlay {
fn default() -> Self { Self::new() }
}
impl std::fmt::Debug for TextOverlay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextOverlay")
.field("labels", &self.labels)
.field("next_id", &self.next_id)
.field("fonts (count)", &self.fonts.len())
.finish()
}
}
impl TextOverlay {
pub fn new() -> Self {
#[allow(unused_mut)]
let mut this = Self { labels: HashMap::new(), next_id: 0, fonts: Vec::new() };
#[cfg(feature = "default-fonts")]
this.load_default_fonts();
this
}
pub fn add_font(
&mut self,
font_id: impl Into<String>,
font_bytes: &[u8],
) -> Result<(), String> {
let id = font_id.into();
if id.is_empty() {
return Err("TextOverlay: font_id must not be empty".to_string());
}
if self.fonts.iter().any(|(k, _)| k == &id) {
return Err(format!("TextOverlay: font id '{id}' is already registered"));
}
let font = fontdue::Font::from_bytes(font_bytes, fontdue::FontSettings::default())
.map_err(|e| format!("TextOverlay: failed to load font '{id}': {e}"))?;
self.fonts.push((id, font));
Ok(())
}
pub fn font_count(&self) -> usize { self.fonts.len() }
pub fn has_font(&self, font_id: &str) -> bool {
self.fonts.iter().any(|(k, _)| k == font_id)
}
#[cfg(feature = "default-fonts")]
pub fn load_default_fonts(&mut self) {
const SANS_BYTES: &[u8] = include_bytes!("fonts/sans.ttf");
const SERIF_BYTES: &[u8] = include_bytes!("fonts/serif.ttf");
const MONO_BYTES: &[u8] = include_bytes!("fonts/mono.ttf");
for (name, bytes) in [("sans", SANS_BYTES), ("serif", SERIF_BYTES), ("mono", MONO_BYTES)] {
if bytes.is_empty() {
eprintln!(
"[vertra] default-fonts: `src/fonts/{name}.ttf` is empty, \
the downloaded binary might be corrupted."
);
continue;
}
if self.has_font(name) { continue; }
if let Err(e) = self.add_font(name, bytes) {
eprintln!("[vertra] default-fonts: failed to parse `src/fonts/{name}.ttf`: {e}");
}
}
}
pub fn add_label(&mut self, text: impl Into<String>) -> TextLabelBuilder<'_> {
TextLabelBuilder {
overlay: self,
text: text.into(),
x: 0.0,
y: 0.0,
font_size: 16.0,
color: [1.0, 1.0, 1.0, 1.0],
font_id: String::new(), visible: true,
zindex: None, horizontal_alignment: HorizontalAlignment::Left,
vertical_alignment: VerticalAlignment::Top,
}
}
pub fn labels(&self) -> impl Iterator<Item = &TextLabel> {
self.labels.values()
}
pub fn label_count(&self) -> usize { self.labels.len() }
pub fn clear(&mut self) { self.labels.clear(); }
fn get_font(&self, font_id: &str) -> Option<&fontdue::Font> {
if font_id.is_empty() {
self.fonts.first().map(|(_, f)| f)
} else {
self.fonts.iter().find(|(k, _)| k == font_id).map(|(_, f)| f)
}
}
pub(crate) fn rasterize_label(&self, label: &TextLabel) -> Option<(Vec<u8>, u32, u32)> {
if self.fonts.is_empty() { return None; }
let font = self.get_font(&label.font_id)?;
Some(rasterize_text(font, &label.text, label.font_size, label.color))
}
pub(crate) fn build_quad(x: f32, y: f32, w: f32, h: f32) -> (Vec<Vertex>, Vec<u32>) {
let verts = vec![
Vertex { position: [x, y, 0.0], color: [1.0; 3], uv: [0.0, 0.0] },
Vertex { position: [x + w, y, 0.0], color: [1.0; 3], uv: [1.0, 0.0] },
Vertex { position: [x + w, y + h, 0.0], color: [1.0; 3], uv: [1.0, 1.0] },
Vertex { position: [x, y + h, 0.0], color: [1.0; 3], uv: [0.0, 1.0] },
];
(verts, vec![0u32, 1, 2, 0, 2, 3])
}
}