rg3d_ui/
formatted_text.rs

1use crate::{
2    brush::Brush,
3    core::{algebra::Vector2, color::Color, math::Rect},
4    ttf::SharedFont,
5    Font, HorizontalAlignment, VerticalAlignment, DEFAULT_FONT,
6};
7use std::ops::Range;
8
9#[derive(Debug, Clone)]
10pub struct TextGlyph {
11    bounds: Rect<f32>,
12    tex_coords: [Vector2<f32>; 4],
13}
14
15impl TextGlyph {
16    pub fn get_bounds(&self) -> Rect<f32> {
17        self.bounds
18    }
19
20    pub fn get_tex_coords(&self) -> &[Vector2<f32>; 4] {
21        &self.tex_coords
22    }
23}
24
25#[derive(Copy, Clone, Debug)]
26pub struct TextLine {
27    /// Index of starting symbol in text array.
28    pub begin: usize,
29    /// Index of ending symbol in text array.
30    pub end: usize,
31    /// Total width of line.
32    pub width: f32,
33    /// Total height of line. Usually just ascender of a font.
34    pub height: f32,
35    /// Local horizontal position of line.
36    pub x_offset: f32,
37    /// Local vertical position of line.
38    pub y_offset: f32,
39}
40
41impl TextLine {
42    fn new() -> TextLine {
43        TextLine {
44            begin: 0,
45            end: 0,
46            width: 0.0,
47            height: 0.0,
48            x_offset: 0.0,
49            y_offset: 0.0,
50        }
51    }
52
53    pub fn len(&self) -> usize {
54        self.end - self.begin
55    }
56
57    pub fn is_empty(&self) -> bool {
58        self.end == self.begin
59    }
60}
61
62/// Wrapping mode for formatted text.
63#[derive(Copy, Clone, PartialOrd, PartialEq, Hash, Debug)]
64pub enum WrapMode {
65    /// No wrapping needed.
66    NoWrap,
67
68    /// Letter-based wrapping.
69    Letter,
70
71    /// Word-based wrapping.
72    Word,
73}
74
75#[derive(Copy, Clone, Debug)]
76pub struct Character {
77    pub char_code: u32,
78    pub glyph_index: u32,
79}
80
81impl Character {
82    pub fn from_char_with_font(char_code: u32, font: &Font) -> Self {
83        Self {
84            char_code,
85            glyph_index: font.glyph_index(char_code).unwrap_or_default() as u32,
86        }
87    }
88}
89
90#[derive(Clone, Debug)]
91pub struct FormattedText {
92    font: SharedFont,
93    text: Vec<Character>,
94    // Temporary buffer used to split text on lines. We need it to reduce memory allocations
95    // when we changing text too frequently, here we sacrifice some memory in order to get
96    // more performance.
97    lines: Vec<TextLine>,
98    // Final glyphs for draw buffer.
99    glyphs: Vec<TextGlyph>,
100    vertical_alignment: VerticalAlignment,
101    horizontal_alignment: HorizontalAlignment,
102    brush: Brush,
103    constraint: Vector2<f32>,
104    wrap: WrapMode,
105    mask_char: Option<Character>,
106}
107
108#[derive(Copy, Clone, Debug)]
109struct Word {
110    width: f32,
111    length: usize,
112}
113
114impl FormattedText {
115    pub fn get_glyphs(&self) -> &[TextGlyph] {
116        &self.glyphs
117    }
118
119    pub fn get_font(&self) -> SharedFont {
120        self.font.clone()
121    }
122
123    pub fn set_font(&mut self, font: SharedFont) -> &mut Self {
124        self.font = font;
125        self
126    }
127
128    pub fn get_lines(&self) -> &[TextLine] {
129        &self.lines
130    }
131
132    pub fn set_vertical_alignment(&mut self, vertical_alignment: VerticalAlignment) -> &mut Self {
133        self.vertical_alignment = vertical_alignment;
134        self
135    }
136
137    pub fn vertical_alignment(&self) -> VerticalAlignment {
138        self.vertical_alignment
139    }
140
141    pub fn set_horizontal_alignment(
142        &mut self,
143        horizontal_alignment: HorizontalAlignment,
144    ) -> &mut Self {
145        self.horizontal_alignment = horizontal_alignment;
146        self
147    }
148
149    pub fn horizontal_alignment(&self) -> HorizontalAlignment {
150        self.horizontal_alignment
151    }
152
153    pub fn set_brush(&mut self, brush: Brush) -> &mut Self {
154        self.brush = brush;
155        self
156    }
157
158    pub fn brush(&self) -> Brush {
159        self.brush.clone()
160    }
161
162    pub fn set_constraint(&mut self, constraint: Vector2<f32>) -> &mut Self {
163        self.constraint = constraint;
164        self
165    }
166
167    pub fn get_raw_text(&self) -> &[Character] {
168        &self.text
169    }
170
171    pub fn text(&self) -> String {
172        self.text
173            .iter()
174            .filter_map(|c| std::char::from_u32(c.char_code))
175            .collect()
176    }
177
178    pub fn get_range_width<T: IntoIterator<Item = usize>>(&self, range: T) -> f32 {
179        let mut width = 0.0;
180        let font = self.font.0.lock().unwrap();
181        for index in range {
182            width += font.glyph_advance(self.text[index].glyph_index);
183        }
184        width
185    }
186
187    pub fn set_text<P: AsRef<str>>(&mut self, text: P) -> &mut Self {
188        // Convert text to UTF32.
189        self.text.clear();
190
191        let font = self.font.0.lock().unwrap();
192
193        for code in text.as_ref().chars().map(|c| c as u32) {
194            self.text.push(Character::from_char_with_font(code, &font));
195        }
196
197        drop(font);
198
199        self
200    }
201
202    pub fn set_wrap(&mut self, wrap: WrapMode) -> &mut Self {
203        self.wrap = wrap;
204        self
205    }
206
207    pub fn wrap_mode(&self) -> WrapMode {
208        self.wrap
209    }
210
211    pub fn insert_char(&mut self, code: char, index: usize) -> &mut Self {
212        let font = self.font.0.lock().unwrap();
213
214        self.text
215            .insert(index, Character::from_char_with_font(code as u32, &font));
216
217        drop(font);
218
219        self
220    }
221
222    pub fn insert_str(&mut self, str: &str, position: usize) -> &mut Self {
223        let font = self.font.0.lock().unwrap();
224
225        for (i, code) in str.chars().enumerate() {
226            self.text.insert(
227                position + i,
228                Character::from_char_with_font(code as u32, &font),
229            );
230        }
231
232        drop(font);
233
234        self
235    }
236
237    pub fn remove_range(&mut self, range: Range<usize>) -> &mut Self {
238        self.text.drain(range);
239        self
240    }
241
242    pub fn remove_at(&mut self, index: usize) -> &mut Self {
243        self.text.remove(index);
244        self
245    }
246
247    pub fn build(&mut self) -> Vector2<f32> {
248        let font = self.font.0.lock().unwrap();
249
250        let masked_text;
251        let text = if let Some(mask_char) = self.mask_char {
252            masked_text = (0..self.text.len()).map(|_| mask_char).collect();
253            &masked_text
254        } else {
255            &self.text
256        };
257
258        // Split on lines.
259        let mut total_height = 0.0;
260        let mut current_line = TextLine::new();
261        let mut word: Option<Word> = None;
262        self.lines.clear();
263        for (i, character) in text.iter().enumerate() {
264            let advance = match font.glyphs().get(character.glyph_index as usize) {
265                Some(glyph) => glyph.advance,
266                None => font.height(),
267            };
268            let is_new_line =
269                character.char_code == u32::from(b'\n') || character.char_code == u32::from(b'\r');
270            let new_width = current_line.width + advance;
271            let is_white_space =
272                char::from_u32(character.char_code).map_or(false, |c| c.is_whitespace());
273            let word_ended = word.is_some() && is_white_space || i == self.text.len() - 1;
274
275            if self.wrap == WrapMode::Word && !is_white_space {
276                match word.as_mut() {
277                    Some(word) => {
278                        word.width += advance;
279                        word.length += 1;
280                    }
281                    None => {
282                        word = Some(Word {
283                            width: advance,
284                            length: 1,
285                        });
286                    }
287                };
288            }
289
290            if is_new_line {
291                if let Some(word) = word.take() {
292                    current_line.width += word.width;
293                    current_line.end += word.length;
294                }
295                self.lines.push(current_line);
296                current_line.begin = if is_new_line { i + 1 } else { i };
297                current_line.end = current_line.begin;
298                current_line.width = advance;
299                total_height += font.ascender();
300            } else {
301                match self.wrap {
302                    WrapMode::NoWrap => {
303                        current_line.width = new_width;
304                        current_line.end += 1;
305                    }
306                    WrapMode::Letter => {
307                        if new_width > self.constraint.x {
308                            self.lines.push(current_line);
309                            current_line.begin = if is_new_line { i + 1 } else { i };
310                            current_line.end = current_line.begin + 1;
311                            current_line.width = advance;
312                            total_height += font.ascender();
313                        } else {
314                            current_line.width = new_width;
315                            current_line.end += 1;
316                        }
317                    }
318                    WrapMode::Word => {
319                        if word_ended {
320                            let word = word.take().unwrap();
321                            if word.width > self.constraint.x {
322                                // The word is longer than available constraints.
323                                // Push the word as a whole.
324                                current_line.width += word.width;
325                                current_line.end += word.length;
326                                self.lines.push(current_line);
327                                current_line.begin = current_line.end;
328                                current_line.width = 0.0;
329                                total_height += font.ascender();
330                            } else if current_line.width + word.width > self.constraint.x {
331                                // The word will exceed horizontal constraint, we have to
332                                // commit current line and move the word in the next line.
333                                self.lines.push(current_line);
334                                current_line.begin = i - word.length;
335                                current_line.end = i;
336                                current_line.width = word.width;
337                                total_height += font.ascender();
338                            } else {
339                                // The word does not exceed horizontal constraint, append it
340                                // to the line.
341                                current_line.width += word.width;
342                                current_line.end += word.length;
343                            }
344                        }
345
346                        // White-space characters are not part of word so pass them through.
347                        if is_white_space {
348                            current_line.end += 1;
349                            current_line.width += advance;
350                        }
351                    }
352                }
353            }
354        }
355        // Commit rest of text.
356        if current_line.begin != current_line.end {
357            for character in text.iter().skip(current_line.end) {
358                let advance = match font.glyphs().get(character.glyph_index as usize) {
359                    Some(glyph) => glyph.advance,
360                    None => font.height(),
361                };
362                current_line.width += advance;
363            }
364            current_line.end = self.text.len();
365            self.lines.push(current_line);
366            total_height += font.ascender();
367        }
368
369        // Align lines according to desired alignment.
370        for line in self.lines.iter_mut() {
371            match self.horizontal_alignment {
372                HorizontalAlignment::Left => line.x_offset = 0.0,
373                HorizontalAlignment::Center => {
374                    if self.constraint.x.is_infinite() {
375                        line.x_offset = 0.0;
376                    } else {
377                        line.x_offset = 0.5 * (self.constraint.x - line.width).max(0.0);
378                    }
379                }
380                HorizontalAlignment::Right => {
381                    if self.constraint.x.is_infinite() {
382                        line.x_offset = 0.0;
383                    } else {
384                        line.x_offset = (self.constraint.x - line.width).max(0.0)
385                    }
386                }
387                HorizontalAlignment::Stretch => line.x_offset = 0.0,
388            }
389        }
390
391        // Generate glyphs for each text line.
392        self.glyphs.clear();
393
394        let cursor_y_start = match self.vertical_alignment {
395            VerticalAlignment::Top => 0.0,
396            VerticalAlignment::Center => {
397                if self.constraint.y.is_infinite() {
398                    0.0
399                } else {
400                    (self.constraint.y - total_height).max(0.0) * 0.5
401                }
402            }
403            VerticalAlignment::Bottom => {
404                if self.constraint.y.is_infinite() {
405                    0.0
406                } else {
407                    (self.constraint.y - total_height).max(0.0)
408                }
409            }
410            VerticalAlignment::Stretch => 0.0,
411        };
412
413        let cursor_x_start = if self.constraint.x.is_infinite() {
414            0.0
415        } else {
416            self.constraint.x
417        };
418
419        let mut cursor = Vector2::new(cursor_x_start, cursor_y_start);
420        for line in self.lines.iter_mut() {
421            cursor.x = line.x_offset;
422
423            for &character in text.iter().take(line.end).skip(line.begin) {
424                match font.glyphs().get(character.glyph_index as usize) {
425                    Some(glyph) => {
426                        // Insert glyph
427                        let rect = Rect::new(
428                            cursor.x + glyph.left.floor(),
429                            cursor.y + font.ascender().floor()
430                                - glyph.top.floor()
431                                - glyph.bitmap_height as f32,
432                            glyph.bitmap_width as f32,
433                            glyph.bitmap_height as f32,
434                        );
435                        let text_glyph = TextGlyph {
436                            bounds: rect,
437                            tex_coords: glyph.tex_coords,
438                        };
439                        self.glyphs.push(text_glyph);
440
441                        cursor.x += glyph.advance;
442                    }
443                    None => {
444                        // Insert invalid symbol
445                        let rect = Rect::new(
446                            cursor.x,
447                            cursor.y + font.ascender(),
448                            font.height(),
449                            font.height(),
450                        );
451                        self.glyphs.push(TextGlyph {
452                            bounds: rect,
453                            tex_coords: [Vector2::default(); 4],
454                        });
455                        cursor.x += rect.w();
456                    }
457                }
458            }
459            line.height = font.ascender();
460            line.y_offset = cursor.y;
461            cursor.y += font.ascender();
462        }
463
464        // Minus here is because descender has negative value.
465        let mut full_size = Vector2::new(0.0, total_height - font.descender());
466        for line in self.lines.iter() {
467            full_size.x = line.width.max(full_size.x);
468        }
469        full_size
470    }
471}
472
473pub struct FormattedTextBuilder {
474    font: SharedFont,
475    brush: Brush,
476    constraint: Vector2<f32>,
477    text: String,
478    vertical_alignment: VerticalAlignment,
479    horizontal_alignment: HorizontalAlignment,
480    wrap: WrapMode,
481    mask_char: Option<char>,
482}
483
484impl Default for FormattedTextBuilder {
485    fn default() -> Self {
486        Self::new()
487    }
488}
489
490impl FormattedTextBuilder {
491    /// Creates new formatted text builder with default parameters.
492    pub fn new() -> FormattedTextBuilder {
493        FormattedTextBuilder {
494            font: DEFAULT_FONT.clone(),
495            text: "".to_owned(),
496            horizontal_alignment: HorizontalAlignment::Left,
497            vertical_alignment: VerticalAlignment::Top,
498            brush: Brush::Solid(Color::WHITE),
499            constraint: Vector2::new(128.0, 128.0),
500            wrap: WrapMode::NoWrap,
501            mask_char: None,
502        }
503    }
504
505    pub fn with_font(mut self, font: SharedFont) -> Self {
506        self.font = font;
507        self
508    }
509
510    pub fn with_vertical_alignment(mut self, vertical_alignment: VerticalAlignment) -> Self {
511        self.vertical_alignment = vertical_alignment;
512        self
513    }
514
515    pub fn with_wrap(mut self, wrap: WrapMode) -> Self {
516        self.wrap = wrap;
517        self
518    }
519
520    pub fn with_horizontal_alignment(mut self, horizontal_alignment: HorizontalAlignment) -> Self {
521        self.horizontal_alignment = horizontal_alignment;
522        self
523    }
524
525    pub fn with_text(mut self, text: String) -> Self {
526        self.text = text;
527        self
528    }
529
530    pub fn with_constraint(mut self, constraint: Vector2<f32>) -> Self {
531        self.constraint = constraint;
532        self
533    }
534
535    pub fn with_brush(mut self, brush: Brush) -> Self {
536        self.brush = brush;
537        self
538    }
539
540    pub fn with_mask_char(mut self, mask_char: Option<char>) -> Self {
541        self.mask_char = mask_char;
542        self
543    }
544
545    pub fn build(self) -> FormattedText {
546        let font = self.font.0.lock().unwrap();
547        FormattedText {
548            text: self
549                .text
550                .chars()
551                .map(|c| Character::from_char_with_font(c as u32, &font))
552                .collect(),
553            lines: Vec::new(),
554            glyphs: Vec::new(),
555            vertical_alignment: self.vertical_alignment,
556            horizontal_alignment: self.horizontal_alignment,
557            brush: self.brush,
558            constraint: self.constraint,
559            wrap: self.wrap,
560            mask_char: self
561                .mask_char
562                .map(|code| Character::from_char_with_font(u32::from(code), &font)),
563            font: {
564                drop(font);
565                self.font
566            },
567        }
568    }
569}