1use alloc::vec::Vec;
29
30use euclid::num::{One, Zero};
31
32use crate::items::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap};
33
34pub const DEFAULT_FONT_SIZE: crate::lengths::LogicalLength =
38 crate::lengths::LogicalLength::new(12 as crate::Coord);
39
40#[cfg(feature = "unicode-linebreak")]
41mod linebreak_unicode;
42#[cfg(feature = "unicode-linebreak")]
43use linebreak_unicode::{BreakOpportunity, LineBreakIterator};
44
45#[cfg(not(feature = "unicode-linebreak"))]
46mod linebreak_simple;
47#[cfg(not(feature = "unicode-linebreak"))]
48use linebreak_simple::{BreakOpportunity, LineBreakIterator};
49
50mod fragments;
51mod glyphclusters;
52mod shaping;
53#[cfg(feature = "shared-parley")]
54pub mod sharedparley;
56use shaping::ShapeBuffer;
57pub use shaping::{AbstractFont, CheckedAdd, DivCount, FontMetrics, Glyph, TextShaper};
58
59mod linebreaker;
60pub use linebreaker::TextLine;
61
62pub use linebreaker::TextLineBreaker;
63
64pub struct TextLayout<'a, Font: AbstractFont> {
65 pub font: &'a Font,
66 pub letter_spacing: Option<<Font as TextShaper>::Length>,
67 pub line_height: Option<<Font as TextShaper>::Length>,
69}
70
71impl<Font: AbstractFont> TextLayout<'_, Font> {
72 fn line_height(&self) -> Font::Length {
73 self.line_height.unwrap_or_else(|| self.font.height())
74 }
75
76 pub fn half_leading(&self) -> Font::Length {
81 let two = Font::LengthPrimitive::one() + Font::LengthPrimitive::one();
82 (self.line_height() - self.font.height()) / two
83 }
84
85 pub fn cursor_band(&self) -> (Font::Length, Font::Length) {
89 (
90 euclid::approxord::min(Font::Length::zero(), self.half_leading()),
91 euclid::approxord::max(self.line_height(), self.font.height()),
92 )
93 }
94
95 pub fn text_size(
99 &self,
100 text: &str,
101 max_width: Option<Font::Length>,
102 text_wrap: TextWrap,
103 max_lines: Option<usize>,
104 ) -> (Font::Length, Font::Length)
105 where
106 Font::Length: core::fmt::Debug,
107 {
108 let mut max_line_width = Font::Length::zero();
109 let mut line_count: i16 = 0;
110 let shape_buffer = ShapeBuffer::new(self, text);
111
112 for line in
113 TextLineBreaker::<Font>::new(text, &shape_buffer, max_width, max_lines, text_wrap)
114 {
115 max_line_width = euclid::approxord::max(max_line_width, line.text_width);
116 line_count += 1;
117 }
118
119 (max_line_width, self.line_height() * line_count.into())
120 }
121
122 pub fn content_widths(
129 &self,
130 text: &str,
131 max_lines: Option<usize>,
132 ) -> (Font::Length, Font::Length)
133 where
134 Font::Length: core::fmt::Debug,
135 {
136 let shape_buffer = ShapeBuffer::new(self, text);
137
138 let mut min = Font::Length::zero();
141 let mut lines = 0;
142 for fragment in fragments::TextFragmentIterator::new(text, &shape_buffer) {
143 if max_lines.is_some_and(|max_lines| lines >= max_lines) {
144 break;
145 }
146 min = euclid::approxord::max(min, fragment.width);
147 if fragment.trailing_mandatory_break {
148 lines += 1;
149 }
150 }
151
152 let mut max = Font::Length::zero();
154 for line in
155 TextLineBreaker::<Font>::new(text, &shape_buffer, None, max_lines, TextWrap::NoWrap)
156 {
157 max = euclid::approxord::max(max, line.text_width);
158 }
159
160 (min, max)
161 }
162}
163
164pub struct PositionedGlyph<Length> {
165 pub x: Length,
166 pub y: Length,
167 pub advance: Length,
168 pub glyph_id: core::num::NonZeroU16,
169 pub text_byte_offset: usize,
170}
171
172pub struct TextParagraphLayout<'a, Font: AbstractFont> {
173 pub string: &'a str,
174 pub layout: TextLayout<'a, Font>,
175 pub max_width: Font::Length,
176 pub max_height: Font::Length,
177 pub horizontal_alignment: TextHorizontalAlignment,
178 pub vertical_alignment: TextVerticalAlignment,
179 pub wrap: TextWrap,
180 pub overflow: TextOverflow,
181 pub single_line: bool,
182 pub max_lines: Option<usize>,
183}
184
185impl<Font: AbstractFont> TextParagraphLayout<'_, Font> {
186 pub fn layout_lines<R>(
190 &self,
191 mut line_callback: impl FnMut(
192 &mut dyn Iterator<Item = PositionedGlyph<Font::Length>>,
193 Font::Length,
194 Font::Length,
195 &TextLine<Font::Length>,
196 Option<core::ops::Range<Font::Length>>,
197 ) -> core::ops::ControlFlow<R>,
198 selection: Option<core::ops::Range<usize>>,
199 ) -> Result<Font::Length, R> {
200 let wrap = self.wrap != TextWrap::NoWrap;
201 let elide = self.overflow == TextOverflow::Elide;
202 let elide_glyph = if elide {
203 self.layout.font.glyph_for_char('…').filter(|glyph| glyph.glyph_id.is_some())
204 } else {
205 None
206 };
207 let elide_width = elide_glyph.as_ref().map_or(Font::Length::zero(), |g| g.advance);
208 let max_width_without_elision = self.max_width - elide_width;
209
210 let shape_buffer = ShapeBuffer::new(&self.layout, self.string);
211
212 let line_height = self.layout.line_height();
218 let max_lines_from_height = elide.then(|| self.max_lines_that_fit(line_height).max(1));
219 let max_lines = [self.max_lines, max_lines_from_height].into_iter().flatten().min();
220
221 let new_line_break_iter = || {
222 TextLineBreaker::<Font>::new(
223 self.string,
224 &shape_buffer,
225 if wrap { Some(self.max_width) } else { None },
226 max_lines,
227 self.wrap,
228 )
229 };
230 let mut text_lines = None;
231
232 let mut text_height = || {
233 if self.single_line {
234 line_height
235 } else {
236 text_lines = Some(new_line_break_iter().collect::<Vec<_>>());
237 line_height * (text_lines.as_ref().unwrap().len() as i16).into()
238 }
239 };
240
241 let two = Font::LengthPrimitive::one() + Font::LengthPrimitive::one();
242
243 let baseline_y = match self.vertical_alignment {
244 TextVerticalAlignment::Top => Font::Length::zero(),
245 TextVerticalAlignment::Center => self.max_height / two - text_height() / two,
246 TextVerticalAlignment::Bottom => self.max_height - text_height(),
247 };
248
249 let mut y = baseline_y;
250 let mut line_index = 0usize;
251
252 let mut process_line = |line: &TextLine<Font::Length>, glyphs: &[Glyph<Font::Length>]| {
253 let elide_long_line =
254 elide && (self.single_line || !wrap) && line.text_width > self.max_width;
255 let reached_line_limit = max_lines.is_some_and(|max_lines| line_index + 1 == max_lines);
258 let elide_last_line = elide
259 && line.glyph_range.end < glyphs.len()
260 && (y + line_height * two > self.max_height || reached_line_limit);
261
262 let glyph_range = if elide_last_line {
268 let trailing_whitespace_glyphs = glyphs[line.glyph_range.clone()]
269 .iter()
270 .rev()
271 .take_while(|glyph| glyph.text_byte_offset >= line.byte_range.end)
272 .count();
273 line.glyph_range.start..line.glyph_range.end - trailing_whitespace_glyphs
274 } else {
275 line.glyph_range.clone()
276 };
277
278 let text_width = || {
279 if elide_long_line || elide_last_line {
280 let mut text_width = Font::Length::zero();
281 for glyph in &glyphs[glyph_range.clone()] {
282 if text_width + glyph.advance > max_width_without_elision {
283 break;
284 }
285 text_width += glyph.advance;
286 }
287 return text_width + elide_width;
288 }
289 euclid::approxord::min(self.max_width, line.text_width)
290 };
291
292 let x = match self.horizontal_alignment {
293 TextHorizontalAlignment::Start | TextHorizontalAlignment::Left => {
294 Font::Length::zero()
295 }
296 TextHorizontalAlignment::Center => self.max_width / two - text_width() / two,
297 TextHorizontalAlignment::End | TextHorizontalAlignment::Right => {
298 self.max_width - text_width()
299 }
300 };
301
302 let mut elide_glyph = elide_glyph.as_ref();
303
304 let selection = selection
305 .as_ref()
306 .filter(|selection| {
307 line.byte_range.start < selection.end && selection.start < line.byte_range.end
308 })
309 .map(|selection| {
310 let mut begin = Font::Length::zero();
311 let mut end = Font::Length::zero();
312 for glyph in glyphs[line.glyph_range.clone()].iter() {
313 if glyph.text_byte_offset < selection.start {
314 begin += glyph.advance;
315 }
316 if glyph.text_byte_offset >= selection.end {
317 break;
318 }
319 end += glyph.advance;
320 }
321 begin..end
322 });
323
324 let glyph_it = glyphs[glyph_range.clone()].iter();
325 let mut glyph_x = Font::Length::zero();
326 let mut positioned_glyph_it = glyph_it.enumerate().flat_map(|(index, glyph)| {
330 let mut output: [Option<PositionedGlyph<Font::Length>>; 2] = [None, None];
331 if glyph_x > self.max_width {
333 return output.into_iter().flatten();
334 }
335 let elide_long_line = (elide_long_line || elide_last_line)
338 && x + glyph_x + glyph.advance > max_width_without_elision;
339 if elide_long_line {
340 if let Some(elide_glyph) = elide_glyph.take() {
341 let x = glyph_x;
342 glyph_x += elide_glyph.advance;
343 output[0] = Some(PositionedGlyph {
344 x,
345 y: Font::Length::zero(),
346 advance: elide_glyph.advance,
347 glyph_id: elide_glyph.glyph_id.unwrap(), text_byte_offset: glyph.text_byte_offset,
349 });
350 }
351 return output.into_iter().flatten();
352 }
353
354 let glyph_pos = glyph_x;
355 glyph_x += glyph.advance;
356 output[0] = glyph.glyph_id.map(|existing_glyph_id| PositionedGlyph {
357 x: glyph_pos,
358 y: Font::Length::zero(),
359 advance: glyph.advance,
360 glyph_id: existing_glyph_id,
361 text_byte_offset: glyph.text_byte_offset,
362 });
363
364 let last_glyph = glyph_range.start + index == glyph_range.end - 1;
368 if elide_last_line
369 && last_glyph
370 && let Some(elide_glyph) = elide_glyph.take()
371 {
372 let x = glyph_x;
373 glyph_x += elide_glyph.advance;
374 output[1] = Some(PositionedGlyph {
375 x,
376 y: Font::Length::zero(),
377 advance: elide_glyph.advance,
378 glyph_id: elide_glyph.glyph_id.unwrap(), text_byte_offset: glyph.text_byte_offset,
380 });
381 }
382
383 output.into_iter().flatten()
384 });
385
386 if let core::ops::ControlFlow::Break(break_val) =
387 line_callback(&mut positioned_glyph_it, x, y, line, selection)
388 {
389 return core::ops::ControlFlow::Break(break_val);
390 }
391 y += line_height;
392 line_index += 1;
393
394 core::ops::ControlFlow::Continue(())
395 };
396
397 if let Some(lines_vec) = text_lines.take() {
398 for line in lines_vec {
399 if let core::ops::ControlFlow::Break(break_val) =
400 process_line(&line, &shape_buffer.glyphs)
401 {
402 return Err(break_val);
403 }
404 }
405 } else {
406 for line in new_line_break_iter() {
407 if let core::ops::ControlFlow::Break(break_val) =
408 process_line(&line, &shape_buffer.glyphs)
409 {
410 return Err(break_val);
411 }
412 }
413 }
414
415 Ok(baseline_y)
416 }
417
418 fn max_lines_that_fit(&self, line_height: Font::Length) -> usize {
422 if line_height <= Font::Length::zero() {
423 return usize::MAX;
424 }
425 self.max_height.div_count(line_height)
426 }
427
428 pub fn cursor_pos_for_byte_offset(&self, byte_offset: usize) -> (Font::Length, Font::Length) {
430 let mut last_glyph_right_edge = Font::Length::zero();
431 let mut last_line_y = Font::Length::zero();
432
433 match self.layout_lines(
434 |glyphs, line_x, line_y, line, _| {
435 last_glyph_right_edge = euclid::approxord::min(
436 self.max_width,
437 line_x + line.width_including_trailing_whitespace(),
438 );
439 last_line_y = line_y;
440 if byte_offset >= line.byte_range.end + line.trailing_whitespace_bytes {
441 return core::ops::ControlFlow::Continue(());
442 }
443
444 for positioned_glyph in glyphs {
445 if positioned_glyph.text_byte_offset == byte_offset {
446 return core::ops::ControlFlow::Break((
447 euclid::approxord::min(self.max_width, line_x + positioned_glyph.x),
448 last_line_y,
449 ));
450 }
451 }
452
453 core::ops::ControlFlow::Break((last_glyph_right_edge, last_line_y))
454 },
455 None,
456 ) {
457 Ok(_) => (last_glyph_right_edge, last_line_y),
458 Err(position) => position,
459 }
460 }
461
462 pub fn byte_offset_for_position(&self, (pos_x, pos_y): (Font::Length, Font::Length)) -> usize {
464 let mut byte_offset = 0;
465 let two = Font::LengthPrimitive::one() + Font::LengthPrimitive::one();
466
467 match self.layout_lines(
468 |glyphs, line_x, line_y, line, _| {
469 if pos_y >= line_y + self.layout.line_height() {
470 byte_offset = line.byte_range.end;
471 return core::ops::ControlFlow::Continue(());
472 }
473
474 if line.is_empty() {
475 return core::ops::ControlFlow::Break(line.byte_range.start);
476 }
477
478 while let Some(positioned_glyph) = glyphs.next() {
479 if pos_x >= line_x + positioned_glyph.x
480 && pos_x <= line_x + positioned_glyph.x + positioned_glyph.advance
481 {
482 if pos_x < line_x + positioned_glyph.x + positioned_glyph.advance / two {
483 return core::ops::ControlFlow::Break(
484 positioned_glyph.text_byte_offset,
485 );
486 } else if let Some(next_glyph) = glyphs.next() {
487 return core::ops::ControlFlow::Break(next_glyph.text_byte_offset);
488 }
489 }
490 }
491
492 core::ops::ControlFlow::Break(line.byte_range.end)
493 },
494 None,
495 ) {
496 Ok(_) => byte_offset,
497 Err(position) => position,
498 }
499 }
500}
501
502#[test]
503fn test_no_linebreak_opportunity_at_eot() {
504 let mut it = LineBreakIterator::new("Hello World");
505 assert_eq!(it.next(), Some((6, BreakOpportunity::Allowed)));
506 assert_eq!(it.next(), None);
507}
508
509#[cfg(test)]
511pub struct FixedTestFont;
512
513#[cfg(test)]
514impl TextShaper for FixedTestFont {
515 type LengthPrimitive = f32;
516 type Length = f32;
517 fn shape_text<GlyphStorage: std::iter::Extend<Glyph<f32>>>(
518 &self,
519 text: &str,
520 glyphs: &mut GlyphStorage,
521 ) {
522 let glyph_iter = text.char_indices().map(|(byte_offset, char)| {
523 let mut utf16_buf = [0; 2];
524 let utf16_char_as_glyph_id = char.encode_utf16(&mut utf16_buf)[0];
525
526 Glyph {
527 offset_x: 0.,
528 offset_y: 0.,
529 glyph_id: core::num::NonZeroU16::new(utf16_char_as_glyph_id),
530 advance: 10.,
531 text_byte_offset: byte_offset,
532 }
533 });
534 glyphs.extend(glyph_iter);
535 }
536
537 fn glyph_for_char(&self, ch: char) -> Option<Glyph<f32>> {
538 let mut utf16_buf = [0; 2];
539 let utf16_char_as_glyph_id = ch.encode_utf16(&mut utf16_buf)[0];
540
541 Glyph {
542 offset_x: 0.,
543 offset_y: 0.,
544 glyph_id: core::num::NonZeroU16::new(utf16_char_as_glyph_id),
545 advance: 10.,
546 text_byte_offset: 0,
547 }
548 .into()
549 }
550}
551
552#[cfg(test)]
553impl FontMetrics<f32> for FixedTestFont {
554 fn ascent(&self) -> f32 {
555 5.
556 }
557
558 fn descent(&self) -> f32 {
559 -5.
560 }
561
562 fn x_height(&self) -> f32 {
563 3.
564 }
565
566 fn cap_height(&self) -> f32 {
567 4.
568 }
569}
570
571#[test]
572fn test_elision() {
573 let font = FixedTestFont;
574 let text = "This is a longer piece of text";
575
576 let mut lines = Vec::new();
577
578 let paragraph = TextParagraphLayout {
579 string: text,
580 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
581 max_width: 13. * 10.,
582 max_height: 10.,
583 horizontal_alignment: TextHorizontalAlignment::Left,
584 vertical_alignment: TextVerticalAlignment::Top,
585 wrap: TextWrap::NoWrap,
586 overflow: TextOverflow::Elide,
587 single_line: true,
588 max_lines: None,
589 };
590 paragraph
591 .layout_lines::<()>(
592 |glyphs, _, _, _, _| {
593 lines.push(
594 glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
595 );
596 core::ops::ControlFlow::Continue(())
597 },
598 None,
599 )
600 .unwrap();
601
602 assert_eq!(lines.len(), 1);
603 let rendered_text = lines[0]
604 .iter()
605 .flat_map(|glyph_id| {
606 core::char::decode_utf16(core::iter::once(glyph_id.get()))
607 .map(|r| r.unwrap())
608 .collect::<Vec<char>>()
609 })
610 .collect::<std::string::String>();
611 debug_assert_eq!(rendered_text, "This is a lo…")
612}
613
614#[test]
615fn test_elision_vertical_truncation() {
616 let font = FixedTestFont;
620 let text = "AB\nCD";
621
622 let mut lines = Vec::new();
623
624 let paragraph = TextParagraphLayout {
625 string: text,
626 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
627 max_width: 4. * 10., max_height: 10., horizontal_alignment: TextHorizontalAlignment::Left,
630 vertical_alignment: TextVerticalAlignment::Top,
631 wrap: TextWrap::NoWrap,
632 overflow: TextOverflow::Elide,
633 single_line: false,
634 max_lines: None,
635 };
636 paragraph
637 .layout_lines::<()>(
638 |glyphs, _, _, _, _| {
639 lines.push(
640 glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
641 );
642 core::ops::ControlFlow::Continue(())
643 },
644 None,
645 )
646 .unwrap();
647
648 assert_eq!(lines.len(), 1);
650 let rendered_text = lines[0]
651 .iter()
652 .flat_map(|glyph_id| {
653 core::char::decode_utf16(core::iter::once(glyph_id.get()))
654 .map(|r| r.unwrap())
655 .collect::<Vec<char>>()
656 })
657 .collect::<std::string::String>();
658 assert_eq!(rendered_text, "AB…");
659}
660
661#[test]
662fn test_exact_fit() {
663 let font = FixedTestFont;
664 let text = "Fits";
665
666 let mut lines = Vec::new();
667
668 let paragraph = TextParagraphLayout {
669 string: text,
670 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
671 max_width: 4. * 10.,
672 max_height: 10.,
673 horizontal_alignment: TextHorizontalAlignment::Left,
674 vertical_alignment: TextVerticalAlignment::Top,
675 wrap: TextWrap::NoWrap,
676 overflow: TextOverflow::Elide,
677 single_line: true,
678 max_lines: None,
679 };
680 paragraph
681 .layout_lines::<()>(
682 |glyphs, _, _, _, _| {
683 lines.push(
684 glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
685 );
686 core::ops::ControlFlow::Continue(())
687 },
688 None,
689 )
690 .unwrap();
691
692 assert_eq!(lines.len(), 1);
693 let rendered_text = lines[0]
694 .iter()
695 .flat_map(|glyph_id| {
696 core::char::decode_utf16(core::iter::once(glyph_id.get()))
697 .map(|r| r.unwrap())
698 .collect::<Vec<char>>()
699 })
700 .collect::<std::string::String>();
701 debug_assert_eq!(rendered_text, "Fits")
702}
703
704#[test]
705fn test_no_line_separators_characters_rendered() {
706 let font = FixedTestFont;
707 let text = "Hello\nWorld\n";
708
709 let mut lines = Vec::new();
710
711 let paragraph = TextParagraphLayout {
712 string: text,
713 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
714 max_width: 13. * 10.,
715 max_height: 10.,
716 horizontal_alignment: TextHorizontalAlignment::Left,
717 vertical_alignment: TextVerticalAlignment::Top,
718 wrap: TextWrap::NoWrap,
719 overflow: TextOverflow::Clip,
720 single_line: true,
721 max_lines: None,
722 };
723 paragraph
724 .layout_lines::<()>(
725 |glyphs, _, _, _, _| {
726 lines.push(
727 glyphs.map(|positioned_glyph| positioned_glyph.glyph_id).collect::<Vec<_>>(),
728 );
729 core::ops::ControlFlow::Continue(())
730 },
731 None,
732 )
733 .unwrap();
734
735 assert_eq!(lines.len(), 2);
736 let rendered_text = lines
737 .iter()
738 .map(|glyphs_per_line| {
739 glyphs_per_line
740 .iter()
741 .flat_map(|glyph_id| {
742 core::char::decode_utf16(core::iter::once(glyph_id.get()))
743 .map(|r| r.unwrap())
744 .collect::<Vec<char>>()
745 })
746 .collect::<std::string::String>()
747 })
748 .collect::<Vec<_>>();
749 debug_assert_eq!(rendered_text, std::vec!["Hello", "World"]);
750}
751
752#[test]
753fn test_max_lines_limits_visible_lines() {
754 let font = FixedTestFont;
755 let text = "Hello\nWorld\nAgain";
756
757 let paragraph = TextParagraphLayout {
758 string: text,
759 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
760 max_width: 100. * 10.,
761 max_height: 100.,
762 horizontal_alignment: TextHorizontalAlignment::Left,
763 vertical_alignment: TextVerticalAlignment::Top,
764 wrap: TextWrap::NoWrap,
765 overflow: TextOverflow::Clip,
766 single_line: false,
767 max_lines: Some(2),
768 };
769 assert_eq!(render_lines(¶graph), std::vec!["Hello", "World"]);
770}
771
772#[cfg(test)]
773fn render_lines(paragraph: &TextParagraphLayout<'_, FixedTestFont>) -> Vec<std::string::String> {
774 let mut lines = Vec::new();
775 paragraph
776 .layout_lines::<()>(
777 |glyphs, _, _, _, _| {
778 lines.push(
779 glyphs
780 .flat_map(|positioned_glyph| {
781 core::char::decode_utf16(core::iter::once(
782 positioned_glyph.glyph_id.get(),
783 ))
784 .map(|r| r.unwrap())
785 .collect::<Vec<char>>()
786 })
787 .collect::<std::string::String>(),
788 );
789 core::ops::ControlFlow::Continue(())
790 },
791 None,
792 )
793 .unwrap();
794 lines
795}
796
797#[test]
798fn test_max_lines_with_word_wrap() {
799 let font = FixedTestFont;
800 let text = "Hello World Again";
802
803 let paragraph = TextParagraphLayout {
804 string: text,
805 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
806 max_width: 6. * 10.,
807 max_height: 100.,
808 horizontal_alignment: TextHorizontalAlignment::Left,
809 vertical_alignment: TextVerticalAlignment::Top,
810 wrap: TextWrap::WordWrap,
811 overflow: TextOverflow::Clip,
812 single_line: false,
813 max_lines: Some(2),
814 };
815 assert_eq!(render_lines(¶graph), std::vec!["Hello ", "World "]);
816}
817
818#[test]
819fn test_max_lines_elide_marks_cut_line() {
820 let font = FixedTestFont;
821 let text = "Hello\nWorld\nAgain";
822
823 let paragraph = TextParagraphLayout {
826 string: text,
827 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
828 max_width: 100. * 10.,
829 max_height: 100.,
830 horizontal_alignment: TextHorizontalAlignment::Left,
831 vertical_alignment: TextVerticalAlignment::Top,
832 wrap: TextWrap::NoWrap,
833 overflow: TextOverflow::Elide,
834 single_line: false,
835 max_lines: Some(2),
836 };
837 assert_eq!(render_lines(¶graph), std::vec!["Hello", "World…"]);
838}
839
840#[test]
841fn test_text_size_with_max_lines() {
842 let font = FixedTestFont;
843 let layout = TextLayout { font: &font, letter_spacing: None, line_height: None };
844 let text = "On\nFour\nLonger";
845
846 let (width, height) = layout.text_size(text, None, TextWrap::NoWrap, None);
847 assert_eq!((width, height), (6. * 10., 3. * 10.));
848
849 let (width, height) = layout.text_size(text, None, TextWrap::NoWrap, Some(2));
851 assert_eq!((width, height), (4. * 10., 2. * 10.));
852}
853
854#[test]
855fn test_line_height() {
856 let font = FixedTestFont;
857 let layout = TextLayout { font: &font, letter_spacing: None, line_height: Some(15.) };
858 let text = "One\nTwo";
859
860 assert_eq!(layout.text_size(text, None, TextWrap::NoWrap, None), (3. * 10., 2. * 15.));
861
862 assert_eq!(layout.half_leading(), 2.5);
864 assert_eq!(layout.cursor_band(), (0., 15.));
865
866 let tight = TextLayout { font: &font, letter_spacing: None, line_height: Some(6.) };
868 assert_eq!(tight.half_leading(), -2.);
869 assert_eq!(tight.cursor_band(), (-2., 10.));
870
871 let paragraph = TextParagraphLayout {
872 string: text,
873 layout,
874 max_width: 100. * 10.,
875 max_height: 100.,
876 horizontal_alignment: TextHorizontalAlignment::Left,
877 vertical_alignment: TextVerticalAlignment::Top,
878 wrap: TextWrap::NoWrap,
879 overflow: TextOverflow::Clip,
880 single_line: false,
881 max_lines: None,
882 };
883
884 assert_eq!(paragraph.cursor_pos_for_byte_offset(4), (0., 15.));
885 assert_eq!(paragraph.byte_offset_for_position((0., 16.)), 4);
886
887 let paragraph = TextParagraphLayout {
888 string: "One\nTwo\nThree",
889 layout: TextLayout { font: &font, letter_spacing: None, line_height: Some(15.) },
890 max_width: 100. * 10.,
891 max_height: 29.,
892 horizontal_alignment: TextHorizontalAlignment::Left,
893 vertical_alignment: TextVerticalAlignment::Top,
894 wrap: TextWrap::NoWrap,
895 overflow: TextOverflow::Elide,
896 single_line: false,
897 max_lines: None,
898 };
899
900 assert_eq!(render_lines(¶graph), std::vec!["One…"]);
901}
902
903#[test]
904fn test_cursor_position() {
905 let font = FixedTestFont;
906 let text = "Hello World";
907
908 let paragraph = TextParagraphLayout {
909 string: text,
910 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
911 max_width: 10. * 10.,
912 max_height: 10.,
913 horizontal_alignment: TextHorizontalAlignment::Left,
914 vertical_alignment: TextVerticalAlignment::Top,
915 wrap: TextWrap::WordWrap,
916 overflow: TextOverflow::Clip,
917 single_line: false,
918 max_lines: None,
919 };
920
921 assert_eq!(paragraph.cursor_pos_for_byte_offset(0), (0., 0.));
922
923 let e_offset = text
924 .char_indices()
925 .find_map(|(offset, ch)| if ch == 'e' { Some(offset) } else { None })
926 .unwrap();
927 assert_eq!(paragraph.cursor_pos_for_byte_offset(e_offset), (10., 0.));
928
929 let w_offset = text
930 .char_indices()
931 .find_map(|(offset, ch)| if ch == 'W' { Some(offset) } else { None })
932 .unwrap();
933 assert_eq!(paragraph.cursor_pos_for_byte_offset(w_offset + 1), (10., 10.));
934
935 assert_eq!(paragraph.cursor_pos_for_byte_offset(text.len()), (10. * 5., 10.));
936
937 let first_space_offset =
938 text.char_indices().find_map(|(offset, ch)| ch.is_whitespace().then_some(offset)).unwrap();
939 assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset), (5. * 10., 0.));
940 assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset + 15), (10. * 10., 0.));
941 assert_eq!(paragraph.cursor_pos_for_byte_offset(first_space_offset + 16), (10. * 10., 0.));
942}
943
944#[test]
945fn test_cursor_position_with_newline() {
946 let font = FixedTestFont;
947 let text = "Hello\nWorld";
948
949 let paragraph = TextParagraphLayout {
950 string: text,
951 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
952 max_width: 100. * 10.,
953 max_height: 10.,
954 horizontal_alignment: TextHorizontalAlignment::Left,
955 vertical_alignment: TextVerticalAlignment::Top,
956 wrap: TextWrap::WordWrap,
957 overflow: TextOverflow::Clip,
958 single_line: false,
959 max_lines: None,
960 };
961
962 assert_eq!(paragraph.cursor_pos_for_byte_offset(5), (5. * 10., 0.));
963}
964
965#[test]
966fn byte_offset_for_empty_line() {
967 let font = FixedTestFont;
968 let text = "Hello\n\nWorld";
969
970 let paragraph = TextParagraphLayout {
971 string: text,
972 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
973 max_width: 100. * 10.,
974 max_height: 10.,
975 horizontal_alignment: TextHorizontalAlignment::Left,
976 vertical_alignment: TextVerticalAlignment::Top,
977 wrap: TextWrap::WordWrap,
978 overflow: TextOverflow::Clip,
979 single_line: false,
980 max_lines: None,
981 };
982
983 assert_eq!(paragraph.byte_offset_for_position((0., 10.)), 6);
984}
985
986#[test]
987fn test_byte_offset() {
988 let font = FixedTestFont;
989 let text = "Hello World";
990 let mut end_helper_text = std::string::String::from(text);
991 end_helper_text.push('!');
992
993 let paragraph = TextParagraphLayout {
994 string: text,
995 layout: TextLayout { font: &font, letter_spacing: None, line_height: None },
996 max_width: 10. * 10.,
997 max_height: 10.,
998 horizontal_alignment: TextHorizontalAlignment::Left,
999 vertical_alignment: TextVerticalAlignment::Top,
1000 wrap: TextWrap::WordWrap,
1001 overflow: TextOverflow::Clip,
1002 single_line: false,
1003 max_lines: None,
1004 };
1005
1006 assert_eq!(paragraph.byte_offset_for_position((0., 0.)), 0);
1007
1008 let e_offset = text
1009 .char_indices()
1010 .find_map(|(offset, ch)| if ch == 'e' { Some(offset) } else { None })
1011 .unwrap();
1012
1013 assert_eq!(paragraph.byte_offset_for_position((14., 0.)), e_offset);
1014
1015 let l_offset = text
1016 .char_indices()
1017 .find_map(|(offset, ch)| if ch == 'l' { Some(offset) } else { None })
1018 .unwrap();
1019 assert_eq!(paragraph.byte_offset_for_position((15., 0.)), l_offset);
1020
1021 let w_offset = text
1022 .char_indices()
1023 .find_map(|(offset, ch)| if ch == 'W' { Some(offset) } else { None })
1024 .unwrap();
1025
1026 assert_eq!(paragraph.byte_offset_for_position((10., 10.)), w_offset + 1);
1027
1028 let o_offset = text
1029 .char_indices()
1030 .rev()
1031 .find_map(|(offset, ch)| if ch == 'o' { Some(offset) } else { None })
1032 .unwrap();
1033
1034 assert_eq!(paragraph.byte_offset_for_position((15., 10.)), o_offset + 1);
1035
1036 let d_offset = text
1037 .char_indices()
1038 .rev()
1039 .find_map(|(offset, ch)| if ch == 'd' { Some(offset) } else { None })
1040 .unwrap();
1041
1042 assert_eq!(paragraph.byte_offset_for_position((40., 10.)), d_offset);
1043
1044 let end_offset = end_helper_text
1045 .char_indices()
1046 .rev()
1047 .find_map(|(offset, ch)| if ch == '!' { Some(offset) } else { None })
1048 .unwrap();
1049
1050 assert_eq!(paragraph.byte_offset_for_position((45., 10.)), end_offset);
1051 assert_eq!(paragraph.byte_offset_for_position((0., 20.)), end_offset);
1052}
1053
1054#[test]
1055fn test_content_widths() {
1056 let font = FixedTestFont;
1058 let layout = TextLayout { font: &font, letter_spacing: None, line_height: None };
1059 let min = |text| layout.content_widths(text, None).0;
1060
1061 assert_eq!(min("a bb longest cc"), 70.);
1064 assert_eq!(min("Hello"), 50.);
1066 assert_eq!(min("Hello World"), 50.);
1068 assert_eq!(min("Hello World "), 50.);
1070 assert_eq!(min("short\nlongestword"), 110.);
1072 assert_eq!(min(""), 0.);
1073 assert_eq!(min(" "), 0.);
1074 assert_eq!(min("aa\u{00a0}bb cc"), 50.);
1076 assert_eq!(min("abcdefgh"), 80.);
1078
1079 assert_eq!(layout.content_widths("a bb longest cc", None).1, 150.);
1081 assert_eq!(layout.content_widths("short\nlongestword", None).1, 110.);
1082}
1083
1084#[test]
1085fn test_content_widths_max_lines() {
1086 let font = FixedTestFont;
1087 let layout = TextLayout { font: &font, letter_spacing: None, line_height: None };
1088
1089 let (min, max) = layout.content_widths("short\nlongestword", Some(1));
1091 assert_eq!(max, 50.);
1092 assert_eq!(min, 50.);
1093
1094 let (min, max) = layout.content_widths("aa bb\nlongestword", Some(1));
1096 assert_eq!(max, 50.);
1097 assert_eq!(min, 20.);
1098
1099 let (min, max) = layout.content_widths("aa bb\nlongestword", None);
1101 assert_eq!(max, 110.);
1102 assert_eq!(min, 110.);
1103}
1104
1105#[test]
1106fn test_content_widths_min_never_exceeds_max() {
1107 let font = FixedTestFont;
1108 let layout = TextLayout { font: &font, letter_spacing: None, line_height: None };
1109 for text in ["", " ", "Hello", "a bb longest cc", "short\nlongestword", "aa\u{00a0}bb cc"] {
1112 for max_lines in [None, Some(1), Some(2)] {
1113 let (min, max) = layout.content_widths(text, max_lines);
1114 assert!(min <= max, "min {min} > max {max} for {text:?} with {max_lines:?}");
1115 }
1116 }
1117}