1#![expect(clippy::unwrap_used)] use core::{iter, ops::Range};
4use std::sync::Arc;
5
6use emath::{Align, GuiRounding as _, NumExt as _, Pos2, Rect, Vec2, pos2, vec2};
7
8use crate::{
9 Color32, Mesh, Stroke, Vertex,
10 stroke::PathStroke,
11 text::{
12 ByteIndex, ByteRange,
13 font::{StyledMetrics, UvRect, is_cjk, is_cjk_break_allowed},
14 fonts::FontFaceKey,
15 },
16};
17
18use super::{
19 ByteRangeExt as _, FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row,
20 RowVisuals, VariationCoords,
21 font::{Font, FontFace, ShapedGlyph},
22};
23
24#[inline]
31fn is_combining_mark(c: char) -> bool {
32 use unicode_general_category::{GeneralCategory, get_general_category};
33 matches!(
34 get_general_category(c),
35 GeneralCategory::NonspacingMark
36 | GeneralCategory::SpacingMark
37 | GeneralCategory::EnclosingMark
38 )
39}
40
41#[derive(Clone, Copy)]
43struct PointScale {
44 pub pixels_per_point: f32,
45}
46
47impl PointScale {
48 #[inline(always)]
49 pub fn new(pixels_per_point: f32) -> Self {
50 Self { pixels_per_point }
51 }
52
53 #[inline(always)]
54 pub fn pixels_per_point(&self) -> f32 {
55 self.pixels_per_point
56 }
57
58 #[inline(always)]
59 pub fn round_to_pixel(&self, point: f32) -> f32 {
60 (point * self.pixels_per_point).round() / self.pixels_per_point
61 }
62
63 #[inline(always)]
64 pub fn floor_to_pixel(&self, point: f32) -> f32 {
65 (point * self.pixels_per_point).floor() / self.pixels_per_point
66 }
67}
68
69#[derive(Clone)]
73struct Paragraph {
74 pub cursor_x_px: f32,
76
77 pub section_index_at_start: u32,
79
80 pub glyphs: Vec<Glyph>,
81
82 pub empty_paragraph_height: f32,
84}
85
86impl Paragraph {
87 pub fn from_section_index(section_index_at_start: u32) -> Self {
88 Self {
89 cursor_x_px: 0.0,
90 section_index_at_start,
91 glyphs: vec![],
92 empty_paragraph_height: 0.0,
93 }
94 }
95}
96
97pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc<LayoutJob>) -> Galley {
102 profiling::function_scope!();
103
104 job.debug_sanity_check();
105
106 if job.wrap.max_rows == 0 {
107 return Galley {
109 job,
110 rows: Default::default(),
111 rect: Rect::ZERO,
112 mesh_bounds: Rect::NOTHING,
113 num_vertices: 0,
114 num_indices: 0,
115 pixels_per_point,
116 elided: true,
117 intrinsic_size: Vec2::ZERO,
118 };
119 }
120
121 let mut paragraphs = vec![Paragraph::from_section_index(0)];
124 {
125 let mut shape_buffer = fonts.take_shape_buffer();
126 for (section_index, section) in job.sections.iter().enumerate() {
127 let mut font = fonts.font(§ion.format.font_id.family);
128 shape_buffer = layout_section(
129 &mut font,
130 shape_buffer,
131 pixels_per_point,
132 &job,
133 section_index as u32,
134 section,
135 &mut paragraphs,
136 );
137 }
138 fonts.return_shape_buffer(shape_buffer);
139 }
140
141 let point_scale = PointScale::new(pixels_per_point);
142
143 let intrinsic_size = calculate_intrinsic_size(point_scale, &job, ¶graphs);
144
145 let mut elided = false;
146 let mut rows = rows_from_paragraphs(paragraphs, &job, pixels_per_point, &mut elided);
147 if elided && let Some(last_placed) = rows.last_mut() {
148 let last_row = Arc::make_mut(&mut last_placed.row);
149 replace_last_glyph_with_overflow_character(fonts, pixels_per_point, &job, last_row);
150 if let Some(last) = last_row.glyphs.last() {
151 last_row.size.x = last.max_x();
152 }
153 }
154
155 let justify = job.justify && job.wrap.max_width.is_finite();
156
157 if justify || job.halign != Align::LEFT {
158 let num_rows = rows.len();
159 for (i, placed_row) in rows.iter_mut().enumerate() {
160 let is_last_row = i + 1 == num_rows;
161 let justify_row = justify && !placed_row.ends_with_newline && !is_last_row;
162 halign_and_justify_row(
163 point_scale,
164 placed_row,
165 job.halign,
166 job.wrap.max_width,
167 justify_row,
168 job.keep_trailing_whitespace,
169 );
170 }
171 }
172
173 galley_from_rows(point_scale, job, rows, elided, intrinsic_size)
175}
176
177struct ShapingContext {
179 pixels_per_point: f32,
180 font_size: f32,
181 line_height: f32,
182 extra_letter_spacing: f32,
183 section_index: u32,
184 font_metrics: StyledMetrics,
185 is_first_glyph_in_section: bool,
186 prev_cluster: Option<u32>,
187}
188
189impl ShapingContext {
190 fn glyph(
191 &self,
192 chr: char,
193 physical_x: i32,
194 advance_width_px: f32,
195 face_metrics: &StyledMetrics,
196 uv_rect: UvRect,
197 ) -> Glyph {
198 Glyph {
199 chr,
200 pos: pos2(physical_x as f32 / self.pixels_per_point, f32::NAN),
201 advance_width: advance_width_px / self.pixels_per_point,
202 line_height: self.line_height,
203 font_face_height: face_metrics.row_height,
204 font_face_ascent: face_metrics.ascent,
205 font_height: self.font_metrics.row_height,
206 font_ascent: self.font_metrics.ascent,
207 uv_rect,
208 section_index: self.section_index,
209 first_vertex: 0,
210 }
211 }
212}
213
214#[derive(Debug)]
216struct TextRun {
217 font_key: FontFaceKey,
219
220 byte_range: ByteRange,
222}
223
224fn layout_shaped_run(
231 font: &mut Font<'_>,
232 run: &TextRun,
233 run_text: &str,
234 glyph_buffer: &harfrust::GlyphBuffer,
235 face_metrics: &StyledMetrics,
236 ctx: &mut ShapingContext,
237 paragraph: &mut Paragraph,
238) {
239 let px_scale = face_metrics.px_scale_factor;
240
241 ctx.prev_cluster = None;
244
245 let mut cluster_start_byte: usize = 0;
248 let mut cluster_glyph_count: usize = 0;
249
250 for (info, pos) in iter::zip(glyph_buffer.glyph_infos(), glyph_buffer.glyph_positions()) {
251 let glyph_id = skrifa::GlyphId::new(info.glyph_id);
252 let cluster = info.cluster;
253 let mut advance_width_px = pos.x_advance as f32 * px_scale;
254 let x_offset_px = pos.x_offset as f32 * px_scale;
255 let y_offset_px = -(pos.y_offset as f32 * px_scale); let chr = run_text
258 .get(cluster as usize..)
259 .and_then(|s| s.chars().next())
260 .unwrap_or('\u{FFFD}'); if chr == '\t' {
265 let tweak = font.fonts_by_id.get(&run.font_key).map(|ff| ff.tweak());
266 let tab_size = tweak.map_or(4.0, |t| t.tab_size);
267 let (_, space_info) = font.glyph_info(' ', face_metrics);
268 let space_width_px = space_info.advance_width_unscaled.0 * px_scale;
269 advance_width_px = tab_size * space_width_px;
270 }
271
272 if chr == '\u{2009}' || chr == '\u{202F}' {
275 let tweak = font.fonts_by_id.get(&run.font_key).map(|ff| ff.tweak());
276 let thin_space_width = tweak.map_or(0.5, |t| t.thin_space_width);
277 let (_, space_info) = font.glyph_info(' ', face_metrics);
278 let space_width_px = space_info.advance_width_unscaled.0 * px_scale;
279 advance_width_px = thin_space_width * space_width_px;
280 }
281
282 let is_new_cluster = ctx.prev_cluster.is_none_or(|pc| pc != cluster);
285 if is_new_cluster {
286 if ctx.prev_cluster.is_some() {
287 emit_continuation_glyphs(
288 ctx,
289 paragraph,
290 run_text,
291 cluster_start_byte..cluster as usize,
292 cluster_glyph_count,
293 face_metrics,
294 );
295 }
296 if !ctx.is_first_glyph_in_section {
297 paragraph.cursor_x_px += ctx.extra_letter_spacing * ctx.pixels_per_point;
298 }
299 cluster_start_byte = cluster as usize;
300 cluster_glyph_count = 0;
301 ctx.is_first_glyph_in_section = false;
302 }
303 ctx.prev_cluster = Some(cluster);
304
305 let glyph = if glyph_id == skrifa::GlyphId::NOTDEF {
306 if is_combining_mark(chr) || !is_new_cluster {
310 continue;
311 }
312
313 let fallback_key = font.resolve_face(chr);
315 let fallback_metrics = font
316 .fonts_by_id
317 .get(&fallback_key)
318 .map(|ff| {
319 ff.styled_metrics(ctx.pixels_per_point, ctx.font_size, &Default::default())
320 })
321 .unwrap_or_default();
322 let (_, glyph_info) = font.glyph_info(chr, &fallback_metrics);
323 let advance_width_px =
324 glyph_info.advance_width_unscaled.0 * fallback_metrics.px_scale_factor;
325 let (glyph_alloc, physical_x) =
326 if let Some(ff) = font.fonts_by_id.get_mut(&fallback_key) {
327 ff.allocate_glyph(
328 font.atlas,
329 &fallback_metrics,
330 &ShapedGlyph {
331 glyph_id: glyph_info.id.unwrap_or(skrifa::GlyphId::NOTDEF),
332 h_pos: paragraph.cursor_x_px,
333 is_cjk: is_cjk(chr),
334 },
335 )
336 } else {
337 Default::default()
338 };
339
340 paragraph.cursor_x_px += advance_width_px;
341
342 ctx.glyph(
343 chr,
344 physical_x,
345 advance_width_px,
346 &fallback_metrics,
347 glyph_alloc.uv_rect,
348 )
349 } else {
350 let (mut glyph_alloc, physical_x) =
351 if let Some(ff) = font.fonts_by_id.get_mut(&run.font_key) {
352 ff.allocate_glyph(
353 font.atlas,
354 face_metrics,
355 &ShapedGlyph {
356 glyph_id,
357 h_pos: paragraph.cursor_x_px + x_offset_px,
358 is_cjk: is_cjk(chr),
359 },
360 )
361 } else {
362 Default::default()
363 };
364
365 glyph_alloc.uv_rect.offset.y += y_offset_px / ctx.pixels_per_point;
368
369 paragraph.cursor_x_px += advance_width_px;
370
371 ctx.glyph(
372 chr,
373 physical_x,
374 advance_width_px,
375 face_metrics,
376 glyph_alloc.uv_rect,
377 )
378 };
379 paragraph.glyphs.push(glyph);
380 cluster_glyph_count += 1;
381 }
382
383 if ctx.prev_cluster.is_some() {
385 emit_continuation_glyphs(
386 ctx,
387 paragraph,
388 run_text,
389 cluster_start_byte..run_text.len(),
390 cluster_glyph_count,
391 face_metrics,
392 );
393 }
394}
395
396fn emit_continuation_glyphs(
403 ctx: &ShapingContext,
404 paragraph: &mut Paragraph,
405 run_text: &str,
406 cluster_bytes: Range<usize>,
407 cluster_glyph_count: usize,
408 face_metrics: &StyledMetrics,
409) {
410 let Some(cluster_text) = run_text.get(cluster_bytes) else {
411 return;
412 };
413 let char_count = cluster_text.chars().count();
414 if char_count <= cluster_glyph_count {
415 return;
416 }
417
418 let physical_x = paragraph.cursor_x_px.round() as i32;
419
420 for chr in cluster_text.chars().skip(cluster_glyph_count) {
421 paragraph
422 .glyphs
423 .push(ctx.glyph(chr, physical_x, 0.0, face_metrics, UvRect::default()));
424 }
425}
426
427#[must_use]
429fn layout_section(
430 font: &mut Font<'_>,
431 mut shape_buffer: harfrust::UnicodeBuffer,
432 pixels_per_point: f32,
433 job: &LayoutJob,
434 section_index: u32,
435 section: &LayoutSection,
436 out_paragraphs: &mut Vec<Paragraph>,
437) -> harfrust::UnicodeBuffer {
438 let LayoutSection {
439 leading_space,
440 byte_range,
441 format,
442 } = section;
443
444 let font_size = format.font_id.size;
445 let font_metrics = font.styled_metrics(pixels_per_point, font_size, &format.coords);
446 let line_height = section
447 .format
448 .line_height
449 .unwrap_or(font_metrics.row_height);
450 let extra_letter_spacing = section.format.extra_letter_spacing;
451
452 let mut paragraph = out_paragraphs.last_mut().unwrap();
453 if paragraph.glyphs.is_empty() {
454 paragraph.empty_paragraph_height = line_height;
455 }
456 paragraph.cursor_x_px += leading_space * pixels_per_point;
457
458 let section_text = &job.text[byte_range.as_usize()];
459 let mut ctx = ShapingContext {
460 pixels_per_point,
461 font_size,
462 line_height,
463 extra_letter_spacing,
464 section_index,
465 font_metrics,
466 is_first_glyph_in_section: paragraph.glyphs.is_empty(),
467 prev_cluster: None,
468 };
469 let mut runs = Vec::new();
470
471 for (seg_idx, segment) in SplitOrWhole::new(section_text, job.break_on_newline).enumerate() {
473 if 0 < seg_idx {
474 paragraph = out_paragraphs.push_mut(Paragraph::from_section_index(section_index));
475 paragraph.empty_paragraph_height = line_height;
476 ctx.is_first_glyph_in_section = true;
477 }
478
479 if segment.is_empty() {
480 continue;
481 }
482
483 segment_into_runs(font, segment, &mut runs);
484
485 let num_runs = runs.len();
486 for (run_idx, run) in runs.iter().enumerate() {
487 let run_text = &segment[run.byte_range.as_usize()];
488 let Some(font_face) = font.fonts_by_id.get(&run.font_key) else {
489 continue;
490 };
491
492 let face_metrics =
493 font_face.styled_metrics(pixels_per_point, font_size, &format.coords);
494
495 let mut flags = harfrust::BufferFlags::empty();
497 if run_idx == 0 {
498 flags |= harfrust::BufferFlags::BEGINNING_OF_TEXT;
499 }
500 if run_idx + 1 == num_runs {
501 flags |= harfrust::BufferFlags::END_OF_TEXT;
502 }
503
504 let glyph_buffer = shape_text(font_face, run_text, &format.coords, shape_buffer, flags);
505
506 layout_shaped_run(
507 font,
508 run,
509 run_text,
510 &glyph_buffer,
511 &face_metrics,
512 &mut ctx,
513 paragraph,
514 );
515
516 shape_buffer = glyph_buffer.clear();
517 }
518 }
519
520 shape_buffer
521}
522
523enum SplitOrWhole<'a> {
526 Split(core::str::Split<'a, char>),
527 Whole(iter::Once<&'a str>),
528}
529
530impl<'a> SplitOrWhole<'a> {
531 fn new(text: &'a str, split: bool) -> Self {
532 if split {
533 Self::Split(text.split('\n'))
534 } else {
535 Self::Whole(iter::once(text))
536 }
537 }
538}
539
540impl<'a> Iterator for SplitOrWhole<'a> {
541 type Item = &'a str;
542
543 fn next(&mut self) -> Option<&'a str> {
544 match self {
545 Self::Split(iter) => iter.next(),
546 Self::Whole(iter) => iter.next(),
547 }
548 }
549}
550
551fn calculate_intrinsic_size(
556 point_scale: PointScale,
557 job: &LayoutJob,
558 paragraphs: &[Paragraph],
559) -> Vec2 {
560 let mut intrinsic_size = Vec2::ZERO;
561 for (idx, paragraph) in paragraphs.iter().enumerate() {
562 let width = paragraph.cursor_x_px / point_scale.pixels_per_point;
568 intrinsic_size.x = f32::max(intrinsic_size.x, width);
569
570 let mut height = paragraph
571 .glyphs
572 .iter()
573 .map(|g| g.line_height)
574 .max_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal))
575 .unwrap_or(paragraph.empty_paragraph_height);
576 if idx == 0 {
577 height = f32::max(height, job.first_row_min_height);
578 }
579 intrinsic_size.y += point_scale.round_to_pixel(height);
580 }
581 intrinsic_size
582}
583
584fn rows_from_paragraphs(
586 paragraphs: Vec<Paragraph>,
587 job: &LayoutJob,
588 pixels_per_point: f32,
589 elided: &mut bool,
590) -> Vec<PlacedRow> {
591 let num_paragraphs = paragraphs.len();
592
593 let mut rows = vec![];
594
595 for (i, paragraph) in paragraphs.into_iter().enumerate() {
596 if job.wrap.max_rows <= rows.len() {
597 *elided = true;
598 break;
599 }
600
601 let is_last_paragraph = (i + 1) == num_paragraphs;
602
603 if paragraph.glyphs.is_empty() {
604 rows.push(PlacedRow {
605 pos: pos2(0.0, f32::NAN),
606 row: Arc::new(Row {
607 section_index_at_start: paragraph.section_index_at_start,
608 glyphs: vec![],
609 visuals: Default::default(),
610 size: vec2(0.0, paragraph.empty_paragraph_height),
611 }),
612 ends_with_newline: !is_last_paragraph,
613 });
614 } else {
615 let paragraph_width = paragraph.cursor_x_px / pixels_per_point;
619 if paragraph_width <= job.effective_wrap_width() {
620 rows.push(PlacedRow {
622 pos: pos2(0.0, f32::NAN),
623 row: Arc::new(Row {
624 section_index_at_start: paragraph.section_index_at_start,
625 glyphs: paragraph.glyphs,
626 visuals: Default::default(),
627 size: vec2(paragraph_width, 0.0),
628 }),
629 ends_with_newline: !is_last_paragraph,
630 });
631 } else {
632 line_break(¶graph, job, &mut rows, elided);
633 let placed_row = rows.last_mut().unwrap();
634 placed_row.ends_with_newline = !is_last_paragraph;
635 }
636 }
637 }
638
639 rows
640}
641
642fn line_break(
643 paragraph: &Paragraph,
644 job: &LayoutJob,
645 out_rows: &mut Vec<PlacedRow>,
646 elided: &mut bool,
647) {
648 let wrap_width = job.effective_wrap_width();
649
650 let mut row_break_candidates = RowBreakCandidates::default();
652
653 let mut first_row_indentation = paragraph.glyphs[0].pos.x;
654 let mut row_start_x = 0.0;
655 let mut row_start_idx = 0;
656
657 for i in 0..paragraph.glyphs.len() {
658 if job.wrap.max_rows <= out_rows.len() {
659 *elided = true;
660 break;
661 }
662
663 let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x;
664
665 if wrap_width < potential_row_width {
666 if first_row_indentation > 0.0
669 && !row_break_candidates.has_good_candidate(job.wrap.break_anywhere)
670 {
671 out_rows.push(PlacedRow {
674 pos: pos2(0.0, f32::NAN),
675 row: Arc::new(Row {
676 section_index_at_start: paragraph.section_index_at_start,
677 glyphs: vec![],
678 visuals: Default::default(),
679 size: Vec2::ZERO,
680 }),
681 ends_with_newline: false,
682 });
683 row_start_x += first_row_indentation;
684 first_row_indentation = 0.0;
685 } else if let Some(last_kept_index) = row_break_candidates.get(job.wrap.break_anywhere)
686 {
687 let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..=last_kept_index]
688 .iter()
689 .copied()
690 .map(|mut glyph| {
691 glyph.pos.x -= row_start_x;
692 glyph
693 })
694 .collect();
695
696 let section_index_at_start = glyphs[0].section_index;
697 let paragraph_max_x = glyphs.last().unwrap().max_x();
698
699 out_rows.push(PlacedRow {
700 pos: pos2(0.0, f32::NAN),
701 row: Arc::new(Row {
702 section_index_at_start,
703 glyphs,
704 visuals: Default::default(),
705 size: vec2(paragraph_max_x, 0.0),
706 }),
707 ends_with_newline: false,
708 });
709
710 row_start_idx = last_kept_index + 1;
712 row_start_x = paragraph.glyphs[row_start_idx].pos.x;
713 row_break_candidates.forget_before_idx(row_start_idx);
714 } else {
715 }
717 }
718
719 row_break_candidates.add(i, ¶graph.glyphs[i..]);
720 }
721
722 if row_start_idx < paragraph.glyphs.len() {
723 if job.wrap.max_rows <= out_rows.len() {
726 *elided = true; } else {
728 let paragraph_min_x = paragraph.glyphs[row_start_idx].pos.x - row_start_x;
729 let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x() - row_start_x;
730
731 let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..]
732 .iter()
733 .copied()
734 .map(|mut glyph| {
735 glyph.pos.x -= row_start_x + paragraph_min_x;
736 glyph
737 })
738 .collect();
739
740 let section_index_at_start = glyphs[0].section_index;
741
742 out_rows.push(PlacedRow {
743 pos: pos2(paragraph_min_x, 0.0),
744 row: Arc::new(Row {
745 section_index_at_start,
746 glyphs,
747 visuals: Default::default(),
748 size: vec2(paragraph_max_x - paragraph_min_x, 0.0),
749 }),
750 ends_with_newline: false,
751 });
752 }
753 }
754}
755
756fn replace_last_glyph_with_overflow_character(
760 fonts: &mut FontsImpl,
761 pixels_per_point: f32,
762 job: &LayoutJob,
763 row: &mut Row,
764) {
765 let Some(overflow_character) = job.wrap.overflow_character else {
766 return;
767 };
768
769 let mut section_index = row
770 .glyphs
771 .last()
772 .map(|g| g.section_index)
773 .unwrap_or(row.section_index_at_start);
774 loop {
775 let section = &job.sections[section_index as usize];
776 let extra_letter_spacing = section.format.extra_letter_spacing;
777 let mut font = fonts.font(§ion.format.font_id.family);
778 let font_size = section.format.font_id.size;
779
780 let font_id = font.resolve_face(overflow_character);
781 let font_face_metrics = font
782 .fonts_by_id
783 .get(&font_id)
784 .map(|f| f.styled_metrics(pixels_per_point, font_size, §ion.format.coords))
785 .unwrap_or_default();
786 let (_, glyph_info) = font.glyph_info(overflow_character, &font_face_metrics);
787 let mut font_face = font.fonts_by_id.get_mut(&font_id);
788
789 let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() {
790 prev_glyph.max_x() + extra_letter_spacing
791 } else {
792 0.0 };
794
795 let advance_width_px =
796 glyph_info.advance_width_unscaled.0 * font_face_metrics.px_scale_factor;
797 let replacement_glyph_width = advance_width_px / pixels_per_point;
798
799 if overflow_glyph_x + replacement_glyph_width <= job.effective_wrap_width()
801 || row.glyphs.is_empty()
802 {
803 let (replacement_glyph_alloc, physical_x) = font_face
806 .as_mut()
807 .map(|f| {
808 f.allocate_glyph(
809 font.atlas,
810 &font_face_metrics,
811 &ShapedGlyph {
812 glyph_id: glyph_info.id.unwrap_or(skrifa::GlyphId::NOTDEF),
813 h_pos: overflow_glyph_x * pixels_per_point,
814 is_cjk: is_cjk(overflow_character),
815 },
816 )
817 })
818 .unwrap_or_default();
819
820 let font_metrics =
821 font.styled_metrics(pixels_per_point, font_size, §ion.format.coords);
822 let line_height = section
823 .format
824 .line_height
825 .unwrap_or(font_metrics.row_height);
826
827 row.glyphs.push(Glyph {
828 chr: overflow_character,
829 pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
830 advance_width: advance_width_px / pixels_per_point,
831 line_height,
832 font_face_height: font_face_metrics.row_height,
833 font_face_ascent: font_face_metrics.ascent,
834 font_height: font_metrics.row_height,
835 font_ascent: font_metrics.ascent,
836 uv_rect: replacement_glyph_alloc.uv_rect,
837 section_index,
838 first_vertex: 0, });
840 return;
841 }
842
843 if let Some(last_glyph) = row.glyphs.pop() {
845 section_index = last_glyph.section_index;
846 } else {
847 section_index = row.section_index_at_start;
848 }
849 }
850}
851
852fn halign_and_justify_row(
856 point_scale: PointScale,
857 placed_row: &mut PlacedRow,
858 halign: Align,
859 wrap_width: f32,
860 justify: bool,
861 keep_trailing_whitespace: bool,
862) {
863 #![expect(clippy::useless_let_if_seq)] let row = Arc::make_mut(&mut placed_row.row);
866
867 if row.glyphs.is_empty() {
868 return;
869 }
870
871 let num_leading_spaces = row
872 .glyphs
873 .iter()
874 .take_while(|glyph| glyph.chr.is_whitespace())
875 .count();
876
877 let glyph_range = if num_leading_spaces == row.glyphs.len() {
878 (0, row.glyphs.len())
880 } else if keep_trailing_whitespace {
881 (num_leading_spaces, row.glyphs.len())
882 } else {
883 let num_trailing_spaces = row
884 .glyphs
885 .iter()
886 .rev()
887 .take_while(|glyph| glyph.chr.is_whitespace())
888 .count();
889
890 (num_leading_spaces, row.glyphs.len() - num_trailing_spaces)
891 };
892 let num_glyphs_in_range = glyph_range.1 - glyph_range.0;
893 assert!(num_glyphs_in_range > 0, "Should have at least one glyph");
894
895 let original_min_x = row.glyphs[glyph_range.0].logical_rect().min.x;
896 let original_max_x = row.glyphs[glyph_range.1 - 1].logical_rect().max.x;
897 let original_width = original_max_x - original_min_x;
898
899 let target_width = if justify && num_glyphs_in_range > 1 {
900 wrap_width
901 } else {
902 original_width
903 };
904
905 let (target_min_x, target_max_x) = match halign {
906 Align::LEFT => (0.0, target_width),
907 Align::Center => (-target_width / 2.0, target_width / 2.0),
908 Align::RIGHT => (-target_width, 0.0),
909 };
910
911 let num_spaces_in_range = row.glyphs[glyph_range.0..glyph_range.1]
912 .iter()
913 .filter(|glyph| glyph.chr.is_whitespace())
914 .count();
915
916 let mut extra_x_per_glyph = if num_glyphs_in_range == 1 {
917 0.0
918 } else {
919 (target_width - original_width) / (num_glyphs_in_range as f32 - 1.0)
920 };
921 extra_x_per_glyph = extra_x_per_glyph.at_least(0.0); let mut extra_x_per_space = 0.0;
924 if 0 < num_spaces_in_range && num_spaces_in_range < num_glyphs_in_range {
925 extra_x_per_glyph = point_scale.floor_to_pixel(extra_x_per_glyph);
929
930 extra_x_per_space = (target_width
931 - original_width
932 - extra_x_per_glyph * (num_glyphs_in_range as f32 - 1.0))
933 / (num_spaces_in_range as f32);
934 }
935
936 placed_row.pos.x = point_scale.round_to_pixel(target_min_x);
937 let mut translate_x = -original_min_x - extra_x_per_glyph * glyph_range.0 as f32;
938
939 for glyph in &mut row.glyphs {
940 glyph.pos.x += translate_x;
941 glyph.pos.x = point_scale.round_to_pixel(glyph.pos.x);
942 translate_x += extra_x_per_glyph;
943 if glyph.chr.is_whitespace() {
944 translate_x += extra_x_per_space;
945 }
946 }
947
948 row.size.x = target_max_x - target_min_x;
950}
951
952fn galley_from_rows(
954 point_scale: PointScale,
955 job: Arc<LayoutJob>,
956 mut rows: Vec<PlacedRow>,
957 elided: bool,
958 intrinsic_size: Vec2,
959) -> Galley {
960 let mut first_row_min_height = job.first_row_min_height;
961 let mut cursor_y = 0.0;
962
963 for placed_row in &mut rows {
964 let mut max_row_height = first_row_min_height.at_least(placed_row.height());
965 let row = Arc::make_mut(&mut placed_row.row);
966
967 first_row_min_height = 0.0;
968 for glyph in &row.glyphs {
969 max_row_height = max_row_height.at_least(glyph.line_height);
970 }
971 max_row_height = point_scale.round_to_pixel(max_row_height);
972
973 for glyph in &mut row.glyphs {
975 let format = &job.sections[glyph.section_index as usize].format;
976
977 glyph.pos.y = glyph.font_face_ascent
978
979 + format.valign.to_factor() * (max_row_height - glyph.line_height)
981
982 + 0.5 * (glyph.font_height - glyph.font_face_height);
985
986 glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y);
987 }
988
989 placed_row.pos.y = cursor_y;
990 row.size.y = max_row_height;
991
992 cursor_y += max_row_height;
993 cursor_y = point_scale.round_to_pixel(cursor_y); }
995
996 let format_summary = format_summary(&job);
997
998 let mut rect = Rect::ZERO;
999 let mut mesh_bounds = Rect::NOTHING;
1000 let mut num_vertices = 0;
1001 let mut num_indices = 0;
1002
1003 for placed_row in &mut rows {
1004 rect |= placed_row.rect();
1005
1006 let row = Arc::make_mut(&mut placed_row.row);
1007 row.visuals = tessellate_row(point_scale, &job, &format_summary, row);
1008
1009 mesh_bounds |= row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2());
1010 num_vertices += row.visuals.mesh.vertices.len();
1011 num_indices += row.visuals.mesh.indices.len();
1012
1013 row.section_index_at_start = u32::MAX; for glyph in &mut row.glyphs {
1015 glyph.section_index = u32::MAX; }
1017 }
1018
1019 let mut galley = Galley {
1020 job,
1021 rows,
1022 elided,
1023 rect,
1024 mesh_bounds,
1025 num_vertices,
1026 num_indices,
1027 pixels_per_point: point_scale.pixels_per_point,
1028 intrinsic_size,
1029 };
1030
1031 if galley.job.round_output_to_gui {
1032 galley.round_output_to_gui();
1033 }
1034
1035 galley
1036}
1037
1038#[derive(Default)]
1039struct FormatSummary {
1040 any_background: bool,
1041 any_underline: bool,
1042 any_strikethrough: bool,
1043}
1044
1045fn format_summary(job: &LayoutJob) -> FormatSummary {
1046 let mut format_summary = FormatSummary::default();
1047 for section in &job.sections {
1048 format_summary.any_background |= section.format.background != Color32::TRANSPARENT;
1049 format_summary.any_underline |= section.format.underline != Stroke::NONE;
1050 format_summary.any_strikethrough |= section.format.strikethrough != Stroke::NONE;
1051 }
1052 format_summary
1053}
1054
1055fn tessellate_row(
1056 point_scale: PointScale,
1057 job: &LayoutJob,
1058 format_summary: &FormatSummary,
1059 row: &mut Row,
1060) -> RowVisuals {
1061 if row.glyphs.is_empty() {
1062 return Default::default();
1063 }
1064
1065 let mut mesh = Mesh::default();
1066
1067 mesh.reserve_triangles(row.glyphs.len() * 2);
1068 mesh.reserve_vertices(row.glyphs.len() * 4);
1069
1070 if format_summary.any_background {
1071 add_row_backgrounds(point_scale, job, row, &mut mesh);
1072 }
1073
1074 let glyph_index_start = mesh.indices.len();
1075 let glyph_vertex_start = mesh.vertices.len();
1076 tessellate_glyphs(point_scale, job, row, &mut mesh);
1077 let glyph_vertex_end = mesh.vertices.len();
1078
1079 if format_summary.any_underline {
1080 add_row_hline(point_scale, row, &mut mesh, |glyph| {
1081 let format = &job.sections[glyph.section_index as usize].format;
1082 let stroke = format.underline;
1083 let y = glyph.logical_rect().bottom();
1084 (stroke, y)
1085 });
1086 }
1087
1088 if format_summary.any_strikethrough {
1089 add_row_hline(point_scale, row, &mut mesh, |glyph| {
1090 let format = &job.sections[glyph.section_index as usize].format;
1091 let stroke = format.strikethrough;
1092 let y = glyph.logical_rect().center().y;
1093 (stroke, y)
1094 });
1095 }
1096
1097 let mesh_bounds = mesh.calc_bounds();
1098
1099 RowVisuals {
1100 mesh,
1101 mesh_bounds,
1102 glyph_index_start,
1103 glyph_vertex_range: glyph_vertex_start..glyph_vertex_end,
1104 }
1105}
1106
1107fn add_row_backgrounds(point_scale: PointScale, job: &LayoutJob, row: &Row, mesh: &mut Mesh) {
1110 if row.glyphs.is_empty() {
1111 return;
1112 }
1113
1114 let mut end_run = |start: Option<(Color32, Rect, f32)>, stop_x: f32| {
1115 if let Some((color, start_rect, expand)) = start {
1116 let rect = Rect::from_min_max(start_rect.left_top(), pos2(stop_x, start_rect.bottom()));
1117 let rect = rect.expand(expand);
1118 let rect = rect.round_to_pixels(point_scale.pixels_per_point());
1119 mesh.add_colored_rect(rect, color);
1120 }
1121 };
1122
1123 let mut run_start = None;
1124 let mut last_rect = Rect::NAN;
1125
1126 for glyph in &row.glyphs {
1127 let format = &job.sections[glyph.section_index as usize].format;
1128 let color = format.background;
1129 let rect = glyph.logical_rect();
1130
1131 if color == Color32::TRANSPARENT {
1132 end_run(run_start.take(), last_rect.right());
1133 } else if let Some((existing_color, start, expand)) = run_start {
1134 if existing_color == color
1135 && start.top() == rect.top()
1136 && start.bottom() == rect.bottom()
1137 && format.expand_bg == expand
1138 {
1139 } else {
1141 end_run(run_start.take(), last_rect.right());
1142 run_start = Some((color, rect, format.expand_bg));
1143 }
1144 } else {
1145 run_start = Some((color, rect, format.expand_bg));
1146 }
1147
1148 last_rect = rect;
1149 }
1150
1151 end_run(run_start.take(), last_rect.right());
1152}
1153
1154fn tessellate_glyphs(point_scale: PointScale, job: &LayoutJob, row: &mut Row, mesh: &mut Mesh) {
1155 for glyph in &mut row.glyphs {
1156 glyph.first_vertex = mesh.vertices.len() as u32;
1157 let uv_rect = glyph.uv_rect;
1158 if !uv_rect.is_nothing() {
1159 let mut left_top = glyph.pos + uv_rect.offset;
1160 left_top.x = point_scale.round_to_pixel(left_top.x);
1161 left_top.y = point_scale.round_to_pixel(left_top.y);
1162
1163 let rect = Rect::from_min_max(left_top, left_top + uv_rect.size);
1164 let uv = Rect::from_min_max(
1165 pos2(uv_rect.min[0] as f32, uv_rect.min[1] as f32),
1166 pos2(uv_rect.max[0] as f32, uv_rect.max[1] as f32),
1167 );
1168
1169 let format = &job.sections[glyph.section_index as usize].format;
1170
1171 let color = format.color;
1172
1173 if format.italics {
1174 let idx = mesh.vertices.len() as u32;
1175 mesh.add_triangle(idx, idx + 1, idx + 2);
1176 mesh.add_triangle(idx + 2, idx + 1, idx + 3);
1177
1178 let top_offset = rect.height() * 0.25 * Vec2::X;
1179
1180 mesh.vertices.push(Vertex {
1181 pos: rect.left_top() + top_offset,
1182 uv: uv.left_top(),
1183 color,
1184 });
1185 mesh.vertices.push(Vertex {
1186 pos: rect.right_top() + top_offset,
1187 uv: uv.right_top(),
1188 color,
1189 });
1190 mesh.vertices.push(Vertex {
1191 pos: rect.left_bottom(),
1192 uv: uv.left_bottom(),
1193 color,
1194 });
1195 mesh.vertices.push(Vertex {
1196 pos: rect.right_bottom(),
1197 uv: uv.right_bottom(),
1198 color,
1199 });
1200 } else {
1201 mesh.add_rect_with_uv(rect, uv, color);
1202 }
1203 }
1204 }
1205}
1206
1207fn add_row_hline(
1209 point_scale: PointScale,
1210 row: &Row,
1211 mesh: &mut Mesh,
1212 stroke_and_y: impl Fn(&Glyph) -> (Stroke, f32),
1213) {
1214 let mut path = crate::tessellator::Path::default(); let mut end_line = |start: Option<(Stroke, Pos2)>, stop_x: f32| {
1217 if let Some((stroke, start)) = start {
1218 let stop = pos2(stop_x, start.y);
1219 path.clear();
1220 path.add_line_segment([start, stop]);
1221 let feathering = 1.0 / point_scale.pixels_per_point();
1222 path.stroke_open(feathering, &PathStroke::from(stroke), mesh);
1223 }
1224 };
1225
1226 let mut line_start = None;
1227 let mut last_right_x = f32::NAN;
1228
1229 for glyph in &row.glyphs {
1230 let (stroke, mut y) = stroke_and_y(glyph);
1231 stroke.round_center_to_pixel(point_scale.pixels_per_point, &mut y);
1232
1233 if stroke.is_empty() {
1234 end_line(line_start.take(), last_right_x);
1235 } else if let Some((existing_stroke, start)) = line_start {
1236 if existing_stroke == stroke && start.y == y {
1237 } else {
1239 end_line(line_start.take(), last_right_x);
1240 line_start = Some((stroke, pos2(glyph.pos.x, y)));
1241 }
1242 } else {
1243 line_start = Some((stroke, pos2(glyph.pos.x, y)));
1244 }
1245
1246 last_right_x = glyph.max_x();
1247 }
1248
1249 end_line(line_start.take(), last_right_x);
1250}
1251
1252#[derive(Clone, Copy, Default)]
1257struct RowBreakCandidates {
1258 space: Option<usize>,
1261
1262 cjk: Option<usize>,
1264
1265 pre_cjk: Option<usize>,
1267
1268 dash: Option<usize>,
1271
1272 punctuation: Option<usize>,
1275
1276 any: Option<usize>,
1279}
1280
1281impl RowBreakCandidates {
1282 fn add(&mut self, index: usize, glyphs: &[Glyph]) {
1283 let chr = glyphs[0].chr;
1284 const NON_BREAKING_SPACE: char = '\u{A0}';
1285 if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
1286 self.space = Some(index);
1287 } else if is_cjk(chr) && (glyphs.len() == 1 || is_cjk_break_allowed(glyphs[1].chr)) {
1288 self.cjk = Some(index);
1289 } else if chr == '-' {
1290 self.dash = Some(index);
1291 } else if chr.is_ascii_punctuation() {
1292 self.punctuation = Some(index);
1293 } else if glyphs.len() > 1 && is_cjk(glyphs[1].chr) {
1294 self.pre_cjk = Some(index);
1295 }
1296 self.any = Some(index);
1297 }
1298
1299 fn word_boundary(&self) -> Option<usize> {
1300 [self.space, self.cjk, self.pre_cjk]
1301 .into_iter()
1302 .max()
1303 .flatten()
1304 }
1305
1306 fn has_good_candidate(&self, break_anywhere: bool) -> bool {
1307 if break_anywhere {
1308 self.any.is_some()
1309 } else {
1310 self.word_boundary().is_some()
1311 }
1312 }
1313
1314 fn get(&self, break_anywhere: bool) -> Option<usize> {
1315 if break_anywhere {
1316 self.any
1317 } else {
1318 self.word_boundary()
1319 .or(self.dash)
1320 .or(self.punctuation)
1321 .or(self.any)
1322 }
1323 }
1324
1325 fn forget_before_idx(&mut self, index: usize) {
1326 let Self {
1327 space,
1328 cjk,
1329 pre_cjk,
1330 dash,
1331 punctuation,
1332 any,
1333 } = self;
1334 if space.is_some_and(|s| s < index) {
1335 *space = None;
1336 }
1337 if cjk.is_some_and(|s| s < index) {
1338 *cjk = None;
1339 }
1340 if pre_cjk.is_some_and(|s| s < index) {
1341 *pre_cjk = None;
1342 }
1343 if dash.is_some_and(|s| s < index) {
1344 *dash = None;
1345 }
1346 if punctuation.is_some_and(|s| s < index) {
1347 *punctuation = None;
1348 }
1349 if any.is_some_and(|s| s < index) {
1350 *any = None;
1351 }
1352 }
1353}
1354
1355fn segment_into_runs(font: &mut Font<'_>, text: &str, out: &mut Vec<TextRun>) {
1371 use unicode_segmentation::UnicodeSegmentation as _;
1372
1373 out.clear();
1374
1375 for (byte_offset, grapheme_str) in text.grapheme_indices(true) {
1376 let byte_offset = ByteIndex(byte_offset);
1377 let byte_end = byte_offset + grapheme_str.len();
1378
1379 let base_char = grapheme_str.chars().next().unwrap_or(' ');
1380 let font_key = font.resolve_face(base_char);
1381
1382 if let Some(last_run) = out.last_mut()
1383 && last_run.font_key == font_key
1384 {
1385 last_run.byte_range.end = byte_end;
1386 continue;
1387 }
1388 out.push(TextRun {
1389 font_key,
1390 byte_range: byte_offset..byte_end,
1391 });
1392 }
1393}
1394
1395fn shape_text(
1401 font_face: &FontFace,
1402 text: &str,
1403 coords: &VariationCoords,
1404 mut buffer: harfrust::UnicodeBuffer,
1405 flags: harfrust::BufferFlags,
1406) -> harfrust::GlyphBuffer {
1407 let font_ref = font_face.skrifa_font_ref();
1408 let tweak = font_face.tweak();
1409
1410 let variations: Vec<harfrust::Variation> = iter::chain(tweak.coords.as_ref(), coords.as_ref())
1412 .map(|&(tag, value)| harfrust::Variation { tag, value })
1413 .collect();
1414
1415 let instance = if variations.is_empty() {
1416 None
1417 } else {
1418 Some(harfrust::ShaperInstance::from_variations(
1419 font_ref, variations,
1420 ))
1421 };
1422
1423 let shaper = font_face
1424 .shaper_data()
1425 .shaper(font_ref)
1426 .instance(instance.as_ref())
1427 .build();
1428
1429 buffer.set_flags(flags);
1430 buffer.push_str(text);
1431 buffer.guess_segment_properties();
1432
1433 shaper.shape(buffer, harfrust::ShapeOptions::new())
1434}
1435
1436#[cfg(test)]
1439mod tests {
1440 use core::iter;
1441
1442 use super::{super::*, *};
1443 use crate::text::cursor::CCursor;
1444
1445 #[test]
1446 fn test_zero_max_width() {
1447 let pixels_per_point = 1.0;
1448 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1449 let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default());
1450 layout_job.wrap.max_width = 0.0;
1451 let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1452 assert_eq!(galley.rows.len(), 1);
1453 }
1454
1455 #[test]
1456 fn test_truncate_with_newline() {
1457 let pixels_per_point = 1.0;
1460
1461 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1462 let text_format = TextFormat {
1463 font_id: FontId::monospace(12.0),
1464 ..Default::default()
1465 };
1466
1467 for text in ["Hello\nworld", "\nfoo"] {
1468 for break_anywhere in [false, true] {
1469 for max_width in [0.0, 5.0, 10.0, 20.0, f32::INFINITY] {
1470 let mut layout_job =
1471 LayoutJob::single_section(text.into(), text_format.clone());
1472 layout_job.wrap.max_width = max_width;
1473 layout_job.wrap.max_rows = 1;
1474 layout_job.wrap.break_anywhere = break_anywhere;
1475
1476 let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1477
1478 assert!(galley.elided);
1479 assert_eq!(galley.rows.len(), 1);
1480 let row_text = galley.rows[0].text();
1481 assert!(
1482 row_text.ends_with('…'),
1483 "Expected row to end with `…`, got {row_text:?} when line-breaking the text {text:?} with max_width {max_width} and break_anywhere {break_anywhere}.",
1484 );
1485 }
1486 }
1487 }
1488
1489 {
1490 let mut layout_job = LayoutJob::single_section("Hello\nworld".into(), text_format);
1491 layout_job.wrap.max_width = 50.0;
1492 layout_job.wrap.max_rows = 1;
1493 layout_job.wrap.break_anywhere = false;
1494
1495 let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1496
1497 assert!(galley.elided);
1498 assert_eq!(galley.rows.len(), 1);
1499 let row_text = galley.rows[0].text();
1500 assert_eq!(row_text, "Hello…");
1501 }
1502 }
1503
1504 #[test]
1505 fn test_cjk() {
1506 let pixels_per_point = 1.0;
1507 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1508 let mut layout_job = LayoutJob::single_section(
1509 "日本語とEnglishの混在した文章".into(),
1510 TextFormat::default(),
1511 );
1512 layout_job.wrap.max_width = 90.0;
1513 let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1514 assert_eq!(
1515 galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
1516 vec!["日本語と", "Englishの混在", "した文章"]
1517 );
1518 }
1519
1520 #[test]
1521 fn test_pre_cjk() {
1522 let pixels_per_point = 1.0;
1523 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1524 let mut layout_job = LayoutJob::single_section(
1525 "日本語とEnglishの混在した文章".into(),
1526 TextFormat::default(),
1527 );
1528 layout_job.wrap.max_width = 110.0;
1529 let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1530 assert_eq!(
1531 galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
1532 vec!["日本語とEnglish", "の混在した文章"]
1533 );
1534 }
1535
1536 #[test]
1537 fn test_truncate_width() {
1538 let pixels_per_point = 1.0;
1539 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1540 let mut layout_job =
1541 LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default());
1542 layout_job.wrap.max_width = f32::INFINITY;
1543 layout_job.wrap.max_rows = 1;
1544 layout_job.round_output_to_gui = false;
1545 let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1546 assert!(galley.elided);
1547 assert_eq!(
1548 galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
1549 vec!["# DNA…"]
1550 );
1551 let row = &galley.rows[0];
1552 assert_eq!(row.pos, Pos2::ZERO);
1553 assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x());
1554 }
1555
1556 #[test]
1557 fn test_truncate_with_pixels_per_point() {
1558 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1559
1560 for pixels_per_point in [
1561 0.33, 0.5, 0.67, 1.0, 1.25, 1.33, 1.5, 1.75, 2.0, 3.0, 4.0, 5.0,
1562 ] {
1563 for ch in ['W', 'A', 'n', 't', 'i'] {
1564 let target_width = 50.0;
1565 let text = (0..20).map(|_| ch).collect::<String>();
1566
1567 let mut job = LayoutJob::single_section(text, TextFormat::default());
1568 job.wrap.max_width = target_width;
1569 job.wrap.max_rows = 1;
1570 let elided_galley = layout(&mut fonts, pixels_per_point, job.into());
1571 assert!(elided_galley.elided);
1572
1573 let test_galley = layout(
1574 &mut fonts,
1575 pixels_per_point,
1576 Arc::new(LayoutJob::single_section(
1577 iter::chain(
1578 (0..elided_galley.rows[0].char_count_excluding_newline().0).map(|_| ch),
1579 iter::once('…'),
1580 )
1581 .collect::<String>(),
1582 TextFormat::default(),
1583 )),
1584 );
1585
1586 assert!(elided_galley.size().x >= 0.0);
1587 assert!(elided_galley.size().x <= target_width);
1588 assert!(test_galley.size().x > target_width);
1589 }
1590 }
1591 }
1592
1593 #[test]
1594 fn test_empty_row() {
1595 let pixels_per_point = 1.0;
1596 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1597
1598 let font_id = FontId::default();
1599 let font_height = fonts
1600 .font(&font_id.family)
1601 .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
1602 .row_height;
1603
1604 let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY);
1605
1606 let galley = layout(&mut fonts, pixels_per_point, job.into());
1607
1608 assert_eq!(galley.rows.len(), 1, "Expected one row");
1609 assert_eq!(
1610 galley.rows[0].row.glyphs.len(),
1611 0,
1612 "Expected no glyphs in the empty row"
1613 );
1614 assert_eq!(
1615 galley.size(),
1616 Vec2::new(0.0, font_height.round()),
1617 "Unexpected galley size"
1618 );
1619 assert_eq!(
1620 galley.intrinsic_size(),
1621 Vec2::new(0.0, font_height.round()),
1622 "Unexpected intrinsic size"
1623 );
1624 }
1625
1626 #[test]
1627 fn test_end_with_newline() {
1628 let pixels_per_point = 1.0;
1629 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1630
1631 let font_id = FontId::default();
1632 let font_height = fonts
1633 .font(&font_id.family)
1634 .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
1635 .row_height;
1636
1637 let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY);
1638
1639 let galley = layout(&mut fonts, pixels_per_point, job.into());
1640
1641 assert_eq!(galley.rows.len(), 2, "Expected two rows");
1642 assert_eq!(
1643 galley.rows[1].row.glyphs.len(),
1644 0,
1645 "Expected no glyphs in the empty row"
1646 );
1647 assert_eq!(
1648 galley.size().round(),
1649 Vec2::new(17.0, font_height.round() * 2.0),
1650 "Unexpected galley size"
1651 );
1652 assert_eq!(
1653 galley.intrinsic_size().round(),
1654 Vec2::new(17.0, font_height.round() * 2.0),
1655 "Unexpected intrinsic size"
1656 );
1657 }
1658
1659 #[test]
1660 fn test_combining_diacritics() {
1661 let pixels_per_point = 1.0;
1667 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1668
1669 let job_combined = LayoutJob::simple(
1670 "ɔ\u{0303}".to_owned(),
1671 FontId::proportional(14.0),
1672 Color32::WHITE,
1673 f32::INFINITY,
1674 );
1675 let galley_combined = layout(&mut fonts, pixels_per_point, job_combined.into());
1676
1677 let job_base = LayoutJob::simple(
1678 "ɔ".to_owned(),
1679 FontId::proportional(14.0),
1680 Color32::WHITE,
1681 f32::INFINITY,
1682 );
1683 let galley_base = layout(&mut fonts, pixels_per_point, job_base.into());
1684
1685 let width_combined = galley_combined.size().x;
1686 let width_base = galley_base.size().x;
1687
1688 assert!(
1689 (width_combined - width_base).abs() < 2.0,
1690 "Combining diacritic should not add significant width. \
1691 Base width: {width_base}, Combined width: {width_combined}"
1692 );
1693
1694 let glyphs = &galley_combined.rows[0].row.glyphs;
1695 assert!(!glyphs.is_empty(), "Expected at least 1 glyph for ɔ̃");
1696 }
1697
1698 #[test]
1699 fn test_shaping_basic_latin() {
1700 let pixels_per_point = 1.0;
1702 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1703
1704 let job = LayoutJob::simple(
1705 "Hello".to_owned(),
1706 FontId::proportional(14.0),
1707 Color32::WHITE,
1708 f32::INFINITY,
1709 );
1710 let galley = layout(&mut fonts, pixels_per_point, job.into());
1711
1712 assert_eq!(galley.rows.len(), 1);
1713 assert_eq!(galley.rows[0].row.glyphs.len(), 5);
1714 assert!(galley.size().x > 0.0);
1715 }
1716
1717 #[test]
1718 fn test_shaping_empty_string() {
1719 let pixels_per_point = 1.0;
1720 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1721
1722 let job = LayoutJob::simple(
1723 String::new(),
1724 FontId::proportional(14.0),
1725 Color32::WHITE,
1726 f32::INFINITY,
1727 );
1728 let galley = layout(&mut fonts, pixels_per_point, job.into());
1729
1730 assert_eq!(galley.rows.len(), 1);
1731 assert_eq!(galley.rows[0].row.glyphs.len(), 0);
1732 }
1733
1734 #[test]
1735 fn test_shaping_multiple_newlines() {
1736 let pixels_per_point = 1.0;
1737 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1738
1739 let job = LayoutJob::simple(
1740 "A\n\nB".to_owned(),
1741 FontId::proportional(14.0),
1742 Color32::WHITE,
1743 f32::INFINITY,
1744 );
1745 let galley = layout(&mut fonts, pixels_per_point, job.into());
1746
1747 assert_eq!(galley.rows.len(), 3, "Expected 3 rows for 'A\\n\\nB'");
1748 assert_eq!(galley.rows[0].row.glyphs.len(), 1); assert_eq!(galley.rows[1].row.glyphs.len(), 0); assert_eq!(galley.rows[2].row.glyphs.len(), 1); }
1752
1753 #[test]
1754 fn test_shaping_mixed_font_fallback() {
1755 let pixels_per_point = 1.0;
1758 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1759
1760 let job = LayoutJob::simple(
1761 "Hi 🎉 bye".to_owned(),
1762 FontId::proportional(14.0),
1763 Color32::WHITE,
1764 f32::INFINITY,
1765 );
1766 let galley = layout(&mut fonts, pixels_per_point, job.into());
1767
1768 assert_eq!(galley.rows.len(), 1);
1769 assert!(
1771 galley.rows[0].row.glyphs.len() >= 8,
1772 "Expected >= 8 glyphs, got {}",
1773 galley.rows[0].row.glyphs.len()
1774 );
1775 }
1776
1777 #[test]
1778 fn test_gpos_kerning() {
1779 let pixels_per_point = 1.0;
1784 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1785 let font_id = FontId::proportional(14.0);
1786
1787 for pair in ["AV", "VA", "AT"] {
1788 let (pair_w, _, _) = measure_text(&mut fonts, pair, &font_id, pixels_per_point);
1789 let chars: Vec<char> = pair.chars().collect();
1790 let (w1, _, _) = measure_text(
1791 &mut fonts,
1792 &chars[0].to_string(),
1793 &font_id,
1794 pixels_per_point,
1795 );
1796 let (w2, _, _) = measure_text(
1797 &mut fonts,
1798 &chars[1].to_string(),
1799 &font_id,
1800 pixels_per_point,
1801 );
1802 let sum = w1 + w2;
1803 let kern_adjustment = sum - pair_w;
1804
1805 assert!(
1806 kern_adjustment > 0.5,
1807 "GPOS kerning for '{pair}': expected pair to be noticeably tighter \
1808 than sum of individuals. pair_width={pair_w:.2}, sum={sum:.2}, \
1809 kern_adjustment={kern_adjustment:.2} (should be > 0.5)",
1810 );
1811 }
1812 }
1813
1814 #[test]
1820 fn test_grapheme_cluster_glyph_count() {
1821 let pixels_per_point = 1.0;
1822 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1823 let font_id = FontId::default();
1824
1825 let cases: &[(&str, usize)] = &[
1827 ("\u{1F1EF}\u{1F1F5}", 2), ("A\u{1F1EB}\u{1F1F7}B", 4), ("e\u{0301}", 2), ("o\u{0302}\u{0323}", 3), ("Hello", 5),
1837 ];
1838
1839 for &(text, expected_chars) in cases {
1840 let job = LayoutJob::simple(
1841 text.to_owned(),
1842 font_id.clone(),
1843 Color32::WHITE,
1844 f32::INFINITY,
1845 );
1846 let galley = layout(&mut fonts, pixels_per_point, job.into());
1847
1848 let total_glyphs: usize = galley.rows.iter().map(|r| r.row.glyphs.len()).sum();
1849
1850 assert_eq!(
1851 total_glyphs,
1852 expected_chars,
1853 "Glyph count mismatch for {text:?}: \
1854 expected {expected_chars} glyphs (one per char), got {total_glyphs}. \
1855 Glyphs: {:?}",
1856 galley.rows[0]
1857 .row
1858 .glyphs
1859 .iter()
1860 .map(|g| (g.chr, g.advance_width))
1861 .collect::<Vec<_>>(),
1862 );
1863
1864 let row_text: String = galley.rows.iter().map(|r| r.text()).collect();
1866 assert_eq!(row_text, text, "Row::text() mismatch for {text:?}");
1867
1868 assert_eq!(
1870 galley.end().index.0,
1871 expected_chars,
1872 "Galley::end().index mismatch for {text:?}",
1873 );
1874 }
1875 }
1876
1877 #[test]
1880 fn test_grapheme_cluster_cursor_roundtrip() {
1881 let pixels_per_point = 1.0;
1882 let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1883 let font_id = FontId::default();
1884
1885 let text = "A\u{1F1EF}\u{1F1F5}B";
1887 let job = LayoutJob::simple(
1888 text.to_owned(),
1889 font_id.clone(),
1890 Color32::WHITE,
1891 f32::INFINITY,
1892 );
1893 let galley = layout(&mut fonts, pixels_per_point, job.into());
1894
1895 for i in 0..=galley.end().index.0 {
1897 let cursor = CCursor {
1898 index: CharIndex(i),
1899 prefer_next_row: false,
1900 };
1901 let rect = galley.pos_from_cursor(cursor);
1902 assert!(
1903 rect.is_finite(),
1904 "pos_from_cursor returned non-finite rect for index {i}",
1905 );
1906
1907 let cursor2 = galley.cursor_from_pos(Vec2::new(rect.center().x, rect.center().y));
1909 let rect2 = galley.pos_from_cursor(cursor2);
1910 assert!(
1911 (rect.min.x - rect2.min.x).abs() < 1.0,
1912 "Cursor round-trip unstable at index {i}: \
1913 first={}, second={}, cursor2.index={}",
1914 rect.min.x,
1915 rect2.min.x,
1916 cursor2.index,
1917 );
1918 }
1919 }
1920
1921 fn measure_text(
1922 fonts: &mut FontsImpl,
1923 text: &str,
1924 font_id: &FontId,
1925 pixels_per_point: f32,
1926 ) -> (f32, usize, Vec<(char, f32)>) {
1927 let job = LayoutJob::simple(
1928 text.to_owned(),
1929 font_id.clone(),
1930 Color32::WHITE,
1931 f32::INFINITY,
1932 );
1933 let galley = layout(fonts, pixels_per_point, job.into());
1934 let glyphs = &galley.rows[0].row.glyphs;
1935 let details: Vec<_> = glyphs.iter().map(|g| (g.chr, g.advance_width)).collect();
1936 (galley.size().x, glyphs.len(), details)
1937 }
1938}