#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![doc(
html_logo_url = "https://libcala.github.io/icon.svg",
html_favicon_url = "https://libcala.github.io/icon.svg"
)]
use ttf_parser as ttf;
pub use footile;
pub use footile::PathOp;
mod direction;
use direction::Direction;
pub enum TextAlign {
Left,
Center,
Right,
Justified,
Vertical,
}
pub const NONE: char = '\x01';
pub const BOLD: char = '\x02';
pub const ITALIC: char = '\x03';
struct LangFont<'a>(ttf::Font<'a>, f32);
struct StyledFont<'a> {
none: LangFont<'a>,
}
#[derive(Default)]
pub struct Font<'a> {
fonts: Vec<StyledFont<'a>>,
}
impl<'a> Font<'a> {
pub fn new() -> Self {
Self::default()
}
pub fn add<B: Into<&'a [u8]>>(
mut self,
none: B,
) -> Result<Self, Box<std::error::Error>> {
let none = ttf::Font::from_data(none.into(), 0)?;
let em_per_unit = (none.units_per_em().ok_or("em")? as f32).recip();
let none = LangFont(none, em_per_unit);
self.fonts.push(StyledFont { none });
Ok(self)
}
pub fn render(
&'a self,
text: &'a str,
bbox: (f32, f32, f32, f32),
wh: (f32, f32),
text_align: TextAlign,
) -> (TextPathIterator<'a>, usize) {
let mut pixel_length = 0.0;
let mut last = None;
let mut left_over = None;
let mut last_space = 0;
for (i, c) in text.char_indices() {
if c == ' ' {
last_space = i;
}
if c == '\n' {
left_over = Some(i);
break;
}
let mut index = 0;
let glyph_id = loop {
match self.fonts[index].none.0.glyph_index(c) {
Ok(v) => break v,
Err(_e) => {
index += 1;
if index == self.fonts.len() {
eprintln!("No Glyph for \"{}\" ({})", c, c as u32);
break self.fonts[index]
.none
.0
.glyph_index('�')
.unwrap();
}
}
}
};
let selected_font = &self.fonts[index].none;
let wh = (wh.0 * selected_font.1, -wh.1 * selected_font.1);
let advance = match selected_font.0.glyph_hor_metrics(glyph_id) {
Ok(v) => {
(v.advance as f32
+ if let Some(last) = last {
selected_font
.0
.glyphs_kerning(glyph_id, last)
.unwrap_or(0) as f32
} else {
0f32
})
* wh.0
}
Err(_) => 0.0,
};
pixel_length += advance;
if pixel_length > bbox.2 {
if last_space != 0 {
left_over = Some(last_space);
break;
} else {
left_over = Some(i);
break;
}
}
last = Some(glyph_id);
}
let mut bbox = (bbox.0, bbox.1, bbox.0 + bbox.2, bbox.1 + bbox.3);
let mut vertical = false;
match text_align {
TextAlign::Left => { }
TextAlign::Right => bbox.0 = bbox.2 - pixel_length,
TextAlign::Center => {
bbox.0 = (bbox.0 + bbox.2 - pixel_length) * 0.5
}
TextAlign::Justified => { }
TextAlign::Vertical => vertical = true,
}
bbox.1 += wh.1;
(
TextPathIterator {
text: if let Some(i) = left_over {
text[..i].chars().peekable()
} else {
text.chars().peekable()
},
temp: vec![],
back: false,
path: CharPathIterator::new(self, bbox, wh, vertical),
},
left_over.unwrap_or(text.bytes().len()),
)
}
}
struct CharPathIterator<'a> {
font: &'a Font<'a>,
path: Vec<PathOp>,
size: (f32, f32),
bbox: (f32, f32, f32, f32),
wh: (f32, f32),
xy: (f32, f32),
direction: Direction,
last: Option<ttf_parser::GlyphId>,
vertical: bool,
bold: bool,
italic: bool,
}
impl<'a> CharPathIterator<'a> {
fn new(
font: &'a Font<'a>,
bbox: (f32, f32, f32, f32),
size: (f32, f32),
vertical: bool,
) -> Self {
Self {
font,
path: vec![],
bbox,
size,
wh: (0.0, 0.0),
xy: (bbox.0, bbox.1),
direction: Direction::CheckNext,
last: None,
vertical,
bold: false,
italic: false,
}
}
fn set(&mut self, c: char) -> Result<(), ttf::Error> {
if c == BOLD {
self.bold = true;
return Ok(());
} else if c == ITALIC {
self.italic = true;
return Ok(());
} else if c == NONE {
self.bold = false;
self.italic = false;
return Ok(());
}
if self.direction == Direction::CheckNext {
self.direction = direction::direction(c);
self.xy = (self.bbox.0, self.bbox.1);
}
let mut index = 0;
let glyph_id = loop {
match self.font.fonts[index].none.0.glyph_index(c) {
Ok(v) => break v,
Err(e) => {
index += 1;
if index == self.font.fonts.len() {
eprintln!("No Glyph for \"{}\" ({})", c, c as u32);
return Err(e);
}
}
}
};
let selected_font = &self.font.fonts[index].none;
self.path.clear();
self.wh = (
self.size.0 * selected_font.1,
-self.size.1 * selected_font.1,
);
match selected_font.0.outline_glyph(glyph_id, self) {
Ok(_v) => {}
Err(ttf::Error::NoOutline) => { }
Err(ttf::Error::NoGlyph) => {
let id = self.font.fonts[0].none.0.glyph_index('�')?;
selected_font.0.outline_glyph(id, self).unwrap();
}
Err(e) => {
eprintln!("Warning (glyph {}): {}.", glyph_id.0, e);
return Err(e);
}
};
if self.vertical {
self.xy.1 += self.size.1;
} else {
let advance = match selected_font.0.glyph_hor_metrics(glyph_id) {
Ok(v) => {
(v.advance as f32
+ if let Some(last) = self.last {
selected_font
.0
.glyphs_kerning(glyph_id, last)
.unwrap_or(0) as f32
} else {
0f32
})
* self.wh.0
}
Err(_) => 0.0,
};
self.xy.0 += advance;
}
self.path.reverse();
self.last = Some(glyph_id);
Ok(())
}
}
impl ttf::OutlineBuilder for CharPathIterator<'_> {
fn move_to(&mut self, x: f32, y: f32) {
self.path.push(PathOp::Move(
x * self.wh.0 + self.xy.0,
y * self.wh.1 + self.xy.1,
));
}
fn line_to(&mut self, x: f32, y: f32) {
self.path.push(PathOp::Line(
x * self.wh.0 + self.xy.0,
y * self.wh.1 + self.xy.1,
));
}
fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) {
self.path.push(PathOp::Quad(
x1 * self.wh.0 + self.xy.0,
y1 * self.wh.1 + self.xy.1,
x * self.wh.0 + self.xy.0,
y * self.wh.1 + self.xy.1,
));
}
fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) {
self.path.push(PathOp::Cubic(
x1 * self.wh.0 + self.xy.0,
y1 * self.wh.1 + self.xy.1,
x2 * self.wh.0 + self.xy.0,
y2 * self.wh.1 + self.xy.1,
x * self.wh.0 + self.xy.0,
y * self.wh.1 + self.xy.1,
));
}
fn close(&mut self) {
self.path.push(PathOp::Close());
}
}
impl<'a> Iterator for CharPathIterator<'a> {
type Item = PathOp;
fn next(&mut self) -> Option<Self::Item> {
self.path.pop()
}
}
pub struct TextPathIterator<'a> {
text: std::iter::Peekable<std::str::Chars<'a>>,
temp: Vec<char>,
back: bool,
path: CharPathIterator<'a>,
}
impl<'a> Iterator for TextPathIterator<'a> {
type Item = PathOp;
fn next(&mut self) -> Option<PathOp> {
if let Some(op) = self.path.next() {
Some(op)
} else {
if let Some(c) = self.text.peek() {
let dir = direction::direction(*c);
let dir = if dir == Direction::CheckNext {
if self.back {
Direction::RightLeft
} else {
Direction::LeftRight
}
} else {
dir
};
if dir == Direction::RightLeft {
let c = self.text.next().unwrap();
if !self.back {
self.back = true;
}
self.temp.push(c);
} else {
if let Some(c) = self.temp.pop() {
let _ = self.path.set(c);
} else {
let c = self.text.next().unwrap();
let _ = self.path.set(c);
}
}
self.next()
} else {
if let Some(c) = self.temp.pop() {
let _ = self.path.set(c);
self.next()
} else {
None
}
}
}
}
}
#[cfg(feature = "monospace-font")]
pub fn monospace_font() -> Font<'static> {
const FONTA: &[u8] = include_bytes!("font/dejavu/SansMono.ttf");
const FONTB: &[u8] = include_bytes!("font/noto/SansDevanagari.ttf");
const FONTC: &[u8] = include_bytes!("font/noto/SansHebrew.ttf");
const FONTD: &[u8] = include_bytes!("font/droid/SansFallback.ttf");
Font::new()
.add(FONTA)
.unwrap()
.add(FONTB)
.unwrap()
.add(FONTC)
.unwrap()
.add(FONTD)
.unwrap()
}
#[cfg(feature = "normal-font")]
pub fn normal_font() -> Font<'static> {
const FONTA: &[u8] = include_bytes!("font/dejavu/Sans.ttf");
const FONTB: &[u8] = include_bytes!("font/noto/SansDevanagari.ttf");
const FONTC: &[u8] = include_bytes!("font/noto/SansHebrew.ttf");
const FONTD: &[u8] = include_bytes!("font/droid/SansFallback.ttf");
Font::new()
.add(FONTA)
.unwrap()
.add(FONTB)
.unwrap()
.add(FONTC)
.unwrap()
.add(FONTD)
.unwrap()
}
#[cfg(any(feature = "monospace-font", feature = "normal-font"))]
pub fn licenses() -> &'static str {
include_str!("bin-licenses.txt")
}