pixel_widgets/
text.rs

1use crate::draw::Color;
2use crate::layout::Rectangle;
3use std::borrow::Cow;
4use std::iter::Peekable;
5
6/// How to wrap text
7#[derive(Clone, Copy, Debug)]
8pub enum TextWrap {
9    /// Don't wrap text at all (fastest)
10    NoWrap,
11    /// Wrap text per character
12    Wrap,
13    /// Try to keep words on the same line (slowest)
14    WordWrap,
15}
16
17/// A font loaded by the [`Ui`](../struct.Ui.html)
18#[derive(Clone)]
19pub struct Font {
20    pub(crate) inner: super::cache::Font,
21    pub(crate) id: super::cache::FontId,
22    pub(crate) tex_slot: usize,
23}
24
25impl std::fmt::Debug for Font {
26    fn fmt(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
27        formatter.debug_struct("Font").finish()
28    }
29}
30
31/// A styled paragraph of text
32#[derive(Clone)]
33pub struct Text<'a> {
34    /// The text
35    pub text: Cow<'a, str>,
36    /// Font to render the text with
37    pub font: Font,
38    /// Font size to render the text with
39    pub size: f32,
40    /// Wrapping style to use
41    pub wrap: TextWrap,
42    /// Color to render the text with
43    pub color: Color,
44}
45
46/// Iterator over characters that have been layout by the rusttype engine.
47pub struct CharPositionIter<'a, 'b: 'a> {
48    font: &'b rusttype::Font<'static>,
49    scale: rusttype::Scale,
50    x: f32,
51    base: Peekable<std::str::Chars<'a>>,
52}
53
54impl<'a, 'b> Iterator for CharPositionIter<'a, 'b> {
55    type Item = (char, rusttype::ScaledGlyph<'static>, f32, f32);
56
57    fn next(&mut self) -> Option<Self::Item> {
58        let c = self.base.next()?;
59        let n = self.base.peek().cloned();
60        let g = self.font.glyph(c);
61        let g = g.scaled(self.scale);
62        let w = g.h_metrics().advance_width + n.map(|n| self.font.pair_kerning(self.scale, g.id(), n)).unwrap_or(0.0);
63        let elem = (c, g, self.x, self.x + w);
64        self.x += w;
65        Some(elem)
66    }
67}
68
69struct WordWrapper<'a, 'b: 'a> {
70    x: f32,
71    y: f32,
72    final_x: f32,
73    final_y: f32,
74    width: f32,
75    height: f32,
76    iter: CharPositionIter<'a, 'b>,
77    f: &'a mut dyn FnMut(rusttype::ScaledGlyph<'static>, f32, f32, f32),
78}
79
80impl<'a, 'b: 'a> WordWrapper<'a, 'b> {
81    fn layout_word(&mut self, glyph: rusttype::ScaledGlyph<'static>, a: f32, b: f32, c: f32, mut word: bool) {
82        if word {
83            self.x = self.final_x;
84            self.y = self.final_y;
85
86            if let Some((ch, glyph, b, c)) = self.iter.next() {
87                if ch.is_alphanumeric() {
88                    if c - self.x > self.width {
89                        self.x = a;
90                        self.y += self.height;
91                        word = false;
92                    }
93                    self.layout_word(glyph, a, b, c, word);
94                }
95            }
96
97            (self.f)(glyph, b - self.x, c - self.x, self.y);
98        } else {
99            self.final_x = self.x;
100            self.final_y = self.y;
101
102            if c - self.final_x > self.width {
103                self.final_x = b;
104                self.final_y += self.height;
105            }
106            (self.f)(glyph, b - self.final_x, c - self.final_x, self.final_y);
107
108            for (ch, glyph, b, c) in &mut self.iter {
109                if c - self.final_x > self.width {
110                    self.final_x = b;
111                    self.final_y += self.height;
112                }
113
114                (self.f)(glyph, b - self.final_x, c - self.final_x, self.final_y);
115
116                if !ch.is_alphanumeric() {
117                    break;
118                }
119            }
120        }
121    }
122}
123
124impl<'t> Text<'t> {
125    pub(crate) fn char_positions<'a, 'b>(&'b self) -> CharPositionIter<'a, 'b> {
126        let scale = rusttype::Scale {
127            x: self.size,
128            y: self.size,
129        };
130        CharPositionIter {
131            font: &self.font.inner,
132            scale,
133            x: 0.0,
134            base: self.text.chars().peekable(),
135        }
136    }
137
138    pub(crate) fn layout<F: FnMut(rusttype::ScaledGlyph<'static>, f32, f32, f32)>(&self, rect: Rectangle, mut f: F) {
139        let line = self.font.inner.v_metrics(rusttype::Scale {
140            x: self.size,
141            y: self.size,
142        });
143
144        let width = rect.width();
145        let height = -line.descent + line.line_gap + line.ascent;
146
147        match self.wrap {
148            TextWrap::NoWrap => {
149                for (_, g, a, b) in self.char_positions() {
150                    f(g, a, b, line.ascent);
151                }
152            }
153
154            TextWrap::Wrap => {
155                let mut x = 0.0;
156                let mut y = line.ascent;
157
158                for (_, g, a, b) in self.char_positions() {
159                    if b - x > width {
160                        x = a;
161                        y += height;
162                    }
163
164                    f(g, a - x, b - x, y);
165                }
166            }
167
168            TextWrap::WordWrap => {
169                let mut wrapper = WordWrapper {
170                    x: 0.0,
171                    y: line.ascent,
172                    final_x: 0.0,
173                    final_y: line.ascent,
174                    width,
175                    height,
176                    iter: self.char_positions(),
177                    f: &mut f,
178                };
179
180                while let Some((ch, glyph, a, b)) = wrapper.iter.next() {
181                    wrapper.layout_word(glyph, a, a, b, ch.is_alphanumeric());
182                }
183            }
184        }
185    }
186
187    /// Measure the size of the text. If a rectangle is supplied and the text wraps,
188    /// the layout will stay within the width of the given rectangle.
189    pub fn measure(&self, rect: Option<Rectangle>) -> Rectangle {
190        let line = self.font.inner.v_metrics(rusttype::Scale {
191            x: self.size,
192            y: self.size,
193        });
194
195        match rect {
196            None => {
197                let mut w = 0.0;
198                self.layout(Rectangle::from_wh(f32::INFINITY, 0.0), |_, _, new_w, _| w = new_w);
199
200                Rectangle::from_wh(w.ceil(), (line.ascent - line.descent).ceil())
201            }
202            Some(r) => {
203                let mut w = 0.0;
204                let mut h = line.ascent;
205                match self.wrap {
206                    TextWrap::NoWrap => self.layout(r, |_, _, new_w, _| w = new_w),
207                    TextWrap::Wrap | TextWrap::WordWrap => {
208                        w = rect.map_or(0.0, |r| r.width());
209                        self.layout(r, |_, _, _, new_h| h = new_h);
210                    }
211                }
212
213                Rectangle::from_xywh(r.left, r.top, w.ceil(), (h - line.descent).ceil())
214            }
215        }
216    }
217
218    /// Measure the start and end coordinates of some selected glyphs
219    pub fn measure_range(&self, from: usize, to: usize, rect: Rectangle) -> ((f32, f32), (f32, f32)) {
220        let mut from_result = (0.0, 0.0);
221        let mut to_result = (0.0, 0.0);
222
223        let mut index = 0;
224        self.layout(rect, |_, begin, end, y| {
225            if index == from {
226                from_result = (begin, y)
227            }
228            if index == to {
229                to_result = (begin, y)
230            }
231            if index + 1 == from {
232                from_result = (end, y)
233            }
234            if index + 1 == to {
235                to_result = (end, y)
236            }
237            index += 1;
238        });
239
240        (from_result, to_result)
241    }
242
243    /// Find out the index of a character where the mouse is.
244    pub fn hitdetect(&self, cursor: (f32, f32), rect: Rectangle) -> usize {
245        let dist = |(x, y)| x * x + y * y;
246
247        let mut nearest = (dist(cursor), 0);
248        let mut index = 0;
249
250        self.layout(rect, |_, begin, end, y| {
251            if dist((begin - cursor.0, y - cursor.1)) < nearest.0 {
252                nearest.0 = dist((begin - cursor.0, y - cursor.1));
253                nearest.1 = index;
254            }
255            if dist((end - cursor.0, y - cursor.1)) < nearest.0 {
256                nearest.0 = dist((end - cursor.0, y - cursor.1));
257                nearest.1 = index + 1;
258            }
259
260            index += 1;
261        });
262
263        nearest.1
264    }
265}
266
267impl<'a> Text<'a> {
268    /// Convert a borrowed to text to owned text.
269    pub fn to_owned(&self) -> Text<'static> {
270        Text {
271            text: Cow::Owned(self.text.clone().into_owned()),
272            font: self.font.clone(),
273            size: self.size,
274            wrap: self.wrap,
275            color: self.color,
276        }
277    }
278}