1use 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#[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 fn from(val: Padding) -> Self {
36 val.left != 0. || val.top != 0. || val.right != 0. || val.bottom != 0.
37 }
38}
39
40struct AnchorAlignment {
42 horizontal_align: f64,
43 vertical_align: f64,
44}
45impl AnchorAlignment {
46 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
84pub 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#[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 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 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 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 self.left = icon_offset[0] + text_left - padding[3];
168 self.right = icon_offset[0] + text_right + padding[1];
169 } else {
170 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 self.top = icon_offset[1] + text_top - padding[0];
181 self.bottom = icon_offset[1] + text_bottom + padding[2];
182 } else {
183 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
191pub 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
268const ZWSP: Char16 = '\u{200b}' as Char16;
270
271fn 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
298fn 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
314fn 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) = §ion.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(§ion.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
357fn 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
385fn 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 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
402fn calculate_penalty(
404 code_point: Char16,
405 next_code_point: Char16,
406 penalizable_ideographic_break: bool,
407) -> f64 {
408 let mut penalty = 0.;
409 if code_point == 0x0au16 {
411 penalty -= 10000.;
412 }
413
414 if code_point == 0x28u16 || code_point == 0xff08u16 {
416 penalty += 50.;
417 }
418
419 if next_code_point == 0x29u16 || next_code_point == 0xff09u16 {
421 penalty += 50.;
422 }
423
424 if penalizable_ideographic_break {
427 penalty += 150.;
428 }
429
430 penalty
431}
432
433#[derive(Clone)]
435struct PotentialBreak {
436 pub index: usize,
437 pub x: f64,
438 pub prior_break: Option<Box<PotentialBreak>>, pub badness: f64,
440}
441
442fn 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 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
477fn 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
489fn 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 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 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
578fn 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 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 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; 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(); let mut metrics: GlyphMetrics = GlyphMetrics::default(); 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 (!allow_vertical_placement && !i18n::has_upright_vertical_orientation(code_point)) ||
642 (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) = §ion.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 section_scale = section_scale * ONE_EM / layout_text_size;
671
672 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 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(§ion.font_stack_hash); 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(§ion.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 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 if !positioned_glyphs.is_empty() {
764 let line_length = x - spacing; 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 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 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, SymbolAnchorType::Center,
848 TextJustifyType::Center,
849 0., &[0.0, 0.0], 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 false,
859 );
860 };
861
862 {
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 {
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 {
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 {
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}