use super::{
gpu::text::{Extra, TextRenderer},
Canvas, Color, Draw, DrawParam, Drawable, GraphicsContext, Rect,
};
use crate::{context::Has, filesystem::Filesystem, GameError, GameResult};
use glyph_brush::{ab_glyph, FontId, GlyphCruncher};
use std::{collections::HashMap, io::Read, path::Path};
#[derive(Debug)]
pub struct FontData {
pub(crate) font: ab_glyph::FontArc,
}
impl FontData {
#[allow(unused_results)]
pub fn from_path(fs: &impl Has<Filesystem>, path: impl AsRef<Path>) -> GameResult<Self> {
let fs = fs.retrieve();
let mut bytes = vec![];
fs.open(path)?.read_to_end(&mut bytes)?;
Ok(FontData {
font: ab_glyph::FontArc::try_from_vec(bytes)?,
})
}
pub fn from_vec(data: Vec<u8>) -> GameResult<Self> {
Ok(FontData {
font: ab_glyph::FontArc::try_from_vec(data)?,
})
}
pub fn from_slice(data: &'static [u8]) -> GameResult<Self> {
Ok(FontData {
font: ab_glyph::FontArc::try_from_slice(data)?,
})
}
}
pub use glyph_brush::ab_glyph::PxScale;
#[derive(Debug, Default, Clone)]
pub struct TextFragment {
pub text: String,
pub font: Option<String>,
pub scale: Option<PxScale>,
pub color: Option<Color>,
}
impl TextFragment {
pub fn new(text: impl Into<String>) -> Self {
TextFragment {
text: text.into(),
..Default::default()
}
}
pub fn font(self, font: impl Into<String>) -> Self {
TextFragment {
font: Some(font.into()),
..self
}
}
pub fn scale(self, scale: impl Into<PxScale>) -> Self {
TextFragment {
scale: Some(scale.into()),
..self
}
}
pub fn color(self, color: impl Into<Color>) -> Self {
TextFragment {
color: Some(color.into()),
..self
}
}
}
impl<S: Into<String>> From<S> for TextFragment {
fn from(text: S) -> Self {
TextFragment::new(text)
}
}
#[derive(Debug, Clone)]
pub struct Text {
fragments: Vec<TextFragment>,
layout: TextLayout,
wrap: bool,
bounds: mint::Vector2<f32>,
scale: PxScale,
font: String,
}
impl Default for Text {
fn default() -> Self {
Self {
fragments: Vec::new(),
layout: TextLayout::top_left(),
wrap: true,
bounds: mint::Vector2::<f32> {
x: f32::INFINITY,
y: f32::INFINITY,
},
scale: 16.0.into(),
font: "LiberationMono-Regular".into(),
}
}
}
impl Text {
pub fn new(fragment: impl Into<TextFragment>) -> Self {
let mut text = Text::default();
let _ = text.add(fragment);
text
}
pub fn add(&mut self, fragment: impl Into<TextFragment>) -> &mut Self {
self.fragments.push(fragment.into());
self
}
pub fn clear(&mut self) {
self.fragments.clear();
}
#[inline]
pub fn fragments(&self) -> &[TextFragment] {
&self.fragments
}
#[inline]
pub fn fragments_mut(&mut self) -> &mut [TextFragment] {
&mut self.fragments
}
pub fn set_bounds(&mut self, bounds: impl Into<mint::Vector2<f32>>) -> &mut Self {
self.bounds = bounds.into();
self
}
pub fn set_layout(&mut self, layout: TextLayout) -> &mut Self {
self.layout = layout;
self
}
pub fn set_wrap(&mut self, wrap: bool) -> &mut Self {
self.wrap = wrap;
self
}
pub fn set_font(&mut self, font: impl Into<String>) -> &mut Self {
self.font = font.into();
self
}
pub fn set_scale(&mut self, scale: impl Into<PxScale>) -> &mut Self {
self.scale = scale.into();
self
}
pub fn contents(&self) -> String {
self.fragments.iter().map(|f| f.text.as_str()).collect()
}
pub fn glyph_positions(
&self,
gfx: &impl Has<GraphicsContext>,
) -> GameResult<Vec<mint::Point2<f32>>> {
let gfx = gfx.retrieve();
Ok(gfx
.text
.glyph_brush
.borrow_mut()
.glyphs(self.as_section(&gfx.fonts, DrawParam::default())?)
.map(|glyph| mint::Point2::<f32> {
x: glyph.glyph.position.x,
y: glyph.glyph.position.y,
})
.collect())
}
#[inline]
pub fn measure(&self, gfx: &impl Has<GraphicsContext>) -> GameResult<mint::Vector2<f32>> {
let gfx = gfx.retrieve();
self.measure_raw(&gfx.text, &gfx.fonts)
}
pub(crate) fn measure_raw(
&self,
text: &TextRenderer,
fonts: &HashMap<String, FontId>,
) -> GameResult<mint::Vector2<f32>> {
Ok(text
.glyph_brush
.borrow_mut()
.glyph_bounds(self.as_section(fonts, DrawParam::default())?)
.map(|rect| mint::Vector2::<f32> {
x: rect.width(),
y: rect.height(),
})
.unwrap_or_else(|| mint::Vector2::<f32> { x: 0., y: 0. }))
}
pub(crate) fn as_section<'a>(
&'a self,
fonts: &HashMap<String, FontId>,
param: DrawParam,
) -> GameResult<glyph_brush::Section<'a, Extra>> {
Ok(glyph_brush::Section {
screen_position: (0., 0.),
bounds: (self.bounds.x, self.bounds.y),
layout: if self.wrap {
glyph_brush::Layout::default_wrap()
} else {
glyph_brush::Layout::default_single_line()
}
.h_align(self.layout.h_align.into())
.v_align(self.layout.v_align.into()),
text: self
.fragments
.iter()
.map(|text| {
let font = text.font.as_ref().unwrap_or(&self.font);
Ok(glyph_brush::Text {
text: &text.text,
scale: text.scale.unwrap_or(self.scale),
font_id: *fonts
.get(font)
.ok_or_else(|| GameError::FontSelectError(font.clone()))?,
extra: Extra {
color: text.color.unwrap_or(param.color).into(),
transform: param.transform.to_bare_matrix().into(),
},
})
})
.collect::<GameResult<Vec<_>>>()?,
})
}
}
impl Drawable for Text {
fn draw(&self, canvas: &mut Canvas, param: impl Into<DrawParam>) {
canvas.push_draw(Draw::BoundedText { text: self.clone() }, param.into());
}
fn dimensions(&self, gfx: &impl Has<GraphicsContext>) -> Option<Rect> {
let bounds = self.measure(gfx).ok()?;
Some(Rect {
x: 0.,
y: 0.,
w: bounds.x,
h: bounds.y,
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum TextAlign {
Begin,
Middle,
End,
}
impl From<TextAlign> for glyph_brush::HorizontalAlign {
fn from(align: TextAlign) -> Self {
match align {
TextAlign::Begin => glyph_brush::HorizontalAlign::Left,
TextAlign::Middle => glyph_brush::HorizontalAlign::Center,
TextAlign::End => glyph_brush::HorizontalAlign::Right,
}
}
}
impl From<TextAlign> for glyph_brush::VerticalAlign {
fn from(align: TextAlign) -> Self {
match align {
TextAlign::Begin => glyph_brush::VerticalAlign::Top,
TextAlign::Middle => glyph_brush::VerticalAlign::Center,
TextAlign::End => glyph_brush::VerticalAlign::Bottom,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextLayout {
pub h_align: TextAlign,
pub v_align: TextAlign,
}
impl TextLayout {
pub fn top_left() -> Self {
TextLayout {
h_align: TextAlign::Begin,
v_align: TextAlign::Begin,
}
}
pub fn center() -> Self {
TextLayout {
h_align: TextAlign::Middle,
v_align: TextAlign::Middle,
}
}
}