use std::collections::HashMap;
#[cfg(feature = "graphics")]
use graphics::{
character::CharacterCache, math::Matrix2d, text as draw_text, Graphics, ImageSize, Transformed,
};
use rusttype::{Error, Font, GlyphId, Scale};
use crate::math::{Rectangle, Scalar, Vector2, ZeroOneTwo};
use crate::Color;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Justification {
Left,
Centered,
Right,
}
pub type PositionedLines<V> = Vec<(V, String)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Resize {
NoLarger,
Max,
None,
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct TextFormat<S>
where
S: Scalar,
{
pub font_size: u32,
pub just: Justification,
pub line_spacing: S,
pub first_line_indent: usize,
pub lines_indent: usize,
pub color: Color,
pub resize: Resize,
}
impl<S> From<u32> for TextFormat<S>
where
S: Scalar,
{
fn from(font_size: u32) -> Self {
TextFormat::new(font_size)
}
}
impl<S> TextFormat<S>
where
S: Scalar,
{
pub fn new(font_size: u32) -> TextFormat<S> {
TextFormat {
font_size,
just: Justification::Left,
line_spacing: S::ONE,
first_line_indent: 0,
lines_indent: 0,
color: [0.0, 0.0, 0.0, 1.0],
resize: Resize::NoLarger,
}
}
pub fn left(mut self) -> Self {
self.just = Justification::Left;
self
}
pub fn centered(mut self) -> Self {
self.just = Justification::Centered;
self
}
pub fn right(mut self) -> Self {
self.just = Justification::Right;
self
}
pub fn font_size(mut self, font_size: u32) -> Self {
self.font_size = font_size;
self
}
pub fn line_spacing(mut self, line_spacing: S) -> Self {
self.line_spacing = line_spacing;
self
}
pub fn map_line_spacing<U>(&self) -> TextFormat<U>
where
U: Scalar + From<S>,
{
TextFormat {
font_size: self.font_size,
just: self.just,
line_spacing: U::from(self.line_spacing),
first_line_indent: self.first_line_indent,
lines_indent: self.lines_indent,
color: self.color,
resize: self.resize,
}
}
pub fn first_line_indent(mut self, first_line_indent: usize) -> Self {
self.first_line_indent = first_line_indent;
self
}
pub fn lines_indent(mut self, lines_indent: usize) -> Self {
self.lines_indent = lines_indent;
self
}
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn resize(mut self, resize: Resize) -> Self {
self.resize = resize;
self
}
pub fn resize_font(mut self, max_size: u32) -> Self {
match self.resize {
Resize::NoLarger => self.font_size = self.font_size.min(max_size),
Resize::Max => self.font_size = max_size,
Resize::None => (),
}
self
}
}
pub trait CharacterWidthCache {
type Scalar: Scalar;
fn char_width(&mut self, character: char, font_size: u32) -> Self::Scalar;
fn width(&mut self, text: &str, font_size: u32) -> Self::Scalar {
text.chars()
.map(|c| self.char_width(c, font_size))
.fold(Self::Scalar::ZERO, std::ops::Add::add)
}
fn format_lines<F>(&mut self, text: &str, max_width: Self::Scalar, format: F) -> Vec<String>
where
F: Into<TextFormat<Self::Scalar>>,
{
let format = format.into();
let mut sized_lines = Vec::new();
let mut first_line = false;
for line in text.lines() {
let mut sized_line = String::new();
let indent = (0..if first_line {
format.first_line_indent
} else {
format.lines_indent
})
.map(|_| ' ')
.collect::<String>();
sized_line.push_str(&indent);
let mut curr_width = self.width(&indent, format.font_size);
for word in line.split_whitespace() {
let width = self.width(word, format.font_size);
if !(curr_width + width < max_width || curr_width == Self::Scalar::ZERO) {
sized_line.pop();
sized_lines.push(sized_line);
first_line = false;
sized_line = String::new();
let indent = (0..if first_line {
format.first_line_indent
} else {
format.lines_indent
})
.map(|_| ' ')
.collect::<String>();
sized_line.push_str(&indent);
curr_width = self.width(&indent, format.font_size);
}
sized_line.push_str(word);
sized_line.push(' ');
curr_width = curr_width + width + self.char_width(' ', format.font_size);
}
sized_line.pop();
sized_lines.push(sized_line);
first_line = false;
}
sized_lines
}
fn max_line_width<F>(&mut self, text: &str, max_width: Self::Scalar, format: F) -> Self::Scalar
where
F: Into<TextFormat<Self::Scalar>>,
{
let format = format.into();
let lines = self.format_lines(text, max_width, format);
lines
.into_iter()
.map(|line| self.width(&line, format.font_size))
.max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.unwrap_or(Self::Scalar::ZERO)
}
fn justify_text<R, F>(&mut self, text: &str, rect: R, format: F) -> PositionedLines<R::Vector>
where
R: Rectangle<Scalar = Self::Scalar>,
F: Into<TextFormat<Self::Scalar>>,
{
let format = format.into();
self.format_lines(text, rect.width(), format)
.into_iter()
.enumerate()
.map(|(i, line)| {
let y_offset = rect.top()
+ format.font_size.into()
+ Self::Scalar::from(i as u32) * format.font_size.into() * format.line_spacing;
use self::Justification::*;
let line_width = self.width(&line, format.font_size);
let x_offset = match format.just {
Left => rect.left(),
Centered => rect.center().x() - line_width / Self::Scalar::TWO,
Right => rect.right() - line_width,
};
(R::Vector::new(x_offset, y_offset), line)
})
.collect()
}
fn text_fits_horizontal<R, F>(&mut self, text: &str, rect: R, format: F) -> bool
where
R: Rectangle<Scalar = Self::Scalar>,
F: Into<TextFormat<Self::Scalar>>,
{
self.max_line_width(text, rect.width(), format) < rect.width()
}
fn text_fits_vertical<R, F>(&mut self, text: &str, rect: R, format: F) -> bool
where
R: Rectangle<Scalar = Self::Scalar>,
F: Into<TextFormat<Self::Scalar>>,
{
let format = format.into();
let lines = self.format_lines(text, rect.width(), format);
if lines.is_empty() {
return true;
}
let last_line_y = rect.top()
+ format.font_size.into()
+ Self::Scalar::from((lines.len() - 1) as u32)
* format.font_size.into()
* format.line_spacing;
last_line_y < rect.bottom()
}
fn text_fits<R, F>(&mut self, text: &str, rect: R, format: F) -> bool
where
R: Rectangle<Scalar = Self::Scalar>,
F: Into<TextFormat<Self::Scalar>>,
{
let format = format.into();
self.text_fits_horizontal(text, rect, format) && self.text_fits_vertical(text, rect, format)
}
fn fit_max_font_size<R, F>(&mut self, text: &str, rect: R, format: F) -> u32
where
R: Rectangle<Scalar = Self::Scalar>,
F: Into<TextFormat<Self::Scalar>>,
{
let mut format = format.into();
while !self.text_fits(text, rect, format) {
format.font_size -= 1;
}
format.font_size
}
fn fit_min_height<R, F>(
&mut self,
text: &str,
mut rect: R,
format: F,
delta: Self::Scalar,
) -> Self::Scalar
where
R: Rectangle<Scalar = Self::Scalar>,
F: Into<TextFormat<Self::Scalar>>,
{
let format = format.into();
let delta = delta.abs().max(Self::Scalar::ONE);
while self.text_fits_vertical(text, rect, format) {
rect = rect.with_size(R::Vector::new(rect.width(), rect.height() - delta))
}
while !self.text_fits_vertical(text, rect, format) {
rect = rect.with_size(R::Vector::new(rect.width(), rect.height() + delta))
}
rect.height()
}
fn fit_min_width<R, F>(
&mut self,
text: &str,
mut rect: R,
format: F,
delta: Self::Scalar,
) -> Self::Scalar
where
R: Rectangle<Scalar = Self::Scalar>,
F: Into<TextFormat<Self::Scalar>>,
{
let format = format.into();
let delta = delta.abs().max(Self::Scalar::ONE);
while self.text_fits(text, rect, format) {
rect = rect.with_size(R::Vector::new(rect.width() - delta, rect.height()))
}
while !self.text_fits(text, rect, format) {
rect = rect.with_size(R::Vector::new(rect.width() + delta, rect.height()))
}
rect.width()
}
}
#[derive(Clone)]
pub struct Glyphs<'f, S = f64>
where
S: Scalar,
{
widths: HashMap<(u32, char), S>,
font: Font<'f>,
}
impl<'f, S> Glyphs<'f, S>
where
S: Scalar,
{
pub fn from_bytes(bytes: &'f [u8]) -> Result<Glyphs<'f, S>, Error> {
Ok(Glyphs {
widths: HashMap::new(),
font: Font::from_bytes(bytes)?,
})
}
pub fn from_font(font: Font<'f>) -> Glyphs<'f, S> {
Glyphs {
widths: HashMap::new(),
font,
}
}
}
impl<'f, S> CharacterWidthCache for Glyphs<'f, S>
where
S: Scalar,
{
type Scalar = S;
fn char_width(&mut self, character: char, font_size: u32) -> Self::Scalar {
let font = &self.font;
*self
.widths
.entry((font_size, character))
.or_insert_with(|| {
let scale = Scale::uniform(font_size as f32);
let glyph = font.glyph(character).scaled(scale);
let glyph = if glyph.id() == GlyphId(0) && glyph.shape().is_none() {
font.glyph('\u{FFFD}').scaled(scale)
} else {
glyph
};
glyph.h_metrics().advance_width.into()
})
}
}
#[cfg(feature = "graphics")]
impl<C> CharacterWidthCache for C
where
C: CharacterCache,
{
type Scalar = f64;
fn char_width(&mut self, character: char, font_size: u32) -> Self::Scalar {
if let Ok(texture) = <Self as CharacterCache>::character(self, font_size, character) {
texture.advance_size.x()
} else {
panic!("CharacterWidthCache::character returned Err")
}
}
}
#[cfg(feature = "graphics")]
pub fn justified_text<R, F, T, C, G>(
text: &str,
rect: R,
format: F,
glyphs: &mut C,
transform: Matrix2d,
graphics: &mut G,
) -> Result<(), C::Error>
where
R: Rectangle<Scalar = f64>,
F: Into<TextFormat<R::Scalar>>,
T: ImageSize,
C: CharacterCache<Texture = T>,
G: Graphics<Texture = T>,
{
let format = format.into();
for (pos, line) in glyphs.justify_text(text, rect, format.map_line_spacing::<f64>()) {
draw_text(
format.color,
format.font_size,
&line,
glyphs,
transform.trans(pos.x(), pos.y()),
graphics,
)?;
}
Ok(())
}