epaint/text/
text_layout.rs

1#![expect(clippy::unwrap_used)] // TODO(emilk): remove unwraps
2
3use std::sync::Arc;
4
5use emath::{Align, GuiRounding as _, NumExt as _, Pos2, Rect, Vec2, pos2, vec2};
6
7use crate::{
8    Color32, Mesh, Stroke, Vertex,
9    stroke::PathStroke,
10    text::{
11        font::{StyledMetrics, is_cjk, is_cjk_break_allowed},
12        fonts::FontFaceKey,
13    },
14};
15
16use super::{FontsImpl, Galley, Glyph, LayoutJob, LayoutSection, PlacedRow, Row, RowVisuals};
17
18// ----------------------------------------------------------------------------
19
20/// Represents GUI scale and convenience methods for rounding to pixels.
21#[derive(Clone, Copy)]
22struct PointScale {
23    pub pixels_per_point: f32,
24}
25
26impl PointScale {
27    #[inline(always)]
28    pub fn new(pixels_per_point: f32) -> Self {
29        Self { pixels_per_point }
30    }
31
32    #[inline(always)]
33    pub fn pixels_per_point(&self) -> f32 {
34        self.pixels_per_point
35    }
36
37    #[inline(always)]
38    pub fn round_to_pixel(&self, point: f32) -> f32 {
39        (point * self.pixels_per_point).round() / self.pixels_per_point
40    }
41
42    #[inline(always)]
43    pub fn floor_to_pixel(&self, point: f32) -> f32 {
44        (point * self.pixels_per_point).floor() / self.pixels_per_point
45    }
46}
47
48// ----------------------------------------------------------------------------
49
50/// Temporary storage before line-wrapping.
51#[derive(Clone)]
52struct Paragraph {
53    /// Start of the next glyph to be added. In screen-space / physical pixels.
54    pub cursor_x_px: f32,
55
56    /// This is included in case there are no glyphs
57    pub section_index_at_start: u32,
58
59    pub glyphs: Vec<Glyph>,
60
61    /// In case of an empty paragraph ("\n"), use this as height.
62    pub empty_paragraph_height: f32,
63}
64
65impl Paragraph {
66    pub fn from_section_index(section_index_at_start: u32) -> Self {
67        Self {
68            cursor_x_px: 0.0,
69            section_index_at_start,
70            glyphs: vec![],
71            empty_paragraph_height: 0.0,
72        }
73    }
74}
75
76/// Layout text into a [`Galley`].
77///
78/// In most cases you should use [`crate::FontsView::layout_job`] instead
79/// since that memoizes the input, making subsequent layouting of the same text much faster.
80pub fn layout(fonts: &mut FontsImpl, pixels_per_point: f32, job: Arc<LayoutJob>) -> Galley {
81    profiling::function_scope!();
82
83    if job.wrap.max_rows == 0 {
84        // Early-out: no text
85        return Galley {
86            job,
87            rows: Default::default(),
88            rect: Rect::ZERO,
89            mesh_bounds: Rect::NOTHING,
90            num_vertices: 0,
91            num_indices: 0,
92            pixels_per_point,
93            elided: true,
94            intrinsic_size: Vec2::ZERO,
95        };
96    }
97
98    // For most of this we ignore the y coordinate:
99
100    let mut paragraphs = vec![Paragraph::from_section_index(0)];
101    for (section_index, section) in job.sections.iter().enumerate() {
102        layout_section(
103            fonts,
104            pixels_per_point,
105            &job,
106            section_index as u32,
107            section,
108            &mut paragraphs,
109        );
110    }
111
112    let point_scale = PointScale::new(pixels_per_point);
113
114    let intrinsic_size = calculate_intrinsic_size(point_scale, &job, &paragraphs);
115
116    let mut elided = false;
117    let mut rows = rows_from_paragraphs(paragraphs, &job, pixels_per_point, &mut elided);
118    if elided && let Some(last_placed) = rows.last_mut() {
119        let last_row = Arc::make_mut(&mut last_placed.row);
120        replace_last_glyph_with_overflow_character(fonts, pixels_per_point, &job, last_row);
121        if let Some(last) = last_row.glyphs.last() {
122            last_row.size.x = last.max_x();
123        }
124    }
125
126    let justify = job.justify && job.wrap.max_width.is_finite();
127
128    if justify || job.halign != Align::LEFT {
129        let num_rows = rows.len();
130        for (i, placed_row) in rows.iter_mut().enumerate() {
131            let is_last_row = i + 1 == num_rows;
132            let justify_row = justify && !placed_row.ends_with_newline && !is_last_row;
133            halign_and_justify_row(
134                point_scale,
135                placed_row,
136                job.halign,
137                job.wrap.max_width,
138                justify_row,
139            );
140        }
141    }
142
143    // Calculate the Y positions and tessellate the text:
144    galley_from_rows(point_scale, job, rows, elided, intrinsic_size)
145}
146
147// Ignores the Y coordinate.
148fn layout_section(
149    fonts: &mut FontsImpl,
150    pixels_per_point: f32,
151    job: &LayoutJob,
152    section_index: u32,
153    section: &LayoutSection,
154    out_paragraphs: &mut Vec<Paragraph>,
155) {
156    let LayoutSection {
157        leading_space,
158        byte_range,
159        format,
160    } = section;
161    let mut font = fonts.font(&format.font_id.family);
162    let font_size = format.font_id.size;
163    let font_metrics = font.styled_metrics(pixels_per_point, font_size, &format.coords);
164    let line_height = section
165        .format
166        .line_height
167        .unwrap_or(font_metrics.row_height);
168    let extra_letter_spacing = section.format.extra_letter_spacing;
169
170    let mut paragraph = out_paragraphs.last_mut().unwrap();
171    if paragraph.glyphs.is_empty() {
172        paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
173    }
174
175    paragraph.cursor_x_px += leading_space * pixels_per_point;
176
177    let mut last_glyph_id = None;
178
179    // Optimization: only recompute `ScaledMetrics` when the concrete `FontImpl` changes.
180    let mut current_font = FontFaceKey::INVALID;
181    let mut current_font_face_metrics = StyledMetrics::default();
182
183    for chr in job.text[byte_range.clone()].chars() {
184        if job.break_on_newline && chr == '\n' {
185            out_paragraphs.push(Paragraph::from_section_index(section_index));
186            paragraph = out_paragraphs.last_mut().unwrap();
187            paragraph.empty_paragraph_height = line_height; // TODO(emilk): replace this hack with actually including `\n` in the glyphs?
188        } else {
189            let (font_id, glyph_info) = font.glyph_info(chr);
190            let mut font_face = font.fonts_by_id.get_mut(&font_id);
191            if current_font != font_id {
192                current_font = font_id;
193                current_font_face_metrics = font_face
194                    .as_ref()
195                    .map(|font_face| {
196                        font_face.styled_metrics(pixels_per_point, font_size, &format.coords)
197                    })
198                    .unwrap_or_default();
199            }
200
201            if let (Some(font_face), Some(last_glyph_id), Some(glyph_id)) =
202                (&font_face, last_glyph_id, glyph_info.id)
203            {
204                paragraph.cursor_x_px += font_face.pair_kerning_pixels(
205                    &current_font_face_metrics,
206                    last_glyph_id,
207                    glyph_id,
208                );
209
210                // Only apply extra_letter_spacing to glyphs after the first one:
211                paragraph.cursor_x_px += extra_letter_spacing * pixels_per_point;
212            }
213
214            let (glyph_alloc, physical_x) = if let Some(font_face) = font_face.as_mut() {
215                font_face.allocate_glyph(
216                    font.atlas,
217                    &current_font_face_metrics,
218                    glyph_info,
219                    chr,
220                    paragraph.cursor_x_px,
221                )
222            } else {
223                Default::default()
224            };
225
226            paragraph.glyphs.push(Glyph {
227                chr,
228                pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
229                advance_width: glyph_alloc.advance_width_px / pixels_per_point,
230                line_height,
231                font_face_height: current_font_face_metrics.row_height,
232                font_face_ascent: current_font_face_metrics.ascent,
233                font_height: font_metrics.row_height,
234                font_ascent: font_metrics.ascent,
235                uv_rect: glyph_alloc.uv_rect,
236                section_index,
237                first_vertex: 0, // filled in later
238            });
239
240            paragraph.cursor_x_px += glyph_alloc.advance_width_px;
241            last_glyph_id = Some(glyph_alloc.id);
242        }
243    }
244}
245
246/// Calculate the intrinsic size of the text.
247///
248/// The result is eventually passed to `Response::intrinsic_size`.
249/// This works by calculating the size of each `Paragraph` (instead of each `Row`).
250fn calculate_intrinsic_size(
251    point_scale: PointScale,
252    job: &LayoutJob,
253    paragraphs: &[Paragraph],
254) -> Vec2 {
255    let mut intrinsic_size = Vec2::ZERO;
256    for (idx, paragraph) in paragraphs.iter().enumerate() {
257        // Use the precise cursor position instead of `last_glyph.max_x()`,
258        // because glyph positions are pixel-snapped but the cursor tracks
259        // the exact subpixel advance. This ensures that when two galleys are
260        // placed side-by-side, the gap matches what it would be within a
261        // single galley.
262        let width = paragraph.cursor_x_px / point_scale.pixels_per_point;
263        intrinsic_size.x = f32::max(intrinsic_size.x, width);
264
265        let mut height = paragraph
266            .glyphs
267            .iter()
268            .map(|g| g.line_height)
269            .max_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
270            .unwrap_or(paragraph.empty_paragraph_height);
271        if idx == 0 {
272            height = f32::max(height, job.first_row_min_height);
273        }
274        intrinsic_size.y += point_scale.round_to_pixel(height);
275    }
276    intrinsic_size
277}
278
279// Ignores the Y coordinate.
280fn rows_from_paragraphs(
281    paragraphs: Vec<Paragraph>,
282    job: &LayoutJob,
283    pixels_per_point: f32,
284    elided: &mut bool,
285) -> Vec<PlacedRow> {
286    let num_paragraphs = paragraphs.len();
287
288    let mut rows = vec![];
289
290    for (i, paragraph) in paragraphs.into_iter().enumerate() {
291        if job.wrap.max_rows <= rows.len() {
292            *elided = true;
293            break;
294        }
295
296        let is_last_paragraph = (i + 1) == num_paragraphs;
297
298        if paragraph.glyphs.is_empty() {
299            rows.push(PlacedRow {
300                pos: pos2(0.0, f32::NAN),
301                row: Arc::new(Row {
302                    section_index_at_start: paragraph.section_index_at_start,
303                    glyphs: vec![],
304                    visuals: Default::default(),
305                    size: vec2(0.0, paragraph.empty_paragraph_height),
306                }),
307                ends_with_newline: !is_last_paragraph,
308            });
309        } else {
310            // Use precise cursor position for width instead of pixel-snapped
311            // `last_glyph.max_x()`, so that side-by-side galleys have the same
312            // spacing as characters within a single galley.
313            let paragraph_width = paragraph.cursor_x_px / pixels_per_point;
314            if paragraph_width <= job.effective_wrap_width() {
315                // Early-out optimization: the whole paragraph fits on one row.
316                rows.push(PlacedRow {
317                    pos: pos2(0.0, f32::NAN),
318                    row: Arc::new(Row {
319                        section_index_at_start: paragraph.section_index_at_start,
320                        glyphs: paragraph.glyphs,
321                        visuals: Default::default(),
322                        size: vec2(paragraph_width, 0.0),
323                    }),
324                    ends_with_newline: !is_last_paragraph,
325                });
326            } else {
327                line_break(&paragraph, job, &mut rows, elided);
328                let placed_row = rows.last_mut().unwrap();
329                placed_row.ends_with_newline = !is_last_paragraph;
330            }
331        }
332    }
333
334    rows
335}
336
337fn line_break(
338    paragraph: &Paragraph,
339    job: &LayoutJob,
340    out_rows: &mut Vec<PlacedRow>,
341    elided: &mut bool,
342) {
343    let wrap_width = job.effective_wrap_width();
344
345    // Keeps track of good places to insert row break if we exceed `wrap_width`.
346    let mut row_break_candidates = RowBreakCandidates::default();
347
348    let mut first_row_indentation = paragraph.glyphs[0].pos.x;
349    let mut row_start_x = 0.0;
350    let mut row_start_idx = 0;
351
352    for i in 0..paragraph.glyphs.len() {
353        if job.wrap.max_rows <= out_rows.len() {
354            *elided = true;
355            break;
356        }
357
358        let potential_row_width = paragraph.glyphs[i].max_x() - row_start_x;
359
360        if wrap_width < potential_row_width {
361            // Row break:
362
363            if first_row_indentation > 0.0
364                && !row_break_candidates.has_good_candidate(job.wrap.break_anywhere)
365            {
366                // Allow the first row to be completely empty, because we know there will be more space on the next row:
367                // 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.
368                out_rows.push(PlacedRow {
369                    pos: pos2(0.0, f32::NAN),
370                    row: Arc::new(Row {
371                        section_index_at_start: paragraph.section_index_at_start,
372                        glyphs: vec![],
373                        visuals: Default::default(),
374                        size: Vec2::ZERO,
375                    }),
376                    ends_with_newline: false,
377                });
378                row_start_x += first_row_indentation;
379                first_row_indentation = 0.0;
380            } else if let Some(last_kept_index) = row_break_candidates.get(job.wrap.break_anywhere)
381            {
382                let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..=last_kept_index]
383                    .iter()
384                    .copied()
385                    .map(|mut glyph| {
386                        glyph.pos.x -= row_start_x;
387                        glyph
388                    })
389                    .collect();
390
391                let section_index_at_start = glyphs[0].section_index;
392                let paragraph_max_x = glyphs.last().unwrap().max_x();
393
394                out_rows.push(PlacedRow {
395                    pos: pos2(0.0, f32::NAN),
396                    row: Arc::new(Row {
397                        section_index_at_start,
398                        glyphs,
399                        visuals: Default::default(),
400                        size: vec2(paragraph_max_x, 0.0),
401                    }),
402                    ends_with_newline: false,
403                });
404
405                // Start a new row:
406                row_start_idx = last_kept_index + 1;
407                row_start_x = paragraph.glyphs[row_start_idx].pos.x;
408                row_break_candidates.forget_before_idx(row_start_idx);
409            } else {
410                // Found no place to break, so we have to overrun wrap_width.
411            }
412        }
413
414        row_break_candidates.add(i, &paragraph.glyphs[i..]);
415    }
416
417    if row_start_idx < paragraph.glyphs.len() {
418        // Final row of text:
419
420        if job.wrap.max_rows <= out_rows.len() {
421            *elided = true; // can't fit another row
422        } else {
423            let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..]
424                .iter()
425                .copied()
426                .map(|mut glyph| {
427                    glyph.pos.x -= row_start_x;
428                    glyph
429                })
430                .collect();
431
432            let section_index_at_start = glyphs[0].section_index;
433            let paragraph_min_x = glyphs[0].pos.x;
434            let paragraph_max_x = glyphs.last().unwrap().max_x();
435
436            out_rows.push(PlacedRow {
437                pos: pos2(paragraph_min_x, 0.0),
438                row: Arc::new(Row {
439                    section_index_at_start,
440                    glyphs,
441                    visuals: Default::default(),
442                    size: vec2(paragraph_max_x - paragraph_min_x, 0.0),
443                }),
444                ends_with_newline: false,
445            });
446        }
447    }
448}
449
450/// Trims the last glyphs in the row and replaces it with an overflow character (e.g. `…`).
451///
452/// Called before we have any Y coordinates.
453fn replace_last_glyph_with_overflow_character(
454    fonts: &mut FontsImpl,
455    pixels_per_point: f32,
456    job: &LayoutJob,
457    row: &mut Row,
458) {
459    let Some(overflow_character) = job.wrap.overflow_character else {
460        return;
461    };
462
463    let mut section_index = row
464        .glyphs
465        .last()
466        .map(|g| g.section_index)
467        .unwrap_or(row.section_index_at_start);
468    loop {
469        let section = &job.sections[section_index as usize];
470        let extra_letter_spacing = section.format.extra_letter_spacing;
471        let mut font = fonts.font(&section.format.font_id.family);
472        let font_size = section.format.font_id.size;
473
474        let (font_id, glyph_info) = font.glyph_info(overflow_character);
475        let mut font_face = font.fonts_by_id.get_mut(&font_id);
476        let font_face_metrics = font_face
477            .as_mut()
478            .map(|f| f.styled_metrics(pixels_per_point, font_size, &section.format.coords))
479            .unwrap_or_default();
480
481        let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() {
482            // Kern the overflow character properly
483            let pair_kerning = font_face
484                .as_mut()
485                .map(|font_face| {
486                    if let (Some(prev_glyph_id), Some(overflow_glyph_id)) = (
487                        font_face.glyph_info(prev_glyph.chr).and_then(|g| g.id),
488                        font_face.glyph_info(overflow_character).and_then(|g| g.id),
489                    ) {
490                        font_face.pair_kerning(&font_face_metrics, prev_glyph_id, overflow_glyph_id)
491                    } else {
492                        0.0
493                    }
494                })
495                .unwrap_or_default();
496
497            prev_glyph.max_x() + extra_letter_spacing + pair_kerning
498        } else {
499            0.0 // TODO(emilk): heed paragraph leading_space 😬
500        };
501
502        let replacement_glyph_width = font_face
503            .as_mut()
504            .and_then(|f| f.glyph_info(overflow_character))
505            .map(|i| {
506                i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor / pixels_per_point
507            })
508            .unwrap_or_default();
509
510        // Check if we're within width budget:
511        if overflow_glyph_x + replacement_glyph_width <= job.effective_wrap_width()
512            || row.glyphs.is_empty()
513        {
514            // we are done
515
516            let (replacement_glyph_alloc, physical_x) = font_face
517                .as_mut()
518                .map(|f| {
519                    f.allocate_glyph(
520                        font.atlas,
521                        &font_face_metrics,
522                        glyph_info,
523                        overflow_character,
524                        overflow_glyph_x * pixels_per_point,
525                    )
526                })
527                .unwrap_or_default();
528
529            let font_metrics =
530                font.styled_metrics(pixels_per_point, font_size, &section.format.coords);
531            let line_height = section
532                .format
533                .line_height
534                .unwrap_or(font_metrics.row_height);
535
536            row.glyphs.push(Glyph {
537                chr: overflow_character,
538                pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
539                advance_width: replacement_glyph_alloc.advance_width_px / pixels_per_point,
540                line_height,
541                font_face_height: font_face_metrics.row_height,
542                font_face_ascent: font_face_metrics.ascent,
543                font_height: font_metrics.row_height,
544                font_ascent: font_metrics.ascent,
545                uv_rect: replacement_glyph_alloc.uv_rect,
546                section_index,
547                first_vertex: 0, // filled in later
548            });
549            return;
550        }
551
552        // We didn't fit - pop the last glyph and try again.
553        if let Some(last_glyph) = row.glyphs.pop() {
554            section_index = last_glyph.section_index;
555        } else {
556            section_index = row.section_index_at_start;
557        }
558    }
559}
560
561/// Horizontally aligned the text on a row.
562///
563/// Ignores the Y coordinate.
564fn halign_and_justify_row(
565    point_scale: PointScale,
566    placed_row: &mut PlacedRow,
567    halign: Align,
568    wrap_width: f32,
569    justify: bool,
570) {
571    #![expect(clippy::useless_let_if_seq)] // False positive
572
573    let row = Arc::make_mut(&mut placed_row.row);
574
575    if row.glyphs.is_empty() {
576        return;
577    }
578
579    let num_leading_spaces = row
580        .glyphs
581        .iter()
582        .take_while(|glyph| glyph.chr.is_whitespace())
583        .count();
584
585    let glyph_range = if num_leading_spaces == row.glyphs.len() {
586        // There is only whitespace
587        (0, row.glyphs.len())
588    } else {
589        let num_trailing_spaces = row
590            .glyphs
591            .iter()
592            .rev()
593            .take_while(|glyph| glyph.chr.is_whitespace())
594            .count();
595
596        (num_leading_spaces, row.glyphs.len() - num_trailing_spaces)
597    };
598    let num_glyphs_in_range = glyph_range.1 - glyph_range.0;
599    assert!(num_glyphs_in_range > 0, "Should have at least one glyph");
600
601    let original_min_x = row.glyphs[glyph_range.0].logical_rect().min.x;
602    let original_max_x = row.glyphs[glyph_range.1 - 1].logical_rect().max.x;
603    let original_width = original_max_x - original_min_x;
604
605    let target_width = if justify && num_glyphs_in_range > 1 {
606        wrap_width
607    } else {
608        original_width
609    };
610
611    let (target_min_x, target_max_x) = match halign {
612        Align::LEFT => (0.0, target_width),
613        Align::Center => (-target_width / 2.0, target_width / 2.0),
614        Align::RIGHT => (-target_width, 0.0),
615    };
616
617    let num_spaces_in_range = row.glyphs[glyph_range.0..glyph_range.1]
618        .iter()
619        .filter(|glyph| glyph.chr.is_whitespace())
620        .count();
621
622    let mut extra_x_per_glyph = if num_glyphs_in_range == 1 {
623        0.0
624    } else {
625        (target_width - original_width) / (num_glyphs_in_range as f32 - 1.0)
626    };
627    extra_x_per_glyph = extra_x_per_glyph.at_least(0.0); // Don't contract
628
629    let mut extra_x_per_space = 0.0;
630    if 0 < num_spaces_in_range && num_spaces_in_range < num_glyphs_in_range {
631        // Add an integral number of pixels between each glyph,
632        // and add the balance to the spaces:
633
634        extra_x_per_glyph = point_scale.floor_to_pixel(extra_x_per_glyph);
635
636        extra_x_per_space = (target_width
637            - original_width
638            - extra_x_per_glyph * (num_glyphs_in_range as f32 - 1.0))
639            / (num_spaces_in_range as f32);
640    }
641
642    placed_row.pos.x = point_scale.round_to_pixel(target_min_x);
643    let mut translate_x = -original_min_x - extra_x_per_glyph * glyph_range.0 as f32;
644
645    for glyph in &mut row.glyphs {
646        glyph.pos.x += translate_x;
647        glyph.pos.x = point_scale.round_to_pixel(glyph.pos.x);
648        translate_x += extra_x_per_glyph;
649        if glyph.chr.is_whitespace() {
650            translate_x += extra_x_per_space;
651        }
652    }
653
654    // Note we ignore the leading/trailing whitespace here!
655    row.size.x = target_max_x - target_min_x;
656}
657
658/// Calculate the Y positions and tessellate the text.
659fn galley_from_rows(
660    point_scale: PointScale,
661    job: Arc<LayoutJob>,
662    mut rows: Vec<PlacedRow>,
663    elided: bool,
664    intrinsic_size: Vec2,
665) -> Galley {
666    let mut first_row_min_height = job.first_row_min_height;
667    let mut cursor_y = 0.0;
668
669    for placed_row in &mut rows {
670        let mut max_row_height = first_row_min_height.at_least(placed_row.height());
671        let row = Arc::make_mut(&mut placed_row.row);
672
673        first_row_min_height = 0.0;
674        for glyph in &row.glyphs {
675            max_row_height = max_row_height.at_least(glyph.line_height);
676        }
677        max_row_height = point_scale.round_to_pixel(max_row_height);
678
679        // Now position each glyph vertically:
680        for glyph in &mut row.glyphs {
681            let format = &job.sections[glyph.section_index as usize].format;
682
683            glyph.pos.y = glyph.font_face_ascent
684
685                // Apply valign to the different in height of the entire row, and the height of this `Font`:
686                + format.valign.to_factor() * (max_row_height - glyph.line_height)
687
688                // When mixing different `FontImpl` (e.g. latin and emojis),
689                // we always center the difference:
690                + 0.5 * (glyph.font_height - glyph.font_face_height);
691
692            glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y);
693        }
694
695        placed_row.pos.y = cursor_y;
696        row.size.y = max_row_height;
697
698        cursor_y += max_row_height;
699        cursor_y = point_scale.round_to_pixel(cursor_y); // TODO(emilk): it would be better to do the calculations in pixels instead.
700    }
701
702    let format_summary = format_summary(&job);
703
704    let mut rect = Rect::ZERO;
705    let mut mesh_bounds = Rect::NOTHING;
706    let mut num_vertices = 0;
707    let mut num_indices = 0;
708
709    for placed_row in &mut rows {
710        rect |= placed_row.rect();
711
712        let row = Arc::make_mut(&mut placed_row.row);
713        row.visuals = tessellate_row(point_scale, &job, &format_summary, row);
714
715        mesh_bounds |= row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2());
716        num_vertices += row.visuals.mesh.vertices.len();
717        num_indices += row.visuals.mesh.indices.len();
718
719        row.section_index_at_start = u32::MAX; // No longer in use.
720        for glyph in &mut row.glyphs {
721            glyph.section_index = u32::MAX; // No longer in use.
722        }
723    }
724
725    let mut galley = Galley {
726        job,
727        rows,
728        elided,
729        rect,
730        mesh_bounds,
731        num_vertices,
732        num_indices,
733        pixels_per_point: point_scale.pixels_per_point,
734        intrinsic_size,
735    };
736
737    if galley.job.round_output_to_gui {
738        galley.round_output_to_gui();
739    }
740
741    galley
742}
743
744#[derive(Default)]
745struct FormatSummary {
746    any_background: bool,
747    any_underline: bool,
748    any_strikethrough: bool,
749}
750
751fn format_summary(job: &LayoutJob) -> FormatSummary {
752    let mut format_summary = FormatSummary::default();
753    for section in &job.sections {
754        format_summary.any_background |= section.format.background != Color32::TRANSPARENT;
755        format_summary.any_underline |= section.format.underline != Stroke::NONE;
756        format_summary.any_strikethrough |= section.format.strikethrough != Stroke::NONE;
757    }
758    format_summary
759}
760
761fn tessellate_row(
762    point_scale: PointScale,
763    job: &LayoutJob,
764    format_summary: &FormatSummary,
765    row: &mut Row,
766) -> RowVisuals {
767    if row.glyphs.is_empty() {
768        return Default::default();
769    }
770
771    let mut mesh = Mesh::default();
772
773    mesh.reserve_triangles(row.glyphs.len() * 2);
774    mesh.reserve_vertices(row.glyphs.len() * 4);
775
776    if format_summary.any_background {
777        add_row_backgrounds(point_scale, job, row, &mut mesh);
778    }
779
780    let glyph_index_start = mesh.indices.len();
781    let glyph_vertex_start = mesh.vertices.len();
782    tessellate_glyphs(point_scale, job, row, &mut mesh);
783    let glyph_vertex_end = mesh.vertices.len();
784
785    if format_summary.any_underline {
786        add_row_hline(point_scale, row, &mut mesh, |glyph| {
787            let format = &job.sections[glyph.section_index as usize].format;
788            let stroke = format.underline;
789            let y = glyph.logical_rect().bottom();
790            (stroke, y)
791        });
792    }
793
794    if format_summary.any_strikethrough {
795        add_row_hline(point_scale, row, &mut mesh, |glyph| {
796            let format = &job.sections[glyph.section_index as usize].format;
797            let stroke = format.strikethrough;
798            let y = glyph.logical_rect().center().y;
799            (stroke, y)
800        });
801    }
802
803    let mesh_bounds = mesh.calc_bounds();
804
805    RowVisuals {
806        mesh,
807        mesh_bounds,
808        glyph_index_start,
809        glyph_vertex_range: glyph_vertex_start..glyph_vertex_end,
810    }
811}
812
813/// Create background for glyphs that have them.
814/// Creates as few rectangular regions as possible.
815fn add_row_backgrounds(point_scale: PointScale, job: &LayoutJob, row: &Row, mesh: &mut Mesh) {
816    if row.glyphs.is_empty() {
817        return;
818    }
819
820    let mut end_run = |start: Option<(Color32, Rect, f32)>, stop_x: f32| {
821        if let Some((color, start_rect, expand)) = start {
822            let rect = Rect::from_min_max(start_rect.left_top(), pos2(stop_x, start_rect.bottom()));
823            let rect = rect.expand(expand);
824            let rect = rect.round_to_pixels(point_scale.pixels_per_point());
825            mesh.add_colored_rect(rect, color);
826        }
827    };
828
829    let mut run_start = None;
830    let mut last_rect = Rect::NAN;
831
832    for glyph in &row.glyphs {
833        let format = &job.sections[glyph.section_index as usize].format;
834        let color = format.background;
835        let rect = glyph.logical_rect();
836
837        if color == Color32::TRANSPARENT {
838            end_run(run_start.take(), last_rect.right());
839        } else if let Some((existing_color, start, expand)) = run_start {
840            if existing_color == color
841                && start.top() == rect.top()
842                && start.bottom() == rect.bottom()
843                && format.expand_bg == expand
844            {
845                // continue the same background rectangle
846            } else {
847                end_run(run_start.take(), last_rect.right());
848                run_start = Some((color, rect, format.expand_bg));
849            }
850        } else {
851            run_start = Some((color, rect, format.expand_bg));
852        }
853
854        last_rect = rect;
855    }
856
857    end_run(run_start.take(), last_rect.right());
858}
859
860fn tessellate_glyphs(point_scale: PointScale, job: &LayoutJob, row: &mut Row, mesh: &mut Mesh) {
861    for glyph in &mut row.glyphs {
862        glyph.first_vertex = mesh.vertices.len() as u32;
863        let uv_rect = glyph.uv_rect;
864        if !uv_rect.is_nothing() {
865            let mut left_top = glyph.pos + uv_rect.offset;
866            left_top.x = point_scale.round_to_pixel(left_top.x);
867            left_top.y = point_scale.round_to_pixel(left_top.y);
868
869            let rect = Rect::from_min_max(left_top, left_top + uv_rect.size);
870            let uv = Rect::from_min_max(
871                pos2(uv_rect.min[0] as f32, uv_rect.min[1] as f32),
872                pos2(uv_rect.max[0] as f32, uv_rect.max[1] as f32),
873            );
874
875            let format = &job.sections[glyph.section_index as usize].format;
876
877            let color = format.color;
878
879            if format.italics {
880                let idx = mesh.vertices.len() as u32;
881                mesh.add_triangle(idx, idx + 1, idx + 2);
882                mesh.add_triangle(idx + 2, idx + 1, idx + 3);
883
884                let top_offset = rect.height() * 0.25 * Vec2::X;
885
886                mesh.vertices.push(Vertex {
887                    pos: rect.left_top() + top_offset,
888                    uv: uv.left_top(),
889                    color,
890                });
891                mesh.vertices.push(Vertex {
892                    pos: rect.right_top() + top_offset,
893                    uv: uv.right_top(),
894                    color,
895                });
896                mesh.vertices.push(Vertex {
897                    pos: rect.left_bottom(),
898                    uv: uv.left_bottom(),
899                    color,
900                });
901                mesh.vertices.push(Vertex {
902                    pos: rect.right_bottom(),
903                    uv: uv.right_bottom(),
904                    color,
905                });
906            } else {
907                mesh.add_rect_with_uv(rect, uv, color);
908            }
909        }
910    }
911}
912
913/// Add a horizontal line over a row of glyphs with a stroke and y decided by a callback.
914fn add_row_hline(
915    point_scale: PointScale,
916    row: &Row,
917    mesh: &mut Mesh,
918    stroke_and_y: impl Fn(&Glyph) -> (Stroke, f32),
919) {
920    let mut path = crate::tessellator::Path::default(); // reusing path to avoid re-allocations.
921
922    let mut end_line = |start: Option<(Stroke, Pos2)>, stop_x: f32| {
923        if let Some((stroke, start)) = start {
924            let stop = pos2(stop_x, start.y);
925            path.clear();
926            path.add_line_segment([start, stop]);
927            let feathering = 1.0 / point_scale.pixels_per_point();
928            path.stroke_open(feathering, &PathStroke::from(stroke), mesh);
929        }
930    };
931
932    let mut line_start = None;
933    let mut last_right_x = f32::NAN;
934
935    for glyph in &row.glyphs {
936        let (stroke, mut y) = stroke_and_y(glyph);
937        stroke.round_center_to_pixel(point_scale.pixels_per_point, &mut y);
938
939        if stroke.is_empty() {
940            end_line(line_start.take(), last_right_x);
941        } else if let Some((existing_stroke, start)) = line_start {
942            if existing_stroke == stroke && start.y == y {
943                // continue the same line
944            } else {
945                end_line(line_start.take(), last_right_x);
946                line_start = Some((stroke, pos2(glyph.pos.x, y)));
947            }
948        } else {
949            line_start = Some((stroke, pos2(glyph.pos.x, y)));
950        }
951
952        last_right_x = glyph.max_x();
953    }
954
955    end_line(line_start.take(), last_right_x);
956}
957
958// ----------------------------------------------------------------------------
959
960/// Keeps track of good places to break a long row of text.
961/// Will focus primarily on spaces, secondarily on things like `-`
962#[derive(Clone, Copy, Default)]
963struct RowBreakCandidates {
964    /// Breaking at ` ` or other whitespace
965    /// is always the primary candidate.
966    space: Option<usize>,
967
968    /// Logograms (single character representing a whole word) or kana (Japanese hiragana and katakana) are good candidates for line break.
969    cjk: Option<usize>,
970
971    /// Breaking anywhere before a CJK character is acceptable too.
972    pre_cjk: Option<usize>,
973
974    /// Breaking at a dash is a super-
975    /// good idea.
976    dash: Option<usize>,
977
978    /// This is nicer for things like URLs, e.g. www.
979    /// example.com.
980    punctuation: Option<usize>,
981
982    /// Breaking after just random character is some
983    /// times necessary.
984    any: Option<usize>,
985}
986
987impl RowBreakCandidates {
988    fn add(&mut self, index: usize, glyphs: &[Glyph]) {
989        let chr = glyphs[0].chr;
990        const NON_BREAKING_SPACE: char = '\u{A0}';
991        if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
992            self.space = Some(index);
993        } else if is_cjk(chr) && (glyphs.len() == 1 || is_cjk_break_allowed(glyphs[1].chr)) {
994            self.cjk = Some(index);
995        } else if chr == '-' {
996            self.dash = Some(index);
997        } else if chr.is_ascii_punctuation() {
998            self.punctuation = Some(index);
999        } else if glyphs.len() > 1 && is_cjk(glyphs[1].chr) {
1000            self.pre_cjk = Some(index);
1001        }
1002        self.any = Some(index);
1003    }
1004
1005    fn word_boundary(&self) -> Option<usize> {
1006        [self.space, self.cjk, self.pre_cjk]
1007            .into_iter()
1008            .max()
1009            .flatten()
1010    }
1011
1012    fn has_good_candidate(&self, break_anywhere: bool) -> bool {
1013        if break_anywhere {
1014            self.any.is_some()
1015        } else {
1016            self.word_boundary().is_some()
1017        }
1018    }
1019
1020    fn get(&self, break_anywhere: bool) -> Option<usize> {
1021        if break_anywhere {
1022            self.any
1023        } else {
1024            self.word_boundary()
1025                .or(self.dash)
1026                .or(self.punctuation)
1027                .or(self.any)
1028        }
1029    }
1030
1031    fn forget_before_idx(&mut self, index: usize) {
1032        let Self {
1033            space,
1034            cjk,
1035            pre_cjk,
1036            dash,
1037            punctuation,
1038            any,
1039        } = self;
1040        if space.is_some_and(|s| s < index) {
1041            *space = None;
1042        }
1043        if cjk.is_some_and(|s| s < index) {
1044            *cjk = None;
1045        }
1046        if pre_cjk.is_some_and(|s| s < index) {
1047            *pre_cjk = None;
1048        }
1049        if dash.is_some_and(|s| s < index) {
1050            *dash = None;
1051        }
1052        if punctuation.is_some_and(|s| s < index) {
1053            *punctuation = None;
1054        }
1055        if any.is_some_and(|s| s < index) {
1056            *any = None;
1057        }
1058    }
1059}
1060
1061// ----------------------------------------------------------------------------
1062
1063#[cfg(test)]
1064mod tests {
1065
1066    use super::{super::*, *};
1067
1068    #[test]
1069    fn test_zero_max_width() {
1070        let pixels_per_point = 1.0;
1071        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1072        let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default());
1073        layout_job.wrap.max_width = 0.0;
1074        let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1075        assert_eq!(galley.rows.len(), 1);
1076    }
1077
1078    #[test]
1079    fn test_truncate_with_newline() {
1080        // No matter where we wrap, we should be appending the newline character.
1081
1082        let pixels_per_point = 1.0;
1083
1084        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1085        let text_format = TextFormat {
1086            font_id: FontId::monospace(12.0),
1087            ..Default::default()
1088        };
1089
1090        for text in ["Hello\nworld", "\nfoo"] {
1091            for break_anywhere in [false, true] {
1092                for max_width in [0.0, 5.0, 10.0, 20.0, f32::INFINITY] {
1093                    let mut layout_job =
1094                        LayoutJob::single_section(text.into(), text_format.clone());
1095                    layout_job.wrap.max_width = max_width;
1096                    layout_job.wrap.max_rows = 1;
1097                    layout_job.wrap.break_anywhere = break_anywhere;
1098
1099                    let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1100
1101                    assert!(galley.elided);
1102                    assert_eq!(galley.rows.len(), 1);
1103                    let row_text = galley.rows[0].text();
1104                    assert!(
1105                        row_text.ends_with('…'),
1106                        "Expected row to end with `…`, got {row_text:?} when line-breaking the text {text:?} with max_width {max_width} and break_anywhere {break_anywhere}.",
1107                    );
1108                }
1109            }
1110        }
1111
1112        {
1113            let mut layout_job = LayoutJob::single_section("Hello\nworld".into(), text_format);
1114            layout_job.wrap.max_width = 50.0;
1115            layout_job.wrap.max_rows = 1;
1116            layout_job.wrap.break_anywhere = false;
1117
1118            let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1119
1120            assert!(galley.elided);
1121            assert_eq!(galley.rows.len(), 1);
1122            let row_text = galley.rows[0].text();
1123            assert_eq!(row_text, "Hello…");
1124        }
1125    }
1126
1127    #[test]
1128    fn test_cjk() {
1129        let pixels_per_point = 1.0;
1130        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1131        let mut layout_job = LayoutJob::single_section(
1132            "日本語とEnglishの混在した文章".into(),
1133            TextFormat::default(),
1134        );
1135        layout_job.wrap.max_width = 90.0;
1136        let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1137        assert_eq!(
1138            galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
1139            vec!["日本語と", "Englishの混在", "した文章"]
1140        );
1141    }
1142
1143    #[test]
1144    fn test_pre_cjk() {
1145        let pixels_per_point = 1.0;
1146        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1147        let mut layout_job = LayoutJob::single_section(
1148            "日本語とEnglishの混在した文章".into(),
1149            TextFormat::default(),
1150        );
1151        layout_job.wrap.max_width = 110.0;
1152        let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1153        assert_eq!(
1154            galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
1155            vec!["日本語とEnglish", "の混在した文章"]
1156        );
1157    }
1158
1159    #[test]
1160    fn test_truncate_width() {
1161        let pixels_per_point = 1.0;
1162        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1163        let mut layout_job =
1164            LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default());
1165        layout_job.wrap.max_width = f32::INFINITY;
1166        layout_job.wrap.max_rows = 1;
1167        layout_job.round_output_to_gui = false;
1168        let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1169        assert!(galley.elided);
1170        assert_eq!(
1171            galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
1172            vec!["# DNA…"]
1173        );
1174        let row = &galley.rows[0];
1175        assert_eq!(row.pos, Pos2::ZERO);
1176        assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x());
1177    }
1178
1179    #[test]
1180    fn test_truncate_with_pixels_per_point() {
1181        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1182
1183        for pixels_per_point in [
1184            0.33, 0.5, 0.67, 1.0, 1.25, 1.33, 1.5, 1.75, 2.0, 3.0, 4.0, 5.0,
1185        ] {
1186            for ch in ['W', 'A', 'n', 't', 'i'] {
1187                let target_width = 50.0;
1188                let text = (0..20).map(|_| ch).collect::<String>();
1189
1190                let mut job = LayoutJob::single_section(text, TextFormat::default());
1191                job.wrap.max_width = target_width;
1192                job.wrap.max_rows = 1;
1193                let elided_galley = layout(&mut fonts, pixels_per_point, job.into());
1194                assert!(elided_galley.elided);
1195
1196                let test_galley = layout(
1197                    &mut fonts,
1198                    pixels_per_point,
1199                    Arc::new(LayoutJob::single_section(
1200                        (0..elided_galley.rows[0].char_count_excluding_newline())
1201                            .map(|_| ch)
1202                            .chain(std::iter::once('…'))
1203                            .collect::<String>(),
1204                        TextFormat::default(),
1205                    )),
1206                );
1207
1208                assert!(elided_galley.size().x >= 0.0);
1209                assert!(elided_galley.size().x <= target_width);
1210                assert!(test_galley.size().x > target_width);
1211            }
1212        }
1213    }
1214
1215    #[test]
1216    fn test_empty_row() {
1217        let pixels_per_point = 1.0;
1218        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1219
1220        let font_id = FontId::default();
1221        let font_height = fonts
1222            .font(&font_id.family)
1223            .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
1224            .row_height;
1225
1226        let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY);
1227
1228        let galley = layout(&mut fonts, pixels_per_point, job.into());
1229
1230        assert_eq!(galley.rows.len(), 1, "Expected one row");
1231        assert_eq!(
1232            galley.rows[0].row.glyphs.len(),
1233            0,
1234            "Expected no glyphs in the empty row"
1235        );
1236        assert_eq!(
1237            galley.size(),
1238            Vec2::new(0.0, font_height.round()),
1239            "Unexpected galley size"
1240        );
1241        assert_eq!(
1242            galley.intrinsic_size(),
1243            Vec2::new(0.0, font_height.round()),
1244            "Unexpected intrinsic size"
1245        );
1246    }
1247
1248    #[test]
1249    fn test_end_with_newline() {
1250        let pixels_per_point = 1.0;
1251        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1252
1253        let font_id = FontId::default();
1254        let font_height = fonts
1255            .font(&font_id.family)
1256            .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
1257            .row_height;
1258
1259        let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY);
1260
1261        let galley = layout(&mut fonts, pixels_per_point, job.into());
1262
1263        assert_eq!(galley.rows.len(), 2, "Expected two rows");
1264        assert_eq!(
1265            galley.rows[1].row.glyphs.len(),
1266            0,
1267            "Expected no glyphs in the empty row"
1268        );
1269        assert_eq!(
1270            galley.size().round(),
1271            Vec2::new(17.0, font_height.round() * 2.0),
1272            "Unexpected galley size"
1273        );
1274        assert_eq!(
1275            galley.intrinsic_size().round(),
1276            Vec2::new(17.0, font_height.round() * 2.0),
1277            "Unexpected intrinsic size"
1278        );
1279    }
1280}