femtovg/
text.rs

1use std::{
2    borrow::Borrow,
3    cell::RefCell,
4    convert::TryInto,
5    ffi::OsStr,
6    fs,
7    hash::{Hash, Hasher},
8    ops::Range,
9    path::Path as FilePath,
10    rc::Rc,
11};
12
13use fnv::{FnvBuildHasher, FnvHashMap, FnvHasher};
14use lru::LruCache;
15use rustybuzz::ttf_parser;
16use slotmap::{DefaultKey, SlotMap};
17
18use unicode_bidi::BidiInfo;
19use unicode_segmentation::UnicodeSegmentation;
20
21use crate::{
22    paint::{PaintFlavor, StrokeSettings, TextSettings},
23    Canvas, Color, ErrorKind, FillRule, ImageFlags, ImageId, ImageInfo, Paint, PixelFormat, PositionedGlyph,
24    RenderTarget, Renderer,
25};
26
27mod atlas;
28pub use atlas::Atlas;
29
30mod font;
31pub use font::FontMetrics;
32use font::{Font, GlyphRendering};
33
34// This padding is an empty border around the glyph’s pixels but inside the
35// sampled area (texture coordinates) for the quad in render_atlas().
36const GLYPH_PADDING: u32 = 1;
37// We add an additional margin of 1 pixel outside of the sampled area,
38// to deal with the linear interpolation of texels at the edge of that area
39// which mixes in the texels just outside of the edge.
40// This manifests as noise around the glyph, outside of the padding.
41const GLYPH_MARGIN: u32 = 1;
42
43const TEXTURE_SIZE: usize = 512;
44const DEFAULT_LRU_CACHE_CAPACITY: usize = 1000;
45
46/// A font handle.
47#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
48pub struct FontId(DefaultKey);
49
50/// Represents the vertical alignment of a text baseline.
51///
52/// The default value is `Alphabetic`.
53#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
54#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
55pub enum Baseline {
56    /// The text baseline is the top of the em square.
57    Top,
58    /// The text baseline is the middle of the em square.
59    Middle,
60    /// The text baseline is the normal alphabetic baseline.
61    #[default]
62    Alphabetic,
63    /// The text baseline is the bottom of the bounding box.
64    Bottom,
65}
66
67/// Represents the horizontal alignment of text.
68///
69/// The default value is `Left`.
70#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
71#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
72pub enum Align {
73    /// The text is left-aligned.
74    #[default]
75    Left,
76    /// The text is centered.
77    Center,
78    /// The text is right-aligned.
79    Right,
80}
81
82/// Represents the rendering mode for a path.
83///
84/// The default value is `Fill`.
85#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq, Default)]
86pub enum RenderMode {
87    /// The path is filled.
88    #[default]
89    Fill,
90    /// The path is stroked.
91    Stroke,
92}
93
94#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
95pub struct RenderedGlyphId {
96    glyph_index: u16,
97    font_id: FontId,
98    size: u32,
99    line_width: u32,
100    render_mode: RenderMode,
101    subpixel_location: u8,
102}
103
104impl RenderedGlyphId {
105    fn new(
106        glyph_index: u16,
107        font_id: FontId,
108        font_size: f32,
109        line_width: f32,
110        mode: RenderMode,
111        subpixel_location: u8,
112    ) -> Self {
113        Self {
114            glyph_index,
115            font_id,
116            size: (font_size * 10.0).trunc() as u32,
117            line_width: (line_width * 10.0).trunc() as u32,
118            render_mode: mode,
119            subpixel_location,
120        }
121    }
122}
123
124#[derive(Copy, Clone, Debug)]
125pub struct RenderedGlyph {
126    texture_index: usize,
127    width: u32,
128    height: u32,
129    bearing_y: i32,
130    atlas_x: u32,
131    atlas_y: u32,
132    color_glyph: bool,
133}
134
135#[derive(Copy, Clone, Debug)]
136pub struct ShapedGlyph {
137    pub x: f32,
138    pub y: f32,
139    pub c: char,
140    pub byte_index: usize,
141    pub font_id: FontId,
142    pub glyph_id: u16,
143    pub width: f32,
144    pub height: f32,
145    pub advance_x: f32,
146    pub advance_y: f32,
147    pub offset_x: f32,
148    pub offset_y: f32,
149    pub bearing_x: f32,
150    pub bearing_y: f32,
151}
152
153#[derive(Clone, Debug, Default)]
154struct ShapedWord {
155    glyphs: Vec<ShapedGlyph>,
156    width: f32,
157}
158
159#[derive(Copy, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
160struct ShapingId {
161    size: u32,
162    word_hash: u64,
163    font_ids: [Option<FontId>; 8],
164}
165
166impl ShapingId {
167    fn new(font_size: f32, font_ids: [Option<FontId>; 8], word: &str, max_width: Option<f32>) -> Self {
168        let mut hasher = FnvHasher::default();
169        word.hash(&mut hasher);
170        if let Some(max_width) = max_width {
171            (max_width.trunc() as i32).hash(&mut hasher);
172        }
173
174        Self {
175            size: (font_size * 10.0).trunc() as u32,
176            word_hash: hasher.finish(),
177            font_ids,
178        }
179    }
180}
181
182type ShapedWordsCache<H> = LruCache<ShapingId, Result<ShapedWord, ErrorKind>, H>;
183type ShapingRunCache<H> = LruCache<ShapingId, TextMetrics, H>;
184
185pub struct FontTexture {
186    pub atlas: Atlas,
187    pub(crate) image_id: ImageId,
188}
189
190/// `TextContext` provides functionality for text processing in femtovg.
191///
192/// You can add fonts using the [`Self::add_font_file()`], [`Self::add_font_mem()`] and
193/// [`Self::add_font_dir()`] functions. For each registered font a [`FontId`] is
194/// returned.
195///
196/// The [`FontId`] can be supplied to [`crate::Paint`] along with additional parameters
197/// such as the font size.
198///
199/// The paint is needed when using `TextContext`'s measurement functions such as
200/// [`Self::measure_text()`].
201///
202/// Note that the measurements are done entirely with the supplied sizes in the paint
203/// parameter. If you need measurements that take a [`crate::Canvas`]'s transform or dpi into
204/// account (see [`crate::Canvas::set_size()`]), you need to use the measurement functions
205/// on the canvas.
206#[derive(Clone, Default)]
207pub struct TextContext(pub(crate) Rc<RefCell<TextContextImpl>>);
208
209impl TextContext {
210    /// Registers all .ttf files from a directory with this text context. If successful, the
211    /// font ids of all registered fonts are returned.
212    pub fn add_font_dir<T: AsRef<FilePath>>(&self, path: T) -> Result<Vec<FontId>, ErrorKind> {
213        self.0.borrow_mut().add_font_dir(path)
214    }
215
216    /// Registers the .ttf file from the specified path with this text context. If successful,
217    /// the font id is returned.
218    pub fn add_font_file<T: AsRef<FilePath>>(&self, path: T) -> Result<FontId, ErrorKind> {
219        self.0.borrow_mut().add_font_file(path)
220    }
221
222    /// Registers the in-memory representation of a TrueType font pointed to by the data
223    /// parameter with this text context. If successful, the font id is returned.
224    pub fn add_font_mem(&self, data: &[u8]) -> Result<FontId, ErrorKind> {
225        self.0.borrow_mut().add_font_mem(data)
226    }
227
228    /// Registers the in-memory representation of a TrueType font pointed to by the shared data
229    /// parameter with this text context. If successful, the font id is returned. The `face_index`
230    /// specifies the face index if the font data is a true type font collection. For plain true
231    /// type fonts, use 0 as index.
232    pub fn add_shared_font_with_index<T: AsRef<[u8]> + 'static>(
233        &self,
234        data: T,
235        face_index: u32,
236    ) -> Result<FontId, ErrorKind> {
237        self.0.borrow_mut().add_shared_font_with_index(data, face_index)
238    }
239
240    /// Returns information on how the provided text will be drawn with the specified paint.
241    pub fn measure_text<S: AsRef<str>>(
242        &self,
243        x: f32,
244        y: f32,
245        text: S,
246        paint: &Paint,
247    ) -> Result<TextMetrics, ErrorKind> {
248        self.0.borrow_mut().measure_text(x, y, text, &paint.text)
249    }
250
251    /// Returns the maximum index-th byte of text that will fit inside `max_width`.
252    ///
253    /// The retuned index will always lie at the start and/or end of a UTF-8 code point sequence or at the start or end of the text
254    pub fn break_text<S: AsRef<str>>(&self, max_width: f32, text: S, paint: &Paint) -> Result<usize, ErrorKind> {
255        self.0.borrow_mut().break_text(max_width, text, &paint.text)
256    }
257
258    /// Returnes a list of ranges representing each line of text that will fit inside `max_width`
259    pub fn break_text_vec<S: AsRef<str>>(
260        &self,
261        max_width: f32,
262        text: S,
263        paint: &Paint,
264    ) -> Result<Vec<Range<usize>>, ErrorKind> {
265        self.0.borrow_mut().break_text_vec(max_width, text, &paint.text)
266    }
267
268    /// Returns font metrics for a particular Paint.
269    pub fn measure_font(&self, paint: &Paint) -> Result<FontMetrics, ErrorKind> {
270        self.0
271            .borrow_mut()
272            .measure_font(paint.text.font_size, &paint.text.font_ids)
273    }
274
275    /// Adjusts the capacity of the shaping run cache. This is a cache for measurements of whole
276    /// strings.
277    pub fn resize_shaping_run_cache(&self, capacity: std::num::NonZeroUsize) {
278        self.0.borrow_mut().resize_shaping_run_cache(capacity)
279    }
280
281    /// Adjusts the capacity of the shaped words cache. This is a cache for measurements of
282    /// individual words. Words are separated by
283    /// [UAX#29 word boundaries](http://www.unicode.org/reports/tr29/#Word_Boundaries).
284    pub fn resize_shaped_words_cache(&self, capacity: std::num::NonZeroUsize) {
285        self.0.borrow_mut().resize_shaped_words_cache(capacity)
286    }
287}
288
289pub struct TextContextImpl {
290    fonts: SlotMap<DefaultKey, Font>,
291    shaping_run_cache: ShapingRunCache<FnvBuildHasher>,
292    shaped_words_cache: ShapedWordsCache<FnvBuildHasher>,
293}
294
295impl Default for TextContextImpl {
296    fn default() -> Self {
297        let fnv_run = FnvBuildHasher::default();
298        let fnv_words = FnvBuildHasher::default();
299
300        Self {
301            fonts: SlotMap::default(),
302            shaping_run_cache: LruCache::with_hasher(
303                std::num::NonZeroUsize::new(DEFAULT_LRU_CACHE_CAPACITY).unwrap(),
304                fnv_run,
305            ),
306            shaped_words_cache: LruCache::with_hasher(
307                std::num::NonZeroUsize::new(DEFAULT_LRU_CACHE_CAPACITY).unwrap(),
308                fnv_words,
309            ),
310        }
311    }
312}
313
314impl TextContextImpl {
315    pub fn resize_shaping_run_cache(&mut self, capacity: std::num::NonZeroUsize) {
316        self.shaping_run_cache.resize(capacity);
317    }
318
319    pub fn resize_shaped_words_cache(&mut self, capacity: std::num::NonZeroUsize) {
320        self.shaped_words_cache.resize(capacity);
321    }
322
323    pub fn add_font_dir<T: AsRef<FilePath>>(&mut self, path: T) -> Result<Vec<FontId>, ErrorKind> {
324        let path = path.as_ref();
325        let mut fonts = Vec::new();
326
327        if path.is_dir() {
328            for entry in fs::read_dir(path)? {
329                let entry = entry?;
330                let path = entry.path();
331
332                if path.is_dir() {
333                    self.add_font_dir(&path)?;
334                } else if Some("ttf") == path.extension().and_then(OsStr::to_str) {
335                    fonts.push(self.add_font_file(path)?);
336                } else if Some("ttc") == path.extension().and_then(OsStr::to_str) {
337                    fonts.extend(self.add_font_file_collection(path)?);
338                }
339            }
340        }
341
342        Ok(fonts)
343    }
344
345    pub fn add_font_file<T: AsRef<FilePath>>(&mut self, path: T) -> Result<FontId, ErrorKind> {
346        let data = std::fs::read(path)?;
347
348        self.add_font_mem(&data)
349    }
350
351    pub fn add_font_file_collection<T: AsRef<FilePath>>(
352        &mut self,
353        path: T,
354    ) -> Result<impl Iterator<Item = FontId> + '_, ErrorKind> {
355        let data = std::fs::read(path)?;
356
357        let count = ttf_parser::fonts_in_collection(&data).unwrap_or(1);
358        Ok((0..count).filter_map(move |index| self.add_font_mem_with_index(&data, index).ok()))
359    }
360
361    pub fn add_font_mem(&mut self, data: &[u8]) -> Result<FontId, ErrorKind> {
362        self.add_font_mem_with_index(data, 0)
363    }
364
365    pub fn add_font_mem_with_index(&mut self, data: &[u8], face_index: u32) -> Result<FontId, ErrorKind> {
366        self.clear_caches();
367
368        let data_copy = data.to_owned();
369        let font = Font::new_with_data(data_copy, face_index)?;
370        Ok(FontId(self.fonts.insert(font)))
371    }
372
373    pub fn add_shared_font_with_index<T: AsRef<[u8]> + 'static>(
374        &mut self,
375        data: T,
376        face_index: u32,
377    ) -> Result<FontId, ErrorKind> {
378        self.clear_caches();
379
380        let font = Font::new_with_data(data, face_index)?;
381        Ok(FontId(self.fonts.insert(font)))
382    }
383
384    pub fn font(&self, id: FontId) -> Option<&Font> {
385        self.fonts.get(id.0)
386    }
387
388    pub fn font_mut(&mut self, id: FontId) -> Option<&mut Font> {
389        self.fonts.get_mut(id.0)
390    }
391
392    pub fn find_font<F, T>(&mut self, font_ids: &[Option<FontId>; 8], mut callback: F) -> Result<T, ErrorKind>
393    where
394        F: FnMut((FontId, &mut Font)) -> (bool, T),
395    {
396        // Try each font in the paint
397        for maybe_font_id in font_ids {
398            if let &Some(font_id) = maybe_font_id {
399                if let Some(font) = self.fonts.get_mut(font_id.0) {
400                    let (has_missing, result) = callback((font_id, font));
401
402                    if !has_missing {
403                        return Ok(result);
404                    }
405                }
406            } else {
407                break;
408            }
409        }
410
411        // Try each registered font
412        // An optimisation here would be to skip fonts that were tried by the paint
413        for (id, font) in &mut self.fonts {
414            let (has_missing, result) = callback((FontId(id), font));
415
416            if !has_missing {
417                return Ok(result);
418            }
419        }
420
421        // Just return the first font at this point and let it render .nodef glyphs
422        if let Some((id, font)) = self.fonts.iter_mut().next() {
423            return Ok(callback((FontId(id), font)).1);
424        }
425
426        Err(ErrorKind::NoFontFound)
427    }
428
429    fn clear_caches(&mut self) {
430        self.shaped_words_cache.clear();
431    }
432
433    pub fn measure_text<S: AsRef<str>>(
434        &mut self,
435        x: f32,
436        y: f32,
437        text: S,
438        text_settings: &TextSettings,
439    ) -> Result<TextMetrics, ErrorKind> {
440        shape(x, y, self, text_settings, text.as_ref(), None)
441    }
442
443    pub fn break_text<S: AsRef<str>>(
444        &mut self,
445        max_width: f32,
446        text: S,
447        text_settings: &TextSettings,
448    ) -> Result<usize, ErrorKind> {
449        let layout = shape(0.0, 0.0, self, text_settings, text.as_ref(), Some(max_width))?;
450
451        Ok(layout.final_byte_index)
452    }
453
454    pub fn break_text_vec<S: AsRef<str>>(
455        &mut self,
456        max_width: f32,
457        text: S,
458        text_settings: &TextSettings,
459    ) -> Result<Vec<Range<usize>>, ErrorKind> {
460        let text = text.as_ref();
461
462        let mut res = Vec::new();
463        let mut start = 0;
464
465        while start < text.len() {
466            let Ok(index) = self.break_text(max_width, &text[start..], text_settings) else {
467                break;
468            };
469
470            if index == 0 {
471                break;
472            }
473
474            let index = start + index;
475            res.push(start..index);
476            start += &text[start..index].len();
477        }
478
479        Ok(res)
480    }
481
482    pub fn measure_font(&self, font_size: f32, font_ids: &[Option<FontId>; 8]) -> Result<FontMetrics, ErrorKind> {
483        if let Some(Some(id)) = font_ids.first() {
484            if let Some(font) = self.font(*id) {
485                return Ok(font.metrics(font_size));
486            }
487        }
488
489        Err(ErrorKind::NoFontFound)
490    }
491}
492
493/// Represents the result of a text shaping run.
494#[derive(Clone, Default, Debug)]
495pub struct TextMetrics {
496    /// X-coordinate of the starting position for the shaped text.
497    pub x: f32,
498    /// Y-coordinate of the starting position for the shaped text.
499    pub y: f32,
500    width: f32,
501    height: f32,
502    /// Vector of shaped glyphs resulting from the text shaping run.
503    pub glyphs: Vec<ShapedGlyph>,
504    pub(crate) final_byte_index: usize,
505}
506
507impl TextMetrics {
508    pub(crate) fn scale(&mut self, scale: f32) {
509        self.x *= scale;
510        self.y *= scale;
511        self.width *= scale;
512        self.height *= scale;
513
514        for glyph in &mut self.glyphs {
515            glyph.x *= scale;
516            glyph.y *= scale;
517            glyph.width *= scale;
518            glyph.height *= scale;
519        }
520    }
521
522    /// width of the glyphs as drawn
523    pub fn width(&self) -> f32 {
524        self.width
525    }
526
527    /// height of the glyphs as drawn
528    pub fn height(&self) -> f32 {
529        self.height
530    }
531}
532
533// Shaper
534
535pub fn shape(
536    x: f32,
537    y: f32,
538    context: &mut TextContextImpl,
539    text_settings: &TextSettings,
540    text: &str,
541    max_width: Option<f32>,
542) -> Result<TextMetrics, ErrorKind> {
543    let id = ShapingId::new(text_settings.font_size, text_settings.font_ids, text, max_width);
544
545    if !context.shaping_run_cache.contains(&id) {
546        let metrics = shape_run(
547            context,
548            text_settings.font_size,
549            text_settings.font_ids,
550            text_settings.letter_spacing,
551            text,
552            max_width,
553        );
554        context.shaping_run_cache.put(id, metrics);
555    }
556
557    if let Some(mut metrics) = context.shaping_run_cache.get(&id).cloned() {
558        layout(x, y, context, &mut metrics, text_settings)?;
559
560        return Ok(metrics);
561    }
562
563    Err(ErrorKind::UnknownError)
564}
565
566fn shape_run(
567    context: &mut TextContextImpl,
568    font_size: f32,
569    font_ids: [Option<FontId>; 8],
570    letter_spacing: f32,
571    text: &str,
572    max_width: Option<f32>,
573) -> TextMetrics {
574    let mut result = TextMetrics {
575        x: 0.0,
576        y: 0.0,
577        width: 0.0,
578        height: 0.0,
579        glyphs: Vec::with_capacity(text.len()),
580        final_byte_index: 0,
581    };
582
583    let bidi_info = BidiInfo::new(text, Some(unicode_bidi::Level::ltr()));
584
585    // this controls whether we should break within words
586    let mut first_word_in_paragraph = true;
587
588    let Some(paragraph) = bidi_info.paragraphs.first() else {
589        return result;
590    };
591
592    let line = paragraph.range.clone();
593
594    let (levels, runs) = bidi_info.visual_runs(paragraph, line);
595
596    for run in runs {
597        let sub_text = &text[run.clone()];
598
599        if sub_text.is_empty() {
600            continue;
601        }
602
603        let hb_direction = if levels[run.start].is_rtl() {
604            rustybuzz::Direction::RightToLeft
605        } else {
606            rustybuzz::Direction::LeftToRight
607        };
608
609        let mut words = Vec::new();
610        let mut word_break_reached = false;
611        let mut byte_index = run.start;
612
613        for mut word_txt in sub_text.split_word_bounds() {
614            let id = ShapingId::new(font_size, font_ids, word_txt, max_width);
615
616            if !context.shaped_words_cache.contains(&id) {
617                let word = shape_word(word_txt, hb_direction, context, font_size, &font_ids, letter_spacing);
618                context.shaped_words_cache.put(id, word);
619            }
620
621            if let Some(Ok(word)) = context.shaped_words_cache.get(&id) {
622                let mut word = word.clone();
623
624                if let Some(max_width) = max_width {
625                    if result.width + word.width >= max_width {
626                        word_break_reached = true;
627                        if first_word_in_paragraph {
628                            // search for the largest prefix of the word that can fit
629                            let mut bytes_included = 0;
630                            let mut subword_width = 0.0;
631                            let target_width = max_width - result.width;
632                            for glyph in word.glyphs {
633                                bytes_included = glyph.byte_index;
634                                let glyph_width = glyph.advance_x + letter_spacing;
635
636                                // nuance: we want to include the first glyph even if it breaks
637                                // the bounds. this is to allow pathologically small bounds to
638                                // at least complete rendering
639                                if subword_width + glyph_width >= target_width && bytes_included != 0 {
640                                    break;
641                                }
642
643                                subword_width += glyph_width;
644                            }
645
646                            if bytes_included == 0 {
647                                // just in case - never mind!
648                                break;
649                            }
650
651                            let subword_txt = &word_txt[..bytes_included];
652                            let id = ShapingId::new(font_size, font_ids, subword_txt, Some(max_width));
653                            if !context.shaped_words_cache.contains(&id) {
654                                let subword = shape_word(
655                                    subword_txt,
656                                    hb_direction,
657                                    context,
658                                    font_size,
659                                    &font_ids,
660                                    letter_spacing,
661                                );
662                                context.shaped_words_cache.put(id, subword);
663                            }
664
665                            if let Some(Ok(subword)) = context.shaped_words_cache.get(&id) {
666                                // replace the outer variables so we can continue normally
667                                word = subword.clone();
668                                word_txt = subword_txt;
669                            } else {
670                                break;
671                            }
672                        } else if word.glyphs.iter().all(|g| g.c.is_whitespace()) {
673                            // the last word we've broken in the middle of is whitespace.
674                            // include this word for now, but we will discard its metrics in a moment.
675                        } else {
676                            // we are not breaking up words - discard this word
677                            break;
678                        }
679                    }
680                }
681
682                // if we have broken in the middle of whitespace, do not include this word in metrics
683                if !word_break_reached || !word.glyphs.iter().all(|g| g.c.is_whitespace()) {
684                    result.width += word.width;
685                }
686
687                for glyph in &mut word.glyphs {
688                    glyph.byte_index += byte_index;
689                    debug_assert!(text.get(glyph.byte_index..).is_some());
690                }
691                words.push(word);
692                first_word_in_paragraph = false;
693            }
694
695            byte_index += word_txt.len();
696
697            if word_break_reached {
698                break;
699            }
700        }
701
702        if levels[run.start].is_rtl() {
703            words.reverse();
704        }
705
706        for word in words {
707            result.glyphs.extend(word.glyphs.clone());
708        }
709
710        result.final_byte_index = byte_index;
711
712        if word_break_reached {
713            break;
714        }
715    }
716
717    result
718}
719
720fn shape_word(
721    word: &str,
722    hb_direction: rustybuzz::Direction,
723    context: &mut TextContextImpl,
724    font_size: f32,
725    font_ids: &[Option<FontId>; 8],
726    letter_spacing: f32,
727) -> Result<ShapedWord, ErrorKind> {
728    // find_font will call the closure with each font matching the provided style
729    // until a font capable of shaping the word is found
730    context.find_font(font_ids, |(font_id, font)| {
731        let face = font.face_ref();
732        // Call harfbuzz
733        let output = {
734            let mut buffer = rustybuzz::UnicodeBuffer::new();
735            buffer.push_str(word);
736            buffer.set_direction(hb_direction);
737
738            rustybuzz::shape(&face, &[], buffer)
739        };
740
741        let positions = output.glyph_positions();
742        let infos = output.glyph_infos();
743
744        let mut shaped_word = ShapedWord {
745            glyphs: Vec::with_capacity(positions.len()),
746            width: 0.0,
747        };
748
749        let mut has_missing = false;
750
751        for (position, (info, c)) in positions.iter().zip(infos.iter().zip(word.chars())) {
752            if info.glyph_id == 0 {
753                has_missing = true;
754            }
755
756            let scale = font.scale(font_size);
757
758            let mut g = ShapedGlyph {
759                x: 0.0,
760                y: 0.0,
761                c,
762                byte_index: info.cluster as usize,
763                font_id,
764                glyph_id: info
765                    .glyph_id
766                    .try_into()
767                    .expect("rustybuzz guarantees the output glyph id is u16"),
768                width: 0.0,
769                height: 0.0,
770                advance_x: position.x_advance as f32 * scale,
771                advance_y: position.y_advance as f32 * scale,
772                offset_x: position.x_offset as f32 * scale,
773                offset_y: position.y_offset as f32 * scale,
774                bearing_x: 0.0,
775                bearing_y: 0.0,
776            };
777
778            if let Some(glyph) = font.glyph(&face, g.glyph_id) {
779                g.width = glyph.metrics.width * scale;
780                g.height = glyph.metrics.height * scale;
781                g.bearing_x = glyph.metrics.bearing_x * scale;
782                g.bearing_y = glyph.metrics.bearing_y * scale;
783            }
784
785            shaped_word.width += g.advance_x + letter_spacing;
786            shaped_word.glyphs.push(g);
787        }
788
789        (has_missing, shaped_word)
790    })
791}
792
793// Calculates the x,y coordinates for each glyph based on their advances. Calculates total width and height of the shaped text run
794fn layout(
795    x: f32,
796    y: f32,
797    context: &mut TextContextImpl,
798    res: &mut TextMetrics,
799    text_settings: &TextSettings,
800) -> Result<(), ErrorKind> {
801    let mut cursor_x = x;
802    let mut cursor_y = y;
803
804    // Horizontal alignment
805    match text_settings.text_align {
806        Align::Center => cursor_x -= res.width / 2.0,
807        Align::Right => cursor_x -= res.width,
808        Align::Left => (),
809    }
810
811    res.x = cursor_x;
812
813    let mut min_y = cursor_y;
814    let mut max_y = cursor_y;
815
816    let mut ascender: f32 = 0.;
817    let mut descender: f32 = 0.;
818
819    for glyph in &mut res.glyphs {
820        let font = context.font_mut(glyph.font_id).ok_or(ErrorKind::NoFontFound)?;
821        let metrics = font.metrics(text_settings.font_size);
822        ascender = ascender.max(metrics.ascender());
823        descender = descender.min(metrics.descender());
824    }
825
826    let primary_metrics = context.find_font(&text_settings.font_ids, |(_, font)| {
827        (false, font.metrics(text_settings.font_size))
828    })?;
829    if ascender.abs() < f32::EPSILON {
830        ascender = primary_metrics.ascender();
831    }
832    if descender.abs() < f32::EPSILON {
833        descender = primary_metrics.descender();
834    }
835
836    // Baseline alignment
837    let alignment_offset_y = match text_settings.text_baseline {
838        Baseline::Top => ascender,
839        Baseline::Middle => (ascender + descender) / 2.0,
840        Baseline::Alphabetic => 0.0,
841        Baseline::Bottom => descender,
842    };
843
844    for glyph in &mut res.glyphs {
845        glyph.x = cursor_x + glyph.offset_x + glyph.bearing_x;
846        glyph.y = (cursor_y + alignment_offset_y).round() + glyph.offset_y - glyph.bearing_y;
847
848        min_y = min_y.min(glyph.y);
849        max_y = max_y.max(glyph.y + glyph.height);
850
851        cursor_x += glyph.advance_x + text_settings.letter_spacing;
852        cursor_y += glyph.advance_y;
853    }
854
855    res.y = min_y;
856    res.height = max_y - min_y;
857
858    Ok(())
859}
860
861// Renderer
862
863/// Represents a command to draw an image with a set of quads.
864#[derive(Clone, Debug)]
865pub struct DrawCommand {
866    /// The ID of the image to draw.
867    pub image_id: ImageId,
868    /// The quads defining the positions and texture coordinates for drawing the image.
869    pub quads: Vec<Quad>,
870}
871
872/// Represents a quad with position and texture coordinates.
873#[derive(Copy, Clone, Default, Debug)]
874pub struct Quad {
875    /// X-coordinate of the top-left corner of the quad.
876    pub x0: f32,
877    /// Y-coordinate of the top-left corner of the quad.
878    pub y0: f32,
879    /// U-coordinate (horizontal texture coordinate) of the top-left corner of the quad.
880    pub s0: f32,
881    /// V-coordinate (vertical texture coordinate) of the top-left corner of the quad.
882    pub t0: f32,
883    /// X-coordinate of the bottom-right corner of the quad.
884    pub x1: f32,
885    /// Y-coordinate of the bottom-right corner of the quad.
886    pub y1: f32,
887    /// U-coordinate (horizontal texture coordinate) of the bottom-right corner of the quad.
888    pub s1: f32,
889    /// V-coordinate (vertical texture coordinate) of the bottom-right corner of the quad.
890    pub t1: f32,
891}
892
893/// Represents the drawing commands for glyphs, separated into alpha and color glyphs.
894#[derive(Default)]
895pub struct GlyphDrawCommands {
896    /// Drawing commands for alpha (opacity) glyphs.
897    pub alpha_glyphs: Vec<DrawCommand>,
898    /// Drawing commands for color glyphs.
899    pub color_glyphs: Vec<DrawCommand>,
900}
901
902#[derive(Default)]
903pub struct GlyphAtlas {
904    pub rendered_glyphs: RefCell<FnvHashMap<RenderedGlyphId, RenderedGlyph>>,
905    pub glyph_textures: RefCell<Vec<FontTexture>>,
906}
907
908impl GlyphAtlas {
909    pub(crate) fn render_atlas<T: Renderer>(
910        &self,
911        canvas: &mut Canvas<T>,
912        font_id: FontId,
913        font: &Font,
914        font_face: &rustybuzz::Face<'_>,
915        glyphs: impl Iterator<Item = PositionedGlyph>,
916        font_size: f32,
917        line_width: f32,
918        mode: RenderMode,
919    ) -> Result<GlyphDrawCommands, ErrorKind> {
920        let mut alpha_cmd_map = FnvHashMap::default();
921        let mut color_cmd_map = FnvHashMap::default();
922
923        let line_width_offset = if mode == RenderMode::Stroke {
924            (line_width / 2.0).ceil()
925        } else {
926            0.0
927        };
928
929        let initial_render_target = canvas.current_render_target;
930
931        for glyph in glyphs {
932            let subpixel_location = crate::geometry::quantize(glyph.x.fract(), 0.1) * 10.0;
933
934            let id = RenderedGlyphId::new(
935                glyph.glyph_id,
936                font_id,
937                font_size,
938                line_width,
939                mode,
940                subpixel_location as u8,
941            );
942
943            if !self.rendered_glyphs.borrow().contains_key(&id) {
944                if let Some(glyph) =
945                    self.render_glyph(canvas, font_size, line_width, mode, font, &font_face, glyph.glyph_id)?
946                {
947                    self.rendered_glyphs.borrow_mut().insert(id, glyph);
948                } else {
949                    continue;
950                }
951            }
952
953            let rendered_glyphs = self.rendered_glyphs.borrow();
954            let rendered = rendered_glyphs.get(&id).unwrap();
955
956            if let Some(texture) = self.glyph_textures.borrow().get(rendered.texture_index) {
957                let image_id = texture.image_id;
958                let size = texture.atlas.size();
959                let itw = 1.0 / size.0 as f32;
960                let ith = 1.0 / size.1 as f32;
961
962                let cmd_map = if rendered.color_glyph {
963                    &mut color_cmd_map
964                } else {
965                    &mut alpha_cmd_map
966                };
967
968                let cmd = cmd_map.entry(rendered.texture_index).or_insert_with(|| DrawCommand {
969                    image_id,
970                    quads: Vec::new(),
971                });
972
973                let mut q = Quad::default();
974
975                let line_width_offset = if rendered.color_glyph { 0. } else { line_width_offset };
976
977                q.x0 = glyph.x.trunc() - line_width_offset - GLYPH_PADDING as f32;
978                q.y0 = glyph.y.round() - rendered.bearing_y as f32 - line_width_offset - GLYPH_PADDING as f32;
979                q.x1 = q.x0 + rendered.width as f32;
980                q.y1 = q.y0 + rendered.height as f32;
981
982                q.s0 = rendered.atlas_x as f32 * itw;
983                q.t0 = rendered.atlas_y as f32 * ith;
984                q.s1 = (rendered.atlas_x + rendered.width) as f32 * itw;
985                q.t1 = (rendered.atlas_y + rendered.height) as f32 * ith;
986
987                cmd.quads.push(q);
988            }
989        }
990
991        canvas.set_render_target(initial_render_target);
992
993        Ok(GlyphDrawCommands {
994            alpha_glyphs: alpha_cmd_map.drain().map(|(_, cmd)| cmd).collect(),
995            color_glyphs: color_cmd_map.drain().map(|(_, cmd)| cmd).collect(),
996        })
997    }
998
999    // Renders the glyph into the atlas and returns the RenderedGlyph struct for it.
1000    // Returns Ok(None) if there exists no path or image for the glyph in the font (missing glyph).
1001    fn render_glyph<T: Renderer>(
1002        &self,
1003        canvas: &mut Canvas<T>,
1004        font_size: f32,
1005        line_width: f32,
1006        mode: RenderMode,
1007        font: &Font,
1008        font_face: &rustybuzz::Face<'_>,
1009        glyph_id: u16,
1010    ) -> Result<Option<RenderedGlyph>, ErrorKind> {
1011        let padding = GLYPH_PADDING + GLYPH_MARGIN;
1012
1013        let (mut glyph_representation, glyph_metrics, scale) = {
1014            let scale = font.scale(font_size);
1015            let maybe_glyph_metrics = font.glyph(&font_face, glyph_id).map(|g| g.metrics.clone());
1016
1017            if let (Some(glyph_representation), Some(glyph_metrics)) = (
1018                font.glyph_rendering_representation(&font_face, glyph_id, font_size as u16),
1019                maybe_glyph_metrics,
1020            ) {
1021                (glyph_representation, glyph_metrics, scale)
1022            } else {
1023                return Ok(None);
1024            }
1025        };
1026
1027        #[cfg(feature = "image-loading")]
1028        let color_glyph = matches!(glyph_representation, GlyphRendering::RenderAsImage(..));
1029        #[cfg(not(feature = "image-loading"))]
1030        let color_glyph = false;
1031
1032        let line_width = if color_glyph || mode != RenderMode::Stroke {
1033            0.0
1034        } else {
1035            line_width
1036        };
1037
1038        let line_width_offset = (line_width / 2.0).ceil();
1039
1040        let width = (glyph_metrics.width * scale).ceil() as u32 + (line_width_offset * 2.0) as u32 + padding * 2;
1041        let height = (glyph_metrics.height * scale).ceil() as u32 + (line_width_offset * 2.0) as u32 + padding * 2;
1042
1043        let (dst_index, dst_image_id, (dst_x, dst_y)) =
1044            self.find_texture_or_alloc(canvas, width as usize, height as usize)?;
1045
1046        // render glyph to image
1047        canvas.save();
1048        canvas.reset();
1049
1050        let rendered_bearing_y = (glyph_metrics.bearing_y * scale).round();
1051        let x = dst_x as f32 - (glyph_metrics.bearing_x * scale) + line_width_offset + padding as f32;
1052        let y = TEXTURE_SIZE as f32 - dst_y as f32 - rendered_bearing_y - line_width_offset - padding as f32;
1053
1054        let rendered_glyph = RenderedGlyph {
1055            width: width - 2 * GLYPH_MARGIN,
1056            height: height - 2 * GLYPH_MARGIN,
1057            bearing_y: rendered_bearing_y as i32,
1058            atlas_x: dst_x as u32 + GLYPH_MARGIN,
1059            atlas_y: dst_y as u32 + GLYPH_MARGIN,
1060            texture_index: dst_index,
1061            color_glyph,
1062        };
1063
1064        match glyph_representation {
1065            GlyphRendering::RenderAsPath(ref mut path) => {
1066                canvas.translate(x, y);
1067
1068                canvas.set_render_target(RenderTarget::Image(dst_image_id));
1069                canvas.clear_rect(
1070                    dst_x as u32,
1071                    TEXTURE_SIZE as u32 - dst_y as u32 - height,
1072                    width,
1073                    height,
1074                    Color::black(),
1075                );
1076                let factor = 1.0 / 8.0;
1077
1078                let mask_color = Color::rgbf(factor, factor, factor);
1079
1080                let mut line_width = line_width;
1081
1082                if mode == RenderMode::Stroke {
1083                    line_width /= scale;
1084                }
1085
1086                canvas.global_composite_blend_func(crate::BlendFactor::SrcAlpha, crate::BlendFactor::One);
1087
1088                // 4x
1089                // let points = [
1090                //     (-3.0/8.0, 1.0/8.0),
1091                //     (1.0/8.0, 3.0/8.0),
1092                //     (3.0/8.0, -1.0/8.0),
1093                //     (-1.0/8.0, -3.0/8.0),
1094                // ];
1095
1096                // 8x
1097                let points = [
1098                    (-7.0 / 16.0, -1.0 / 16.0),
1099                    (-1.0 / 16.0, -5.0 / 16.0),
1100                    (3.0 / 16.0, -7.0 / 16.0),
1101                    (5.0 / 16.0, -3.0 / 16.0),
1102                    (7.0 / 16.0, 1.0 / 16.0),
1103                    (1.0 / 16.0, 5.0 / 16.0),
1104                    (-3.0 / 16.0, 7.0 / 16.0),
1105                    (-5.0 / 16.0, 3.0 / 16.0),
1106                ];
1107
1108                for point in &points {
1109                    canvas.save();
1110                    canvas.translate(point.0, point.1);
1111
1112                    canvas.scale(scale, scale);
1113
1114                    if mode == RenderMode::Stroke {
1115                        canvas.stroke_path_internal(
1116                            path,
1117                            &PaintFlavor::Color(mask_color),
1118                            false,
1119                            &StrokeSettings {
1120                                line_width,
1121                                ..Default::default()
1122                            },
1123                        );
1124                    } else {
1125                        canvas.fill_path_internal(path, &PaintFlavor::Color(mask_color), false, FillRule::NonZero);
1126                    }
1127
1128                    canvas.restore();
1129                }
1130            }
1131            #[cfg(feature = "image-loading")]
1132            GlyphRendering::RenderAsImage(image_buffer) => {
1133                let target_x = rendered_glyph.atlas_x as usize;
1134                let target_y = rendered_glyph.atlas_y as usize;
1135                let target_width = rendered_glyph.width;
1136                let target_height = rendered_glyph.height;
1137
1138                let image_buffer =
1139                    image_buffer.resize(target_width, target_height, image::imageops::FilterType::Nearest);
1140                if let Ok(image) = crate::image::ImageSource::try_from(&image_buffer) {
1141                    canvas.update_image(dst_image_id, image, target_x, target_y).unwrap();
1142                }
1143            }
1144        }
1145
1146        canvas.restore();
1147
1148        Ok(Some(rendered_glyph))
1149    }
1150
1151    // Returns (texture index, image id, glyph padding box)
1152    fn find_texture_or_alloc<T: Renderer>(
1153        &self,
1154        canvas: &mut Canvas<T>,
1155        width: usize,
1156        height: usize,
1157    ) -> Result<(usize, ImageId, (usize, usize)), ErrorKind> {
1158        // Find a free location in one of the atlases
1159        let mut texture_search_result = {
1160            let mut glyph_textures = self.glyph_textures.borrow_mut();
1161            let mut textures = glyph_textures.iter_mut().enumerate();
1162            textures.find_map(|(index, texture)| {
1163                texture
1164                    .atlas
1165                    .add_rect(width, height)
1166                    .map(|loc| (index, texture.image_id, loc))
1167            })
1168        };
1169
1170        if texture_search_result.is_none() {
1171            // All atlases are exausted and a new one must be created
1172            let mut atlas = Atlas::new(TEXTURE_SIZE, TEXTURE_SIZE);
1173
1174            let loc = atlas
1175                .add_rect(width, height)
1176                .ok_or(ErrorKind::FontSizeTooLargeForAtlas)?;
1177
1178            // Using PixelFormat::Gray8 works perfectly and takes less VRAM.
1179            // We keep Rgba8 for now because it might be useful for sub-pixel
1180            // anti-aliasing (ClearType®), and the atlas debug display is much
1181            // clearer with different colors. Also, Rgba8 is required for color
1182            // fonts (typically used for emojis).
1183            let info = ImageInfo::new(ImageFlags::NEAREST, atlas.size().0, atlas.size().1, PixelFormat::Rgba8);
1184            let image_id = canvas.images.alloc(&mut canvas.renderer, info)?;
1185
1186            #[cfg(feature = "debug_inspector")]
1187            if cfg!(debug_assertions) {
1188                // Fill the texture with red pixels only in debug builds.
1189                if let Ok(size) = canvas.image_size(image_id) {
1190                    // With image-loading we then subsequently support color fonts, where
1191                    // the color glyphs are uploaded directly. Since that's immediately and
1192                    // the clear_rect() is run much later, it would overwrite any uploaded
1193                    // glyphs. So then when for the debug-inspector, use an image to clear.
1194                    #[cfg(feature = "image-loading")]
1195                    {
1196                        use rgb::FromSlice;
1197                        let clear_image = image::RgbaImage::from_pixel(
1198                            size.0 as u32,
1199                            size.1 as u32,
1200                            image::Rgba::<u8>([255, 0, 0, 0]),
1201                        );
1202                        canvas
1203                            .update_image(
1204                                image_id,
1205                                crate::image::ImageSource::from(imgref::Img::new(
1206                                    clear_image.as_rgba(),
1207                                    clear_image.width() as usize,
1208                                    clear_image.height() as usize,
1209                                )),
1210                                0,
1211                                0,
1212                            )
1213                            .unwrap();
1214                    }
1215                    #[cfg(not(feature = "image-loading"))]
1216                    {
1217                        canvas.save();
1218                        canvas.reset();
1219                        canvas.set_render_target(RenderTarget::Image(image_id));
1220                        canvas.clear_rect(
1221                            0,
1222                            0,
1223                            size.0 as u32,
1224                            size.1 as u32,
1225                            Color::rgb(255, 0, 0), // Shown as white if using Gray8.,
1226                        );
1227                        canvas.restore();
1228                    }
1229                }
1230            }
1231
1232            self.glyph_textures.borrow_mut().push(FontTexture { atlas, image_id });
1233
1234            let index = self.glyph_textures.borrow().len() - 1;
1235            texture_search_result = Some((index, image_id, loc));
1236        }
1237
1238        texture_search_result.ok_or(ErrorKind::UnknownError)
1239    }
1240
1241    pub(crate) fn clear<T: Renderer>(&self, canvas: &mut Canvas<T>) {
1242        let image_ids = std::mem::take(&mut *self.glyph_textures.borrow_mut())
1243            .into_iter()
1244            .map(|font_texture| font_texture.image_id);
1245        image_ids.for_each(|id| canvas.delete_image(id));
1246
1247        self.rendered_glyphs.borrow_mut().clear();
1248    }
1249}
1250
1251pub fn render_direct<T: Renderer>(
1252    canvas: &mut Canvas<T>,
1253    font: &Font,
1254    glyphs: impl Iterator<Item = PositionedGlyph>,
1255    paint_flavor: &PaintFlavor,
1256    anti_alias: bool,
1257    stroke: &StrokeSettings,
1258    font_size: f32,
1259    mode: RenderMode,
1260) -> Result<(), ErrorKind> {
1261    let face = font.face_ref();
1262
1263    for glyph in glyphs {
1264        let (glyph_rendering, scale) = {
1265            let scale = font.scale(font_size);
1266
1267            let Some(glyph_rendering) = font.glyph_rendering_representation(&face, glyph.glyph_id, font_size as u16)
1268            else {
1269                continue;
1270            };
1271
1272            (glyph_rendering, scale)
1273        };
1274
1275        canvas.save();
1276
1277        let line_width = match mode {
1278            RenderMode::Fill => stroke.line_width,
1279            RenderMode::Stroke => stroke.line_width / scale,
1280        };
1281
1282        canvas.translate(glyph.x, glyph.y);
1283        canvas.scale(scale, -scale);
1284
1285        match glyph_rendering {
1286            GlyphRendering::RenderAsPath(path) => {
1287                if mode == RenderMode::Stroke {
1288                    canvas.stroke_path_internal(
1289                        path.borrow(),
1290                        paint_flavor,
1291                        anti_alias,
1292                        &StrokeSettings {
1293                            line_width,
1294                            ..stroke.clone()
1295                        },
1296                    );
1297                } else {
1298                    canvas.fill_path_internal(path.borrow(), paint_flavor, anti_alias, FillRule::NonZero);
1299                }
1300            }
1301            #[cfg(feature = "image-loading")]
1302            GlyphRendering::RenderAsImage(_) => unreachable!(),
1303        }
1304
1305        canvas.restore();
1306    }
1307
1308    Ok(())
1309}