summaryrefslogtreecommitdiff
path: root/src/layout/text.rs
blob: 5c18cd328796736a40d2f2b4fdb2c85cbb308b65 (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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! The text layouter layouts continous pieces of text into boxes.
//!
//! The layouter picks the most suitable font for each individual character.
//! When the primary layouting axis horizontally inversed, the word is spelled
//! backwards. Vertical word layout is not yet supported.

use ttf_parser::GlyphId;
use fontdock::{FaceId, FaceQuery, FontStyle};
use crate::font::SharedFontLoader;
use crate::geom::Size;
use crate::style::TextStyle;
use super::elements::{LayoutElement, Shaped};
use super::*;

/// Performs the text layouting.
#[derive(Debug)]
struct TextLayouter<'a> {
    ctx: TextContext<'a>,
    text: &'a str,
    shaped: Shaped,
    elements: LayoutElements,
    start: f64,
    width: f64,
}

/// The context for text layouting.
#[derive(Debug, Copy, Clone)]
pub struct TextContext<'a> {
    /// The font loader to retrieve fonts from when typesetting text
    /// using [`layout_text`].
    pub loader: &'a SharedFontLoader,
    /// The style for text: Font selection with classes, weights and variants,
    /// font sizes, spacing and so on.
    pub style: &'a TextStyle,
    /// The axes along which the word is laid out. For now, only
    /// primary-horizontal layouting is supported.
    pub axes: LayoutAxes,
    /// The alignment of the finished layout.
    pub align: LayoutAlign,
}

/// Layouts text into a box.
pub async fn layout_text(text: &str, ctx: TextContext<'_>) -> BoxLayout {
    TextLayouter::new(text, ctx).layout().await
}

impl<'a> TextLayouter<'a> {
    /// Create a new text layouter.
    fn new(text: &'a str, ctx: TextContext<'a>) -> TextLayouter<'a> {
        TextLayouter {
            ctx,
            text,
            shaped: Shaped::new(FaceId::MAX, ctx.style.font_size()),
            elements: LayoutElements::new(),
            start: 0.0,
            width: 0.0,
        }
    }

    /// Do the layouting.
    async fn layout(mut self) -> BoxLayout {
        // If the primary axis is negative, we layout the characters reversed.
        if self.ctx.axes.primary.is_positive() {
            for c in self.text.chars() {
                self.layout_char(c).await;
            }
        } else {
            for c in self.text.chars().rev() {
                self.layout_char(c).await;
            }
        }

        // Flush the last buffered parts of the word.
        if !self.shaped.text.is_empty() {
            let pos = Size::new(self.start, 0.0);
            self.elements.push(pos, LayoutElement::Text(self.shaped));
        }

        BoxLayout {
            size: Size::new(self.width, self.ctx.style.font_size()),
            align: self.ctx.align,
            elements: self.elements,
        }
    }

    /// Layout an individual character.
    async fn layout_char(&mut self, c: char) {
        let (index, glyph, char_width) = match self.select_font(c).await {
            Some(selected) => selected,
            // TODO: Issue warning about missing character.
            None => return,
        };

        // Flush the buffer and issue a font setting action if the font differs
        // from the last character's one.
        if self.shaped.face != index {
            if !self.shaped.text.is_empty() {
                let pos = Size::new(self.start, 0.0);
                let shaped = std::mem::replace(
                    &mut self.shaped,
                    Shaped::new(FaceId::MAX, self.ctx.style.font_size()),
                );

                self.elements.push(pos, LayoutElement::Text(shaped));
                self.start = self.width;
            }

            self.shaped.face = index;
        }

        self.shaped.text.push(c);
        self.shaped.glyphs.push(glyph);
        self.shaped.offsets.push(self.width);

        self.width += char_width;
    }

    /// Select the best font for a character and return its index along with
    /// the width of the char in the font.
    async fn select_font(&mut self, c: char) -> Option<(FaceId, GlyphId, f64)> {
        let mut loader = self.ctx.loader.borrow_mut();

        let mut variant = self.ctx.style.variant;

        if self.ctx.style.bolder {
            variant.weight.0 += 300;
        }

        if self.ctx.style.italic {
            variant.style = match variant.style {
                FontStyle::Normal => FontStyle::Italic,
                FontStyle::Italic => FontStyle::Normal,
                FontStyle::Oblique => FontStyle::Normal,
            }
        }

        let query = FaceQuery {
            fallback: self.ctx.style.fallback.iter(),
            variant,
            c,
        };

        if let Some((id, face)) = loader.query(query).await {
            // Determine the width of the char.
            let units_per_em = face.units_per_em().unwrap_or(1000) as f64;
            let ratio = 1.0 / units_per_em;
            let to_raw = |x| ratio * x as f64;

            let glyph = face.glyph_index(c)?;
            let glyph_width = face.glyph_hor_advance(glyph)?;
            let char_width = to_raw(glyph_width) * self.ctx.style.font_size();

            Some((id, glyph, char_width))
        } else {
            None
        }
    }
}