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