summaryrefslogtreecommitdiff
path: root/src/layout/shaping.rs
blob: f7eece929a9aa81dc6046d3d03f786f8bfcfaa1d (plain) (blame)
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));
}