Skip to main content

epaint/text/
text_layout.rs

1#![expect(clippy::unwrap_used)] // TODO(emilk): remove unwraps
2
3use 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// ----------------------------------------------------------------------------
25
26/// Returns `true` if the character is a Unicode combining mark (categories Mn, Mc, Me).
27///
28/// These characters modify the preceding base character and should not be
29/// rendered as standalone replacement glyphs when the shaper can't handle them.
30#[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/// Represents GUI scale and convenience methods for rounding to pixels.
42#[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// ----------------------------------------------------------------------------
70
71/// Temporary storage before line-wrapping.
72#[derive(Clone)]
73struct Paragraph {
74    /// Start of the next glyph to be added. In screen-space / physical pixels.
75    pub cursor_x_px: f32,
76
77    /// This is included in case there are no glyphs
78    pub section_index_at_start: u32,
79
80    pub glyphs: Vec<Glyph>,
81
82    /// In case of an empty paragraph ("\n"), use this as height.
83    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
97/// Layout text into a [`Galley`].
98///
99/// In most cases you should use [`crate::FontsView::layout_job`] instead
100/// since that memoizes the input, making subsequent layouting of the same text much faster.
101pub 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        // Early-out: no text
108        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    // For most of this we ignore the y coordinate:
122
123    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(&section.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, &paragraphs);
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    // Calculate the Y positions and tessellate the text:
174    galley_from_rows(point_scale, job, rows, elided, intrinsic_size)
175}
176
177/// Shared context for emitting shaped glyphs into a [`Paragraph`].
178struct 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/// Produced by [`segment_into_runs`] for text shaping.
215#[derive(Debug)]
216struct TextRun {
217    /// Which font face should shape this run.
218    font_key: FontFaceKey,
219
220    /// Byte range within the section text.
221    byte_range: ByteRange,
222}
223
224/// Emit shaped glyphs from a [`harfrust::GlyphBuffer`] into a [`Paragraph`].
225///
226/// When a cluster maps multiple characters to fewer glyphs (e.g. flag emojis,
227/// ligatures), zero-width "continuation" glyphs are emitted for the extra
228/// characters so that `glyphs.len() == char_count` — an invariant that all
229/// cursor and selection code relies on.
230fn 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    // Reset cluster tracking — cluster values are byte offsets within run_text,
242    // so they are not comparable across runs.
243    ctx.prev_cluster = None;
244
245    // Track how many glyphs we emit per cluster so we can add zero-width
246    // continuation glyphs when a cluster has more chars than glyphs.
247    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); // harfrust Y+ up → screen Y+ down
256
257        let chr = run_text
258            .get(cluster as usize..)
259            .and_then(|s| s.chars().next())
260            .unwrap_or('\u{FFFD}'); // Unicode Replacement Character
261
262        // Tab is a layout concept, not a glyph — the shaper doesn't know about tab stops.
263        // Override the advance width using the font's configured tab size.
264        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        // Thin space (U+2009) and narrow no-break space (U+202F):
273        // override the shaper's advance width with the configured fraction of a space.
274        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        // Apply extra_letter_spacing only at cluster boundaries,
283        // never between glyphs within the same cluster (e.g. base + mark).
284        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            // The shaper couldn't map this character. Drop combining marks
307            // (Unicode category M) and duplicate NOTDEF glyphs within the same
308            // cluster — only the first base character gets a replacement glyph.
309            if is_combining_mark(chr) || !is_new_cluster {
310                continue;
311            }
312
313            // Use the fallback font face (not run.font_key which returned NOTDEF).
314            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            // Apply shaper y_offset — this varies per glyph instance so it
366            // is not part of the cached ShapedGlyph / GlyphAllocation.
367            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    // Emit continuation glyphs for the last cluster in the run.
384    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
396/// Emit zero-width continuation glyphs when a cluster has more characters than
397/// shaped glyphs.
398///
399/// This preserves the invariant `glyphs.len() == char_count` that all cursor
400/// and text-selection code depends on. Continuation glyphs have
401/// [`UvRect::default()`] so [`tessellate_glyphs`] skips them entirely.
402fn 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// Ignores the Y coordinate.
428#[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    // Process each paragraph segment (split on newlines — the shaper can't handle them).
472    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            // Set buffer flags for paragraph boundary context.
496            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
523/// Iterator that either splits on `'\n'` or yields the whole string once.
524/// Avoids `Box<dyn Iterator>` and `Vec<&str>` allocation.
525enum 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
551/// Calculate the intrinsic size of the text.
552///
553/// The result is eventually passed to `Response::intrinsic_size`.
554/// This works by calculating the size of each `Paragraph` (instead of each `Row`).
555fn 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        // Use the precise cursor position instead of `last_glyph.max_x()`,
563        // because glyph positions are pixel-snapped but the cursor tracks
564        // the exact subpixel advance. This ensures that when two galleys are
565        // placed side-by-side, the gap matches what it would be within a
566        // single galley.
567        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
584// Ignores the Y coordinate.
585fn 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            // Use precise cursor position for width instead of pixel-snapped
616            // `last_glyph.max_x()`, so that side-by-side galleys have the same
617            // spacing as characters within a single galley.
618            let paragraph_width = paragraph.cursor_x_px / pixels_per_point;
619            if paragraph_width <= job.effective_wrap_width() {
620                // Early-out optimization: the whole paragraph fits on one row.
621                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(&paragraph, 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    // Keeps track of good places to insert row break if we exceed `wrap_width`.
651    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            // Row break:
667
668            if first_row_indentation > 0.0
669                && !row_break_candidates.has_good_candidate(job.wrap.break_anywhere)
670            {
671                // Allow the first row to be completely empty, because we know there will be more space on the next row:
672                // TODO(emilk): this records the height of this first row as zero, though that is probably fine since first_row_indentation usually comes with a first_row_min_height.
673                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                // Start a new row:
711                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                // Found no place to break, so we have to overrun wrap_width.
716            }
717        }
718
719        row_break_candidates.add(i, &paragraph.glyphs[i..]);
720    }
721
722    if row_start_idx < paragraph.glyphs.len() {
723        // Final row of text:
724
725        if job.wrap.max_rows <= out_rows.len() {
726            *elided = true; // can't fit another row
727        } 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
756/// Trims the last glyphs in the row and replaces it with an overflow character (e.g. `…`).
757///
758/// Called before we have any Y coordinates.
759fn 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(&section.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, &section.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 // TODO(emilk): heed paragraph leading_space 😬
793        };
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        // Check if we're within width budget:
800        if overflow_glyph_x + replacement_glyph_width <= job.effective_wrap_width()
801            || row.glyphs.is_empty()
802        {
803            // we are done
804
805            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, &section.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, // filled in later
839            });
840            return;
841        }
842
843        // We didn't fit - pop the last glyph and try again.
844        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
852/// Horizontally aligned the text on a row.
853///
854/// Ignores the Y coordinate.
855fn 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)] // False positive
864
865    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        // There is only whitespace
879        (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); // Don't contract
922
923    let mut extra_x_per_space = 0.0;
924    if 0 < num_spaces_in_range && num_spaces_in_range < num_glyphs_in_range {
925        // Add an integral number of pixels between each glyph,
926        // and add the balance to the spaces:
927
928        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    // Note we ignore the leading/trailing whitespace here!
949    row.size.x = target_max_x - target_min_x;
950}
951
952/// Calculate the Y positions and tessellate the text.
953fn 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        // Now position each glyph vertically:
974        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                // Apply valign to the different in height of the entire row, and the height of this `Font`:
980                + format.valign.to_factor() * (max_row_height - glyph.line_height)
981
982                // When mixing different `FontImpl` (e.g. latin and emojis),
983                // we always center the difference:
984                + 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); // TODO(emilk): it would be better to do the calculations in pixels instead.
994    }
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; // No longer in use.
1014        for glyph in &mut row.glyphs {
1015            glyph.section_index = u32::MAX; // No longer in use.
1016        }
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
1107/// Create background for glyphs that have them.
1108/// Creates as few rectangular regions as possible.
1109fn 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                // continue the same background rectangle
1140            } 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
1207/// Add a horizontal line over a row of glyphs with a stroke and y decided by a callback.
1208fn 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(); // reusing path to avoid re-allocations.
1215
1216    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                // continue the same line
1238            } 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// ----------------------------------------------------------------------------
1253
1254/// Keeps track of good places to break a long row of text.
1255/// Will focus primarily on spaces, secondarily on things like `-`
1256#[derive(Clone, Copy, Default)]
1257struct RowBreakCandidates {
1258    /// Breaking at ` ` or other whitespace
1259    /// is always the primary candidate.
1260    space: Option<usize>,
1261
1262    /// Logograms (single character representing a whole word) or kana (Japanese hiragana and katakana) are good candidates for line break.
1263    cjk: Option<usize>,
1264
1265    /// Breaking anywhere before a CJK character is acceptable too.
1266    pre_cjk: Option<usize>,
1267
1268    /// Breaking at a dash is a super-
1269    /// good idea.
1270    dash: Option<usize>,
1271
1272    /// This is nicer for things like URLs, e.g. www.
1273    /// example.com.
1274    punctuation: Option<usize>,
1275
1276    /// Breaking after just random character is some
1277    /// times necessary.
1278    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
1355// ----------------------------------------------------------------------------
1356
1357/// Segment text into runs where each run uses a single font face.
1358///
1359/// Grapheme clusters are never split across runs: if a combining mark
1360/// falls back to a different font than its base character, it stays
1361/// with the base character's font (the shaper will handle it).
1362///
1363/// NOTE: Segmentation is by font face, not by Unicode script. A run may
1364/// mix scripts (e.g. Latin + Cyrillic) when they share the same font.
1365/// This is acceptable for scripts with similar shaping rules, but would
1366/// need script-aware splitting once RTL/bidi support is added.
1367///
1368/// Results are appended to `out` (which is cleared first) to allow
1369/// the caller to reuse the allocation across calls.
1370fn 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
1395/// Shape a text run and return the raw [`harfrust::GlyphBuffer`].
1396///
1397/// The caller should iterate `glyph_infos()` / `glyph_positions()` (both
1398/// `Copy` slices) and convert font units to pixels using `metrics.px_scale_factor`.
1399/// After iteration, recycle the buffer via `glyph_buffer.clear()`.
1400fn 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    // Build shaper with variable font instance if variation coordinates are set.
1411    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// ----------------------------------------------------------------------------
1437
1438#[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        // No matter where we wrap, we should be appending the newline character.
1458
1459        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        // ɔ̃ = U+0254 (LATIN SMALL LETTER OPEN O) + U+0303 (COMBINING TILDE)
1662        // With text shaping, the combining tilde should NOT produce a separate
1663        // advance — it should be positioned above ɔ via GPOS anchors.
1664        // Note: the default fonts don't contain U+0254, so the replacement glyph
1665        // is used. The key test is that the combining mark does NOT add extra width.
1666        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        // Basic test: shaped Latin text should produce the same number of glyphs as characters.
1701        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); // "A"
1749        assert_eq!(galley.rows[1].row.glyphs.len(), 0); // empty line
1750        assert_eq!(galley.rows[2].row.glyphs.len(), 1); // "B"
1751    }
1752
1753    #[test]
1754    fn test_shaping_mixed_font_fallback() {
1755        // Text with both Latin and emoji should work without panicking,
1756        // even though they use different font faces.
1757        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        // "Hi " (3) + "🎉" (1) + " bye" (4) = at least 8 glyphs
1770        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        // GPOS kerning: pairs like "AV", "VA", "AT" should be tighter than
1780        // the sum of individual character widths. Without text shaping, egui
1781        // only uses the legacy `kern` table, so these pairs had diff ≈ 0.
1782        // With harfrust, GPOS kerning applies proper negative adjustments.
1783        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    /// Regression test for <https://github.com/emilk/egui/issues/8087>.
1815    ///
1816    /// Multi-codepoint grapheme clusters (flag emojis, combining marks) must
1817    /// produce exactly as many glyphs as characters so that cursor positioning
1818    /// and text selection remain correct.
1819    #[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        // Each test case: (input text, expected char count)
1826        let cases: &[(&str, usize)] = &[
1827            // Flag emoji: two Regional Indicator codepoints → one visual glyph
1828            ("\u{1F1EF}\u{1F1F5}", 2), // 🇯🇵
1829            // Flag surrounded by ASCII
1830            ("A\u{1F1EB}\u{1F1F7}B", 4), // A🇫🇷B
1831            // Base char + combining acute accent
1832            ("e\u{0301}", 2), // é as decomposed
1833            // Multiple combining marks
1834            ("o\u{0302}\u{0323}", 3), // ộ
1835            // Plain ASCII (sanity check)
1836            ("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            // Verify that Row::text() reconstructs the input text.
1865            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            // Verify cursor round-trip: end cursor index == char count.
1869            assert_eq!(
1870                galley.end().index.0,
1871                expected_chars,
1872                "Galley::end().index mismatch for {text:?}",
1873            );
1874        }
1875    }
1876
1877    /// Verify that cursor positioning round-trips correctly for text
1878    /// containing multi-codepoint grapheme clusters (regression test for #8087).
1879    #[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        // "A" + flag emoji (2 codepoints) + "B" = 4 chars
1886        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        // Walking through every cursor index should produce valid positions.
1896        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            // Round-trip: position → cursor → position should be stable.
1908            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}