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