Skip to main content

ferroui_harfbuzz/
harf_buzz_text_shaper.rs

1use crate::harf_buzz_typeface::HarfBuzzTypeface;
2use crate::hb;
3use ferroui_base::media::fonts::OpenTypeTag;
4use ferroui_base::media::text_formatting::unicode::Codepoint;
5use ferroui_base::media::text_formatting::{GlyphInfo, ShapedBuffer, TextShaperOptions};
6use ferroui_base::media::{GlyphTypeface, ITextShaperTypeface};
7use ferroui_base::platform::ITextShaperImpl;
8use ferroui_base::utilities::{CultureInfo, ReadOnlyMemory};
9use ferroui_base::Vector;
10use std::cell::RefCell;
11use std::rc::Rc;
12
13ferroui_base::ferro_thread_local! {
14    /// The shaping buffer, reused between calls.
15    static BUFFER: RefCell<hb::Buffer> = RefCell::new(hb::Buffer::new());
16}
17
18const ZERO_WIDTH_NON_JOINER: u32 = 0x200C;
19
20/// A text shaper backed by HarfBuzz.
21#[derive(Default)]
22pub struct HarfBuzzTextShaper;
23
24impl HarfBuzzTextShaper {
25    /// Creates the text shaper.
26    pub fn new() -> Self {
27        Self
28    }
29
30    /// Replaces a trailing line break by zero width non joiners so that it
31    /// shapes to nothing; a `\r\n` pair becomes one cluster.
32    fn merge_break_pair(buffer: &mut hb::Buffer) {
33        let glyph_infos = buffer.glyph_infos_mut();
34        let length = glyph_infos.len();
35
36        if length == 0 {
37            return;
38        }
39
40        let second = glyph_infos[length - 1];
41
42        if !Codepoint::new(second.codepoint).is_break_char() {
43            return;
44        }
45
46        if length > 1 && glyph_infos[length - 2].codepoint == '\r' as u32 && second.codepoint == '\n' as u32 {
47            let first_cluster = glyph_infos[length - 2].cluster;
48
49            glyph_infos[length - 2].codepoint = ZERO_WIDTH_NON_JOINER;
50
51            glyph_infos[length - 1].codepoint = ZERO_WIDTH_NON_JOINER;
52            glyph_infos[length - 1].cluster = first_cluster;
53        } else {
54            glyph_infos[length - 1].codepoint = ZERO_WIDTH_NON_JOINER;
55        }
56    }
57
58    fn get_glyph_offset(position: &hb::GlyphPosition, text_scale: f64) -> Vector {
59        let offset_x = position.x_offset as f64 * text_scale;
60
61        let offset_y = -(position.y_offset as f64) * text_scale;
62
63        Vector::new(offset_x, offset_y)
64    }
65
66    fn get_glyph_advance(position: &hb::GlyphPosition, text_scale: f64) -> f64 {
67        // Depends on the direction of the layout: vertical layout would use
68        // the y advance.
69        position.x_advance as f64 * text_scale
70    }
71
72    fn get_features(options: &TextShaperOptions) -> Vec<hb::Feature> {
73        let Some(font_features) = options.font_features() else {
74            return Vec::new();
75        };
76
77        font_features
78            .iter()
79            .map(|font_feature| hb::Feature {
80                tag: OpenTypeTag::parse(&font_feature.tag).value(),
81                value: font_feature.value as u32,
82                start: font_feature.start as u32,
83                end: font_feature.end as u32,
84            })
85            .collect()
86    }
87}
88
89impl ITextShaperImpl for HarfBuzzTextShaper {
90    fn shape_text(&self, text: &ReadOnlyMemory<u16>, options: &TextShaperOptions) -> Rc<ShapedBuffer> {
91        let glyph_typeface = options.glyph_typeface();
92        let font_rendering_em_size = options.font_rendering_em_size();
93        let bidi_level = options.bidi_level();
94
95        if text.is_empty() {
96            return ShapedBuffer::new(text.clone(), 0, glyph_typeface.clone(), font_rendering_em_size, bidi_level);
97        }
98
99        let text_shaper_typeface = glyph_typeface.text_shaper_typeface();
100        let harf_buzz_typeface = text_shaper_typeface
101            .as_any()
102            .downcast_ref::<HarfBuzzTypeface>()
103            .unwrap_or_else(|| panic!("The provided GlyphTypeface is not supported by this text shaper."));
104
105        let culture = options.culture();
106
107        // HarfBuzz needs the surrounding characters to correctly shape the
108        // text.
109        let (containing_text, start): (&[u16], usize) = match text.owner() {
110            Some(owner) => (owner, text.offset_in_owner()),
111            None => (text.span(), 0),
112        };
113        let length = text.len();
114
115        BUFFER.with(|buffer| {
116            let mut buffer = buffer.borrow_mut();
117
118            buffer.reset();
119
120            buffer.add_utf16(containing_text, start, length);
121
122            Self::merge_break_pair(&mut buffer);
123
124            buffer.guess_segment_properties();
125
126            buffer.set_direction(if (bidi_level & 1) == 0 {
127                hb::Direction::LeftToRight
128            } else {
129                hb::Direction::RightToLeft
130            });
131
132            let current_culture;
133            let used_culture = match culture {
134                Some(culture) => culture,
135                None => {
136                    current_culture = CultureInfo::current_culture();
137                    &current_culture
138                }
139            };
140
141            buffer.set_language(used_culture.two_letter_iso_language_name());
142
143            let features = Self::get_features(options);
144
145            // HarfBuzz produces glyphs in visual order for RTL by default:
146            // the first glyph in the buffer is the leftmost visual glyph
147            // (highest cluster value). LTR output already has clusters in
148            // ascending (logical = visual) order. That order is preserved in
149            // the shaped buffer so downstream rendering and hit-testing can
150            // operate on visual-order glyphs without an extra bidi reversal
151            // step.
152            let scale_x = harf_buzz_typeface.with_font(|font| {
153                font.shape(&mut buffer, &features);
154                font.scale().0
155            });
156
157            let text_scale = font_rendering_em_size / scale_x as f64;
158
159            let buffer_length = buffer.len();
160
161            let shaped_buffer =
162                ShapedBuffer::new(text.clone(), buffer_length, glyph_typeface.clone(), font_rendering_em_size, bidi_level);
163
164            let glyph_infos = buffer.glyph_infos();
165            let glyph_positions = buffer.glyph_positions();
166
167            for i in 0..buffer_length {
168                let source_info = &glyph_infos[i];
169
170                let mut glyph_index = source_info.codepoint as u16;
171
172                let original_cluster = source_info.cluster as usize;
173
174                let glyph_cluster = original_cluster as i32 - start as i32;
175
176                let mut glyph_advance =
177                    Self::get_glyph_advance(&glyph_positions[i], text_scale) + options.letter_spacing();
178
179                let glyph_offset = Self::get_glyph_offset(&glyph_positions[i], text_scale);
180
181                if original_cluster < containing_text.len() && containing_text[original_cluster] == '\t' as u16 {
182                    glyph_index = glyph_typeface.character_to_glyph_map().get_glyph(' ' as i32);
183
184                    if options.incremental_tab_width() > 0.0 {
185                        glyph_advance = options.incremental_tab_width();
186                    } else {
187                        let advance = glyph_typeface.try_get_horizontal_glyph_advance(glyph_index).unwrap_or(0);
188                        glyph_advance = 4.0 * advance as f64 * text_scale;
189                    }
190                }
191
192                shaped_buffer.set(i, GlyphInfo::with_offset(glyph_index, glyph_cluster, glyph_advance, glyph_offset));
193            }
194
195            shaped_buffer
196        })
197    }
198
199    fn create_typeface(&self, glyph_typeface: &GlyphTypeface) -> Rc<dyn ITextShaperTypeface> {
200        Rc::new(HarfBuzzTypeface::new(glyph_typeface))
201    }
202}