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