1use 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")]
44pub 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 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 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 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(), 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 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 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#[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}