glyph_brush_layout/
builtin.rs

1use super::{BuiltInLineBreaker, GlyphPositioner, LineBreaker, SectionGeometry, ToSectionText};
2use crate::{characters::Characters, GlyphChange, SectionGlyph};
3use ab_glyph::*;
4
5/// Built-in [`GlyphPositioner`](trait.GlyphPositioner.html) implementations.
6///
7/// Takes generic [`LineBreaker`](trait.LineBreaker.html) to indicate the wrapping style.
8/// See [`BuiltInLineBreaker`](enum.BuiltInLineBreaker.html).
9///
10/// # Example
11/// ```
12/// # use glyph_brush_layout::*;
13/// let layout = Layout::default().h_align(HorizontalAlign::Right);
14/// ```
15#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
16pub enum Layout<L: LineBreaker> {
17    /// Renders a single line from left-to-right according to the inner alignment.
18    /// Hard breaking will end the line, partially hitting the width bound will end the line.
19    SingleLine {
20        line_breaker: L,
21        h_align: HorizontalAlign,
22        v_align: VerticalAlign,
23    },
24    /// Renders multiple lines from left-to-right according to the inner alignment.
25    /// Hard breaking characters will cause advancement to another line.
26    /// A characters hitting the width bound will also cause another line to start.
27    Wrap {
28        line_breaker: L,
29        h_align: HorizontalAlign,
30        v_align: VerticalAlign,
31    },
32}
33
34impl Default for Layout<BuiltInLineBreaker> {
35    #[inline]
36    fn default() -> Self {
37        Layout::default_wrap()
38    }
39}
40
41impl Layout<BuiltInLineBreaker> {
42    #[inline]
43    pub fn default_single_line() -> Self {
44        Layout::SingleLine {
45            line_breaker: BuiltInLineBreaker::default(),
46            h_align: HorizontalAlign::Left,
47            v_align: VerticalAlign::Top,
48        }
49    }
50
51    #[inline]
52    pub fn default_wrap() -> Self {
53        Layout::Wrap {
54            line_breaker: BuiltInLineBreaker::default(),
55            h_align: HorizontalAlign::Left,
56            v_align: VerticalAlign::Top,
57        }
58    }
59}
60
61impl<L: LineBreaker> Layout<L> {
62    /// Returns an identical `Layout` but with the input `h_align`
63    pub fn h_align(self, h_align: HorizontalAlign) -> Self {
64        use crate::Layout::*;
65        match self {
66            SingleLine {
67                line_breaker,
68                v_align,
69                ..
70            } => SingleLine {
71                line_breaker,
72                v_align,
73                h_align,
74            },
75            Wrap {
76                line_breaker,
77                v_align,
78                ..
79            } => Wrap {
80                line_breaker,
81                v_align,
82                h_align,
83            },
84        }
85    }
86
87    /// Returns an identical `Layout` but with the input `v_align`
88    pub fn v_align(self, v_align: VerticalAlign) -> Self {
89        use crate::Layout::*;
90        match self {
91            SingleLine {
92                line_breaker,
93                h_align,
94                ..
95            } => SingleLine {
96                line_breaker,
97                v_align,
98                h_align,
99            },
100            Wrap {
101                line_breaker,
102                h_align,
103                ..
104            } => Wrap {
105                line_breaker,
106                v_align,
107                h_align,
108            },
109        }
110    }
111
112    /// Returns an identical `Layout` but with the input `line_breaker`
113    pub fn line_breaker<L2: LineBreaker>(self, line_breaker: L2) -> Layout<L2> {
114        use crate::Layout::*;
115        match self {
116            SingleLine {
117                h_align, v_align, ..
118            } => SingleLine {
119                line_breaker,
120                v_align,
121                h_align,
122            },
123            Wrap {
124                h_align, v_align, ..
125            } => Wrap {
126                line_breaker,
127                v_align,
128                h_align,
129            },
130        }
131    }
132}
133
134impl<L: LineBreaker> GlyphPositioner for Layout<L> {
135    fn calculate_glyphs<F, S>(
136        &self,
137        fonts: &[F],
138        geometry: &SectionGeometry,
139        sections: &[S],
140    ) -> Vec<SectionGlyph>
141    where
142        F: Font,
143        S: ToSectionText,
144    {
145        use crate::Layout::{SingleLine, Wrap};
146
147        let SectionGeometry {
148            screen_position,
149            bounds: (bound_w, bound_h),
150            ..
151        } = *geometry;
152
153        match *self {
154            SingleLine {
155                h_align,
156                v_align,
157                line_breaker,
158            } => Characters::new(
159                fonts,
160                sections.iter().map(|s| s.to_section_text()),
161                line_breaker,
162            )
163            .words()
164            .lines(bound_w)
165            .next()
166            .map(|line| line.aligned_on_screen(screen_position, h_align, v_align))
167            .unwrap_or_default(),
168
169            Wrap {
170                h_align,
171                v_align,
172                line_breaker,
173            } => {
174                let mut out = vec![];
175                let mut caret = screen_position;
176                let v_align_top = v_align == VerticalAlign::Top;
177
178                let lines = Characters::new(
179                    fonts,
180                    sections.iter().map(|s| s.to_section_text()),
181                    line_breaker,
182                )
183                .words()
184                .lines(bound_w);
185
186                for line in lines {
187                    // top align can bound check & exit early
188                    if v_align_top && caret.1 >= screen_position.1 + bound_h {
189                        break;
190                    }
191
192                    let line_height = line.line_height();
193                    out.extend(line.aligned_on_screen(caret, h_align, VerticalAlign::Top));
194                    caret.1 += line_height;
195                }
196
197                if !out.is_empty() {
198                    match v_align {
199                        // already aligned
200                        VerticalAlign::Top => {}
201                        // convert from top
202                        VerticalAlign::Center | VerticalAlign::Bottom => {
203                            let shift_up = if v_align == VerticalAlign::Center {
204                                (caret.1 - screen_position.1) / 2.0
205                            } else {
206                                caret.1 - screen_position.1
207                            };
208
209                            let (min_x, max_x) = h_align.x_bounds(screen_position.0, bound_w);
210                            let (min_y, max_y) = v_align.y_bounds(screen_position.1, bound_h);
211
212                            out = out
213                                .drain(..)
214                                .filter_map(|mut sg| {
215                                    // shift into position
216                                    sg.glyph.position.y -= shift_up;
217
218                                    // filter away out-of-bounds glyphs
219                                    let sfont = fonts[sg.font_id].as_scaled(sg.glyph.scale);
220                                    let h_advance = sfont.h_advance(sg.glyph.id);
221                                    let h_side_bearing = sfont.h_side_bearing(sg.glyph.id);
222                                    let height = sfont.height();
223
224                                    Some(sg).filter(|sg| {
225                                        sg.glyph.position.x - h_side_bearing <= max_x
226                                            && sg.glyph.position.x + h_advance >= min_x
227                                            && sg.glyph.position.y - height <= max_y
228                                            && sg.glyph.position.y + height >= min_y
229                                    })
230                                })
231                                .collect();
232                        }
233                    }
234                }
235
236                out
237            }
238        }
239    }
240
241    fn bounds_rect(&self, geometry: &SectionGeometry) -> Rect {
242        use crate::Layout::{SingleLine, Wrap};
243
244        let SectionGeometry {
245            screen_position: (screen_x, screen_y),
246            bounds: (bound_w, bound_h),
247        } = *geometry;
248
249        let (h_align, v_align) = match *self {
250            Wrap {
251                h_align, v_align, ..
252            }
253            | SingleLine {
254                h_align, v_align, ..
255            } => (h_align, v_align),
256        };
257
258        let (x_min, x_max) = h_align.x_bounds(screen_x, bound_w);
259        let (y_min, y_max) = v_align.y_bounds(screen_y, bound_h);
260
261        Rect {
262            min: point(x_min, y_min),
263            max: point(x_max, y_max),
264        }
265    }
266
267    #[allow(clippy::float_cmp)]
268    fn recalculate_glyphs<F, S, P>(
269        &self,
270        previous: P,
271        change: GlyphChange,
272        fonts: &[F],
273        geometry: &SectionGeometry,
274        sections: &[S],
275    ) -> Vec<SectionGlyph>
276    where
277        F: Font,
278        S: ToSectionText,
279        P: IntoIterator<Item = SectionGlyph>,
280    {
281        match change {
282            GlyphChange::Geometry(old) if old.bounds == geometry.bounds => {
283                // position change
284                let adjustment = point(
285                    geometry.screen_position.0 - old.screen_position.0,
286                    geometry.screen_position.1 - old.screen_position.1,
287                );
288
289                let mut glyphs: Vec<_> = previous.into_iter().collect();
290                glyphs
291                    .iter_mut()
292                    .for_each(|sg| sg.glyph.position += adjustment);
293                glyphs
294            }
295            _ => self.calculate_glyphs(fonts, geometry, sections),
296        }
297    }
298}
299
300/// Describes horizontal alignment preference for positioning & bounds.
301#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
302pub enum HorizontalAlign {
303    /// Leftmost character is immediately to the right of the render position.<br/>
304    /// Bounds start from the render position and advance rightwards.
305    Left,
306    /// Leftmost & rightmost characters are equidistant to the render position.<br/>
307    /// Bounds start from the render position and advance equally left & right.
308    Center,
309    /// Rightmost character is immetiately to the left of the render position.<br/>
310    /// Bounds start from the render position and advance leftwards.
311    Right,
312}
313
314impl HorizontalAlign {
315    #[inline]
316    pub(crate) fn x_bounds(self, screen_x: f32, bound_w: f32) -> (f32, f32) {
317        let (min, max) = match self {
318            HorizontalAlign::Left => (screen_x, screen_x + bound_w),
319            HorizontalAlign::Center => (screen_x - bound_w / 2.0, screen_x + bound_w / 2.0),
320            HorizontalAlign::Right => (screen_x - bound_w, screen_x),
321        };
322
323        (min.floor(), max.ceil())
324    }
325}
326
327/// Describes vertical alignment preference for positioning & bounds. Currently a placeholder
328/// for future functionality.
329#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
330pub enum VerticalAlign {
331    /// Characters/bounds start underneath the render position and progress downwards.
332    Top,
333    /// Characters/bounds center at the render position and progress outward equally.
334    Center,
335    /// Characters/bounds start above the render position and progress upward.
336    Bottom,
337}
338
339impl VerticalAlign {
340    #[inline]
341    pub(crate) fn y_bounds(self, screen_y: f32, bound_h: f32) -> (f32, f32) {
342        let (min, max) = match self {
343            VerticalAlign::Top => (screen_y, screen_y + bound_h),
344            VerticalAlign::Center => (screen_y - bound_h / 2.0, screen_y + bound_h / 2.0),
345            VerticalAlign::Bottom => (screen_y - bound_h, screen_y),
346        };
347
348        (min.floor(), max.ceil())
349    }
350}
351
352#[cfg(test)]
353mod bounds_test {
354    use super::*;
355
356    const fn inf() -> f32 {
357        f32::INFINITY
358    }
359
360    #[test]
361    fn v_align_y_bounds_inf() {
362        assert_eq!(VerticalAlign::Top.y_bounds(0.0, inf()), (0.0, inf()));
363        assert_eq!(VerticalAlign::Center.y_bounds(0.0, inf()), (-inf(), inf()));
364        assert_eq!(VerticalAlign::Bottom.y_bounds(0.0, inf()), (-inf(), 0.0));
365    }
366
367    #[test]
368    fn h_align_x_bounds_inf() {
369        assert_eq!(HorizontalAlign::Left.x_bounds(0.0, inf()), (0.0, inf()));
370        assert_eq!(
371            HorizontalAlign::Center.x_bounds(0.0, inf()),
372            (-inf(), inf())
373        );
374        assert_eq!(HorizontalAlign::Right.x_bounds(0.0, inf()), (-inf(), 0.0));
375    }
376}
377
378#[cfg(test)]
379mod layout_test {
380    use super::*;
381    use crate::{BuiltInLineBreaker::*, FontId, SectionText};
382    use approx::assert_relative_eq;
383    use once_cell::sync::Lazy;
384    use ordered_float::OrderedFloat;
385    use std::{collections::*, f32};
386
387    static A_FONT: Lazy<FontRef<'static>> = Lazy::new(|| {
388        FontRef::try_from_slice(include_bytes!("../../fonts/DejaVuSansMono.ttf")).unwrap()
389    });
390    static CJK_FONT: Lazy<FontRef<'static>> = Lazy::new(|| {
391        FontRef::try_from_slice(include_bytes!("../../fonts/WenQuanYiMicroHei.ttf")).unwrap()
392    });
393    static FONT_MAP: Lazy<[&'static FontRef<'static>; 2]> = Lazy::new(|| [&*A_FONT, &*CJK_FONT]);
394
395    /// All the chars used in testing, so we can reverse lookup the glyph-ids
396    const TEST_CHARS: &[char] = &[
397        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R',
398        'S', 'T', 'U', 'V', 'W', 'X', 'Q', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
399        'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', ' ', ',',
400        '.', '提', '高', '代', '碼', '執', '行', '率', '❤', 'é', 'ß', '\'', '_',
401    ];
402
403    /// Turns glyphs into a string, uses `☐` to denote that it didn't work
404    fn glyphs_to_common_string<F>(glyphs: &[SectionGlyph], font: &F) -> String
405    where
406        F: Font,
407    {
408        glyphs
409            .iter()
410            .map(|sg| {
411                TEST_CHARS
412                    .iter()
413                    .find(|cc| font.glyph_id(**cc) == sg.glyph.id)
414                    .unwrap_or(&'☐')
415            })
416            .collect()
417    }
418
419    /// Checks the order of glyphs in the first arg iterable matches the
420    /// second arg string characters
421    /// $glyphs: Vec<(Glyph, Color, FontId)>
422    macro_rules! assert_glyph_order {
423        ($glyphs:expr, $string:expr) => {
424            assert_glyph_order!($glyphs, $string, font = &*A_FONT)
425        };
426        ($glyphs:expr, $string:expr, font = $font:expr) => {{
427            assert_eq!($string, glyphs_to_common_string(&$glyphs, $font));
428        }};
429    }
430
431    /// Compile test for trait stability
432    #[allow(unused)]
433    #[derive(Hash)]
434    enum SimpleCustomGlyphPositioner {}
435
436    impl GlyphPositioner for SimpleCustomGlyphPositioner {
437        fn calculate_glyphs<F, S>(
438            &self,
439            _fonts: &[F],
440            _geometry: &SectionGeometry,
441            _sections: &[S],
442        ) -> Vec<SectionGlyph>
443        where
444            F: Font,
445            S: ToSectionText,
446        {
447            <_>::default()
448        }
449
450        /// Return a screen rectangle according to the requested render position and bounds
451        /// appropriate for the glyph layout.
452        fn bounds_rect(&self, _: &SectionGeometry) -> Rect {
453            Rect {
454                min: point(0.0, 0.0),
455                max: point(0.0, 0.0),
456            }
457        }
458    }
459
460    #[test]
461    fn zero_scale_glyphs() {
462        let glyphs = Layout::default_single_line()
463            .line_breaker(AnyCharLineBreaker)
464            .calculate_glyphs(
465                &*FONT_MAP,
466                &SectionGeometry::default(),
467                &[SectionText {
468                    text: "hello world",
469                    scale: 0.0.into(),
470                    ..<_>::default()
471                }],
472            );
473
474        assert!(glyphs.is_empty(), "{:?}", glyphs);
475    }
476
477    #[test]
478    fn negative_scale_glyphs() {
479        let glyphs = Layout::default_single_line()
480            .line_breaker(AnyCharLineBreaker)
481            .calculate_glyphs(
482                &*FONT_MAP,
483                &SectionGeometry::default(),
484                &[SectionText {
485                    text: "hello world",
486                    scale: PxScale::from(-20.0),
487                    ..<_>::default()
488                }],
489            );
490
491        assert!(glyphs.is_empty(), "{:?}", glyphs);
492    }
493
494    #[test]
495    fn single_line_chars_left_simple() {
496        let glyphs = Layout::default_single_line()
497            .line_breaker(AnyCharLineBreaker)
498            .calculate_glyphs(
499                &*FONT_MAP,
500                &SectionGeometry::default(),
501                &[SectionText {
502                    text: "hello world",
503                    scale: PxScale::from(20.0),
504                    ..SectionText::default()
505                }],
506            );
507
508        assert_glyph_order!(glyphs, "hello world");
509
510        assert_relative_eq!(glyphs[0].glyph.position.x, 0.0);
511        let last_glyph = &glyphs.last().unwrap().glyph;
512        assert!(
513            last_glyph.position.x > 0.0,
514            "unexpected last position {:?}",
515            last_glyph.position
516        );
517    }
518
519    #[test]
520    fn single_line_chars_right() {
521        let glyphs = Layout::default_single_line()
522            .line_breaker(AnyCharLineBreaker)
523            .h_align(HorizontalAlign::Right)
524            .calculate_glyphs(
525                &*FONT_MAP,
526                &SectionGeometry::default(),
527                &[SectionText {
528                    text: "hello world",
529                    scale: PxScale::from(20.0),
530                    ..SectionText::default()
531                }],
532            );
533
534        assert_glyph_order!(glyphs, "hello world");
535        let last_glyph = &glyphs.last().unwrap().glyph;
536        assert!(glyphs[0].glyph.position.x < last_glyph.position.x);
537        assert!(
538            last_glyph.position.x <= 0.0,
539            "unexpected last position {:?}",
540            last_glyph.position
541        );
542
543        let sfont = A_FONT.as_scaled(20.0);
544        let rightmost_x = last_glyph.position.x + sfont.h_advance(last_glyph.id);
545        assert_relative_eq!(rightmost_x, 0.0, epsilon = 1e-1);
546    }
547
548    #[test]
549    fn single_line_chars_center() {
550        let glyphs = Layout::default_single_line()
551            .line_breaker(AnyCharLineBreaker)
552            .h_align(HorizontalAlign::Center)
553            .calculate_glyphs(
554                &*FONT_MAP,
555                &SectionGeometry::default(),
556                &[SectionText {
557                    text: "hello world",
558                    scale: PxScale::from(20.0),
559                    ..SectionText::default()
560                }],
561            );
562
563        assert_glyph_order!(glyphs, "hello world");
564        assert!(
565            glyphs[0].glyph.position.x < 0.0,
566            "unexpected first glyph position {:?}",
567            glyphs[0].glyph.position
568        );
569
570        let last_glyph = &glyphs.last().unwrap().glyph;
571        assert!(
572            last_glyph.position.x > 0.0,
573            "unexpected last glyph position {:?}",
574            last_glyph.position
575        );
576
577        let leftmost_x = glyphs[0].glyph.position.x;
578        let sfont = A_FONT.as_scaled(20.0);
579        let rightmost_x = last_glyph.position.x + sfont.h_advance(last_glyph.id);
580        assert_relative_eq!(rightmost_x, -leftmost_x, epsilon = 1e-1);
581    }
582
583    #[test]
584    fn single_line_chars_left_finish_at_newline() {
585        let glyphs = Layout::default_single_line()
586            .line_breaker(AnyCharLineBreaker)
587            .calculate_glyphs(
588                &*FONT_MAP,
589                &SectionGeometry::default(),
590                &[SectionText {
591                    text: "hello\nworld",
592                    scale: PxScale::from(20.0),
593                    ..SectionText::default()
594                }],
595            );
596
597        assert_glyph_order!(glyphs, "hello");
598        assert_relative_eq!(glyphs[0].glyph.position.x, 0.0);
599        assert!(
600            glyphs[4].glyph.position.x > 0.0,
601            "unexpected last position {:?}",
602            glyphs[4].glyph.position
603        );
604    }
605
606    #[test]
607    fn wrap_word_left() {
608        let glyphs = Layout::default_single_line().calculate_glyphs(
609            &*FONT_MAP,
610            &SectionGeometry {
611                bounds: (85.0, f32::INFINITY), // should only be enough room for the 1st word
612                ..SectionGeometry::default()
613            },
614            &[SectionText {
615                text: "hello what's _happening_?",
616                scale: PxScale::from(20.0),
617                ..SectionText::default()
618            }],
619        );
620
621        assert_glyph_order!(glyphs, "hello ");
622        assert_relative_eq!(glyphs[0].glyph.position.x, 0.0);
623        let last_glyph = &glyphs.last().unwrap().glyph;
624        assert!(
625            last_glyph.position.x > 0.0,
626            "unexpected last position {:?}",
627            last_glyph.position
628        );
629
630        let glyphs = Layout::default_single_line().calculate_glyphs(
631            &*FONT_MAP,
632            &SectionGeometry {
633                bounds: (125.0, f32::INFINITY),
634                ..SectionGeometry::default()
635            },
636            &[SectionText {
637                text: "hello what's _happening_?",
638                scale: PxScale::from(20.0),
639                ..SectionText::default()
640            }],
641        );
642
643        assert_glyph_order!(glyphs, "hello what's ");
644        assert_relative_eq!(glyphs[0].glyph.position.x, 0.0);
645        let last_glyph = &glyphs.last().unwrap().glyph;
646        assert!(
647            last_glyph.position.x > 0.0,
648            "unexpected last position {:?}",
649            last_glyph.position
650        );
651    }
652
653    #[test]
654    fn single_line_limited_horizontal_room() {
655        let glyphs = Layout::default_single_line()
656            .line_breaker(AnyCharLineBreaker)
657            .calculate_glyphs(
658                &*FONT_MAP,
659                &SectionGeometry {
660                    bounds: (50.0, f32::INFINITY),
661                    ..SectionGeometry::default()
662                },
663                &[SectionText {
664                    text: "hello world",
665                    scale: PxScale::from(20.0),
666                    ..SectionText::default()
667                }],
668            );
669
670        assert_glyph_order!(glyphs, "hell");
671        assert_relative_eq!(glyphs[0].glyph.position.x, 0.0);
672    }
673
674    #[test]
675    fn wrap_layout_with_new_lines() {
676        let test_str = "Autumn moonlight\n\
677                        a worm digs silently\n\
678                        into the chestnut.";
679
680        let glyphs = Layout::default_wrap().calculate_glyphs(
681            &*FONT_MAP,
682            &SectionGeometry::default(),
683            &[SectionText {
684                text: test_str,
685                scale: PxScale::from(20.0),
686                ..SectionText::default()
687            }],
688        );
689
690        // newlines don't turn up as glyphs
691        assert_glyph_order!(
692            glyphs,
693            "Autumn moonlighta worm digs silentlyinto the chestnut."
694        );
695        assert!(
696            glyphs[16].glyph.position.y > glyphs[0].glyph.position.y,
697            "second line should be lower than first"
698        );
699        assert!(
700            glyphs[36].glyph.position.y > glyphs[16].glyph.position.y,
701            "third line should be lower than second"
702        );
703    }
704
705    #[test]
706    fn leftover_max_vmetrics() {
707        let glyphs = Layout::default_single_line().calculate_glyphs(
708            &*FONT_MAP,
709            &SectionGeometry {
710                bounds: (750.0, f32::INFINITY),
711                ..SectionGeometry::default()
712            },
713            &[
714                SectionText {
715                    text: "Autumn moonlight, ",
716                    scale: PxScale::from(30.0),
717                    ..SectionText::default()
718                },
719                SectionText {
720                    text: "a worm digs silently ",
721                    scale: PxScale::from(40.0),
722                    ..SectionText::default()
723                },
724                SectionText {
725                    text: "into the chestnut.",
726                    scale: PxScale::from(10.0),
727                    ..SectionText::default()
728                },
729            ],
730        );
731
732        for g in glyphs {
733            println!("{:?}", (g.glyph.scale, g.glyph.position));
734            // all glyphs should have the same ascent drawing position
735            let y_pos = g.glyph.position.y;
736            assert_relative_eq!(y_pos, A_FONT.as_scaled(40.0).ascent());
737        }
738    }
739
740    #[test]
741    fn eol_new_line_hard_breaks() {
742        let glyphs = Layout::default_wrap().calculate_glyphs(
743            &*FONT_MAP,
744            &SectionGeometry::default(),
745            &[
746                SectionText {
747                    text: "Autumn moonlight, \n",
748                    ..SectionText::default()
749                },
750                SectionText {
751                    text: "a worm digs silently ",
752                    ..SectionText::default()
753                },
754                SectionText {
755                    text: "\n",
756                    ..SectionText::default()
757                },
758                SectionText {
759                    text: "into the chestnut.",
760                    ..SectionText::default()
761                },
762            ],
763        );
764
765        let y_ords: HashSet<OrderedFloat<f32>> = glyphs
766            .iter()
767            .map(|g| OrderedFloat(g.glyph.position.y))
768            .collect();
769
770        println!("Y ords: {y_ords:?}");
771        assert_eq!(y_ords.len(), 3, "expected 3 distinct lines");
772
773        assert_glyph_order!(
774            glyphs,
775            "Autumn moonlight, a worm digs silently into the chestnut."
776        );
777
778        let line_2_glyph = &glyphs[18].glyph;
779        let line_3_glyph = &&glyphs[39].glyph;
780        assert_eq!(line_2_glyph.id, A_FONT.glyph_id('a'));
781        assert!(line_2_glyph.position.y > glyphs[0].glyph.position.y);
782
783        assert_eq!(line_3_glyph.id, A_FONT.glyph_id('i'));
784        assert!(line_3_glyph.position.y > line_2_glyph.position.y);
785    }
786
787    #[test]
788    fn single_line_multibyte_chars_finish_at_break() {
789        let unicode_str = "❤❤é❤❤\n❤ß❤";
790        assert_eq!(
791            unicode_str, "\u{2764}\u{2764}\u{e9}\u{2764}\u{2764}\n\u{2764}\u{df}\u{2764}",
792            "invisible char funny business",
793        );
794        assert_eq!(unicode_str.len(), 23);
795        assert_eq!(
796            xi_unicode::LineBreakIterator::new(unicode_str).find(|n| n.1),
797            Some((15, true)),
798        );
799
800        let glyphs = Layout::default_single_line().calculate_glyphs(
801            &*FONT_MAP,
802            &SectionGeometry::default(),
803            &[SectionText {
804                text: unicode_str,
805                scale: PxScale::from(20.0),
806                ..SectionText::default()
807            }],
808        );
809
810        assert_glyph_order!(glyphs, "\u{2764}\u{2764}\u{e9}\u{2764}\u{2764}");
811        assert_relative_eq!(glyphs[0].glyph.position.x, 0.0);
812        assert!(
813            glyphs[4].glyph.position.x > 0.0,
814            "unexpected last position {:?}",
815            glyphs[4].glyph.position
816        );
817    }
818
819    #[test]
820    fn no_inherent_section_break() {
821        let glyphs = Layout::default_wrap().calculate_glyphs(
822            &*FONT_MAP,
823            &SectionGeometry {
824                bounds: (50.0, f32::INFINITY),
825                ..SectionGeometry::default()
826            },
827            &[
828                SectionText {
829                    text: "The ",
830                    ..SectionText::default()
831                },
832                SectionText {
833                    text: "moon",
834                    ..SectionText::default()
835                },
836                SectionText {
837                    text: "light",
838                    ..SectionText::default()
839                },
840            ],
841        );
842
843        assert_glyph_order!(glyphs, "The moonlight");
844
845        let y_ords: HashSet<OrderedFloat<f32>> = glyphs
846            .iter()
847            .map(|g| OrderedFloat(g.glyph.position.y))
848            .collect();
849
850        assert_eq!(y_ords.len(), 2, "Y ords: {y_ords:?}");
851
852        let first_line_y = y_ords.iter().min().unwrap();
853        let second_line_y = y_ords.iter().max().unwrap();
854
855        assert_relative_eq!(glyphs[0].glyph.position.y, first_line_y);
856        assert_relative_eq!(glyphs[4].glyph.position.y, second_line_y);
857    }
858
859    #[test]
860    fn recalculate_identical() {
861        let glyphs = Layout::default().calculate_glyphs(
862            &*FONT_MAP,
863            &SectionGeometry::default(),
864            &[SectionText {
865                text: "hello world",
866                scale: PxScale::from(20.0),
867                ..SectionText::default()
868            }],
869        );
870
871        let recalc = Layout::default().recalculate_glyphs(
872            glyphs,
873            GlyphChange::Unknown,
874            &*FONT_MAP,
875            &SectionGeometry::default(),
876            &[SectionText {
877                text: "hello world",
878                scale: PxScale::from(20.0),
879                ..SectionText::default()
880            }],
881        );
882
883        assert_glyph_order!(recalc, "hello world");
884
885        assert_relative_eq!(recalc[0].glyph.position.x, 0.0);
886        let last_glyph = &recalc.last().unwrap().glyph;
887        assert!(
888            last_glyph.position.x > 0.0,
889            "unexpected last position {:?}",
890            last_glyph.position
891        );
892    }
893
894    #[test]
895    fn recalculate_position() {
896        let geometry_1 = SectionGeometry {
897            screen_position: (0.0, 0.0),
898            ..<_>::default()
899        };
900
901        let glyphs = Layout::default().calculate_glyphs(
902            &*FONT_MAP,
903            &geometry_1,
904            &[SectionText {
905                text: "hello world",
906                scale: PxScale::from(20.0),
907                font_id: FontId(0),
908            }],
909        );
910
911        let original_y = glyphs[0].glyph.position.y;
912
913        let recalc = Layout::default().recalculate_glyphs(
914            glyphs,
915            GlyphChange::Geometry(geometry_1),
916            &*FONT_MAP,
917            &SectionGeometry {
918                screen_position: (0.0, 50.0),
919                ..geometry_1
920            },
921            &[SectionText {
922                text: "hello world",
923                scale: PxScale::from(20.0),
924                ..SectionText::default()
925            }],
926        );
927
928        assert_glyph_order!(recalc, "hello world");
929
930        assert_relative_eq!(recalc[0].glyph.position.x, 0.0);
931        assert_relative_eq!(recalc[0].glyph.position.y, original_y + 50.0);
932        let last_glyph = &recalc.last().unwrap().glyph;
933        assert!(
934            last_glyph.position.x > 0.0,
935            "unexpected last position {:?}",
936            last_glyph.position
937        );
938    }
939
940    /// Chinese sentence squeezed into a vertical pipe meaning each character is on
941    /// a separate line.
942    #[test]
943    fn wrap_word_chinese() {
944        let glyphs = Layout::default().calculate_glyphs(
945            &*FONT_MAP,
946            &SectionGeometry {
947                bounds: (25.0, f32::INFINITY),
948                ..<_>::default()
949            },
950            &[SectionText {
951                text: "提高代碼執行率",
952                scale: PxScale::from(20.0),
953                font_id: FontId(1),
954            }],
955        );
956
957        assert_glyph_order!(glyphs, "提高代碼執行率", font = &*CJK_FONT);
958
959        let x_positions: HashSet<_> = glyphs
960            .iter()
961            .map(|g| OrderedFloat(g.glyph.position.x))
962            .collect();
963        assert_eq!(x_positions, std::iter::once(OrderedFloat(0.0)).collect());
964
965        let y_positions: HashSet<_> = glyphs
966            .iter()
967            .map(|g| OrderedFloat(g.glyph.position.y))
968            .collect();
969
970        assert_eq!(y_positions.len(), 7, "{y_positions:?}");
971    }
972
973    /// #130 - Respect trailing whitespace in words if directly preceding a hard break.
974    /// So right-aligned wrapped on 2 lines `Foo bar` will look different to `Foo \nbar`.
975    #[test]
976    fn include_spaces_in_layout_width_preceded_hard_break() {
977        // should wrap due to width bound
978        let glyphs_no_newline = Layout::default()
979            .h_align(HorizontalAlign::Right)
980            .calculate_glyphs(
981                &*FONT_MAP,
982                &SectionGeometry {
983                    bounds: (50.0, f32::INFINITY),
984                    ..<_>::default()
985                },
986                &[SectionText {
987                    text: "Foo bar",
988                    ..<_>::default()
989                }],
990            );
991
992        let y_positions: HashSet<_> = glyphs_no_newline
993            .iter()
994            .map(|g| OrderedFloat(g.glyph.position.y))
995            .collect();
996        assert_eq!(y_positions.len(), 2, "{y_positions:?}");
997
998        // explicit wrap
999        let glyphs_newline = Layout::default()
1000            .h_align(HorizontalAlign::Right)
1001            .calculate_glyphs(
1002                &*FONT_MAP,
1003                &SectionGeometry {
1004                    bounds: (50.0, f32::INFINITY),
1005                    ..<_>::default()
1006                },
1007                &[SectionText {
1008                    text: "Foo \nbar",
1009                    ..<_>::default()
1010                }],
1011            );
1012
1013        let y_positions: HashSet<_> = glyphs_newline
1014            .iter()
1015            .map(|g| OrderedFloat(g.glyph.position.y))
1016            .collect();
1017        assert_eq!(y_positions.len(), 2, "{y_positions:?}");
1018
1019        // explicit wrap should include the space in the layout width,
1020        // so the explicit newline `F` should be to the left of the no_newline `F`.
1021        let newline_f = &glyphs_newline[0];
1022        let no_newline_f = &glyphs_no_newline[0];
1023        assert!(
1024            newline_f.glyph.position.x < no_newline_f.glyph.position.x,
1025            "explicit newline `F` ({}) should be 1 space to the left of no-newline `F` ({})",
1026            newline_f.glyph.position.x,
1027            no_newline_f.glyph.position.x,
1028        );
1029    }
1030
1031    /// #130 - Respect trailing whitespace in words if directly preceding end-of-glyphs.
1032    /// So right-aligned `Foo ` will look different to `Foo`.
1033    #[test]
1034    fn include_spaces_in_layout_width_preceded_end() {
1035        let glyphs_no_newline = Layout::default()
1036            .h_align(HorizontalAlign::Right)
1037            .calculate_glyphs(
1038                &*FONT_MAP,
1039                &<_>::default(),
1040                &[SectionText {
1041                    text: "Foo",
1042                    ..<_>::default()
1043                }],
1044            );
1045
1046        let glyphs_space = Layout::default()
1047            .h_align(HorizontalAlign::Right)
1048            .calculate_glyphs(
1049                &*FONT_MAP,
1050                &<_>::default(),
1051                &[SectionText {
1052                    text: "Foo   ",
1053                    ..<_>::default()
1054                }],
1055            );
1056
1057        let space_f = &glyphs_space[0];
1058        let no_space_f = &glyphs_no_newline[0];
1059        assert!(
1060            space_f.glyph.position.x < no_space_f.glyph.position.x,
1061            "with-space `F` ({}) should be 3 spaces to the left of no-space `F` ({})",
1062            space_f.glyph.position.x,
1063            no_space_f.glyph.position.x,
1064        );
1065    }
1066}