Text library moved from libcosmic
This commit is contained in:
commit
410d4ee674
37 changed files with 12909 additions and 0 deletions
261
src/buffer.rs
Normal file
261
src/buffer.rs
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
use std::time::Instant;
|
||||
|
||||
use crate::{FontLayoutLine, FontLineIndex, FontMatches, FontShapeLine};
|
||||
|
||||
pub enum TextAction {
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
Backspace,
|
||||
Delete,
|
||||
Insert(char),
|
||||
}
|
||||
|
||||
#[derive(Default, Eq, PartialEq)]
|
||||
pub struct TextCursor {
|
||||
pub line: usize,
|
||||
pub glyph: usize,
|
||||
}
|
||||
|
||||
impl TextCursor {
|
||||
pub fn new(line: usize, glyph: usize) -> Self {
|
||||
Self { line, glyph }
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TextBuffer<'a> {
|
||||
font_matches: &'a FontMatches<'a>,
|
||||
text_lines: Vec<String>,
|
||||
shape_lines: Vec<FontShapeLine<'a>>,
|
||||
layout_lines: Vec<FontLayoutLine<'a>>,
|
||||
font_size: i32,
|
||||
line_width: i32,
|
||||
pub cursor: TextCursor,
|
||||
pub redraw: bool,
|
||||
}
|
||||
|
||||
impl<'a> TextBuffer<'a> {
|
||||
pub fn new(font_matches: &'a FontMatches<'a>, text: &str, font_size: i32, line_width: i32) -> Self {
|
||||
let mut text_lines: Vec<String> = text.lines().map(String::from).collect();
|
||||
if text_lines.is_empty() {
|
||||
text_lines.push(String::new());
|
||||
}
|
||||
Self {
|
||||
font_matches,
|
||||
text_lines,
|
||||
shape_lines: Vec::new(),
|
||||
layout_lines: Vec::new(),
|
||||
font_size,
|
||||
line_width,
|
||||
cursor: TextCursor::default(),
|
||||
redraw: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shape_until(&mut self, lines: i32) {
|
||||
let instant = Instant::now();
|
||||
|
||||
let mut reshaped = 0;
|
||||
while self.shape_lines.len() < self.text_lines.len()
|
||||
&& (self.layout_lines.len() as i32) < lines
|
||||
{
|
||||
let line_i = FontLineIndex::new(self.shape_lines.len());
|
||||
self.reshape_line(line_i);
|
||||
reshaped += 1;
|
||||
}
|
||||
|
||||
let duration = instant.elapsed();
|
||||
if reshaped > 0 {
|
||||
log::debug!("shape_until {}: {:?}", reshaped, duration);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reshape_line(&mut self, line_i: FontLineIndex) {
|
||||
let instant = Instant::now();
|
||||
|
||||
let shape_line = self
|
||||
.font_matches
|
||||
.shape_line(line_i, &self.text_lines[line_i.get()]);
|
||||
if line_i.get() < self.shape_lines.len() {
|
||||
self.shape_lines[line_i.get()] = shape_line;
|
||||
} else {
|
||||
self.shape_lines.insert(line_i.get(), shape_line);
|
||||
}
|
||||
|
||||
let duration = instant.elapsed();
|
||||
log::debug!("reshape line {}: {:?}", line_i.get(), duration);
|
||||
|
||||
self.relayout_line(line_i);
|
||||
}
|
||||
|
||||
pub fn relayout(&mut self) {
|
||||
let instant = Instant::now();
|
||||
|
||||
self.layout_lines.clear();
|
||||
for line in self.shape_lines.iter() {
|
||||
let layout_i = self.layout_lines.len();
|
||||
line.layout(
|
||||
self.font_size,
|
||||
self.line_width,
|
||||
&mut self.layout_lines,
|
||||
layout_i,
|
||||
);
|
||||
}
|
||||
|
||||
self.redraw = true;
|
||||
|
||||
let duration = instant.elapsed();
|
||||
log::debug!("relayout: {:?}", duration);
|
||||
}
|
||||
|
||||
pub fn relayout_line(&mut self, line_i: FontLineIndex) {
|
||||
let instant = Instant::now();
|
||||
|
||||
let mut insert_opt = None;
|
||||
let mut layout_i = 0;
|
||||
while layout_i < self.layout_lines.len() {
|
||||
let layout_line = &self.layout_lines[layout_i];
|
||||
if layout_line.line_i == line_i {
|
||||
if insert_opt.is_none() {
|
||||
insert_opt = Some(layout_i);
|
||||
}
|
||||
self.layout_lines.remove(layout_i);
|
||||
} else {
|
||||
layout_i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let insert_i = insert_opt.unwrap_or(self.layout_lines.len());
|
||||
|
||||
let shape_line = &self.shape_lines[line_i.get()];
|
||||
shape_line.layout(
|
||||
self.font_size,
|
||||
self.line_width,
|
||||
&mut self.layout_lines,
|
||||
insert_i,
|
||||
);
|
||||
|
||||
self.redraw = true;
|
||||
|
||||
let duration = instant.elapsed();
|
||||
log::debug!("relayout line {}: {:?}", line_i.get(), duration);
|
||||
}
|
||||
|
||||
pub fn font_matches(&self) -> &FontMatches {
|
||||
&self.font_matches
|
||||
}
|
||||
|
||||
pub fn font_size(&self) -> i32 {
|
||||
self.font_size
|
||||
}
|
||||
|
||||
pub fn set_font_size(&mut self, font_size: i32) {
|
||||
self.font_size = font_size;
|
||||
self.relayout();
|
||||
}
|
||||
|
||||
pub fn line_width(&self) -> i32 {
|
||||
self.line_width
|
||||
}
|
||||
|
||||
pub fn set_line_width(&mut self, line_width: i32) {
|
||||
self.line_width = line_width;
|
||||
self.relayout();
|
||||
}
|
||||
|
||||
pub fn layout_lines(&self) -> &[FontLayoutLine] {
|
||||
&self.layout_lines
|
||||
}
|
||||
|
||||
pub fn text_lines(&self) -> &[String] {
|
||||
&self.text_lines
|
||||
}
|
||||
|
||||
pub fn action(&mut self, action: TextAction) {
|
||||
match action {
|
||||
TextAction::Left => {
|
||||
let line = &self.layout_lines[self.cursor.line];
|
||||
if self.cursor.glyph > line.glyphs.len() {
|
||||
self.cursor.glyph = line.glyphs.len();
|
||||
self.redraw = true;
|
||||
}
|
||||
if self.cursor.glyph > 0 {
|
||||
self.cursor.glyph -= 1;
|
||||
self.redraw = true;
|
||||
}
|
||||
}
|
||||
TextAction::Right => {
|
||||
let line = &self.layout_lines[self.cursor.line];
|
||||
if self.cursor.glyph > line.glyphs.len() {
|
||||
self.cursor.glyph = line.glyphs.len();
|
||||
self.redraw = true;
|
||||
}
|
||||
if self.cursor.glyph < line.glyphs.len() {
|
||||
self.cursor.glyph += 1;
|
||||
self.redraw = true;
|
||||
}
|
||||
}
|
||||
TextAction::Up => {
|
||||
if self.cursor.line > 0 {
|
||||
self.cursor.line -= 1;
|
||||
self.redraw = true;
|
||||
}
|
||||
}
|
||||
TextAction::Down => {
|
||||
if self.cursor.line + 1 < self.layout_lines.len() {
|
||||
self.cursor.line += 1;
|
||||
self.redraw = true;
|
||||
}
|
||||
}
|
||||
TextAction::Backspace => {
|
||||
let line = &self.layout_lines[self.cursor.line];
|
||||
if self.cursor.glyph > line.glyphs.len() {
|
||||
self.cursor.glyph = line.glyphs.len();
|
||||
self.redraw = true;
|
||||
}
|
||||
if self.cursor.glyph > 0 {
|
||||
self.cursor.glyph -= 1;
|
||||
let glyph = &line.glyphs[self.cursor.glyph];
|
||||
let text_line = &mut self.text_lines[line.line_i.get()];
|
||||
text_line.remove(glyph.start);
|
||||
self.reshape_line(line.line_i);
|
||||
}
|
||||
}
|
||||
TextAction::Delete => {
|
||||
let line = &self.layout_lines[self.cursor.line];
|
||||
if self.cursor.glyph < line.glyphs.len() {
|
||||
let glyph = &line.glyphs[self.cursor.glyph];
|
||||
let text_line = &mut self.text_lines[line.line_i.get()];
|
||||
text_line.remove(glyph.start);
|
||||
self.reshape_line(line.line_i);
|
||||
}
|
||||
}
|
||||
TextAction::Insert(character) => {
|
||||
let line = &self.layout_lines[self.cursor.line];
|
||||
if self.cursor.glyph >= line.glyphs.len() {
|
||||
match line.glyphs.last() {
|
||||
Some(glyph) => {
|
||||
let text_line = &mut self.text_lines[line.line_i.get()];
|
||||
text_line.insert(glyph.end, character);
|
||||
self.cursor.glyph += 1;
|
||||
self.reshape_line(line.line_i);
|
||||
}
|
||||
None => {
|
||||
let text_line = &mut self.text_lines[line.line_i.get()];
|
||||
text_line.push(character);
|
||||
self.cursor.glyph += 1;
|
||||
self.reshape_line(line.line_i);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let glyph = &line.glyphs[self.cursor.glyph];
|
||||
let text_line = &mut self.text_lines[line.line_i.get()];
|
||||
text_line.insert(glyph.start, character);
|
||||
self.cursor.glyph += 1;
|
||||
self.reshape_line(line.line_i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
132
src/font/cache.rs
Normal file
132
src/font/cache.rs
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub struct CacheKey {
|
||||
pub glyph_id: u16,
|
||||
pub font_size: i32,
|
||||
pub x_bin: SubpixelBin,
|
||||
pub y_bin: SubpixelBin,
|
||||
}
|
||||
|
||||
impl CacheKey {
|
||||
pub fn new(glyph_id: u16, font_size: i32, pos: (f32, f32)) -> (Self, i32, i32) {
|
||||
let (x, x_bin) = SubpixelBin::new(pos.0);
|
||||
let (y, y_bin) = SubpixelBin::new(pos.1);
|
||||
(
|
||||
Self {
|
||||
glyph_id,
|
||||
font_size,
|
||||
x_bin,
|
||||
y_bin,
|
||||
},
|
||||
x,
|
||||
y,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub type CacheItem = Option<swash::scale::image::Image>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum SubpixelBin {
|
||||
Zero,
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
}
|
||||
|
||||
impl SubpixelBin {
|
||||
pub fn new(pos: f32) -> (i32, Self) {
|
||||
let trunc = pos.trunc() as i32;
|
||||
let fract = pos.fract();
|
||||
if pos.is_sign_negative() {
|
||||
if fract > -0.125 {
|
||||
(trunc, Self::Zero)
|
||||
} else if fract > -0.375 {
|
||||
(trunc - 1, Self::Three)
|
||||
} else if fract > -0.625 {
|
||||
(trunc - 1, Self::Two)
|
||||
} else if fract > -0.875 {
|
||||
(trunc - 1, Self::One)
|
||||
} else {
|
||||
(trunc - 1, Self::Zero)
|
||||
}
|
||||
} else {
|
||||
if fract < 0.125 {
|
||||
(trunc, Self::Zero)
|
||||
} else if fract < 0.375 {
|
||||
(trunc, Self::One)
|
||||
} else if fract < 0.625 {
|
||||
(trunc, Self::Two)
|
||||
} else if fract < 0.875 {
|
||||
(trunc, Self::Three)
|
||||
} else {
|
||||
(trunc + 1, Self::Zero)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_float(&self) -> f32 {
|
||||
match self {
|
||||
Self::Zero => 0.0,
|
||||
Self::One => 0.25,
|
||||
Self::Two => 0.5,
|
||||
Self::Three => 0.75,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_subpixel_bins() {
|
||||
// POSITIVE TESTS
|
||||
|
||||
// Maps to 0.0
|
||||
assert_eq!(SubpixelBin::new(0.0), (0, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(0.124), (0, SubpixelBin::Zero));
|
||||
|
||||
// Maps to 0.25
|
||||
assert_eq!(SubpixelBin::new(0.125), (0, SubpixelBin::One));
|
||||
assert_eq!(SubpixelBin::new(0.25), (0, SubpixelBin::One));
|
||||
assert_eq!(SubpixelBin::new(0.374), (0, SubpixelBin::One));
|
||||
|
||||
// Maps to 0.5
|
||||
assert_eq!(SubpixelBin::new(0.375), (0, SubpixelBin::Two));
|
||||
assert_eq!(SubpixelBin::new(0.5), (0, SubpixelBin::Two));
|
||||
assert_eq!(SubpixelBin::new(0.624), (0, SubpixelBin::Two));
|
||||
|
||||
// Maps to 0.75
|
||||
assert_eq!(SubpixelBin::new(0.625), (0, SubpixelBin::Three));
|
||||
assert_eq!(SubpixelBin::new(0.75), (0, SubpixelBin::Three));
|
||||
assert_eq!(SubpixelBin::new(0.874), (0, SubpixelBin::Three));
|
||||
|
||||
// Maps to 1.0
|
||||
assert_eq!(SubpixelBin::new(0.875), (1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(0.999), (1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(1.0), (1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(1.124), (1, SubpixelBin::Zero));
|
||||
|
||||
// NEGATIVE TESTS
|
||||
|
||||
// Maps to 0.0
|
||||
assert_eq!(SubpixelBin::new(-0.0), (0, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(-0.124), (0, SubpixelBin::Zero));
|
||||
|
||||
// Maps to 0.25
|
||||
assert_eq!(SubpixelBin::new(-0.125), (-1, SubpixelBin::Three));
|
||||
assert_eq!(SubpixelBin::new(-0.25), (-1, SubpixelBin::Three));
|
||||
assert_eq!(SubpixelBin::new(-0.374), (-1, SubpixelBin::Three));
|
||||
|
||||
// Maps to 0.5
|
||||
assert_eq!(SubpixelBin::new(-0.375), (-1, SubpixelBin::Two));
|
||||
assert_eq!(SubpixelBin::new(-0.5), (-1, SubpixelBin::Two));
|
||||
assert_eq!(SubpixelBin::new(-0.624), (-1, SubpixelBin::Two));
|
||||
|
||||
// Maps to 0.75
|
||||
assert_eq!(SubpixelBin::new(-0.625), (-1, SubpixelBin::One));
|
||||
assert_eq!(SubpixelBin::new(-0.75), (-1, SubpixelBin::One));
|
||||
assert_eq!(SubpixelBin::new(-0.874), (-1, SubpixelBin::One));
|
||||
|
||||
// Maps to 1.0
|
||||
assert_eq!(SubpixelBin::new(-0.875), (-1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(-0.999), (-1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(-1.0), (-1, SubpixelBin::Zero));
|
||||
assert_eq!(SubpixelBin::new(-1.124), (-1, SubpixelBin::Zero));
|
||||
}
|
||||
85
src/font/fallback/macos.rs
Normal file
85
src/font/fallback/macos.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
use unicode_script::Script;
|
||||
|
||||
// Fallbacks to use after any script specific fallbacks
|
||||
pub fn common_fallback() -> &'static [&'static str] {
|
||||
&[
|
||||
".SF NS",
|
||||
"Apple Color Emoji",
|
||||
"Geneva",
|
||||
"Arial Unicode MS",
|
||||
]
|
||||
}
|
||||
|
||||
// Fallbacks to never use
|
||||
pub fn forbidden_fallback() -> &'static [&'static str] {
|
||||
&[
|
||||
".LastResort",
|
||||
]
|
||||
}
|
||||
|
||||
fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||
match locale {
|
||||
// Japan
|
||||
"ja" => &["Hiragino Sans"],
|
||||
// Korea
|
||||
"ko" => &["Apple SD Gothic Neo"],
|
||||
// Hong Kong
|
||||
"zh-HK" => &["PingFang HK"],
|
||||
// Taiwan
|
||||
"zh-TW" => &["PingFang TC"],
|
||||
// Simplified Chinese is the default (also catches "zh-CN" for China)
|
||||
_ => &["PingFang SC"],
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use per script
|
||||
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
|
||||
//TODO: abstract style (sans/serif/monospaced)
|
||||
//TODO: pull more data from about:config font.name-list.sans-serif in Firefox
|
||||
match script {
|
||||
Script::Adlam => &["Noto Sans Adlam"],
|
||||
Script::Arabic => &["Geeza Pro"],
|
||||
Script::Armenian => &["Noto Sans Armenian"],
|
||||
Script::Bengali => &["Bangla Sangam MN"],
|
||||
Script::Buhid => &["Noto Sans Buhid"],
|
||||
Script::Canadian_Aboriginal => &["Euphemia UCAS"],
|
||||
Script::Chakma => &["Noto Sans Chakma"],
|
||||
Script::Devanagari => &["Devanagari Sangam MN"],
|
||||
Script::Ethiopic => &["Kefa"],
|
||||
Script::Gothic => &["Noto Sans Gothic"],
|
||||
Script::Grantha => &["Grantha Sangam MN"],
|
||||
Script::Gujarati => &["Gujarati Sangam MN"],
|
||||
Script::Gurmukhi => &["Gurmukhi Sangam MN"],
|
||||
Script::Han => han_unification(locale),
|
||||
Script::Hangul => han_unification("ko"),
|
||||
Script::Hanunoo => &["Noto Sans Hanunoo"],
|
||||
Script::Hebrew => &["Arial"],
|
||||
Script::Hiragana => han_unification("ja"),
|
||||
Script::Javanese => &["Noto Sans Javanese"],
|
||||
Script::Kannada => &["Noto Sans Kannada"],
|
||||
Script::Katakana => han_unification("ja"),
|
||||
Script::Khmer => &["Khmer Sangam MN"],
|
||||
Script::Lao => &["Lao Sangam MN"],
|
||||
Script::Malayalam => &["Malayalam Sangam MN"],
|
||||
Script::Mongolian => &["Noto Sans Mongolian"],
|
||||
Script::Myanmar => &["Noto Sans Myanmar"],
|
||||
Script::Oriya => &["Noto Sans Oriya"],
|
||||
Script::Sinhala => &["Sinhala Sangam MN"],
|
||||
Script::Syriac => &["Noto Sans Syriac"],
|
||||
Script::Tagalog => &["Noto Sans Tagalog"],
|
||||
Script::Tagbanwa => &["Noto Sans Tagbanwa"],
|
||||
Script::Tai_Le => &["Noto Sans Tai Le"],
|
||||
Script::Tai_Tham => &["Noto Sans Tai Tham"],
|
||||
Script::Tai_Viet => &["Noto Sans Tai Viet"],
|
||||
Script::Tamil => &["InaiMathi"],
|
||||
Script::Telugu => &["Telugu Sangam MN"],
|
||||
Script::Thaana => &["Noto Sans Thaana"],
|
||||
Script::Thai => &["Ayuthaya"],
|
||||
Script::Tibetan => &["Kailasa"],
|
||||
Script::Tifinagh => &["Noto Sans Tifinagh"],
|
||||
Script::Vai => &["Noto Sans Vai"],
|
||||
//TODO: Use han_unification?
|
||||
Script::Yi => &["Noto Sans Yi", "PingFang SC"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
138
src/font/fallback/mod.rs
Normal file
138
src/font/fallback/mod.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
use unicode_script::Script;
|
||||
|
||||
use super::Font;
|
||||
|
||||
use self::platform::*;
|
||||
|
||||
#[cfg(not(any(
|
||||
target_os = "linux",
|
||||
target_os = "macos",
|
||||
target_os = "windows",
|
||||
)))]
|
||||
#[path = "other.rs"]
|
||||
mod platform;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[path = "macos.rs"]
|
||||
mod platform;
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[path = "unix.rs"]
|
||||
mod platform;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[path = "windows.rs"]
|
||||
mod platform;
|
||||
|
||||
pub struct FontFallbackIter<'a> {
|
||||
fonts: &'a [Font<'a>],
|
||||
default_family_opt: Option<&'a str>,
|
||||
scripts: Vec<Script>,
|
||||
locale: &'a str,
|
||||
script_i: (usize, usize),
|
||||
common_i: usize,
|
||||
other_i: usize,
|
||||
end: bool,
|
||||
}
|
||||
|
||||
impl<'a> FontFallbackIter<'a> {
|
||||
pub fn new(fonts: &'a [Font<'a>], default_family_opt: Option<&'a str>, scripts: Vec<Script>, locale: &'a str) -> Self {
|
||||
Self {
|
||||
fonts,
|
||||
default_family_opt,
|
||||
scripts,
|
||||
locale,
|
||||
script_i: (0, 0),
|
||||
common_i: 0,
|
||||
other_i: 0,
|
||||
end: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn check_missing(&self, word: &str) {
|
||||
if self.end {
|
||||
log::warn!(
|
||||
"Failed to find any fallback for {:?} locale '{}': '{}'",
|
||||
self.scripts,
|
||||
self.locale,
|
||||
word
|
||||
);
|
||||
} else if self.other_i > 0 {
|
||||
let font = &self.fonts[self.other_i - 1];
|
||||
log::warn!(
|
||||
"Failed to find preset fallback for {:?} locale '{}', used '{}': '{}'",
|
||||
self.scripts,
|
||||
self.locale,
|
||||
font.info.family,
|
||||
word
|
||||
);
|
||||
} else if ! self.scripts.is_empty() && self.common_i > 0 {
|
||||
let family = common_fallback()[self.common_i - 1];
|
||||
log::debug!(
|
||||
"Failed to find script fallback for {:?} locale '{}', used '{}': '{}'",
|
||||
self.scripts,
|
||||
self.locale,
|
||||
family,
|
||||
word
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Iterator for FontFallbackIter<'a> {
|
||||
type Item = &'a Font<'a>;
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if let Some(default_family) = self.default_family_opt.take() {
|
||||
for font in self.fonts.iter() {
|
||||
if font.info.family == default_family {
|
||||
return Some(font);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
while self.script_i.0 < self.scripts.len() {
|
||||
let script = self.scripts[self.script_i.0];
|
||||
|
||||
let script_families = script_fallback(&script, self.locale);
|
||||
while self.script_i.1 < script_families.len() {
|
||||
let script_family = script_families[self.script_i.1];
|
||||
self.script_i.1 += 1;
|
||||
for font in self.fonts.iter() {
|
||||
if font.info.family == script_family {
|
||||
return Some(font);
|
||||
}
|
||||
}
|
||||
log::warn!("failed to find family '{}' for script {:?} and locale '{}'", script_family, script, self.locale);
|
||||
}
|
||||
|
||||
self.script_i.0 += 1;
|
||||
self.script_i.1 = 0;
|
||||
}
|
||||
|
||||
let common_families = common_fallback();
|
||||
while self.common_i < common_families.len() {
|
||||
let common_family = common_families[self.common_i];
|
||||
self.common_i += 1;
|
||||
for font in self.fonts.iter() {
|
||||
if font.info.family == common_family {
|
||||
return Some(font);
|
||||
}
|
||||
}
|
||||
log::warn!("failed to find family '{}'", common_family)
|
||||
}
|
||||
|
||||
//TODO: do we need to do this?
|
||||
//TODO: do not evaluate fonts more than once!
|
||||
let forbidden_families = forbidden_fallback();
|
||||
while self.other_i < self.fonts.len() {
|
||||
let font = &self.fonts[self.other_i];
|
||||
self.other_i += 1;
|
||||
if ! forbidden_families.contains(&font.info.family.as_str()) {
|
||||
return Some(font);
|
||||
}
|
||||
}
|
||||
|
||||
self.end = true;
|
||||
None
|
||||
}
|
||||
}
|
||||
16
src/font/fallback/other.rs
Normal file
16
src/font/fallback/other.rs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
use unicode_script::Script;
|
||||
|
||||
// Fallbacks to use after any script specific fallbacks
|
||||
pub fn common_fallback() -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
|
||||
// Fallbacks to never use
|
||||
pub fn forbidden_fallback() -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
|
||||
// Fallbacks to use per script
|
||||
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
90
src/font/fallback/unix.rs
Normal file
90
src/font/fallback/unix.rs
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
use unicode_script::Script;
|
||||
|
||||
// Fallbacks to use after any script specific fallbacks
|
||||
pub fn common_fallback() -> &'static [&'static str] {
|
||||
//TODO: abstract style (sans/serif/monospaced)
|
||||
&[
|
||||
"DejaVu Sans",
|
||||
"FreeSans",
|
||||
"Noto Sans Symbols",
|
||||
"Noto Sans Symbols2",
|
||||
"Noto Color Emoji",
|
||||
//TODO: Add CJK script here for doublewides?
|
||||
]
|
||||
}
|
||||
|
||||
// Fallbacks to never use
|
||||
pub fn forbidden_fallback() -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||
match locale {
|
||||
// Japan
|
||||
"ja" => &["Noto Sans CJK JA"],
|
||||
// Korea
|
||||
"ko" => &["Noto Sans CJK KR"],
|
||||
// Hong Kong
|
||||
"zh-HK" => &["Noto Sans CJK HK"],
|
||||
// Taiwan
|
||||
"zh-TW" => &["Noto Sans CJK TC"],
|
||||
// Simplified Chinese is the default (also catches "zh-CN" for China)
|
||||
_ => &["Noto Sans CJK SC"],
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use per script
|
||||
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
|
||||
//TODO: abstract style (sans/serif/monospaced)
|
||||
match script {
|
||||
Script::Adlam => &["Noto Sans Adlam", "Noto Sans Adlam Unjoined"],
|
||||
Script::Arabic => &["Noto Sans Arabic"],
|
||||
Script::Armenian => &["Noto Sans Armenian"],
|
||||
Script::Bengali => &["Noto Sans Bengali"],
|
||||
Script::Bopomofo => han_unification(locale),
|
||||
Script::Buhid => &["Noto Sans Buhid"],
|
||||
Script::Chakma => &["Noto Sans Chakma"],
|
||||
Script::Cherokee => &["Noto Sans Cherokee"],
|
||||
Script::Deseret => &["Noto Sans Deseret"],
|
||||
Script::Devanagari => &["Noto Sans Devanagari"],
|
||||
Script::Ethiopic => &["Noto Sans Ethiopic"],
|
||||
Script::Georgian => &["Noto Sans Georgian"],
|
||||
Script::Gothic => &["Noto Sans Gothic"],
|
||||
Script::Grantha => &["Noto Sans Grantha"],
|
||||
Script::Gujarati => &["Noto Sans Gujarati"],
|
||||
Script::Gurmukhi => &["Noto Sans Gurmukhi"],
|
||||
Script::Han => han_unification(locale),
|
||||
Script::Hangul => han_unification("ko"),
|
||||
Script::Hanunoo => &["Noto Sans Hanunoo"],
|
||||
Script::Hebrew => &["Noto Sans Hebrew"],
|
||||
Script::Hiragana => han_unification("ja"),
|
||||
Script::Javanese => &["Noto Sans Javanese"],
|
||||
Script::Kannada => &["Noto Sans Kannada"],
|
||||
Script::Katakana => han_unification("ja"),
|
||||
Script::Khmer => &["Noto Sans Khmer"],
|
||||
Script::Lao => &["Noto Sans Lao"],
|
||||
Script::Malayalam => &["Noto Sans Malayalam"],
|
||||
Script::Mongolian => &["Noto Sans Mongolian"],
|
||||
Script::Myanmar => &["Noto Sans Myanmar"],
|
||||
Script::Oriya => &["Noto Sans Oriya"],
|
||||
Script::Runic => &["Noto Sans Runic"],
|
||||
Script::Sinhala => &["Noto Sans Sinhala"],
|
||||
Script::Syriac => &["Noto Sans Syriac"],
|
||||
Script::Tagalog => &["Noto Sans Tagalog"],
|
||||
Script::Tagbanwa => &["Noto Sans Tagbanwa"],
|
||||
Script::Tai_Le => &["Noto Sans Tai Le"],
|
||||
Script::Tai_Tham => &["Noto Sans Tai Tham"],
|
||||
Script::Tai_Viet => &["Noto Sans Tai Viet"],
|
||||
Script::Tamil => &["Noto Sans Tamil"],
|
||||
Script::Telugu => &["Noto Sans Telugu"],
|
||||
Script::Thaana => &["Noto Sans Thaana"],
|
||||
Script::Thai => &["Noto Sans Thai"],
|
||||
//TODO: no sans script?
|
||||
Script::Tibetan => &["Noto Serif Tibetan"],
|
||||
Script::Tifinagh => &["Noto Sans Tifinagh"],
|
||||
Script::Vai => &["Noto Sans Vai"],
|
||||
//TODO: Use han_unification?
|
||||
Script::Yi => &["Noto Sans Yi", "Noto Sans CJK SC"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
72
src/font/fallback/windows.rs
Normal file
72
src/font/fallback/windows.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use unicode_script::Script;
|
||||
|
||||
// Fallbacks to use after any script specific fallbacks
|
||||
pub fn common_fallback() -> &'static [&'static str] {
|
||||
//TODO: abstract style (sans/serif/monospaced)
|
||||
&[
|
||||
"Segoe UI",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
"Segoe UI Historic",
|
||||
//TODO: Add CJK script here for doublewides?
|
||||
]
|
||||
}
|
||||
|
||||
// Fallbacks to never use
|
||||
pub fn forbidden_fallback() -> &'static [&'static str] {
|
||||
&[]
|
||||
}
|
||||
|
||||
fn han_unification(locale: &str) -> &'static [&'static str] {
|
||||
//TODO!
|
||||
match locale {
|
||||
// Japan
|
||||
"ja" => &["Yu Gothic"],
|
||||
// Korea
|
||||
"ko" => &["Malgun Gothic"],
|
||||
// Hong Kong"
|
||||
"zh-HK" => &["MingLiU_HKSCS"],
|
||||
// Taiwan
|
||||
"zh-TW" => &["Microsoft JhengHei UI"],
|
||||
// Simplified Chinese is the default (also catches "zh-CN" for China)
|
||||
_ => &["Microsoft YaHei UI"]
|
||||
}
|
||||
}
|
||||
|
||||
// Fallbacks to use per script
|
||||
pub fn script_fallback(script: &Script, locale: &str) -> &'static [&'static str] {
|
||||
//TODO: better match https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/platform/fonts/win/font_fallback_win.cc#L99
|
||||
match script {
|
||||
Script::Adlam => &["Ebrima"],
|
||||
Script::Bengali => &["Nirmala UI"],
|
||||
Script::Canadian_Aboriginal => &["Gadugi"],
|
||||
Script::Chakma => &["Nirmala UI"],
|
||||
Script::Cherokee => &["Gadugi"],
|
||||
Script::Devanagari => &["Nirmala UI"],
|
||||
Script::Ethiopic => &["Ebrima"],
|
||||
Script::Gujarati => &["Nirmala UI"],
|
||||
Script::Gurmukhi => &["Nirmala UI"],
|
||||
Script::Han => han_unification(locale),
|
||||
Script::Hangul => han_unification("ko"),
|
||||
Script::Hiragana => han_unification("ja"),
|
||||
Script::Javanese => &["Javanese Text"],
|
||||
Script::Kannada => &["Nirmala UI"],
|
||||
Script::Katakana => han_unification("ja"),
|
||||
Script::Khmer => &["Leelawadee UI"],
|
||||
Script::Lao => &["Leelawadee UI"],
|
||||
Script::Malayalam => &["Nirmala UI"],
|
||||
Script::Mongolian => &["Mongolian Baiti"],
|
||||
Script::Myanmar => &["Myanmar Text"],
|
||||
Script::Oriya => &["Nirmala UI"],
|
||||
Script::Sinhala => &["Nirmala UI"],
|
||||
Script::Tamil => &["Nirmala UI"],
|
||||
Script::Telugu => &["Nirmala UI"],
|
||||
Script::Thaana => &["MV Boli"],
|
||||
Script::Thai => &["Leelawadee UI"],
|
||||
Script::Tibetan => &["Microsoft Himalaya"],
|
||||
Script::Tifinagh => &["Ebrima"],
|
||||
Script::Vai => &["Ebrima"],
|
||||
Script::Yi => &["Microsoft Yi Baiti"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
96
src/font/layout.rs
Normal file
96
src/font/layout.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
use super::{CacheKey, Font, FontLineIndex};
|
||||
|
||||
pub struct FontLayoutGlyph<'a> {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub x: f32,
|
||||
pub w: f32,
|
||||
pub font: &'a Font<'a>,
|
||||
pub inner: (CacheKey, i32, i32),
|
||||
}
|
||||
|
||||
pub struct FontLayoutLine<'a> {
|
||||
pub line_i: FontLineIndex,
|
||||
pub glyphs: Vec<FontLayoutGlyph<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> FontLayoutLine<'a> {
|
||||
pub fn draw<F: FnMut(i32, i32, u32)>(&self, base: u32, mut f: F) {
|
||||
for glyph in self.glyphs.iter() {
|
||||
use swash::scale::{Render, Source, StrikeWith};
|
||||
use swash::zeno::{Format, Vector};
|
||||
|
||||
let mut cache = glyph.font.cache.lock().unwrap();
|
||||
|
||||
let (cache_key, x_int, y_int) = glyph.inner;
|
||||
|
||||
let image_opt = cache.entry(cache_key).or_insert_with(|| {
|
||||
let mut scale_context = glyph.font.scale_context.lock().unwrap();
|
||||
|
||||
// Build the scaler
|
||||
let mut scaler = scale_context
|
||||
.builder(glyph.font.swash)
|
||||
.size(cache_key.font_size as f32)
|
||||
.hint(true)
|
||||
.build();
|
||||
|
||||
// Compute the fractional offset-- you'll likely want to quantize this
|
||||
// in a real renderer
|
||||
let offset =
|
||||
Vector::new(cache_key.x_bin.as_float(), cache_key.y_bin.as_float());
|
||||
|
||||
// Select our source order
|
||||
Render::new(&[
|
||||
// Color outline with the first palette
|
||||
Source::ColorOutline(0),
|
||||
// Color bitmap with best fit selection mode
|
||||
Source::ColorBitmap(StrikeWith::BestFit),
|
||||
// Standard scalable outline
|
||||
Source::Outline,
|
||||
])
|
||||
// Select a subpixel format
|
||||
.format(Format::Alpha)
|
||||
// Apply the fractional offset
|
||||
.offset(offset)
|
||||
// Render the image
|
||||
.render(&mut scaler, cache_key.glyph_id)
|
||||
});
|
||||
|
||||
if let Some(ref image) = image_opt {
|
||||
use swash::scale::image::Content;
|
||||
|
||||
let x = x_int + image.placement.left;
|
||||
let y = y_int - image.placement.top;
|
||||
|
||||
match image.content {
|
||||
Content::Mask => {
|
||||
let mut i = 0;
|
||||
for off_y in 0..image.placement.height as i32 {
|
||||
for off_x in 0..image.placement.width as i32 {
|
||||
let color = (image.data[i] as u32) << 24 | base & 0xFFFFFF;
|
||||
f(x + off_x, y + off_y, color);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Content::Color => {
|
||||
let mut i = 0;
|
||||
for off_y in 0..image.placement.height as i32 {
|
||||
for off_x in 0..image.placement.width as i32 {
|
||||
let color = (image.data[i + 3] as u32) << 24
|
||||
| (image.data[i] as u32) << 16
|
||||
| (image.data[i + 1] as u32) << 8
|
||||
| (image.data[i + 2] as u32);
|
||||
f(x + off_x, y + off_y, color);
|
||||
i += 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
Content::SubpixelMask => {
|
||||
log::warn!("TODO: SubpixelMask");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
320
src/font/matches.rs
Normal file
320
src/font/matches.rs
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
use unicode_script::{Script, UnicodeScript};
|
||||
|
||||
use super::{Font, FontLineIndex, FontShapeGlyph, FontShapeLine, FontShapeSpan, FontShapeWord};
|
||||
use super::fallback::{FontFallbackIter};
|
||||
|
||||
pub struct FontMatches<'a> {
|
||||
pub locale: &'a str,
|
||||
pub fonts: Vec<Font<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> FontMatches<'a> {
|
||||
fn shape_fallback(
|
||||
&self,
|
||||
font: &'a Font<'a>,
|
||||
line: &str,
|
||||
start_word: usize,
|
||||
end_word: usize,
|
||||
span_rtl: bool,
|
||||
) -> (Vec<FontShapeGlyph>, Vec<usize>) {
|
||||
let word = &line[start_word..end_word];
|
||||
|
||||
let font_scale = font.rustybuzz.units_per_em() as f32;
|
||||
|
||||
let mut buffer = rustybuzz::UnicodeBuffer::new();
|
||||
buffer.set_direction(if span_rtl {
|
||||
rustybuzz::Direction::RightToLeft
|
||||
} else {
|
||||
rustybuzz::Direction::LeftToRight
|
||||
});
|
||||
buffer.push_str(word);
|
||||
buffer.guess_segment_properties();
|
||||
|
||||
let rtl = match buffer.direction() {
|
||||
rustybuzz::Direction::RightToLeft => true,
|
||||
//TODO: other directions?
|
||||
_ => false,
|
||||
};
|
||||
assert_eq!(rtl, span_rtl);
|
||||
|
||||
let glyph_buffer = rustybuzz::shape(&font.rustybuzz, &[], buffer);
|
||||
let glyph_infos = glyph_buffer.glyph_infos();
|
||||
let glyph_positions = glyph_buffer.glyph_positions();
|
||||
|
||||
let mut missing = Vec::new();
|
||||
let mut glyphs = Vec::with_capacity(glyph_infos.len());
|
||||
for (info, pos) in glyph_infos.iter().zip(glyph_positions.iter()) {
|
||||
let x_advance = pos.x_advance as f32 / font_scale;
|
||||
let y_advance = pos.y_advance as f32 / font_scale;
|
||||
let x_offset = pos.x_offset as f32 / font_scale;
|
||||
let y_offset = pos.y_offset as f32 / font_scale;
|
||||
|
||||
//println!(" {:?} {:?}", info, pos);
|
||||
if info.glyph_id == 0 {
|
||||
missing.push(start_word + info.cluster as usize);
|
||||
}
|
||||
|
||||
let inner = info.glyph_id as swash::GlyphId;
|
||||
glyphs.push(FontShapeGlyph {
|
||||
start: start_word + info.cluster as usize,
|
||||
end: end_word, // Set later
|
||||
x_advance,
|
||||
y_advance,
|
||||
x_offset,
|
||||
y_offset,
|
||||
font,
|
||||
inner,
|
||||
});
|
||||
}
|
||||
|
||||
// Adjust end of glyphs
|
||||
if rtl {
|
||||
for i in 1..glyphs.len() {
|
||||
let next_start = glyphs[i - 1].start;
|
||||
let next_end = glyphs[i - 1].end;
|
||||
let prev = &mut glyphs[i];
|
||||
if prev.start == next_start {
|
||||
prev.end = next_end;
|
||||
} else {
|
||||
prev.end = next_start;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for i in (1..glyphs.len()).rev() {
|
||||
let next_start = glyphs[i].start;
|
||||
let next_end = glyphs[i].end;
|
||||
let prev = &mut glyphs[i - 1];
|
||||
if prev.start == next_start {
|
||||
prev.end = next_end;
|
||||
} else {
|
||||
prev.end = next_start;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(glyphs, missing)
|
||||
}
|
||||
|
||||
fn shape_word(
|
||||
&self,
|
||||
line: &str,
|
||||
start_word: usize,
|
||||
end_word: usize,
|
||||
span_rtl: bool,
|
||||
blank: bool,
|
||||
) -> FontShapeWord {
|
||||
//TODO: use smallvec?
|
||||
let mut scripts = Vec::new();
|
||||
for c in line[start_word..end_word].chars() {
|
||||
match c.script() {
|
||||
Script::Common |
|
||||
Script::Inherited |
|
||||
Script::Latin |
|
||||
Script::Unknown => (),
|
||||
script => if ! scripts.contains(&script) {
|
||||
scripts.push(script);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
log::trace!(
|
||||
" Word {:?}{}: '{}'",
|
||||
scripts,
|
||||
if blank { " BLANK" } else { "" },
|
||||
&line[start_word..end_word],
|
||||
);
|
||||
|
||||
//TODO: configure default family
|
||||
let mut font_iter = FontFallbackIter::new(&self.fonts, Some("Fira Sans"), scripts, &self.locale);
|
||||
|
||||
let (mut glyphs, mut missing) = self.shape_fallback(
|
||||
font_iter.next().unwrap(),
|
||||
line,
|
||||
start_word,
|
||||
end_word,
|
||||
span_rtl
|
||||
);
|
||||
|
||||
//TODO: improve performance!
|
||||
while !missing.is_empty() {
|
||||
let font = match font_iter.next() {
|
||||
Some(some) => some,
|
||||
None => break,
|
||||
};
|
||||
|
||||
log::trace!("Evaluating fallback with font '{}'", font.info.family);
|
||||
let (mut fb_glyphs, fb_missing) = self.shape_fallback(
|
||||
font,
|
||||
line,
|
||||
start_word,
|
||||
end_word,
|
||||
span_rtl
|
||||
);
|
||||
|
||||
// Insert all matching glyphs
|
||||
let mut fb_i = 0;
|
||||
while fb_i < fb_glyphs.len() {
|
||||
let start = fb_glyphs[fb_i].start;
|
||||
let end = fb_glyphs[fb_i].end;
|
||||
|
||||
// Skip clusters that are not missing, or where the fallback font is missing
|
||||
if !missing.contains(&start) || fb_missing.contains(&start) {
|
||||
fb_i += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut missing_i = 0;
|
||||
while missing_i < missing.len() {
|
||||
if missing[missing_i] >= start && missing[missing_i] < end {
|
||||
// println!("No longer missing {}", missing[missing_i]);
|
||||
missing.remove(missing_i);
|
||||
} else {
|
||||
missing_i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Find prior glyphs
|
||||
let mut i = 0;
|
||||
while i < glyphs.len() {
|
||||
if glyphs[i].start >= start && glyphs[i].end <= end {
|
||||
break;
|
||||
} else {
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove prior glyphs
|
||||
while i < glyphs.len() {
|
||||
if glyphs[i].start >= start && glyphs[i].end <= end {
|
||||
let _glyph = glyphs.remove(i);
|
||||
// log::trace!("Removed {},{} from {}", _glyph.start, _glyph.end, i);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
while fb_i < fb_glyphs.len() {
|
||||
if fb_glyphs[fb_i].start >= start && fb_glyphs[fb_i].end <= end {
|
||||
let fb_glyph = fb_glyphs.remove(fb_i);
|
||||
// log::trace!("Insert {},{} from font {} at {}", fb_glyph.start, fb_glyph.end, font_i, i);
|
||||
glyphs.insert(i, fb_glyph);
|
||||
i += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug missing font fallbacks
|
||||
font_iter.check_missing(&line[start_word..end_word]);
|
||||
|
||||
/*
|
||||
for glyph in glyphs.iter() {
|
||||
log::trace!("'{}': {}, {}, {}, {}", &line[glyph.start..glyph.end], glyph.x_advance, glyph.y_advance, glyph.x_offset, glyph.y_offset);
|
||||
}
|
||||
*/
|
||||
|
||||
FontShapeWord { blank, glyphs }
|
||||
}
|
||||
|
||||
fn shape_span(
|
||||
&self,
|
||||
line: &str,
|
||||
start_span: usize,
|
||||
end_span: usize,
|
||||
line_rtl: bool,
|
||||
span_rtl: bool,
|
||||
) -> FontShapeSpan {
|
||||
let span = &line[start_span..end_span];
|
||||
|
||||
log::trace!(
|
||||
" Span {}: '{}'",
|
||||
if span_rtl { "RTL" } else { "LTR" },
|
||||
span
|
||||
);
|
||||
|
||||
let mut words = Vec::new();
|
||||
|
||||
let mut start_word = 0;
|
||||
for (end_lb, _) in unicode_linebreak::linebreaks(span) {
|
||||
let mut start_lb = end_lb;
|
||||
for (i, c) in span[start_word..end_lb].char_indices() {
|
||||
if start_word + i == end_lb {
|
||||
break;
|
||||
} else if c.is_whitespace() {
|
||||
start_lb = start_word + i;
|
||||
}
|
||||
}
|
||||
if start_word < start_lb {
|
||||
words.push(self.shape_word(
|
||||
line,
|
||||
start_span + start_word,
|
||||
start_span + start_lb,
|
||||
span_rtl,
|
||||
false,
|
||||
));
|
||||
}
|
||||
if start_lb < end_lb {
|
||||
words.push(self.shape_word(
|
||||
line,
|
||||
start_span + start_lb,
|
||||
start_span + end_lb,
|
||||
span_rtl,
|
||||
true,
|
||||
));
|
||||
}
|
||||
start_word = end_lb;
|
||||
}
|
||||
|
||||
// Reverse glyphs in RTL lines
|
||||
if line_rtl {
|
||||
for word in words.iter_mut() {
|
||||
word.glyphs.reverse();
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse words in spans that do not match line direction
|
||||
if line_rtl != span_rtl {
|
||||
words.reverse();
|
||||
}
|
||||
|
||||
FontShapeSpan {
|
||||
rtl: span_rtl,
|
||||
words,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shape_line(&self, line_i: FontLineIndex, line: &str) -> FontShapeLine {
|
||||
let mut spans = Vec::new();
|
||||
|
||||
let bidi = unicode_bidi::BidiInfo::new(line, None);
|
||||
let rtl = if bidi.paragraphs.is_empty() {
|
||||
false
|
||||
} else {
|
||||
assert_eq!(bidi.paragraphs.len(), 1);
|
||||
let para_info = &bidi.paragraphs[0];
|
||||
let line_rtl = para_info.level.is_rtl();
|
||||
|
||||
log::trace!("Line {}: '{}'", if line_rtl { "RTL" } else { "LTR" }, line);
|
||||
|
||||
let paragraph = unicode_bidi::Paragraph::new(&bidi, ¶_info);
|
||||
|
||||
let mut start = 0;
|
||||
let mut span_rtl = line_rtl;
|
||||
for i in paragraph.para.range.clone() {
|
||||
let next_rtl = paragraph.info.levels[i].is_rtl();
|
||||
if span_rtl != next_rtl {
|
||||
spans.push(self.shape_span(line, start, i, line_rtl, span_rtl));
|
||||
span_rtl = next_rtl;
|
||||
start = i;
|
||||
}
|
||||
}
|
||||
spans.push(self.shape_span(line, start, line.len(), line_rtl, span_rtl));
|
||||
|
||||
line_rtl
|
||||
};
|
||||
|
||||
FontShapeLine { line_i, rtl, spans }
|
||||
}
|
||||
}
|
||||
60
src/font/mod.rs
Normal file
60
src/font/mod.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use std::{collections::HashMap, sync::Mutex};
|
||||
|
||||
pub mod fallback;
|
||||
|
||||
pub use self::cache::*;
|
||||
mod cache;
|
||||
|
||||
pub use self::layout::*;
|
||||
mod layout;
|
||||
|
||||
pub use self::matches::*;
|
||||
mod matches;
|
||||
|
||||
pub use self::shape::*;
|
||||
mod shape;
|
||||
|
||||
pub use self::system::*;
|
||||
mod system;
|
||||
|
||||
#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct FontCacheKey {
|
||||
glyph_id: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
|
||||
pub struct FontLineIndex(usize);
|
||||
|
||||
impl FontLineIndex {
|
||||
pub fn new(index: usize) -> Self {
|
||||
Self(index)
|
||||
}
|
||||
|
||||
pub fn get(&self) -> usize {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Font<'a> {
|
||||
pub info: &'a fontdb::FaceInfo,
|
||||
pub data: &'a [u8],
|
||||
pub index: u32,
|
||||
pub rustybuzz: rustybuzz::Face<'a>,
|
||||
pub swash: swash::FontRef<'a>,
|
||||
pub scale_context: Mutex<swash::scale::ScaleContext>,
|
||||
pub cache: Mutex<HashMap<CacheKey, CacheItem>>,
|
||||
}
|
||||
|
||||
impl<'a> Font<'a> {
|
||||
pub fn new(info: &'a fontdb::FaceInfo, data: &'a [u8], index: u32) -> Option<Self> {
|
||||
Some(Self {
|
||||
info,
|
||||
data,
|
||||
index,
|
||||
rustybuzz: rustybuzz::Face::from_slice(data, index)?,
|
||||
swash: swash::FontRef::from_index(data, index as usize)?,
|
||||
scale_context: Mutex::new(swash::scale::ScaleContext::new()),
|
||||
cache: Mutex::new(HashMap::new()),
|
||||
})
|
||||
}
|
||||
}
|
||||
223
src/font/shape.rs
Normal file
223
src/font/shape.rs
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
use super::{CacheKey, Font, FontLayoutGlyph, FontLayoutLine, FontLineIndex};
|
||||
|
||||
pub struct FontShapeGlyph<'a> {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
pub x_advance: f32,
|
||||
pub y_advance: f32,
|
||||
pub x_offset: f32,
|
||||
pub y_offset: f32,
|
||||
pub font: &'a Font<'a>,
|
||||
pub inner: swash::GlyphId,
|
||||
}
|
||||
|
||||
impl<'a> FontShapeGlyph<'a> {
|
||||
fn layout(&self, font_size: i32, x: f32, y: f32) -> FontLayoutGlyph<'a> {
|
||||
let x_offset = font_size as f32 * self.x_offset;
|
||||
let y_offset = font_size as f32 * self.y_offset;
|
||||
let x_advance = font_size as f32 * self.x_advance;
|
||||
|
||||
let inner = CacheKey::new(self.inner, font_size, (x + x_offset, y - y_offset));
|
||||
FontLayoutGlyph {
|
||||
start: self.start,
|
||||
end: self.end,
|
||||
x: x,
|
||||
w: x_advance,
|
||||
font: self.font,
|
||||
inner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FontShapeWord<'a> {
|
||||
pub blank: bool,
|
||||
pub glyphs: Vec<FontShapeGlyph<'a>>,
|
||||
}
|
||||
|
||||
pub struct FontShapeSpan<'a> {
|
||||
pub rtl: bool,
|
||||
pub words: Vec<FontShapeWord<'a>>,
|
||||
}
|
||||
|
||||
pub struct FontShapeLine<'a> {
|
||||
pub line_i: FontLineIndex,
|
||||
pub rtl: bool,
|
||||
pub spans: Vec<FontShapeSpan<'a>>,
|
||||
}
|
||||
|
||||
impl<'a> FontShapeLine<'a> {
|
||||
pub fn layout(
|
||||
&self,
|
||||
font_size: i32,
|
||||
line_width: i32,
|
||||
layout_lines: &mut Vec<FontLayoutLine<'a>>,
|
||||
mut layout_i: usize,
|
||||
) {
|
||||
let mut push_line = true;
|
||||
let mut glyphs = Vec::new();
|
||||
|
||||
let start_x = if self.rtl { line_width as f32 } else { 0.0 };
|
||||
let end_x = if self.rtl { 0.0 } else { line_width as f32 };
|
||||
let mut x = start_x;
|
||||
let mut y = 0.0;
|
||||
for span in self.spans.iter() {
|
||||
//TODO: improve performance!
|
||||
let mut word_ranges = Vec::new();
|
||||
if self.rtl != span.rtl {
|
||||
let mut fit_x = x;
|
||||
let mut fitting_end = span.words.len();
|
||||
for i in (0..span.words.len()).rev() {
|
||||
let word = &span.words[i];
|
||||
|
||||
let mut word_size = 0.0;
|
||||
for glyph in word.glyphs.iter() {
|
||||
word_size += font_size as f32 * glyph.x_advance;
|
||||
}
|
||||
|
||||
let wrap = if self.rtl {
|
||||
fit_x - word_size < end_x
|
||||
} else {
|
||||
fit_x + word_size > end_x
|
||||
};
|
||||
|
||||
if wrap {
|
||||
let mut fitting_start = i + 1;
|
||||
while fitting_start < fitting_end {
|
||||
if span.words[fitting_start].blank {
|
||||
fitting_start += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
word_ranges.push((fitting_start..fitting_end, true));
|
||||
fitting_end = i + 1;
|
||||
|
||||
fit_x = start_x;
|
||||
}
|
||||
|
||||
if self.rtl {
|
||||
fit_x -= word_size;
|
||||
} else {
|
||||
fit_x += word_size;
|
||||
}
|
||||
}
|
||||
if !word_ranges.is_empty() {
|
||||
while fitting_end > 0 {
|
||||
if span.words[fitting_end - 1].blank {
|
||||
fitting_end -= 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
word_ranges.push((0..fitting_end, false));
|
||||
} else {
|
||||
let mut fit_x = x;
|
||||
let mut fitting_start = 0;
|
||||
for i in 0..span.words.len() {
|
||||
let word = &span.words[i];
|
||||
|
||||
let mut word_size = 0.0;
|
||||
for glyph in word.glyphs.iter() {
|
||||
word_size += font_size as f32 * glyph.x_advance;
|
||||
}
|
||||
|
||||
let wrap = if self.rtl {
|
||||
fit_x - word_size < end_x
|
||||
} else {
|
||||
fit_x + word_size > end_x
|
||||
};
|
||||
|
||||
if wrap {
|
||||
//TODO: skip blanks
|
||||
word_ranges.push((fitting_start..i, true));
|
||||
fitting_start = i;
|
||||
|
||||
fit_x = start_x;
|
||||
}
|
||||
|
||||
if self.rtl {
|
||||
fit_x -= word_size;
|
||||
} else {
|
||||
fit_x += word_size;
|
||||
}
|
||||
}
|
||||
word_ranges.push((fitting_start..span.words.len(), false));
|
||||
}
|
||||
|
||||
for (range, wrap) in word_ranges {
|
||||
for word in span.words[range].iter() {
|
||||
let mut word_size = 0.0;
|
||||
for glyph in word.glyphs.iter() {
|
||||
word_size += font_size as f32 * glyph.x_advance;
|
||||
}
|
||||
|
||||
//TODO: make wrapping optional
|
||||
let wrap = if self.rtl {
|
||||
x - word_size < end_x
|
||||
} else {
|
||||
x + word_size > end_x
|
||||
};
|
||||
if wrap && !glyphs.is_empty() {
|
||||
let mut glyphs_swap = Vec::new();
|
||||
std::mem::swap(&mut glyphs, &mut glyphs_swap);
|
||||
layout_lines.insert(
|
||||
layout_i,
|
||||
FontLayoutLine {
|
||||
line_i: self.line_i,
|
||||
glyphs: glyphs_swap,
|
||||
},
|
||||
);
|
||||
layout_i += 1;
|
||||
|
||||
x = start_x;
|
||||
y = 0.0;
|
||||
}
|
||||
|
||||
for glyph in word.glyphs.iter() {
|
||||
let x_advance = font_size as f32 * glyph.x_advance;
|
||||
let y_advance = font_size as f32 * glyph.y_advance;
|
||||
|
||||
if self.rtl {
|
||||
x -= x_advance
|
||||
}
|
||||
|
||||
glyphs.push(glyph.layout(font_size, x, y));
|
||||
push_line = true;
|
||||
|
||||
if !self.rtl {
|
||||
x += x_advance;
|
||||
}
|
||||
y += y_advance;
|
||||
}
|
||||
}
|
||||
|
||||
if wrap {
|
||||
let mut glyphs_swap = Vec::new();
|
||||
std::mem::swap(&mut glyphs, &mut glyphs_swap);
|
||||
layout_lines.insert(
|
||||
layout_i,
|
||||
FontLayoutLine {
|
||||
line_i: self.line_i,
|
||||
glyphs: glyphs_swap,
|
||||
},
|
||||
);
|
||||
layout_i += 1;
|
||||
|
||||
x = start_x;
|
||||
y = 0.0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if push_line {
|
||||
layout_lines.insert(
|
||||
layout_i,
|
||||
FontLayoutLine {
|
||||
line_i: self.line_i,
|
||||
glyphs,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/font/system.rs
Normal file
83
src/font/system.rs
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
use std::ops::Deref;
|
||||
|
||||
use super::{Font, FontMatches};
|
||||
|
||||
pub struct FontSystem {
|
||||
pub locale: String,
|
||||
db: fontdb::Database,
|
||||
}
|
||||
|
||||
impl FontSystem {
|
||||
pub fn new() -> Self {
|
||||
let locale = sys_locale::get_locale().unwrap_or_else(|| {
|
||||
log::warn!("failed to get system locale, falling back to en-US");
|
||||
String::from("en-US")
|
||||
});
|
||||
log::info!("Locale: {}", locale);
|
||||
|
||||
let mut db = fontdb::Database::new();
|
||||
let now = std::time::Instant::now();
|
||||
db.load_system_fonts();
|
||||
//TODO: configurable default fonts
|
||||
db.set_monospace_family("Fira Mono");
|
||||
db.set_sans_serif_family("Fira Sans");
|
||||
db.set_serif_family("DejaVu Serif");
|
||||
log::info!(
|
||||
"Loaded {} font faces in {}ms.",
|
||||
db.len(),
|
||||
now.elapsed().as_millis()
|
||||
);
|
||||
|
||||
//TODO only do this on demand!
|
||||
assert_eq!(db.len(), db.faces().len());
|
||||
for i in 0..db.len() {
|
||||
let id = db.faces()[i].id;
|
||||
unsafe {
|
||||
db.make_shared_face_data(id);
|
||||
}
|
||||
}
|
||||
|
||||
Self { locale, db }
|
||||
}
|
||||
|
||||
pub fn matches<'a, F: Fn(&fontdb::FaceInfo) -> bool>(
|
||||
&'a self,
|
||||
f: F,
|
||||
) -> Option<FontMatches<'a>> {
|
||||
let mut fonts = Vec::new();
|
||||
for face in self.db.faces() {
|
||||
if !f(face) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let font_opt = Font::new(
|
||||
face,
|
||||
match &face.source {
|
||||
fontdb::Source::Binary(data) => data.deref().as_ref(),
|
||||
fontdb::Source::File(path) => {
|
||||
log::warn!("Unsupported fontdb Source::File('{}')", path.display());
|
||||
continue;
|
||||
}
|
||||
fontdb::Source::SharedFile(_path, data) => data.deref().as_ref(),
|
||||
},
|
||||
face.index,
|
||||
);
|
||||
|
||||
match font_opt {
|
||||
Some(font) => fonts.push(font),
|
||||
None => {
|
||||
log::warn!("failed to load font '{}'", face.post_script_name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !fonts.is_empty() {
|
||||
Some(FontMatches {
|
||||
locale: &self.locale,
|
||||
fonts
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
5
src/lib.rs
Normal file
5
src/lib.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub use self::buffer::*;
|
||||
mod buffer;
|
||||
|
||||
pub use self::font::*;
|
||||
mod font;
|
||||
Loading…
Add table
Add a link
Reference in a new issue