ruffle/core/src/font.rs

251 lines
8.6 KiB
Rust
Raw Normal View History

2019-07-19 08:32:41 +00:00
use crate::backend::render::{RenderBackend, ShapeHandle};
2019-10-08 02:27:31 +00:00
use crate::prelude::*;
use crate::transform::Transform;
2019-12-17 05:21:59 +00:00
use gc_arena::{Collect, Gc, MutationContext};
2019-05-04 18:45:11 +00:00
type Error = Box<dyn std::error::Error>;
2019-05-04 18:45:11 +00:00
mod text_format;
pub use text_format::TextFormat;
2019-12-17 05:21:59 +00:00
#[derive(Debug, Clone, Collect, Copy)]
#[collect(no_drop)]
pub struct Font<'gc>(Gc<'gc, FontData>);
#[derive(Debug, Clone, Collect)]
#[collect(require_static)]
struct FontData {
2019-10-08 02:27:31 +00:00
/// The list of glyphs defined in the font.
/// Used directly by `DefineText` tags.
glyphs: Vec<Glyph>,
/// A map from a Unicode code point to glyph in the `glyphs` array.
/// Used by `DefineEditText` tags.
code_point_to_glyph: fnv::FnvHashMap<u16, usize>,
/// The scaling applied to the font height to render at the proper size.
/// This depends on the DefineFont tag version.
scale: f32,
2019-10-08 02:27:31 +00:00
/// Kerning infomration.
/// Maps from a pair of unicode code points to horizontal offset value.
kerning_pairs: fnv::FnvHashMap<(u16, u16), Twips>,
2019-05-04 18:45:11 +00:00
}
2019-12-17 05:21:59 +00:00
impl<'gc> Font<'gc> {
pub fn from_swf_tag(
gc_context: MutationContext<'gc, '_>,
renderer: &mut dyn RenderBackend,
tag: &swf::Font,
) -> Result<Font<'gc>, Error> {
2019-05-04 18:45:11 +00:00
let mut glyphs = vec![];
2019-10-08 02:27:31 +00:00
let mut code_point_to_glyph = fnv::FnvHashMap::default();
for swf_glyph in &tag.glyphs {
let glyph = Glyph {
shape: renderer.register_glyph_shape(swf_glyph),
advance: swf_glyph.advance.unwrap_or(0),
};
let index = glyphs.len();
glyphs.push(glyph);
code_point_to_glyph.insert(swf_glyph.code, index);
2019-05-04 18:45:11 +00:00
}
2019-10-08 02:27:31 +00:00
let kerning_pairs: fnv::FnvHashMap<(u16, u16), Twips> = if let Some(layout) = &tag.layout {
layout
.kerning
.iter()
.map(|kerning| ((kerning.left_code, kerning.right_code), kerning.adjustment))
.collect()
} else {
fnv::FnvHashMap::default()
};
2019-12-17 05:21:59 +00:00
Ok(Font(Gc::allocate(
gc_context,
FontData {
glyphs,
code_point_to_glyph,
2019-12-17 05:21:59 +00:00
/// DefineFont3 stores coordinates at 20x the scale of DefineFont1/2.
/// (SWF19 p.164)
scale: if tag.version >= 3 { 20480.0 } else { 1024.0 },
kerning_pairs,
},
)))
2019-05-04 18:45:11 +00:00
}
/// Returns whether this font contains glyph shapes.
/// If not, this font should be rendered as a device font.
2019-12-17 05:21:59 +00:00
pub fn has_glyphs(self) -> bool {
!self.0.glyphs.is_empty()
}
2019-10-08 02:27:31 +00:00
/// Returns a glyph entry by index.
/// Used by `Text` display objects.
2019-12-17 05:21:59 +00:00
pub fn get_glyph(self, i: usize) -> Option<Glyph> {
self.0.glyphs.get(i).cloned()
2019-05-04 18:45:11 +00:00
}
2019-10-08 02:27:31 +00:00
/// Returns a glyph entry by character.
/// Used by `EditText` display objects.
2019-12-17 05:21:59 +00:00
pub fn get_glyph_for_char(self, c: char) -> Option<Glyph> {
2019-10-08 02:27:31 +00:00
// TODO: Properly handle UTF-16/out-of-bounds code points.
let code_point = c as u16;
2019-12-17 05:21:59 +00:00
if let Some(index) = self.0.code_point_to_glyph.get(&code_point) {
2019-10-08 02:27:31 +00:00
self.get_glyph(*index)
} else {
None
}
}
/// Given a pair of characters, applies the offset that should be applied
/// to the advance value between these two characters.
/// Returns 0 twips if no kerning offset exists between these two characters.
2019-12-17 05:21:59 +00:00
pub fn get_kerning_offset(self, left: char, right: char) -> Twips {
2019-10-08 02:27:31 +00:00
// TODO: Properly handle UTF-16/out-of-bounds code points.
let left_code_point = left as u16;
let right_code_point = right as u16;
2019-12-17 05:21:59 +00:00
self.0
.kerning_pairs
2019-10-08 02:27:31 +00:00
.get(&(left_code_point, right_code_point))
.cloned()
.unwrap_or_default()
}
/// Returns whether this font contains kerning information.
2019-12-17 05:21:59 +00:00
pub fn has_kerning_info(self) -> bool {
!self.0.kerning_pairs.is_empty()
2019-10-08 02:27:31 +00:00
}
2019-12-17 05:21:59 +00:00
pub fn scale(self) -> f32 {
self.0.scale
}
/// Evaluate this font against a particular string on a glyph-by-glyph
/// basis.
///
/// This function takes the text string to evaluate against, the base
/// transform to start from, the height of each glyph, and produces a list
/// of transforms and glyphs which will be consumed by the `glyph_func`
/// closure. This corresponds to the series of drawing operations necessary
/// to render the text on a single horizontal line.
pub fn evaluate<FGlyph>(
self,
text: &str,
mut transform: Transform,
height: f32,
is_html: bool,
mut glyph_func: FGlyph,
) where
FGlyph: FnMut(&Transform, &Glyph),
{
transform.matrix.ty += height * Twips::TWIPS_PER_PIXEL as f32;
let scale = (height * Twips::TWIPS_PER_PIXEL as f32) / self.scale();
transform.matrix.a = scale;
transform.matrix.d = scale;
let mut chars = text.chars().peekable();
let has_kerning_info = self.has_kerning_info();
while let Some(c) = chars.next() {
// TODO: SWF text fields can contain a limited subset of HTML (and often do in SWF versions >6).
// This is a quicky-and-dirty way to skip the HTML tags. This is obviously not correct
// and we will need to properly parse and handle the HTML at some point.
// See SWF19 pp. 173-174 for supported HTML tags.
if is_html && c == '<' {
// Skip characters until we see a close bracket.
chars.by_ref().skip_while(|&x| x != '>').next();
} else if let Some(glyph) = self.get_glyph_for_char(c) {
glyph_func(&transform, &glyph);
// Step horizontally.
let mut advance = f32::from(glyph.advance);
if has_kerning_info {
advance += self
.get_kerning_offset(c, chars.peek().cloned().unwrap_or('\0'))
.get() as f32;
}
transform.matrix.tx += advance * scale;
}
}
}
/// Measure a particular string's metrics (width and height).
pub fn measure(self, text: &str, height: f32, is_html: bool) -> (f32, f32) {
let mut size = (0.0, 0.0);
self.evaluate(
text,
Default::default(),
height,
is_html,
|transform, _glyph| {
let tx = transform.matrix.tx / Twips::TWIPS_PER_PIXEL as f32;
let ty = transform.matrix.ty / Twips::TWIPS_PER_PIXEL as f32;
size.0 = f32::max(size.0, tx);
size.1 = f32::max(size.1, ty);
},
);
size
}
/// Given a line of text, split it into the shortest number of lines that
/// are shorter than `width`.
///
/// This function assumes only `" "` is valid whitespace to split words on,
/// and will not attempt to break words that are longer than `width`.
pub fn split_wrapped_lines(
self,
text: &str,
height: f32,
width: f32,
is_html: bool,
) -> Vec<&str> {
let mut result = vec![];
let mut current_width = width;
let mut current_word = &text[0..0];
// TODO: This function should include the spaces
for word in text.split(' ') {
let measure = self.measure(word, height, is_html);
let line_start = current_word.as_ptr() as usize - text.as_ptr() as usize;
let start = word.as_ptr() as usize - text.as_ptr() as usize;
let end_w_spc = if (start + word.len() + 1) < text.len() {
start + word.len() + 1
} else {
start + word.len()
};
if measure.0 > current_width && measure.0 > width {
//Failsafe for if we get a word wider than the field.
if !current_word.is_empty() {
result.push(current_word);
}
result.push(&text[start..end_w_spc]);
current_word = &text[end_w_spc..end_w_spc];
current_width = width;
} else if measure.0 > current_width {
if !current_word.is_empty() {
result.push(current_word);
}
current_word = &text[start..end_w_spc];
current_width = width;
} else {
current_word = &text[line_start..end_w_spc];
current_width -= measure.0;
}
}
if !current_word.is_empty() {
result.push(current_word);
}
result
}
2019-05-04 18:45:11 +00:00
}
2019-10-08 02:27:31 +00:00
#[derive(Debug, Clone)]
pub struct Glyph {
pub shape: ShapeHandle,
pub advance: i16,
}