1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
|
use fontdock::FaceId;
use rustybuzz::UnicodeBuffer;
use ttf_parser::GlyphId;
use super::{Element, Frame, ShapedText};
use crate::env::FontLoader;
use crate::exec::FontProps;
use crate::geom::{Length, Point, Size};
/// Shape text into a frame containing shaped [`ShapedText`] runs.
pub fn shape(text: &str, loader: &mut FontLoader, props: &FontProps) -> Frame {
let mut frame = Frame::new(Size::new(Length::ZERO, Length::ZERO));
shape_segment(&mut frame, text, props.families.iter(), None, loader, props);
frame
}
/// Shape text into a frame with font fallback using the `families` iterator.
fn shape_segment<'a>(
frame: &mut Frame,
text: &str,
mut families: impl Iterator<Item = &'a str> + Clone,
mut first: Option<FaceId>,
loader: &mut FontLoader,
props: &FontProps,
) {
// Select the font family.
let (id, fallback) = loop {
// Try to load the next available font family.
match families.next() {
Some(family) => match loader.query(family, props.variant) {
Some(id) => break (id, true),
None => {}
},
// We're out of families, so we don't do any more fallback and just
// shape the tofus with the first face we originally used.
None => match first {
Some(id) => break (id, false),
None => return,
},
}
};
// Register that this is the first available font.
let face = loader.face(id);
if first.is_none() {
first = Some(id);
}
// Find out some metrics and prepare the shaped text container.
let ttf = face.ttf();
let units_per_em = f64::from(ttf.units_per_em().unwrap_or(1000));
let convert = |units| f64::from(units) / units_per_em * props.size;
let top = convert(i32::from(props.top_edge.lookup(ttf)));
let bottom = convert(i32::from(props.bottom_edge.lookup(ttf)));
let mut shaped = ShapedText::new(id, props.size, top, bottom, props.color);
// Fill the buffer with our text.
let mut buffer = UnicodeBuffer::new();
buffer.push_str(text);
buffer.guess_segment_properties();
// Find out the text direction.
// TODO: Replace this once we do BiDi.
let rtl = matches!(buffer.direction(), rustybuzz::Direction::RightToLeft);
// Shape!
let glyphs = rustybuzz::shape(face.buzz(), &[], buffer);
let info = glyphs.glyph_infos();
let pos = glyphs.glyph_positions();
let mut iter = info.iter().zip(pos).peekable();
while let Some((info, pos)) = iter.next() {
// Do font fallback if the glyph is a tofu.
if info.codepoint == 0 && fallback {
// Flush what we have so far.
if !shaped.glyphs.is_empty() {
place(frame, shaped);
shaped = ShapedText::new(id, props.size, top, bottom, props.color);
}
// Determine the start and end cluster index of the tofu sequence.
let mut start = info.cluster as usize;
let mut end = info.cluster as usize;
while let Some((info, _)) = iter.peek() {
if info.codepoint != 0 {
break;
}
end = info.cluster as usize;
iter.next();
}
// Because Harfbuzz outputs glyphs in visual order, the start
// cluster actually corresponds to the last codepoint in
// right-to-left text.
if rtl {
assert!(end <= start);
std::mem::swap(&mut start, &mut end);
}
// The end cluster index points right before the last character that
// mapped to the tofu sequence. So we have to offset the end by one
// char.
let offset = text[end ..].chars().next().unwrap().len_utf8();
let range = start .. end + offset;
// Recursively shape the tofu sequence with the next family.
shape_segment(frame, &text[range], families.clone(), first, loader, props);
} else {
// Add the glyph to the shaped output.
// TODO: Don't ignore y_advance and y_offset.
let glyph = GlyphId(info.codepoint as u16);
shaped.glyphs.push(glyph);
shaped.offsets.push(shaped.width + convert(pos.x_offset));
shaped.width += convert(pos.x_advance);
}
}
if !shaped.glyphs.is_empty() {
place(frame, shaped)
}
}
/// Place shaped text into a frame.
fn place(frame: &mut Frame, shaped: ShapedText) {
let offset = frame.size.width;
frame.size.width += shaped.width;
frame.size.height = frame.size.height.max(shaped.top - shaped.bottom);
frame.push(Point::new(offset, shaped.top), Element::Text(shaped));
}
|