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)) } 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 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(), 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 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 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#[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}