1use alloc::vec::Vec;
27
28use euclid::num::{One, Zero};
29
30use crate::items::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap};
31
32pub 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")]
52pub 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 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 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).max(1)) } else { None },
151 self.wrap,
152 )
153 };
154 let mut text_lines = None;
155
156 let mut text_height = || {
157 if self.single_line {
158 self.layout.font.height()
159 } else {
160 text_lines = Some(new_line_break_iter().collect::<Vec<_>>());
161 self.layout.font.height() * (text_lines.as_ref().unwrap().len() as i16).into()
162 }
163 };
164
165 let two = Font::LengthPrimitive::one() + Font::LengthPrimitive::one();
166
167 let baseline_y = match self.vertical_alignment {
168 TextVerticalAlignment::Top => Font::Length::zero(),
169 TextVerticalAlignment::Center => self.max_height / two - text_height() / two,
170 TextVerticalAlignment::Bottom => self.max_height - text_height(),
171 };
172
173 let mut y = baseline_y;
174
175 let mut process_line = |line: &TextLine<Font::Length>, glyphs: &[Glyph<Font::Length>]| {
176 let elide_long_line =
177 elide && (self.single_line || !wrap) && line.text_width > self.max_width;
178 let elide_last_line = elide
179 && line.glyph_range.end < glyphs.len()
180 && y + self.layout.font.height() * two > self.max_height;
181
182 let glyph_range = if elide_last_line {
188 let trailing_whitespace_glyphs = glyphs[line.glyph_range.clone()]
189 .iter()
190 .rev()
191 .take_while(|glyph| glyph.text_byte_offset >= line.byte_range.end)
192 .count();
193 line.glyph_range.start..line.glyph_range.end - trailing_whitespace_glyphs
194 } else {
195 line.glyph_range.clone()
196 };
197
198 let text_width = || {
199 if elide_long_line || elide_last_line {
200 let mut text_width = Font::Length::zero();
201 for glyph in &glyphs[glyph_range.clone()] {
202 if text_width + glyph.advance > max_width_without_elision {
203 break;
204 }
205 text_width += glyph.advance;
206 }
207 return text_width + elide_width;
208 }
209 euclid::approxord::min(self.max_width, line.text_width)
210 };
211
212 let x = match self.horizontal_alignment {
213 TextHorizontalAlignment::Start | TextHorizontalAlignment::Left => {
214 Font::Length::zero()
215 }
216 TextHorizontalAlignment::Center => self.max_width / two - text_width() / two,
217 TextHorizontalAlignment::End | TextHorizontalAlignment::Right => {
218 self.max_width - text_width()
219 }
220 };
221
222 let mut elide_glyph = elide_glyph.as_ref();
223
224 let selection = selection
225 .as_ref()
226 .filter(|selection| {
227 line.byte_range.start < selection.end && selection.start < line.byte_range.end
228 })
229 .map(|selection| {
230 let mut begin = Font::Length::zero();
231 let mut end = Font::Length::zero();
232 for glyph in glyphs[line.glyph_range.clone()].iter() {
233 if glyph.text_byte_offset < selection.start {
234 begin += glyph.advance;
235 }
236 if glyph.text_byte_offset >= selection.end {
237 break;
238 }
239 end += glyph.advance;
240 }
241 begin..end
242 });
243
244 let glyph_it = glyphs[glyph_range.clone()].iter();
245 let mut glyph_x = Font::Length::zero();
246 let mut positioned_glyph_it = glyph_it.enumerate().flat_map(|(index, glyph)| {
250 let mut output: [Option<PositionedGlyph<Font::Length>>; 2] = [None, None];
251 if glyph_x > self.max_width {
253 return output.into_iter().flatten();
254 }
255 let elide_long_line = (elide_long_line || elide_last_line)
258 && x + glyph_x + glyph.advance > max_width_without_elision;
259 if elide_long_line {
260 if let Some(elide_glyph) = elide_glyph.take() {
261 let x = glyph_x;
262 glyph_x += elide_glyph.advance;
263 output[0] = Some(PositionedGlyph {
264 x,
265 y: Font::Length::zero(),
266 advance: elide_glyph.advance,
267 glyph_id: elide_glyph.glyph_id.unwrap(), text_byte_offset: glyph.text_byte_offset,
269 });
270 }
271 return output.into_iter().flatten();
272 }
273
274 let glyph_pos = glyph_x;
275 glyph_x += glyph.advance;
276 output[0] = glyph.glyph_id.map(|existing_glyph_id| PositionedGlyph {
277 x: glyph_pos,
278 y: Font::Length::zero(),
279 advance: glyph.advance,
280 glyph_id: existing_glyph_id,
281 text_byte_offset: glyph.text_byte_offset,
282 });
283
284 let last_glyph = glyph_range.start + index == glyph_range.end - 1;
288 if elide_last_line
289 && last_glyph
290 && let Some(elide_glyph) = elide_glyph.take()
291 {
292 let x = glyph_x;
293 glyph_x += elide_glyph.advance;
294 output[1] = Some(PositionedGlyph {
295 x,
296 y: Font::Length::zero(),
297 advance: elide_glyph.advance,
298 glyph_id: elide_glyph.glyph_id.unwrap(), text_byte_offset: glyph.text_byte_offset,
300 });
301 }
302
303 output.into_iter().flatten()
304 });
305
306 if let core::ops::ControlFlow::Break(break_val) =
307 line_callback(&mut positioned_glyph_it, x, y, line, selection)
308 {
309 return core::ops::ControlFlow::Break(break_val);
310 }
311 y += self.layout.font.height();
312
313 core::ops::ControlFlow::Continue(())
314 };
315
316 if let Some(lines_vec) = text_lines.take() {
317 for line in lines_vec {
318 if let core::ops::ControlFlow::Break(break_val) =
319 process_line(&line, &shape_buffer.glyphs)
320 {
321 return Err(break_val);
322 }
323 }
324 } else {
325 for line in new_line_break_iter() {
326 if let core::ops::ControlFlow::Break(break_val) =
327 process_line(&line, &shape_buffer.glyphs)
328 {
329 return Err(break_val);
330 }
331 }
332 }
333
334 Ok(baseline_y)
335 }
336
337 pub fn cursor_pos_for_byte_offset(&self, byte_offset: usize) -> (Font::Length, Font::Length) {
339 let mut last_glyph_right_edge = Font::Length::zero();
340 let mut last_line_y = Font::Length::zero();
341
342 match self.layout_lines(
343 |glyphs, line_x, line_y, line, _| {
344 last_glyph_right_edge = euclid::approxord::min(
345 self.max_width,
346 line_x + line.width_including_trailing_whitespace(),
347 );
348 last_line_y = line_y;
349 if byte_offset >= line.byte_range.end + line.trailing_whitespace_bytes {
350 return core::ops::ControlFlow::Continue(());
351 }
352
353 for positioned_glyph in glyphs {
354 if positioned_glyph.text_byte_offset == byte_offset {
355 return core::ops::ControlFlow::Break((
356 euclid::approxord::min(self.max_width, line_x + positioned_glyph.x),
357 last_line_y,
358 ));
359 }
360 }
361
362 core::ops::ControlFlow::Break((last_glyph_right_edge, last_line_y))
363 },
364 None,
365 ) {
366 Ok(_) => (last_glyph_right_edge, last_line_y),
367 Err(position) => position,
368 }
369 }
370
371 pub fn byte_offset_for_position(&self, (pos_x, pos_y): (Font::Length, Font::Length)) -> usize {
373 let mut byte_offset = 0;
374 let two = Font::LengthPrimitive::one() + Font::LengthPrimitive::one();
375
376 match self.layout_lines(
377 |glyphs, line_x, line_y, line, _| {
378 if pos_y >= line_y + self.layout.font.height() {
379 byte_offset = line.byte_range.end;
380 return core::ops::ControlFlow::Continue(());
381 }
382
383 if line.is_empty() {
384 return core::ops::ControlFlow::Break(line.byte_range.start);
385 }
386
387 while let Some(positioned_glyph) = glyphs.next() {
388 if pos_x >= line_x + positioned_glyph.x
389 && pos_x <= line_x + positioned_glyph.x + positioned_glyph.advance
390 {
391 if pos_x < line_x + positioned_glyph.x + positioned_glyph.advance / two {
392 return core::ops::ControlFlow::Break(
393 positioned_glyph.text_byte_offset,
394 );
395 } else if let Some(next_glyph) = glyphs.next() {
396 return core::ops::ControlFlow::Break(next_glyph.text_byte_offset);
397 }
398 }
399 }
400
401 core::ops::ControlFlow::Break(line.byte_range.end)
402 },
403 None,
404 ) {
405 Ok(_) => byte_offset,
406 Err(position) => position,
407 }
408 }
409}
410
411#[test]
412fn test_no_linebreak_opportunity_at_eot() {
413 let mut it = LineBreakIterator::new("Hello World");
414 assert_eq!(it.next(), Some((6, BreakOpportunity::Allowed)));
415 assert_eq!(it.next(), None);
416}
417
418#[cfg(test)]
420pub struct FixedTestFont;
421
422#[cfg(test)]
423impl TextShaper for FixedTestFont {
424 type LengthPrimitive = f32;
425 type Length = f32;
426 fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
427 &self,
428 text: &str,
429 glyphs: &mut GlyphStorage,
430 ) {
431 let glyph_iter = text.char_indices().map(|(byte_offset, char)| {
432 let mut utf16_buf = [0; 2];
433 let utf16_char_as_glyph_id = char.encode_utf16(&mut utf16_buf)[0];
434
435 Glyph {
436 offset_x: 0.,
437 offset_y: 0.,
438 glyph_id: core::num::NonZeroU16::new(utf16_char_as_glyph_id),
439 advance: 10.,
440 text_byte_offset: byte_offset,
441 }
442 });
443 glyphs.extend(glyph_iter);
444 }
445
446 fn glyph_for_char(&self, ch: char) -> Option<Glyph<f32>> {
447 let mut utf16_buf = [0; 2];
448 let utf16_char_as_glyph_id = ch.encode_utf16(&mut utf16_buf)[0];
449
450 Glyph {
451 offset_x: 0.,
452 offset_y: 0.,
453 glyph_id: core::num::NonZeroU16::new(utf16_char_as_glyph_id),
454 advance: 10.,
455 text_byte_offset: 0,
456 }
457 .into()
458 }
459
460 fn max_lines(&self, max_height: f32) -> usize {
461 let height = self.ascent() - self.descent();
462 (max_height / height).floor() as _
463 }
464}
465
466#[cfg(test)]
467impl FontMetrics<f32> for FixedTestFont {
468 fn ascent(&self) -> f32 {
469 5.
470 }
471
472 fn descent(&self) -> f32 {
473 -5.
474 }
475
476 fn x_height(&self) -> f32 {
477 3.
478 }
479
480 fn cap_height(&self) -> f32 {
481 4.
482 }
483}
484
485#[test]
486fn test_elision() {
487 let font = FixedTestFont;
488 let text = "This is a longer piece of text";
489
490 let mut lines = Vec::new();
491
492 let paragraph = TextParagraphLayout {
493 string: text,
494 layout: TextLayout { font: &font, letter_spacing: None },
495 max_width: 13. * 10.,
496 max_height: 10.,
497 horizontal_alignment: TextHorizontalAlignment::Left,
498 vertical_alignment: TextVerticalAlignment::Top,
499 wrap: TextWrap::NoWrap,
500 overflow: TextOverflow::Elide,
501 single_line: true,
502 };
503 paragraph
504 .layout_lines::<()>(
505 |glyphs, _, _, _, _| {
506 lines.push(
507 glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
508 );
509 core::ops::ControlFlow::Continue(())
510 },
511 None,
512 )
513 .unwrap();
514
515 assert_eq!(lines.len(), 1);
516 let rendered_text = lines[0]
517 .iter()
518 .flat_map(|glyph_id| {
519 core::char::decode_utf16(core::iter::once(glyph_id.get()))
520 .map(|r| r.unwrap())
521 .collect::<Vec<char>>()
522 })
523 .collect::<std::string::String>();
524 debug_assert_eq!(rendered_text, "This is a lo…")
525}
526
527#[test]
528fn test_elision_vertical_truncation() {
529 let font = FixedTestFont;
533 let text = "AB\nCD";
534
535 let mut lines = Vec::new();
536
537 let paragraph = TextParagraphLayout {
538 string: text,
539 layout: TextLayout { font: &font, letter_spacing: None },
540 max_width: 4. * 10., max_height: 10., horizontal_alignment: TextHorizontalAlignment::Left,
543 vertical_alignment: TextVerticalAlignment::Top,
544 wrap: TextWrap::NoWrap,
545 overflow: TextOverflow::Elide,
546 single_line: false,
547 };
548 paragraph
549 .layout_lines::<()>(
550 |glyphs, _, _, _, _| {
551 lines.push(
552 glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
553 );
554 core::ops::ControlFlow::Continue(())
555 },
556 None,
557 )
558 .unwrap();
559
560 assert_eq!(lines.len(), 1);
562 let rendered_text = lines[0]
563 .iter()
564 .flat_map(|glyph_id| {
565 core::char::decode_utf16(core::iter::once(glyph_id.get()))
566 .map(|r| r.unwrap())
567 .collect::<Vec<char>>()
568 })
569 .collect::<std::string::String>();
570 assert_eq!(rendered_text, "AB…");
571}
572
573#[test]
574fn test_exact_fit() {
575 let font = FixedTestFont;
576 let text = "Fits";
577
578 let mut lines = Vec::new();
579
580 let paragraph = TextParagraphLayout {
581 string: text,
582 layout: TextLayout { font: &font, letter_spacing: None },
583 max_width: 4. * 10.,
584 max_height: 10.,
585 horizontal_alignment: TextHorizontalAlignment::Left,
586 vertical_alignment: TextVerticalAlignment::Top,
587 wrap: TextWrap::NoWrap,
588 overflow: TextOverflow::Elide,
589 single_line: true,
590 };
591 paragraph
592 .layout_lines::<()>(
593 |glyphs, _, _, _, _| {
594 lines.push(
595 glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
596 );
597 core::ops::ControlFlow::Continue(())
598 },
599 None,
600 )
601 .unwrap();
602
603 assert_eq!(lines.len(), 1);
604 let rendered_text = lines[0]
605 .iter()
606 .flat_map(|glyph_id| {
607 core::char::decode_utf16(core::iter::once(glyph_id.get()))
608 .map(|r| r.unwrap())
609 .collect::<Vec<char>>()
610 })
611 .collect::<std::string::String>();
612 debug_assert_eq!(rendered_text, "Fits")
613}
614
615#[test]
616fn test_no_line_separators_characters_rendered() {
617 let font = FixedTestFont;
618 let text = "Hello\nWorld\n";
619
620 let mut lines = Vec::new();
621
622 let paragraph = TextParagraphLayout {
623 string: text,
624 layout: TextLayout { font: &font, letter_spacing: None },
625 max_width: 13. * 10.,
626 max_height: 10.,
627 horizontal_alignment: TextHorizontalAlignment::Left,
628 vertical_alignment: TextVerticalAlignment::Top,
629 wrap: TextWrap::NoWrap,
630 overflow: TextOverflow::Clip,
631 single_line: true,
632 };
633 paragraph
634 .layout_lines::<()>(
635 |glyphs, _, _, _, _| {
636 lines.push(
637 glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
638 );
639 core::ops::ControlFlow::Continue(())
640 },
641 None,
642 )
643 .unwrap();
644
645 assert_eq!(lines.len(), 2);
646 let rendered_text = lines
647 .iter()
648 .map(|glyphs_per_line| {
649 glyphs_per_line
650 .iter()
651 .flat_map(|glyph_id| {
652 core::char::decode_utf16(core::iter::once(glyph_id.get()))
653 .map(|r| r.unwrap())
654 .collect::<Vec<char>>()
655 })
656 .collect::<std::string::String>()
657 })
658 .collect::<Vec<_>>();
659 debug_assert_eq!(rendered_text, std::vec!["Hello", "World"]);
660}
661
662#[test]
663fn test_cursor_position() {
664 let font = FixedTestFont;
665 let text = "Hello World";
666
667 let paragraph = TextParagraphLayout {
668 string: text,
669 layout: TextLayout { font: &font, letter_spacing: None },
670 max_width: 10. * 10.,
671 max_height: 10.,
672 horizontal_alignment: TextHorizontalAlignment::Left,
673 vertical_alignment: TextVerticalAlignment::Top,
674 wrap: TextWrap::WordWrap,
675 overflow: TextOverflow::Clip,
676 single_line: false,
677 };
678
679 assert_eq!(paragraph.cursor_pos_for_byte_offset(0), (0., 0.));
680
681 let e_offset = text
682 .char_indices()
683 .find_map(|(offset, ch)| if ch == 'e' { Some(offset) } else { None })
684 .unwrap();
685 assert_eq!(paragraph.cursor_pos_for_byte_offset(e_offset), (10., 0.));
686
687 let w_offset = text
688 .char_indices()
689 .find_map(|(offset, ch)| if ch == 'W' { Some(offset) } else { None })
690 .unwrap();
691 assert_eq!(paragraph.cursor_pos_for_byte_offset(w_offset + 1), (10., 10.));
692
693 assert_eq!(paragraph.cursor_pos_for_byte_offset(text.len()), (10. * 5., 10.));
694
695 let first_space_offset =
696 text.char_indices().find_map(|(offset, ch)| ch.is_whitespace().then_some(offset)).unwrap();
697 assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset), (5. * 10., 0.));
698 assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset + 15), (10. * 10., 0.));
699 assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset + 16), (10. * 10., 0.));
700}
701
702#[test]
703fn test_cursor_position_with_newline() {
704 let font = FixedTestFont;
705 let text = "Hello\nWorld";
706
707 let paragraph = TextParagraphLayout {
708 string: text,
709 layout: TextLayout { font: &font, letter_spacing: None },
710 max_width: 100. * 10.,
711 max_height: 10.,
712 horizontal_alignment: TextHorizontalAlignment::Left,
713 vertical_alignment: TextVerticalAlignment::Top,
714 wrap: TextWrap::WordWrap,
715 overflow: TextOverflow::Clip,
716 single_line: false,
717 };
718
719 assert_eq!(paragraph.cursor_pos_for_byte_offset(5), (5. * 10., 0.));
720}
721
722#[test]
723fn byte_offset_for_empty_line() {
724 let font = FixedTestFont;
725 let text = "Hello\n\nWorld";
726
727 let paragraph = TextParagraphLayout {
728 string: text,
729 layout: TextLayout { font: &font, letter_spacing: None },
730 max_width: 100. * 10.,
731 max_height: 10.,
732 horizontal_alignment: TextHorizontalAlignment::Left,
733 vertical_alignment: TextVerticalAlignment::Top,
734 wrap: TextWrap::WordWrap,
735 overflow: TextOverflow::Clip,
736 single_line: false,
737 };
738
739 assert_eq!(paragraph.byte_offset_for_position((0., 10.)), 6);
740}
741
742#[test]
743fn test_byte_offset() {
744 let font = FixedTestFont;
745 let text = "Hello World";
746 let mut end_helper_text = std::string::String::from(text);
747 end_helper_text.push('!');
748
749 let paragraph = TextParagraphLayout {
750 string: text,
751 layout: TextLayout { font: &font, letter_spacing: None },
752 max_width: 10. * 10.,
753 max_height: 10.,
754 horizontal_alignment: TextHorizontalAlignment::Left,
755 vertical_alignment: TextVerticalAlignment::Top,
756 wrap: TextWrap::WordWrap,
757 overflow: TextOverflow::Clip,
758 single_line: false,
759 };
760
761 assert_eq!(paragraph.byte_offset_for_position((0., 0.)), 0);
762
763 let e_offset = text
764 .char_indices()
765 .find_map(|(offset, ch)| if ch == 'e' { Some(offset) } else { None })
766 .unwrap();
767
768 assert_eq!(paragraph.byte_offset_for_position((14., 0.)), e_offset);
769
770 let l_offset = text
771 .char_indices()
772 .find_map(|(offset, ch)| if ch == 'l' { Some(offset) } else { None })
773 .unwrap();
774 assert_eq!(paragraph.byte_offset_for_position((15., 0.)), l_offset);
775
776 let w_offset = text
777 .char_indices()
778 .find_map(|(offset, ch)| if ch == 'W' { Some(offset) } else { None })
779 .unwrap();
780
781 assert_eq!(paragraph.byte_offset_for_position((10., 10.)), w_offset + 1);
782
783 let o_offset = text
784 .char_indices()
785 .rev()
786 .find_map(|(offset, ch)| if ch == 'o' { Some(offset) } else { None })
787 .unwrap();
788
789 assert_eq!(paragraph.byte_offset_for_position((15., 10.)), o_offset + 1);
790
791 let d_offset = text
792 .char_indices()
793 .rev()
794 .find_map(|(offset, ch)| if ch == 'd' { Some(offset) } else { None })
795 .unwrap();
796
797 assert_eq!(paragraph.byte_offset_for_position((40., 10.)), d_offset);
798
799 let end_offset = end_helper_text
800 .char_indices()
801 .rev()
802 .find_map(|(offset, ch)| if ch == '!' { Some(offset) } else { None })
803 .unwrap();
804
805 assert_eq!(paragraph.byte_offset_for_position((45., 10.)), end_offset);
806 assert_eq!(paragraph.byte_offset_for_position((0., 20.)), end_offset);
807}