maplibre/legacy/
shaping.rs

1//! Translated from https://github.com/maplibre/maplibre-native/blob/4add9ea/src/mbgl/text/shaping.cpp
2
3use std::collections::BTreeSet;
4
5use cgmath::num_traits::Pow;
6
7use crate::{
8    euclid::Rect,
9    legacy::{
10        bidi::{BiDi, Char16},
11        glyph::{
12            Glyph, GlyphMap, GlyphMetrics, PositionedGlyph, PositionedLine, Shaping,
13            WritingModeType,
14        },
15        glyph_atlas::GlyphPositions,
16        image_atlas::{ImagePosition, ImagePositions},
17        style_types::{IconTextFitType, SymbolAnchorType, TextJustifyType},
18        tagged_string::{SectionOptions, TaggedString},
19        util::{constants::ONE_EM, i18n},
20        TileSpace,
21    },
22};
23
24/// maplibre/maplibre-native#4add9ea original name: Padding
25#[derive(Clone, Copy, Default, PartialEq)]
26pub struct Padding {
27    pub left: f64,
28    pub top: f64,
29    pub right: f64,
30    pub bottom: f64,
31}
32
33impl From<Padding> for bool {
34    /// maplibre/maplibre-native#4add9ea original name: into
35    fn from(val: Padding) -> Self {
36        val.left != 0. || val.top != 0. || val.right != 0. || val.bottom != 0.
37    }
38}
39
40/// maplibre/maplibre-native#4add9ea original name: AnchorAlignment
41struct AnchorAlignment {
42    horizontal_align: f64,
43    vertical_align: f64,
44}
45impl AnchorAlignment {
46    /// maplibre/maplibre-native#4add9ea original name: getAnchorAlignment
47    fn get_anchor_alignment(anchor: SymbolAnchorType) -> AnchorAlignment {
48        let mut result = AnchorAlignment {
49            horizontal_align: 0.5,
50            vertical_align: 0.5,
51        };
52
53        match anchor {
54            SymbolAnchorType::Right
55            | SymbolAnchorType::TopRight
56            | SymbolAnchorType::BottomRight => {
57                result.horizontal_align = 1.0;
58            }
59
60            SymbolAnchorType::Left | SymbolAnchorType::TopLeft | SymbolAnchorType::BottomLeft => {
61                result.horizontal_align = 0.0;
62            }
63            _ => {}
64        }
65
66        match anchor {
67            SymbolAnchorType::Bottom
68            | SymbolAnchorType::BottomLeft
69            | SymbolAnchorType::BottomRight => {
70                result.vertical_align = 1.0;
71            }
72
73            SymbolAnchorType::Top | SymbolAnchorType::TopLeft | SymbolAnchorType::TopRight => {
74                result.vertical_align = 0.0;
75            }
76
77            _ => {}
78        }
79
80        result
81    }
82}
83
84// Choose the justification that matches the direction of the TextAnchor
85/// maplibre/maplibre-native#4add9ea original name: getAnchorJustification
86pub fn get_anchor_justification(anchor: &SymbolAnchorType) -> TextJustifyType {
87    match anchor {
88        SymbolAnchorType::Right | SymbolAnchorType::TopRight | SymbolAnchorType::BottomRight => {
89            TextJustifyType::Right
90        }
91        SymbolAnchorType::Left | SymbolAnchorType::TopLeft | SymbolAnchorType::BottomLeft => {
92            TextJustifyType::Left
93        }
94        _ => TextJustifyType::Center,
95    }
96}
97
98/// maplibre/maplibre-native#4add9ea original name: PositionedIcon
99#[derive(Clone)]
100pub struct PositionedIcon {
101    pub image: ImagePosition,
102    pub top: f64,
103    pub bottom: f64,
104    pub left: f64,
105    pub right: f64,
106    pub collision_padding: Padding,
107}
108
109impl PositionedIcon {
110    /// maplibre/maplibre-native#4add9ea original name: shapeIcon
111    pub fn shape_icon(
112        image: ImagePosition,
113        icon_offset: &[f64; 2],
114        icon_anchor: SymbolAnchorType,
115    ) -> PositionedIcon {
116        let anchor_align = AnchorAlignment::get_anchor_alignment(icon_anchor);
117        let dx = icon_offset[0];
118        let dy = icon_offset[1];
119        let left = dx - image.display_size()[0] * anchor_align.horizontal_align;
120        let right = left + image.display_size()[0];
121        let top = dy - image.display_size()[1] * anchor_align.vertical_align;
122        let bottom = top + image.display_size()[1];
123
124        let mut collision_padding: Padding = Padding::default();
125        if let Some(content) = &image.content {
126            let content = content;
127            let pixel_ratio = image.pixel_ratio;
128            collision_padding.left = content.left / pixel_ratio;
129            collision_padding.top = content.top / pixel_ratio;
130            collision_padding.right = image.display_size()[0] - content.right / pixel_ratio;
131            collision_padding.bottom = image.display_size()[1] - content.bottom / pixel_ratio;
132        }
133
134        PositionedIcon {
135            image,
136            top,
137            bottom,
138            left,
139            right,
140            collision_padding: collision_padding,
141        }
142    }
143
144    // Updates shaped icon's bounds based on shaped text's bounds and provided
145    // layout properties.
146    /// maplibre/maplibre-native#4add9ea original name: fitIconToText
147    pub fn fit_icon_to_text(
148        &mut self,
149        shaped_text: &Shaping,
150        text_fit: IconTextFitType,
151        padding: &[f64; 4],
152        icon_offset: &[f64; 2],
153        font_scale: f64,
154    ) {
155        assert!(text_fit != IconTextFitType::None);
156        // TODO assert!(shapedText);
157
158        // We don't respect the icon-anchor, because icon-text-fit is set. Instead,
159        // the icon will be centered on the text, then stretched in the given
160        // dimensions.
161
162        let text_left = shaped_text.left * font_scale;
163        let text_right = shaped_text.right * font_scale;
164
165        if text_fit == IconTextFitType::Width || text_fit == IconTextFitType::Both {
166            // Stretched horizontally to the text width
167            self.left = icon_offset[0] + text_left - padding[3];
168            self.right = icon_offset[0] + text_right + padding[1];
169        } else {
170            // Centered on the text
171            self.left =
172                icon_offset[0] + (text_left + text_right - self.image.display_size()[0]) / 2.0;
173            self.right = self.left + self.image.display_size()[0];
174        }
175
176        let text_top = shaped_text.top * font_scale;
177        let text_bottom = shaped_text.bottom * font_scale;
178        if text_fit == IconTextFitType::Height || text_fit == IconTextFitType::Both {
179            // Stretched vertically to the text height
180            self.top = icon_offset[1] + text_top - padding[0];
181            self.bottom = icon_offset[1] + text_bottom + padding[2];
182        } else {
183            // Centered on the text
184            self.top =
185                icon_offset[1] + (text_top + text_bottom - self.image.display_size()[1]) / 2.0;
186            self.bottom = self.top + self.image.display_size()[1];
187        }
188    }
189}
190
191/// maplibre/maplibre-native#4add9ea original name: getShaping
192pub fn get_shaping(
193    formatted_string: &TaggedString,
194    max_width: f64,
195    line_height: f64,
196    text_anchor: SymbolAnchorType,
197
198    text_justify: TextJustifyType,
199    spacing: f64,
200    translate: &[f64; 2],
201    writing_mode: WritingModeType,
202    bidi: &BiDi,
203    glyph_map: &GlyphMap,
204    glyph_positions: &GlyphPositions,
205    image_positions: &ImagePositions,
206    layout_text_size: f64,
207    layout_text_size_at_bucket_zoom_level: f64,
208    allow_vertical_placement: bool,
209) -> Shaping {
210    assert!(layout_text_size != 0.);
211    let mut reordered_lines: Vec<TaggedString> = Vec::new();
212    if formatted_string.section_count() == 1 {
213        let untagged_lines = bidi.process_text(
214            formatted_string.raw_text(),
215            determine_line_breaks(
216                formatted_string,
217                spacing,
218                max_width,
219                glyph_map,
220                image_positions,
221                layout_text_size,
222            ),
223        );
224        for line in untagged_lines {
225            reordered_lines.push(TaggedString::new_from_raw(
226                line,
227                formatted_string.section_at(0).clone(),
228            ));
229        }
230    } else {
231        let processed_lines = bidi.process_styled_text(
232            formatted_string.get_styled_text(),
233            determine_line_breaks(
234                formatted_string,
235                spacing,
236                max_width,
237                glyph_map,
238                image_positions,
239                layout_text_size,
240            ),
241        );
242        for line in processed_lines {
243            reordered_lines.push(TaggedString::new(
244                line,
245                formatted_string.get_sections().clone(),
246            ));
247        }
248    }
249    let mut shaping = Shaping::new(translate[0], translate[1], writing_mode);
250    shape_lines(
251        &mut shaping,
252        &mut reordered_lines,
253        spacing,
254        line_height,
255        text_anchor,
256        text_justify,
257        writing_mode,
258        glyph_map,
259        glyph_positions,
260        image_positions,
261        layout_text_size_at_bucket_zoom_level,
262        allow_vertical_placement,
263    );
264
265    shaping
266}
267
268// Zero width space that is used to suggest break points for Japanese labels.
269const ZWSP: Char16 = '\u{200b}' as Char16;
270
271/// maplibre/maplibre-native#4add9ea original name: align
272fn align(
273    shaping: &mut Shaping,
274    justify: f64,
275    horizontal_align: f64,
276    vertical_align: f64,
277    max_line_length: f64,
278    max_line_height: f64,
279    line_height: f64,
280    block_height: f64,
281    line_count: usize,
282) {
283    let shift_x = (justify - horizontal_align) * max_line_length;
284    let shift_y = if max_line_height != line_height {
285        -block_height * vertical_align - Shaping::Y_OFFSET as f64
286    } else {
287        (-vertical_align * (line_count) as f64 + 0.5) * line_height
288    };
289
290    for line in &mut shaping.positioned_lines {
291        for positioned_glyph in &mut line.positioned_glyphs {
292            positioned_glyph.x += shift_x;
293            positioned_glyph.y += shift_y;
294        }
295    }
296}
297
298// justify left = 0, right = 1, center = .5
299/// maplibre/maplibre-native#4add9ea original name: justifyLine
300fn justify_line(positioned_glyphs: &mut Vec<PositionedGlyph>, justify: f64, line_offset: f64) {
301    if justify == 0.0 && line_offset == 0.0 {
302        return;
303    }
304
305    let last_glyph = positioned_glyphs.last().unwrap();
306    let last_advance: f64 = last_glyph.metrics.advance as f64 * last_glyph.scale;
307    let line_indent = (last_glyph.x + last_advance) * justify;
308    for positioned_glyph in positioned_glyphs {
309        positioned_glyph.x -= line_indent;
310        positioned_glyph.y += line_offset;
311    }
312}
313
314/// maplibre/maplibre-native#4add9ea original name: getGlyphAdvance
315fn get_glyph_advance(
316    code_point: Char16,
317    section: &SectionOptions,
318    glyph_map: &GlyphMap,
319    image_positions: &ImagePositions,
320    layout_text_size: f64,
321    spacing: f64,
322) -> f64 {
323    if let Some(image_id) = &section.image_id {
324        let image = image_positions.get(image_id);
325        if image.is_none() {
326            return 0.0;
327        }
328        let image = image.unwrap();
329        image.display_size()[0] * section.scale * ONE_EM / layout_text_size + spacing
330    } else {
331        let glyphs = glyph_map.get(&section.font_stack_hash);
332        if glyphs.is_none() {
333            return 0.0;
334        }
335        let glyphs = glyphs.unwrap();
336        let it = glyphs.get(&code_point);
337
338        if it.is_none() {
339            return 0.0;
340        }
341
342        if it.expect("cant be none").is_none() {
343            return 0.0;
344        }
345
346        return (it
347            .expect("cant be none")
348            .as_ref()
349            .expect("cant be none")
350            .metrics
351            .advance as f64
352            * section.scale)
353            + spacing;
354    }
355}
356
357/// maplibre/maplibre-native#4add9ea original name: determine_average_line_width
358fn determine_average_line_width(
359    logical_input: &TaggedString,
360    spacing: f64,
361    max_width: f64,
362    glyph_map: &GlyphMap,
363    image_positions: &ImagePositions,
364    layout_text_size: f64,
365) -> f64 {
366    let mut total_width: f64 = 0.;
367
368    for i in 0..logical_input.length() {
369        let section = logical_input.get_section(i);
370        let code_point: Char16 = logical_input.get_char_code_at(i);
371        total_width += get_glyph_advance(
372            code_point,
373            section,
374            glyph_map,
375            image_positions,
376            layout_text_size,
377            spacing,
378        );
379    }
380
381    let target_line_count = (1.0f64).max((total_width / max_width).ceil()) as i32;
382    total_width / target_line_count as f64
383}
384
385/// maplibre/maplibre-native#4add9ea original name: calculateBadness
386fn calculate_badness(line_width: f64, target_width: f64, penalty: f64, is_last_break: bool) -> f64 {
387    let raggedness = (line_width - target_width).pow(2);
388    if is_last_break {
389        // Favor finals lines shorter than average over longer than average
390        if line_width < target_width {
391            return raggedness / 2.;
392        } else {
393            return raggedness * 2.;
394        }
395    }
396    if penalty < 0. {
397        return raggedness - penalty * penalty;
398    }
399    raggedness + penalty * penalty
400}
401
402/// maplibre/maplibre-native#4add9ea original name: calculatePenalty
403fn calculate_penalty(
404    code_point: Char16,
405    next_code_point: Char16,
406    penalizable_ideographic_break: bool,
407) -> f64 {
408    let mut penalty = 0.;
409    // Force break on newline
410    if code_point == 0x0au16 {
411        penalty -= 10000.;
412    }
413
414    // Penalize open parenthesis at end of line
415    if code_point == 0x28u16 || code_point == 0xff08u16 {
416        penalty += 50.;
417    }
418
419    // Penalize close parenthesis at beginning of line
420    if next_code_point == 0x29u16 || next_code_point == 0xff09u16 {
421        penalty += 50.;
422    }
423
424    // Penalize breaks between characters that allow ideographic breaking because
425    // they are less preferable than breaks at spaces (or zero width spaces)
426    if penalizable_ideographic_break {
427        penalty += 150.;
428    }
429
430    penalty
431}
432
433/// maplibre/maplibre-native#4add9ea original name: PotentialBreak
434#[derive(Clone)]
435struct PotentialBreak {
436    pub index: usize,
437    pub x: f64,
438    pub prior_break: Option<Box<PotentialBreak>>, // TODO avoid Box
439    pub badness: f64,
440}
441
442/// maplibre/maplibre-native#4add9ea original name: evaluateBreak
443fn evaluate_break(
444    break_index: usize,
445    break_x: f64,
446    target_width: f64,
447    potential_breaks: &Vec<PotentialBreak>,
448    penalty: f64,
449    is_last_break: bool,
450) -> PotentialBreak {
451    // We could skip evaluating breaks where the line length (breakX - priorBreak.x) > maxWidth
452    //  ...but in fact we allow lines longer than maxWidth (if there's no break points)
453    //  ...and when targetWidth and maxWidth are close, strictly enforcing maxWidth can give
454    //     more lopsided results.
455
456    let mut best_prior_break: Option<Box<PotentialBreak>> = None;
457    let mut best_break_badness: f64 =
458        calculate_badness(break_x, target_width, penalty, is_last_break);
459    for potential_break in potential_breaks {
460        let line_width = break_x - potential_break.x;
461        let break_badness = calculate_badness(line_width, target_width, penalty, is_last_break)
462            + potential_break.badness;
463        if break_badness <= best_break_badness {
464            best_prior_break = Some(Box::new(potential_break.clone()));
465            best_break_badness = break_badness;
466        }
467    }
468
469    PotentialBreak {
470        index: break_index,
471        x: break_x,
472        prior_break: best_prior_break,
473        badness: best_break_badness,
474    }
475}
476
477/// maplibre/maplibre-native#4add9ea original name: leastBadBreaks
478fn least_bad_breaks(last_line_break: &PotentialBreak) -> BTreeSet<usize> {
479    let mut least_bad_breaks: BTreeSet<usize> = BTreeSet::from([last_line_break.index]);
480    let mut prior_break = &last_line_break.prior_break;
481
482    while let Some(prior_break_) = prior_break {
483        least_bad_breaks.insert(prior_break_.index);
484        prior_break = &prior_break_.prior_break;
485    }
486    least_bad_breaks
487}
488
489// We determine line breaks based on shaped text in logical order. Working in visual order would be
490//  more intuitive, but we can't do that because the visual order may be changed by line breaks!
491/// maplibre/maplibre-native#4add9ea original name: determine_line_breaks
492fn determine_line_breaks(
493    logical_input: &TaggedString,
494    spacing: f64,
495    max_width: f64,
496    glyph_map: &GlyphMap,
497    image_positions: &ImagePositions,
498    layout_text_size: f64,
499) -> BTreeSet<usize> {
500    if max_width == 0.0 {
501        return BTreeSet::default();
502    }
503
504    if logical_input.empty() {
505        return BTreeSet::default();
506    }
507
508    let target_width = determine_average_line_width(
509        logical_input,
510        spacing,
511        max_width,
512        glyph_map,
513        image_positions,
514        layout_text_size,
515    );
516
517    let mut potential_breaks: Vec<PotentialBreak> = Vec::new();
518    let mut current_x: f64 = 0.;
519    // Find first occurance of zero width space (ZWSP) character.
520    let has_server_suggested_breaks = logical_input
521        .raw_text()
522        .as_slice()
523        .iter()
524        .any(|c| *c == ZWSP);
525
526    for i in 0..logical_input.length() {
527        let section = logical_input.get_section(i);
528        let code_point: Char16 = logical_input.get_char_code_at(i);
529        if !i18n::is_whitespace(code_point) {
530            current_x += get_glyph_advance(
531                code_point,
532                section,
533                glyph_map,
534                image_positions,
535                layout_text_size,
536                spacing,
537            );
538        }
539
540        // Ideographic characters, spaces, and word-breaking punctuation that
541        // often appear without surrounding spaces.
542        if i < logical_input.length() - 1 {
543            let allows_ideographic_break = i18n::allows_ideographic_breaking(code_point);
544            if section.image_id.is_some()
545                || allows_ideographic_break
546                || i18n::allows_word_breaking(code_point)
547            {
548                let penalizable_ideographic_break =
549                    allows_ideographic_break && has_server_suggested_breaks;
550                let next_index: usize = i + 1;
551                let potential_break = evaluate_break(
552                    next_index,
553                    current_x,
554                    target_width,
555                    &potential_breaks,
556                    calculate_penalty(
557                        code_point,
558                        logical_input.get_char_code_at(next_index),
559                        penalizable_ideographic_break,
560                    ),
561                    false,
562                );
563                potential_breaks.push(potential_break);
564            }
565        }
566    }
567
568    least_bad_breaks(&evaluate_break(
569        logical_input.length(),
570        current_x,
571        target_width,
572        &potential_breaks,
573        0.,
574        true,
575    ))
576}
577
578/// maplibre/maplibre-native#4add9ea original name: shapeLines
579fn shape_lines(
580    shaping: &mut Shaping,
581    lines: &mut Vec<TaggedString>,
582    spacing: f64,
583    line_height: f64,
584    text_anchor: SymbolAnchorType,
585    text_justify: TextJustifyType,
586    writing_mode: WritingModeType,
587    glyph_map: &GlyphMap,
588    glyph_positions: &GlyphPositions,
589    image_positions: &ImagePositions,
590    layout_text_size: f64,
591    allow_vertical_placement: bool,
592) {
593    let mut x = 0.0;
594    let mut y = Shaping::Y_OFFSET as f64;
595
596    let mut max_line_length = 0.0;
597    let mut max_line_height = 0.0;
598
599    // TODO was this translated correctly?
600    let justify = if text_justify == TextJustifyType::Right {
601        1.0
602    } else if text_justify == TextJustifyType::Left {
603        0.0
604    } else {
605        0.5
606    };
607
608    let n_lines = lines.len();
609
610    for line in lines {
611        // Collapse whitespace so it doesn't throw off justification
612        line.trim();
613
614        let line_max_scale = line.get_max_scale();
615        let max_line_offset = (line_max_scale - 1.0) * ONE_EM;
616        let mut line_offset = 0.0;
617        shaping.positioned_lines.push(PositionedLine::default());
618        let positioned_line = shaping.positioned_lines.last_mut().unwrap();
619        let positioned_glyphs = &mut positioned_line.positioned_glyphs;
620
621        if line.empty() {
622            y += line_height; // Still need a line feed after empty line
623            continue;
624        }
625
626        for i in 0..line.length() {
627            let section_index = line.get_section_index(i) as usize;
628            let section = line.section_at(section_index);
629            let code_point: Char16 = line.get_char_code_at(i);
630            let mut baseline_offset = 0.0;
631            let mut rect: Rect<u16, TileSpace> = Rect::default(); // TODO are these default values fine?
632            let mut metrics: GlyphMetrics = GlyphMetrics::default(); // TODO are these default values fine?
633            let mut advance = 0.0;
634            let mut vertical_advance = ONE_EM;
635            let mut section_scale = section.scale;
636            assert_ne!(section_scale, 0.0);
637
638            let vertical = !(writing_mode == WritingModeType::Horizontal ||
639                // Don't verticalize glyphs that have no upright orientation
640                // if vertical placement is disabled.
641                (!allow_vertical_placement && !i18n::has_upright_vertical_orientation(code_point)) ||
642                // If vertical placement is ebabled, don't verticalize glyphs
643                // that are from complex text layout script, or whitespaces.
644                (allow_vertical_placement &&
645                 (i18n::is_whitespace(code_point) || i18n::is_char_in_complex_shaping_script(code_point))));
646
647            if let Some(image_id) = &section.image_id {
648                let image = image_positions.get(image_id);
649                if image.is_none() {
650                    continue;
651                }
652                let image = image.expect("is some");
653
654                shaping.icons_in_text |= true;
655                let display_size = image.display_size();
656                metrics.width = (display_size[0]) as u32;
657                metrics.height = (display_size[1]) as u32;
658                metrics.left = ImagePosition::PADDING as i32;
659                metrics.top = -(Glyph::BORDER_SIZE as i32);
660                metrics.advance = if vertical {
661                    metrics.height
662                } else {
663                    metrics.width
664                };
665                rect = image.padded_rect;
666
667                // If needed, allow to set scale factor for an image using
668                // alias "image-scale" that could be alias for "font-scale"
669                // when FormattedSection is an image section.
670                section_scale = section_scale * ONE_EM / layout_text_size;
671
672                // Difference between one EM and an image size.
673                // Aligns bottom of an image to a baseline level.
674                let image_offset = ONE_EM - display_size[1] * section_scale;
675                baseline_offset = max_line_offset + image_offset;
676
677                vertical_advance = metrics.advance as f64;
678                advance = vertical_advance;
679
680                // Difference between height of an image and one EM at max line scale.
681                // Pushes current line down if an image size is over 1 EM at max line scale.
682                let offset = (if vertical {
683                    display_size[0]
684                } else {
685                    display_size[1]
686                }) * section_scale
687                    - ONE_EM * line_max_scale;
688                if offset > 0.0 && offset > line_offset {
689                    line_offset = offset;
690                }
691            } else {
692                let glyph_position_map = glyph_positions.get(&section.font_stack_hash); // TODO was .find
693                if glyph_position_map.is_none() {
694                    continue;
695                }
696
697                let glyph_position_map = glyph_position_map.expect("cant be none");
698
699                let glyph_position = glyph_position_map.get(&code_point);
700                if let Some(glyph_position) = glyph_position {
701                    rect = glyph_position.rect;
702                    metrics = glyph_position.metrics;
703                } else {
704                    let glyphs = glyph_map.get(&section.font_stack_hash);
705                    if glyphs.is_none() {
706                        continue;
707                    }
708                    let glyphs = glyphs.expect("cant be none");
709
710                    let glyph = glyphs.get(&code_point);
711
712                    if glyph.is_none() {
713                        continue;
714                    }
715
716                    if glyph.expect("cant be none").is_none() {
717                        continue;
718                    }
719
720                    metrics =
721                        (glyph.expect("cant be none").as_ref().expect("cant be none")).metrics;
722                }
723                advance = metrics.advance as f64;
724                // We don't know the baseline, but since we're laying out
725                // at 24 points, we can calculate how much it will move when
726                // we scale up or down.
727                baseline_offset = (line_max_scale - section_scale) * ONE_EM;
728            }
729
730            if !vertical {
731                positioned_glyphs.push(PositionedGlyph {
732                    glyph: code_point,
733                    x,
734                    y: y + baseline_offset,
735                    vertical,
736                    font: section.font_stack_hash,
737                    scale: section_scale,
738                    rect,
739                    metrics,
740                    image_id: section.image_id.clone(),
741                    section_index,
742                });
743                x += advance * section_scale + spacing;
744            } else {
745                positioned_glyphs.push(PositionedGlyph {
746                    glyph: code_point,
747                    x,
748                    y: y + baseline_offset,
749                    vertical,
750                    font: section.font_stack_hash,
751                    scale: section_scale,
752                    rect,
753                    metrics,
754                    image_id: section.image_id.clone(),
755                    section_index,
756                });
757                x += vertical_advance * section_scale + spacing;
758                shaping.verticalizable |= true;
759            }
760        }
761
762        // Only justify if we placed at least one glyph
763        if !positioned_glyphs.is_empty() {
764            let line_length = x - spacing; // Don't count trailing spacing
765            max_line_length = (line_length).max(max_line_length);
766            justify_line(positioned_glyphs, justify, line_offset);
767        }
768
769        let current_line_height = line_height * line_max_scale + line_offset;
770        x = 0.0;
771        y += current_line_height;
772        positioned_line.line_offset = (line_offset).max(max_line_offset);
773        max_line_height = (current_line_height).max(max_line_height);
774    }
775
776    let anchor_align = AnchorAlignment::get_anchor_alignment(text_anchor);
777    let height = y - Shaping::Y_OFFSET as f64;
778    align(
779        shaping,
780        justify,
781        anchor_align.horizontal_align,
782        anchor_align.vertical_align,
783        max_line_length,
784        max_line_height,
785        line_height,
786        height,
787        n_lines,
788    );
789
790    // Calculate the bounding box
791    shaping.top += -anchor_align.vertical_align * height;
792    shaping.bottom = shaping.top + height;
793    shaping.left += -anchor_align.horizontal_align * max_line_length;
794    shaping.right = shaping.left + max_line_length;
795}
796
797#[cfg(test)]
798mod test {
799    use crate::legacy::{
800        bidi::{BiDi, Char16},
801        font_stack::FontStackHasher,
802        glyph::{Glyph, GlyphMap, Glyphs, WritingModeType},
803        glyph_atlas::{GlyphPosition, GlyphPositionMap, GlyphPositions},
804        image_atlas::ImagePositions,
805        shaping::get_shaping,
806        style_types::{SymbolAnchorType, TextJustifyType},
807        tagged_string::{SectionOptions, TaggedString},
808        util::constants::ONE_EM,
809    };
810
811    #[test]
812    /// maplibre/maplibre-native#4add9ea original name: Shaping_ZWSP
813    fn shaping_zwsp() {
814        let mut glyph_position = GlyphPosition::default();
815        glyph_position.metrics.width = 18;
816        glyph_position.metrics.height = 18;
817        glyph_position.metrics.left = 2;
818        glyph_position.metrics.top = -8;
819        glyph_position.metrics.advance = 21;
820
821        let mut glyph = Glyph::default();
822        glyph.id = '中' as Char16;
823        glyph.metrics = glyph_position.metrics;
824
825        let bidi = BiDi;
826        let font_stack = vec!["font-stack".to_string()];
827        let section_options = SectionOptions::new(1.0, font_stack.clone(), None);
828        let layout_text_size = 16.0;
829        let layout_text_size_at_bucket_zoom_level = 16.0;
830
831        let glyphs: GlyphMap = GlyphMap::from([(
832            FontStackHasher::new(&font_stack),
833            Glyphs::from([('中' as Char16, Some(glyph))]),
834        )]);
835
836        let glyph_positions: GlyphPositions = GlyphPositions::from([(
837            FontStackHasher::new(&font_stack),
838            GlyphPositionMap::from([('中' as Char16, glyph_position)]),
839        )]);
840        let image_positions: ImagePositions = ImagePositions::default();
841
842        let test_get_shaping = |string: &TaggedString, max_width_in_chars| {
843            return get_shaping(
844                string,
845                max_width_in_chars as f64 * ONE_EM,
846                ONE_EM, // lineHeight
847                SymbolAnchorType::Center,
848                TextJustifyType::Center,
849                0.,          // spacing
850                &[0.0, 0.0], // translate
851                WritingModeType::Horizontal,
852                &bidi,
853                &glyphs,
854                &glyph_positions,
855                &image_positions,
856                layout_text_size,
857                layout_text_size_at_bucket_zoom_level,
858                /*allowVerticalPlacement*/ false,
859            );
860        };
861
862        // 3 lines
863        // 中中中中中中
864        // 中中中中中中
865        // 中中
866        {
867            let string = TaggedString::new_from_raw(
868                "中中\u{200b}中中\u{200b}中中\u{200b}中中中中中中\u{200b}中中".into(),
869                section_options.clone(),
870            );
871            let shaping = test_get_shaping(&string, 5);
872            assert_eq!(shaping.positioned_lines.len(), 3);
873            assert_eq!(shaping.top, -36.);
874            assert_eq!(shaping.bottom, 36.);
875            assert_eq!(shaping.left, -63.);
876            assert_eq!(shaping.right, 63.);
877            assert_eq!(shaping.writing_mode, WritingModeType::Horizontal);
878        }
879
880        // 2 lines
881        // 中中
882        // 中
883        {
884            let string =
885                TaggedString::new_from_raw("中中\u{200b}中".into(), section_options.clone());
886            let shaping = test_get_shaping(&string, 1);
887            assert_eq!(shaping.positioned_lines.len(), 2);
888            assert_eq!(shaping.top, -24.);
889            assert_eq!(shaping.bottom, 24.);
890            assert_eq!(shaping.left, -21.);
891            assert_eq!(shaping.right, 21.);
892            assert_eq!(shaping.writing_mode, WritingModeType::Horizontal);
893        }
894
895        // 1 line
896        // 中中
897        {
898            let string = TaggedString::new_from_raw("中中\u{200b}".into(), section_options.clone());
899            let shaping = test_get_shaping(&string, 2);
900            assert_eq!(shaping.positioned_lines.len(), 1);
901            assert_eq!(shaping.top, -12.);
902            assert_eq!(shaping.bottom, 12.);
903            assert_eq!(shaping.left, -21.);
904            assert_eq!(shaping.right, 21.);
905            assert_eq!(shaping.writing_mode, WritingModeType::Horizontal);
906        }
907
908        // 5 'new' lines.
909        {
910            let string = TaggedString::new_from_raw(
911                "\u{200b}\u{200b}\u{200b}\u{200b}\u{200b}".into(),
912                section_options.clone(),
913            );
914            let shaping = test_get_shaping(&string, 1);
915            assert_eq!(shaping.positioned_lines.len(), 5);
916            assert_eq!(shaping.top, -60.);
917            assert_eq!(shaping.bottom, 60.);
918            assert_eq!(shaping.left, 0.);
919            assert_eq!(shaping.right, 0.);
920            assert_eq!(shaping.writing_mode, WritingModeType::Horizontal);
921        }
922    }
923}