#![deny(missing_docs)]
use std::{collections::HashMap, sync::Mutex};
#[cfg(feature = "graphics")]
use graphics::{character::CharacterCache, math::Matrix2d, Graphics, ImageSize, Transformed};
use once_cell::sync::Lazy;
use rusttype::{Error, Font, GlyphId, Scale};
use vector2math::*;
use crate::color::Color;
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub enum Indent {
Space(usize),
Point(f64),
}
impl Indent {
pub fn in_points<C>(self, cache: &mut C, font_size: u32) -> f64
where
C: CharacterWidthCache,
{
match self {
Indent::Space(spaces) => cache.char_width(' ', font_size) * spaces as f64,
Indent::Point(points) => points,
}
}
}
pub type PositionedList<V, I> = Vec<(V, I)>;
pub type PositionedLines<V> = PositionedList<V, String>;
pub type PositionedLinesMeta<V, M> = PositionedList<V, (String, M)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Resize {
NoLarger,
Max,
None,
}
impl Default for Resize {
fn default() -> Self {
Resize::NoLarger
}
}
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct TextFormat {
pub font_size: u32,
pub line_spacing: f64,
pub first_line_indent: Indent,
pub lines_indent: Indent,
pub color: Color,
pub resize: Resize,
}
impl From<u32> for TextFormat {
fn from(font_size: u32) -> Self {
TextFormat::new(font_size)
}
}
static DEFAULT_TEXT_FORMAT: Lazy<Mutex<TextFormat>> = Lazy::new(|| {
Mutex::new(TextFormat {
font_size: 30,
line_spacing: 1.1,
first_line_indent: Indent::Space(0),
lines_indent: Indent::Space(0),
color: color::WHITE,
resize: Resize::NoLarger,
})
});
impl Default for TextFormat {
fn default() -> TextFormat {
*DEFAULT_TEXT_FORMAT
.lock()
.expect("fit-text default TextFormat thread panicked")
}
}
impl TextFormat {
pub fn new(font_size: u32) -> TextFormat {
TextFormat {
font_size,
..Default::default()
}
}
pub fn set_as_default(&self) {
*DEFAULT_TEXT_FORMAT
.lock()
.expect("fit-text default TextFormat thread panicked") = *self;
}
pub fn font_size(self, font_size: u32) -> Self {
TextFormat { font_size, ..self }
}
pub fn line_spacing(self, line_spacing: f64) -> Self {
TextFormat {
line_spacing,
..self
}
}
pub fn first_line_indent(self, first_line_indent: Indent) -> Self {
TextFormat {
first_line_indent,
..self
}
}
pub fn lines_indent(self, lines_indent: Indent) -> Self {
TextFormat {
lines_indent,
..self
}
}
pub fn color(self, color: Color) -> Self {
TextFormat { color, ..self }
}
pub fn resize(self, resize: Resize) -> Self {
TextFormat { resize, ..self }
}
pub fn resize_font(self, max_size: u32) -> Self {
TextFormat {
font_size: match self.resize {
Resize::NoLarger => self.font_size.min(max_size),
Resize::Max => max_size,
Resize::None => self.font_size,
},
..self
}
}
}
pub trait CharacterWidthCache: Sized {
fn char_width(&mut self, character: char, font_size: u32) -> f64;
fn width(&mut self, text: &str, font_size: u32) -> f64 {
text.chars().map(|c| self.char_width(c, font_size)).sum()
}
fn format_lines<S, F>(&mut self, text: S, max_width: f64, format: F) -> Vec<String>
where
S: AsRef<str>,
F: Into<TextFormat>,
{
let format = format.into();
let mut sized_lines = Vec::new();
let mut first_line = true;
for line in text.as_ref().lines() {
let mut sized_line = String::new();
let indent = if first_line {
format.first_line_indent
} else {
format.lines_indent
};
let indent_width = indent.in_points(self, format.font_size);
let mut curr_width = indent_width;
for word in line.split_whitespace() {
let width = self.width(word, format.font_size);
let fits_here = curr_width + width < max_width;
let first_word_on_line = (curr_width - indent_width).abs() < f64::EPSILON;
let fits_at_all = width < max_width;
if !(fits_here || first_word_on_line && !fits_at_all) {
sized_line.pop();
sized_lines.push(sized_line);
first_line = false;
sized_line = String::new();
let indent = if first_line {
format.first_line_indent
} else {
format.lines_indent
};
let indent_width = indent.in_points(self, format.font_size);
curr_width = indent_width;
}
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<S, F>(&mut self, text: S, max_width: f64, format: F) -> f64
where
S: AsRef<str>,
F: Into<TextFormat>,
{
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(0.0)
}
fn justify_text<S, R, F>(&mut self, text: S, rect: R, format: F) -> PositionedLines<R::Vector>
where
S: AsRef<str>,
R: Rectangle<Scalar = f64>,
F: Into<TextFormat>,
{
let format = format.into();
self.format_lines(text, rect.width(), format)
.into_iter()
.enumerate()
.map(|(i, line)| {
let y_offset = rect.top()
+ f64::from(format.font_size)
+ i as f64 * f64::from(format.font_size) * format.line_spacing;
let x_offset = rect.left()
+ if i == 0 {
format.first_line_indent.in_points(self, format.font_size)
} else {
format.lines_indent.in_points(self, format.font_size)
};
(R::Vector::new(x_offset, y_offset), line)
})
.collect()
}
fn justify_meta_fragments<I, M, S, R, F>(
&mut self,
fragments: I,
rect: R,
format: F,
) -> PositionedLinesMeta<R::Vector, M>
where
I: IntoIterator<Item = (S, M)>,
M: Clone,
S: AsRef<str>,
R: Rectangle<Scalar = f64>,
F: Into<TextFormat>,
{
let format = format.into();
let mut plm = Vec::new();
let mut vert = 0.0;
let space_width = self.char_width(' ', format.font_size);
let mut horiz = format.first_line_indent.in_points(self, format.font_size);
for (text, meta) in fragments {
let sub_rect = R::new(
R::Vector::new(rect.left(), rect.top() + vert),
R::Vector::new(rect.width(), rect.height() - vert),
);
let sub_format = format.first_line_indent(Indent::Point(horiz));
let positioned_lines = self.justify_text(text, sub_rect, sub_format);
horiz = match positioned_lines.len() {
0 => 0.0,
1 => horiz + self.width(&positioned_lines[0].1, format.font_size),
_ => self.width(&positioned_lines.last().unwrap().1, format.font_size),
} + space_width;
if !positioned_lines.is_empty() {
vert += (positioned_lines.len() - 1) as f64
* f64::from(format.font_size)
* format.line_spacing;
}
plm.extend(
positioned_lines
.into_iter()
.map(|(v, l)| (v, (l, meta.clone()))),
);
}
plm
}
fn text_fits_horizontal<R, F>(&mut self, text: &str, rect: R, format: F) -> bool
where
R: Rectangle<Scalar = f64>,
F: Into<TextFormat>,
{
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 = f64>,
F: Into<TextFormat>,
{
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()
+ f64::from(format.font_size)
+ (lines.len() - 1) as f64 * f64::from(format.font_size) * 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 = f64>,
F: Into<TextFormat>,
{
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 = f64>,
F: Into<TextFormat>,
{
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: f64) -> f64
where
R: Rectangle<Scalar = f64>,
F: Into<TextFormat>,
{
let format = format.into();
let delta = delta.abs().max(1.0);
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: f64) -> f64
where
R: Rectangle<Scalar = f64>,
F: Into<TextFormat>,
{
let format = format.into();
let delta = delta.abs().max(1.0);
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()
}
fn ideal_text_size<R, F>(&mut self, text: &str, rect: R, format: F) -> TextFormat
where
R: Rectangle<Scalar = f64>,
F: Into<TextFormat>,
{
let mut format = format.into();
match format.resize {
Resize::None => {}
Resize::NoLarger => {
while format.font_size > 0 && !self.text_fits(text, rect, format) {
format.font_size -= 1;
}
}
Resize::Max => {
while format.font_size > 0 && !self.text_fits(text, rect, format) {
format.font_size -= 1;
}
while self.text_fits(text, rect, format) {
format.font_size += 1;
}
format.font_size -= 1;
}
}
format
}
}
#[derive(Clone)]
pub struct BasicGlyphs<'f> {
widths: HashMap<(u32, char), f64>,
font: Font<'f>,
}
impl<'f> BasicGlyphs<'f> {
pub fn from_bytes(bytes: &'f [u8]) -> Result<BasicGlyphs<'f>, Error> {
Ok(BasicGlyphs {
widths: HashMap::new(),
font: Font::from_bytes(bytes)?,
})
}
pub fn from_font(font: Font<'f>) -> BasicGlyphs<'f> {
BasicGlyphs {
widths: HashMap::new(),
font,
}
}
}
impl<'f> CharacterWidthCache for BasicGlyphs<'f> {
fn char_width(&mut self, character: char, font_size: u32) -> f64 {
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,
{
fn char_width(&mut self, character: char, font_size: u32) -> f64 {
if let Ok(character) = <Self as CharacterCache>::character(self, font_size, character) {
character.advance_size.x()
} else {
panic!("CharacterWidthCache::character returned Err")
}
}
}
#[cfg(feature = "graphics")]
pub fn fitted_text<S, R, F, C, G>(
text: S,
rect: R,
format: F,
glyphs: &mut C,
transform: Matrix2d,
graphics: &mut G,
) -> Result<(), C::Error>
where
S: AsRef<str>,
R: Rectangle<Scalar = f64>,
F: Into<TextFormat>,
C: CharacterCache,
C::Texture: ImageSize,
G: Graphics<Texture = C::Texture>,
{
let format = glyphs.ideal_text_size(text.as_ref(), rect, format);
for (pos, line) in glyphs.justify_text(text.as_ref(), rect, format) {
graphics::text(
format.color,
format.font_size,
&line,
glyphs,
transform.trans(pos.x(), pos.y()),
graphics,
)?;
}
Ok(())
}
#[cfg(feature = "graphics")]
pub fn fitted_colored_text<I, S, R, F, C, G>(
fragments: I,
rect: R,
format: F,
glyphs: &mut C,
transform: Matrix2d,
graphics: &mut G,
) -> Result<(), C::Error>
where
I: IntoIterator<Item = (S, Color)>,
S: AsRef<str>,
R: Rectangle<Scalar = f64>,
F: Into<TextFormat>,
C: CharacterCache,
C::Texture: ImageSize,
G: Graphics<Texture = C::Texture>,
{
let fragments: Vec<_> = fragments.into_iter().collect();
let whole_string: String = fragments.iter().map(|(s, _)| s.as_ref()).collect();
let format = glyphs.ideal_text_size(whole_string.as_ref(), rect, format);
for (pos, (fragment, color)) in glyphs.justify_meta_fragments(fragments, rect, format) {
graphics::text(
color,
format.font_size,
&fragment,
glyphs,
transform.trans(pos.x(), pos.y()),
graphics,
)?;
}
Ok(())
}
#[cfg(feature = "graphics")]
pub struct Scribe<'a, C, G> {
pub format: TextFormat,
pub glyphs: &'a mut C,
pub transform: Matrix2d,
pub graphics: &'a mut G,
}
#[cfg(feature = "graphics")]
impl<'a, C, G> Scribe<'a, C, G> {
pub fn new(
format: TextFormat,
glyphs: &'a mut C,
transform: Matrix2d,
graphics: &'a mut G,
) -> Self {
Scribe {
format,
glyphs,
transform,
graphics,
}
}
}
#[cfg(feature = "graphics")]
impl<'a, C, G> Scribe<'a, C, G>
where
C: CharacterCache,
G: Graphics<Texture = C::Texture>,
{
pub fn write<S, R>(&mut self, text: S, rectangle: R) -> Result<(), C::Error>
where
S: AsRef<str>,
R: Rectangle<Scalar = f64>,
{
fitted_text(
text.as_ref(),
rectangle,
self.format,
self.glyphs,
self.transform,
self.graphics,
)
}
pub fn write_colored<I, S, R>(&mut self, fragments: I, rectangle: R) -> Result<(), C::Error>
where
I: IntoIterator<Item = (S, Color)>,
S: AsRef<str>,
R: Rectangle<Scalar = f64>,
{
fitted_colored_text(
fragments,
rectangle,
self.format,
self.glyphs,
self.transform,
self.graphics,
)
}
}
pub mod color {
pub type Color = [f32; 4];
pub const RED: Color = [1.0, 0.0, 0.0, 1.0];
pub const ORANGE: Color = [1.0, 0.5, 0.0, 1.0];
pub const YELLOW: Color = [1.0, 1.0, 0.0, 1.0];
pub const GREEN: Color = [0.0, 1.0, 0.0, 1.0];
pub const CYAN: Color = [0.0, 1.0, 1.0, 1.0];
pub const BLUE: Color = [0.0, 0.0, 1.0, 1.0];
pub const PURPLE: Color = [0.5, 0.0, 0.5, 1.0];
pub const MAGENTA: Color = [1.0, 0.0, 1.0, 1.0];
pub const BLACK: Color = [0.0, 0.0, 0.0, 1.0];
pub const GRAY: Color = [0.5, 0.5, 0.5, 1.0];
pub const GREY: Color = GRAY;
pub const WHITE: Color = [1.0; 4];
pub const TRANSPARENT: Color = [0.0; 4];
}