Skip to main content

i_slint_core/
textlayout.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! module for basic text layout
5//!
6//! The basic algorithm for breaking text into multiple lines:
7//! 1. First we determine the boundaries for text shaping. As shaping happens based on a single font and we know that different fonts cater different
8//!    writing systems, we split up the text into chunks that maximize our chances of finding a font that covers all glyphs in the chunk. This way for
9//!    example arabic text can be covered by a font that has excellent arabic coverage while latin text is rendered using a different font.
10//!    Shaping boundaries are always also grapheme boundaries.
11//! 2. Then we shape the text at shaping boundaries, to determine the metrics of glyphs and glyph clusters
12//! 3. Loop over all glyph clusters as well as the line break opportunities produced by the unicode line break algorithm:
13//!    Sum up the width of all glyph clusters until the next line break opportunity (encapsulated in FragmentIterator), record separately the width of
14//!    trailing space within the fragment.
15//!    ```text
16//!    If the width of the current line (including trailing whitespace) and the new fragment of glyph clusters (without trailing whitespace) is less or
17//!    equal to the available width:
18//!        Add fragment of glyph clusters to the current line
19//!    Else:
20//!        Emit current line as new line
21//!    If encountering a mandatory line break opportunity:
22//!        Emit current line as new line
23//!    ```
24
25use alloc::vec::Vec;
26
27use euclid::num::{One, Zero};
28
29use crate::items::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap};
30
31#[cfg(feature = "unicode-linebreak")]
32mod linebreak_unicode;
33#[cfg(feature = "unicode-linebreak")]
34use linebreak_unicode::{BreakOpportunity, LineBreakIterator};
35
36#[cfg(not(feature = "unicode-linebreak"))]
37mod linebreak_simple;
38#[cfg(not(feature = "unicode-linebreak"))]
39use linebreak_simple::{BreakOpportunity, LineBreakIterator};
40
41mod fragments;
42mod glyphclusters;
43mod shaping;
44#[cfg(feature = "shared-parley")]
45/// cbindgen:ignore
46pub mod sharedparley;
47use shaping::ShapeBuffer;
48pub use shaping::{AbstractFont, FontMetrics, Glyph, TextShaper};
49
50mod linebreaker;
51pub use linebreaker::TextLine;
52
53pub use linebreaker::TextLineBreaker;
54
55pub struct TextLayout<'a, Font: AbstractFont> {
56    pub font: &'a Font,
57    pub letter_spacing: Option<<Font as TextShaper>::Length>,
58}
59
60impl<Font: AbstractFont> TextLayout<'_, Font> {
61    // Measures the size of the given text when rendered with the specified font and optionally constrained
62    // by the provided `max_width`.
63    // Returns a tuple of the width of the longest line as well as height of all lines.
64    pub fn text_size(
65        &self,
66        text: &str,
67        max_width: Option<Font::Length>,
68        text_wrap: TextWrap,
69    ) -> (Font::Length, Font::Length)
70    where
71        Font::Length: core::fmt::Debug,
72    {
73        let mut max_line_width = Font::Length::zero();
74        let mut line_count: i16 = 0;
75        let shape_buffer = ShapeBuffer::new(self, text);
76
77        for line in TextLineBreaker::<Font>::new(text, &shape_buffer, max_width, None, text_wrap) {
78            max_line_width = euclid::approxord::max(max_line_width, line.text_width);
79            line_count += 1;
80        }
81
82        (max_line_width, self.font.height() * line_count.into())
83    }
84}
85
86pub struct PositionedGlyph<Length> {
87    pub x: Length,
88    pub y: Length,
89    pub advance: Length,
90    pub glyph_id: core::num::NonZeroU16,
91    pub text_byte_offset: usize,
92}
93
94pub struct TextParagraphLayout<'a, Font: AbstractFont> {
95    pub string: &'a str,
96    pub layout: TextLayout<'a, Font>,
97    pub max_width: Font::Length,
98    pub max_height: Font::Length,
99    pub horizontal_alignment: TextHorizontalAlignment,
100    pub vertical_alignment: TextVerticalAlignment,
101    pub wrap: TextWrap,
102    pub overflow: TextOverflow,
103    pub single_line: bool,
104}
105
106impl<Font: AbstractFont> TextParagraphLayout<'_, Font> {
107    /// Layout the given string in lines, and call the `layout_line` callback with the line to draw at position y.
108    /// The signature of the `layout_line` function is: `(glyph_iterator, line_x, line_y, text_line, selection)`.
109    /// Returns the baseline y coordinate as Ok, or the break value if `line_callback` returns `core::ops::ControlFlow::Break`.
110    pub fn layout_lines<R>(
111        &self,
112        mut line_callback: impl FnMut(
113            &mut dyn Iterator<Item = PositionedGlyph<Font::Length>>,
114            Font::Length,
115            Font::Length,
116            &TextLine<Font::Length>,
117            Option<core::ops::Range<Font::Length>>,
118        ) -> core::ops::ControlFlow<R>,
119        selection: Option<core::ops::Range<usize>>,
120    ) -> Result<Font::Length, R> {
121        let wrap = self.wrap != TextWrap::NoWrap;
122        let elide = self.overflow == TextOverflow::Elide;
123        let elide_glyph = if elide {
124            self.layout.font.glyph_for_char('…').filter(|glyph| glyph.glyph_id.is_some())
125        } else {
126            None
127        };
128        let elide_width = elide_glyph.as_ref().map_or(Font::Length::zero(), |g| g.advance);
129        let max_width_without_elision = self.max_width - elide_width;
130
131        let shape_buffer = ShapeBuffer::new(&self.layout, self.string);
132
133        let new_line_break_iter = || {
134            TextLineBreaker::<Font>::new(
135                self.string,
136                &shape_buffer,
137                if wrap { Some(self.max_width) } else { None },
138                if elide { Some(self.layout.font.max_lines(self.max_height)) } else { None },
139                self.wrap,
140            )
141        };
142        let mut text_lines = None;
143
144        let mut text_height = || {
145            if self.single_line {
146                self.layout.font.height()
147            } else {
148                text_lines = Some(new_line_break_iter().collect::<Vec<_>>());
149                self.layout.font.height() * (text_lines.as_ref().unwrap().len() as i16).into()
150            }
151        };
152
153        let two = Font::LengthPrimitive::one() + Font::LengthPrimitive::one();
154
155        let baseline_y = match self.vertical_alignment {
156            TextVerticalAlignment::Top => Font::Length::zero(),
157            TextVerticalAlignment::Center => self.max_height / two - text_height() / two,
158            TextVerticalAlignment::Bottom => self.max_height - text_height(),
159        };
160
161        let mut y = baseline_y;
162
163        let mut process_line = |line: &TextLine<Font::Length>, glyphs: &[Glyph<Font::Length>]| {
164            let elide_long_line =
165                elide && (self.single_line || !wrap) && line.text_width > self.max_width;
166            let elide_last_line = elide
167                && line.glyph_range.end < glyphs.len()
168                && y + self.layout.font.height() * two > self.max_height;
169
170            let text_width = || {
171                if elide_long_line || elide_last_line {
172                    let mut text_width = Font::Length::zero();
173                    for glyph in &glyphs[line.glyph_range.clone()] {
174                        if text_width + glyph.advance > max_width_without_elision {
175                            break;
176                        }
177                        text_width += glyph.advance;
178                    }
179                    return text_width + elide_width;
180                }
181                euclid::approxord::min(self.max_width, line.text_width)
182            };
183
184            let x = match self.horizontal_alignment {
185                TextHorizontalAlignment::Start | TextHorizontalAlignment::Left => {
186                    Font::Length::zero()
187                }
188                TextHorizontalAlignment::Center => self.max_width / two - text_width() / two,
189                TextHorizontalAlignment::End | TextHorizontalAlignment::Right => {
190                    self.max_width - text_width()
191                }
192            };
193
194            let mut elide_glyph = elide_glyph.as_ref();
195
196            let selection = selection
197                .as_ref()
198                .filter(|selection| {
199                    line.byte_range.start < selection.end && selection.start < line.byte_range.end
200                })
201                .map(|selection| {
202                    let mut begin = Font::Length::zero();
203                    let mut end = Font::Length::zero();
204                    for glyph in glyphs[line.glyph_range.clone()].iter() {
205                        if glyph.text_byte_offset < selection.start {
206                            begin += glyph.advance;
207                        }
208                        if glyph.text_byte_offset >= selection.end {
209                            break;
210                        }
211                        end += glyph.advance;
212                    }
213                    begin..end
214                });
215
216            let glyph_it = glyphs[line.glyph_range.clone()].iter();
217            let mut glyph_x = Font::Length::zero();
218            let mut positioned_glyph_it = glyph_it.enumerate().filter_map(|(index, glyph)| {
219                // TODO: cut off at grapheme boundaries
220                if glyph_x > self.max_width {
221                    return None;
222                }
223                let elide_long_line = (elide_long_line || elide_last_line)
224                    && x + glyph_x + glyph.advance > max_width_without_elision;
225                let elide_last_line =
226                    elide_last_line && line.glyph_range.start + index == line.glyph_range.end - 1;
227                if elide_long_line || elide_last_line {
228                    if let Some(elide_glyph) = elide_glyph.take() {
229                        let x = glyph_x;
230                        glyph_x += elide_glyph.advance;
231                        return Some(PositionedGlyph {
232                            x,
233                            y: Font::Length::zero(),
234                            advance: elide_glyph.advance,
235                            glyph_id: elide_glyph.glyph_id.unwrap(), // checked earlier when initializing elide_glyph
236                            text_byte_offset: glyph.text_byte_offset,
237                        });
238                    } else {
239                        return None;
240                    }
241                }
242                let x = glyph_x;
243                glyph_x += glyph.advance;
244
245                glyph.glyph_id.map(|existing_glyph_id| PositionedGlyph {
246                    x,
247                    y: Font::Length::zero(),
248                    advance: glyph.advance,
249                    glyph_id: existing_glyph_id,
250                    text_byte_offset: glyph.text_byte_offset,
251                })
252            });
253
254            if let core::ops::ControlFlow::Break(break_val) =
255                line_callback(&mut positioned_glyph_it, x, y, line, selection)
256            {
257                return core::ops::ControlFlow::Break(break_val);
258            }
259            y += self.layout.font.height();
260
261            core::ops::ControlFlow::Continue(())
262        };
263
264        if let Some(lines_vec) = text_lines.take() {
265            for line in lines_vec {
266                if let core::ops::ControlFlow::Break(break_val) =
267                    process_line(&line, &shape_buffer.glyphs)
268                {
269                    return Err(break_val);
270                }
271            }
272        } else {
273            for line in new_line_break_iter() {
274                if let core::ops::ControlFlow::Break(break_val) =
275                    process_line(&line, &shape_buffer.glyphs)
276                {
277                    return Err(break_val);
278                }
279            }
280        }
281
282        Ok(baseline_y)
283    }
284
285    /// Returns the leading edge of the glyph at the given byte offset
286    pub fn cursor_pos_for_byte_offset(&self, byte_offset: usize) -> (Font::Length, Font::Length) {
287        let mut last_glyph_right_edge = Font::Length::zero();
288        let mut last_line_y = Font::Length::zero();
289
290        match self.layout_lines(
291            |glyphs, line_x, line_y, line, _| {
292                last_glyph_right_edge = euclid::approxord::min(
293                    self.max_width,
294                    line_x + line.width_including_trailing_whitespace(),
295                );
296                last_line_y = line_y;
297                if byte_offset >= line.byte_range.end + line.trailing_whitespace_bytes {
298                    return core::ops::ControlFlow::Continue(());
299                }
300
301                for positioned_glyph in glyphs {
302                    if positioned_glyph.text_byte_offset == byte_offset {
303                        return core::ops::ControlFlow::Break((
304                            euclid::approxord::min(self.max_width, line_x + positioned_glyph.x),
305                            last_line_y,
306                        ));
307                    }
308                }
309
310                core::ops::ControlFlow::Break((last_glyph_right_edge, last_line_y))
311            },
312            None,
313        ) {
314            Ok(_) => (last_glyph_right_edge, last_line_y),
315            Err(position) => position,
316        }
317    }
318
319    /// Returns the bytes offset for the given position
320    pub fn byte_offset_for_position(&self, (pos_x, pos_y): (Font::Length, Font::Length)) -> usize {
321        let mut byte_offset = 0;
322        let two = Font::LengthPrimitive::one() + Font::LengthPrimitive::one();
323
324        match self.layout_lines(
325            |glyphs, line_x, line_y, line, _| {
326                if pos_y >= line_y + self.layout.font.height() {
327                    byte_offset = line.byte_range.end;
328                    return core::ops::ControlFlow::Continue(());
329                }
330
331                if line.is_empty() {
332                    return core::ops::ControlFlow::Break(line.byte_range.start);
333                }
334
335                while let Some(positioned_glyph) = glyphs.next() {
336                    if pos_x >= line_x + positioned_glyph.x
337                        && pos_x <= line_x + positioned_glyph.x + positioned_glyph.advance
338                    {
339                        if pos_x < line_x + positioned_glyph.x + positioned_glyph.advance / two {
340                            return core::ops::ControlFlow::Break(
341                                positioned_glyph.text_byte_offset,
342                            );
343                        } else if let Some(next_glyph) = glyphs.next() {
344                            return core::ops::ControlFlow::Break(next_glyph.text_byte_offset);
345                        }
346                    }
347                }
348
349                core::ops::ControlFlow::Break(line.byte_range.end)
350            },
351            None,
352        ) {
353            Ok(_) => byte_offset,
354            Err(position) => position,
355        }
356    }
357}
358
359#[test]
360fn test_no_linebreak_opportunity_at_eot() {
361    let mut it = LineBreakIterator::new("Hello World");
362    assert_eq!(it.next(), Some((6, BreakOpportunity::Allowed)));
363    assert_eq!(it.next(), None);
364}
365
366// All glyphs are 10 pixels wide, break on ascii rules
367#[cfg(test)]
368pub struct FixedTestFont;
369
370#[cfg(test)]
371impl TextShaper for FixedTestFont {
372    type LengthPrimitive = f32;
373    type Length = f32;
374    fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
375        &self,
376        text: &str,
377        glyphs: &mut GlyphStorage,
378    ) {
379        let glyph_iter = text.char_indices().map(|(byte_offset, char)| {
380            let mut utf16_buf = [0; 2];
381            let utf16_char_as_glyph_id = char.encode_utf16(&mut utf16_buf)[0];
382
383            Glyph {
384                offset_x: 0.,
385                offset_y: 0.,
386                glyph_id: core::num::NonZeroU16::new(utf16_char_as_glyph_id),
387                advance: 10.,
388                text_byte_offset: byte_offset,
389            }
390        });
391        glyphs.extend(glyph_iter);
392    }
393
394    fn glyph_for_char(&self, ch: char) -> Option<Glyph<f32>> {
395        let mut utf16_buf = [0; 2];
396        let utf16_char_as_glyph_id = ch.encode_utf16(&mut utf16_buf)[0];
397
398        Glyph {
399            offset_x: 0.,
400            offset_y: 0.,
401            glyph_id: core::num::NonZeroU16::new(utf16_char_as_glyph_id),
402            advance: 10.,
403            text_byte_offset: 0,
404        }
405        .into()
406    }
407
408    fn max_lines(&self, max_height: f32) -> usize {
409        let height = self.ascent() - self.descent();
410        (max_height / height).floor() as _
411    }
412}
413
414#[cfg(test)]
415impl FontMetrics<f32> for FixedTestFont {
416    fn ascent(&self) -> f32 {
417        5.
418    }
419
420    fn descent(&self) -> f32 {
421        -5.
422    }
423
424    fn x_height(&self) -> f32 {
425        3.
426    }
427
428    fn cap_height(&self) -> f32 {
429        4.
430    }
431}
432
433#[test]
434fn test_elision() {
435    let font = FixedTestFont;
436    let text = "This is a longer piece of text";
437
438    let mut lines = Vec::new();
439
440    let paragraph = TextParagraphLayout {
441        string: text,
442        layout: TextLayout { font: &font, letter_spacing: None },
443        max_width: 13. * 10.,
444        max_height: 10.,
445        horizontal_alignment: TextHorizontalAlignment::Left,
446        vertical_alignment: TextVerticalAlignment::Top,
447        wrap: TextWrap::NoWrap,
448        overflow: TextOverflow::Elide,
449        single_line: true,
450    };
451    paragraph
452        .layout_lines::<()>(
453            |glyphs, _, _, _, _| {
454                lines.push(
455                    glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
456                );
457                core::ops::ControlFlow::Continue(())
458            },
459            None,
460        )
461        .unwrap();
462
463    assert_eq!(lines.len(), 1);
464    let rendered_text = lines[0]
465        .iter()
466        .flat_map(|glyph_id| {
467            core::char::decode_utf16(core::iter::once(glyph_id.get()))
468                .map(|r| r.unwrap())
469                .collect::<Vec<char>>()
470        })
471        .collect::<std::string::String>();
472    debug_assert_eq!(rendered_text, "This is a lo…")
473}
474
475#[test]
476fn test_exact_fit() {
477    let font = FixedTestFont;
478    let text = "Fits";
479
480    let mut lines = Vec::new();
481
482    let paragraph = TextParagraphLayout {
483        string: text,
484        layout: TextLayout { font: &font, letter_spacing: None },
485        max_width: 4. * 10.,
486        max_height: 10.,
487        horizontal_alignment: TextHorizontalAlignment::Left,
488        vertical_alignment: TextVerticalAlignment::Top,
489        wrap: TextWrap::NoWrap,
490        overflow: TextOverflow::Elide,
491        single_line: true,
492    };
493    paragraph
494        .layout_lines::<()>(
495            |glyphs, _, _, _, _| {
496                lines.push(
497                    glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
498                );
499                core::ops::ControlFlow::Continue(())
500            },
501            None,
502        )
503        .unwrap();
504
505    assert_eq!(lines.len(), 1);
506    let rendered_text = lines[0]
507        .iter()
508        .flat_map(|glyph_id| {
509            core::char::decode_utf16(core::iter::once(glyph_id.get()))
510                .map(|r| r.unwrap())
511                .collect::<Vec<char>>()
512        })
513        .collect::<std::string::String>();
514    debug_assert_eq!(rendered_text, "Fits")
515}
516
517#[test]
518fn test_no_line_separators_characters_rendered() {
519    let font = FixedTestFont;
520    let text = "Hello\nWorld\n";
521
522    let mut lines = Vec::new();
523
524    let paragraph = TextParagraphLayout {
525        string: text,
526        layout: TextLayout { font: &font, letter_spacing: None },
527        max_width: 13. * 10.,
528        max_height: 10.,
529        horizontal_alignment: TextHorizontalAlignment::Left,
530        vertical_alignment: TextVerticalAlignment::Top,
531        wrap: TextWrap::NoWrap,
532        overflow: TextOverflow::Clip,
533        single_line: true,
534    };
535    paragraph
536        .layout_lines::<()>(
537            |glyphs, _, _, _, _| {
538                lines.push(
539                    glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
540                );
541                core::ops::ControlFlow::Continue(())
542            },
543            None,
544        )
545        .unwrap();
546
547    assert_eq!(lines.len(), 2);
548    let rendered_text = lines
549        .iter()
550        .map(|glyphs_per_line| {
551            glyphs_per_line
552                .iter()
553                .flat_map(|glyph_id| {
554                    core::char::decode_utf16(core::iter::once(glyph_id.get()))
555                        .map(|r| r.unwrap())
556                        .collect::<Vec<char>>()
557                })
558                .collect::<std::string::String>()
559        })
560        .collect::<Vec<_>>();
561    debug_assert_eq!(rendered_text, std::vec!["Hello", "World"]);
562}
563
564#[test]
565fn test_cursor_position() {
566    let font = FixedTestFont;
567    let text = "Hello                    World";
568
569    let paragraph = TextParagraphLayout {
570        string: text,
571        layout: TextLayout { font: &font, letter_spacing: None },
572        max_width: 10. * 10.,
573        max_height: 10.,
574        horizontal_alignment: TextHorizontalAlignment::Left,
575        vertical_alignment: TextVerticalAlignment::Top,
576        wrap: TextWrap::WordWrap,
577        overflow: TextOverflow::Clip,
578        single_line: false,
579    };
580
581    assert_eq!(paragraph.cursor_pos_for_byte_offset(0), (0., 0.));
582
583    let e_offset = text
584        .char_indices()
585        .find_map(|(offset, ch)| if ch == 'e' { Some(offset) } else { None })
586        .unwrap();
587    assert_eq!(paragraph.cursor_pos_for_byte_offset(e_offset), (10., 0.));
588
589    let w_offset = text
590        .char_indices()
591        .find_map(|(offset, ch)| if ch == 'W' { Some(offset) } else { None })
592        .unwrap();
593    assert_eq!(paragraph.cursor_pos_for_byte_offset(w_offset + 1), (10., 10.));
594
595    assert_eq!(paragraph.cursor_pos_for_byte_offset(text.len()), (10. * 5., 10.));
596
597    let first_space_offset =
598        text.char_indices().find_map(|(offset, ch)| ch.is_whitespace().then_some(offset)).unwrap();
599    assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset), (5. * 10., 0.));
600    assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset + 15), (10. * 10., 0.));
601    assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset + 16), (10. * 10., 0.));
602}
603
604#[test]
605fn test_cursor_position_with_newline() {
606    let font = FixedTestFont;
607    let text = "Hello\nWorld";
608
609    let paragraph = TextParagraphLayout {
610        string: text,
611        layout: TextLayout { font: &font, letter_spacing: None },
612        max_width: 100. * 10.,
613        max_height: 10.,
614        horizontal_alignment: TextHorizontalAlignment::Left,
615        vertical_alignment: TextVerticalAlignment::Top,
616        wrap: TextWrap::WordWrap,
617        overflow: TextOverflow::Clip,
618        single_line: false,
619    };
620
621    assert_eq!(paragraph.cursor_pos_for_byte_offset(5), (5. * 10., 0.));
622}
623
624#[test]
625fn byte_offset_for_empty_line() {
626    let font = FixedTestFont;
627    let text = "Hello\n\nWorld";
628
629    let paragraph = TextParagraphLayout {
630        string: text,
631        layout: TextLayout { font: &font, letter_spacing: None },
632        max_width: 100. * 10.,
633        max_height: 10.,
634        horizontal_alignment: TextHorizontalAlignment::Left,
635        vertical_alignment: TextVerticalAlignment::Top,
636        wrap: TextWrap::WordWrap,
637        overflow: TextOverflow::Clip,
638        single_line: false,
639    };
640
641    assert_eq!(paragraph.byte_offset_for_position((0., 10.)), 6);
642}
643
644#[test]
645fn test_byte_offset() {
646    let font = FixedTestFont;
647    let text = "Hello                    World";
648    let mut end_helper_text = std::string::String::from(text);
649    end_helper_text.push('!');
650
651    let paragraph = TextParagraphLayout {
652        string: text,
653        layout: TextLayout { font: &font, letter_spacing: None },
654        max_width: 10. * 10.,
655        max_height: 10.,
656        horizontal_alignment: TextHorizontalAlignment::Left,
657        vertical_alignment: TextVerticalAlignment::Top,
658        wrap: TextWrap::WordWrap,
659        overflow: TextOverflow::Clip,
660        single_line: false,
661    };
662
663    assert_eq!(paragraph.byte_offset_for_position((0., 0.)), 0);
664
665    let e_offset = text
666        .char_indices()
667        .find_map(|(offset, ch)| if ch == 'e' { Some(offset) } else { None })
668        .unwrap();
669
670    assert_eq!(paragraph.byte_offset_for_position((14., 0.)), e_offset);
671
672    let l_offset = text
673        .char_indices()
674        .find_map(|(offset, ch)| if ch == 'l' { Some(offset) } else { None })
675        .unwrap();
676    assert_eq!(paragraph.byte_offset_for_position((15., 0.)), l_offset);
677
678    let w_offset = text
679        .char_indices()
680        .find_map(|(offset, ch)| if ch == 'W' { Some(offset) } else { None })
681        .unwrap();
682
683    assert_eq!(paragraph.byte_offset_for_position((10., 10.)), w_offset + 1);
684
685    let o_offset = text
686        .char_indices()
687        .rev()
688        .find_map(|(offset, ch)| if ch == 'o' { Some(offset) } else { None })
689        .unwrap();
690
691    assert_eq!(paragraph.byte_offset_for_position((15., 10.)), o_offset + 1);
692
693    let d_offset = text
694        .char_indices()
695        .rev()
696        .find_map(|(offset, ch)| if ch == 'd' { Some(offset) } else { None })
697        .unwrap();
698
699    assert_eq!(paragraph.byte_offset_for_position((40., 10.)), d_offset);
700
701    let end_offset = end_helper_text
702        .char_indices()
703        .rev()
704        .find_map(|(offset, ch)| if ch == '!' { Some(offset) } else { None })
705        .unwrap();
706
707    assert_eq!(paragraph.byte_offset_for_position((45., 10.)), end_offset);
708    assert_eq!(paragraph.byte_offset_for_position((0., 20.)), end_offset);
709}