Skip to main content

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 paragraph_min_x = paragraph.glyphs[row_start_idx].pos.x - row_start_x;
424            let paragraph_max_x = paragraph.glyphs.last().unwrap().max_x() - row_start_x;
425
426            let glyphs: Vec<Glyph> = paragraph.glyphs[row_start_idx..]
427                .iter()
428                .copied()
429                .map(|mut glyph| {
430                    glyph.pos.x -= row_start_x + paragraph_min_x;
431                    glyph
432                })
433                .collect();
434
435            let section_index_at_start = glyphs[0].section_index;
436
437            out_rows.push(PlacedRow {
438                pos: pos2(paragraph_min_x, 0.0),
439                row: Arc::new(Row {
440                    section_index_at_start,
441                    glyphs,
442                    visuals: Default::default(),
443                    size: vec2(paragraph_max_x - paragraph_min_x, 0.0),
444                }),
445                ends_with_newline: false,
446            });
447        }
448    }
449}
450
451/// Trims the last glyphs in the row and replaces it with an overflow character (e.g. `…`).
452///
453/// Called before we have any Y coordinates.
454fn replace_last_glyph_with_overflow_character(
455    fonts: &mut FontsImpl,
456    pixels_per_point: f32,
457    job: &LayoutJob,
458    row: &mut Row,
459) {
460    let Some(overflow_character) = job.wrap.overflow_character else {
461        return;
462    };
463
464    let mut section_index = row
465        .glyphs
466        .last()
467        .map(|g| g.section_index)
468        .unwrap_or(row.section_index_at_start);
469    loop {
470        let section = &job.sections[section_index as usize];
471        let extra_letter_spacing = section.format.extra_letter_spacing;
472        let mut font = fonts.font(&section.format.font_id.family);
473        let font_size = section.format.font_id.size;
474
475        let (font_id, glyph_info) = font.glyph_info(overflow_character);
476        let mut font_face = font.fonts_by_id.get_mut(&font_id);
477        let font_face_metrics = font_face
478            .as_mut()
479            .map(|f| f.styled_metrics(pixels_per_point, font_size, &section.format.coords))
480            .unwrap_or_default();
481
482        let overflow_glyph_x = if let Some(prev_glyph) = row.glyphs.last() {
483            // Kern the overflow character properly
484            let pair_kerning = font_face
485                .as_mut()
486                .map(|font_face| {
487                    if let (Some(prev_glyph_id), Some(overflow_glyph_id)) = (
488                        font_face.glyph_info(prev_glyph.chr).and_then(|g| g.id),
489                        font_face.glyph_info(overflow_character).and_then(|g| g.id),
490                    ) {
491                        font_face.pair_kerning(&font_face_metrics, prev_glyph_id, overflow_glyph_id)
492                    } else {
493                        0.0
494                    }
495                })
496                .unwrap_or_default();
497
498            prev_glyph.max_x() + extra_letter_spacing + pair_kerning
499        } else {
500            0.0 // TODO(emilk): heed paragraph leading_space 😬
501        };
502
503        let replacement_glyph_width = font_face
504            .as_mut()
505            .and_then(|f| f.glyph_info(overflow_character))
506            .map(|i| {
507                i.advance_width_unscaled.0 * font_face_metrics.px_scale_factor / pixels_per_point
508            })
509            .unwrap_or_default();
510
511        // Check if we're within width budget:
512        if overflow_glyph_x + replacement_glyph_width <= job.effective_wrap_width()
513            || row.glyphs.is_empty()
514        {
515            // we are done
516
517            let (replacement_glyph_alloc, physical_x) = font_face
518                .as_mut()
519                .map(|f| {
520                    f.allocate_glyph(
521                        font.atlas,
522                        &font_face_metrics,
523                        glyph_info,
524                        overflow_character,
525                        overflow_glyph_x * pixels_per_point,
526                    )
527                })
528                .unwrap_or_default();
529
530            let font_metrics =
531                font.styled_metrics(pixels_per_point, font_size, &section.format.coords);
532            let line_height = section
533                .format
534                .line_height
535                .unwrap_or(font_metrics.row_height);
536
537            row.glyphs.push(Glyph {
538                chr: overflow_character,
539                pos: pos2(physical_x as f32 / pixels_per_point, f32::NAN),
540                advance_width: replacement_glyph_alloc.advance_width_px / pixels_per_point,
541                line_height,
542                font_face_height: font_face_metrics.row_height,
543                font_face_ascent: font_face_metrics.ascent,
544                font_height: font_metrics.row_height,
545                font_ascent: font_metrics.ascent,
546                uv_rect: replacement_glyph_alloc.uv_rect,
547                section_index,
548                first_vertex: 0, // filled in later
549            });
550            return;
551        }
552
553        // We didn't fit - pop the last glyph and try again.
554        if let Some(last_glyph) = row.glyphs.pop() {
555            section_index = last_glyph.section_index;
556        } else {
557            section_index = row.section_index_at_start;
558        }
559    }
560}
561
562/// Horizontally aligned the text on a row.
563///
564/// Ignores the Y coordinate.
565fn halign_and_justify_row(
566    point_scale: PointScale,
567    placed_row: &mut PlacedRow,
568    halign: Align,
569    wrap_width: f32,
570    justify: bool,
571) {
572    #![expect(clippy::useless_let_if_seq)] // False positive
573
574    let row = Arc::make_mut(&mut placed_row.row);
575
576    if row.glyphs.is_empty() {
577        return;
578    }
579
580    let num_leading_spaces = row
581        .glyphs
582        .iter()
583        .take_while(|glyph| glyph.chr.is_whitespace())
584        .count();
585
586    let glyph_range = if num_leading_spaces == row.glyphs.len() {
587        // There is only whitespace
588        (0, row.glyphs.len())
589    } else {
590        let num_trailing_spaces = row
591            .glyphs
592            .iter()
593            .rev()
594            .take_while(|glyph| glyph.chr.is_whitespace())
595            .count();
596
597        (num_leading_spaces, row.glyphs.len() - num_trailing_spaces)
598    };
599    let num_glyphs_in_range = glyph_range.1 - glyph_range.0;
600    assert!(num_glyphs_in_range > 0, "Should have at least one glyph");
601
602    let original_min_x = row.glyphs[glyph_range.0].logical_rect().min.x;
603    let original_max_x = row.glyphs[glyph_range.1 - 1].logical_rect().max.x;
604    let original_width = original_max_x - original_min_x;
605
606    let target_width = if justify && num_glyphs_in_range > 1 {
607        wrap_width
608    } else {
609        original_width
610    };
611
612    let (target_min_x, target_max_x) = match halign {
613        Align::LEFT => (0.0, target_width),
614        Align::Center => (-target_width / 2.0, target_width / 2.0),
615        Align::RIGHT => (-target_width, 0.0),
616    };
617
618    let num_spaces_in_range = row.glyphs[glyph_range.0..glyph_range.1]
619        .iter()
620        .filter(|glyph| glyph.chr.is_whitespace())
621        .count();
622
623    let mut extra_x_per_glyph = if num_glyphs_in_range == 1 {
624        0.0
625    } else {
626        (target_width - original_width) / (num_glyphs_in_range as f32 - 1.0)
627    };
628    extra_x_per_glyph = extra_x_per_glyph.at_least(0.0); // Don't contract
629
630    let mut extra_x_per_space = 0.0;
631    if 0 < num_spaces_in_range && num_spaces_in_range < num_glyphs_in_range {
632        // Add an integral number of pixels between each glyph,
633        // and add the balance to the spaces:
634
635        extra_x_per_glyph = point_scale.floor_to_pixel(extra_x_per_glyph);
636
637        extra_x_per_space = (target_width
638            - original_width
639            - extra_x_per_glyph * (num_glyphs_in_range as f32 - 1.0))
640            / (num_spaces_in_range as f32);
641    }
642
643    placed_row.pos.x = point_scale.round_to_pixel(target_min_x);
644    let mut translate_x = -original_min_x - extra_x_per_glyph * glyph_range.0 as f32;
645
646    for glyph in &mut row.glyphs {
647        glyph.pos.x += translate_x;
648        glyph.pos.x = point_scale.round_to_pixel(glyph.pos.x);
649        translate_x += extra_x_per_glyph;
650        if glyph.chr.is_whitespace() {
651            translate_x += extra_x_per_space;
652        }
653    }
654
655    // Note we ignore the leading/trailing whitespace here!
656    row.size.x = target_max_x - target_min_x;
657}
658
659/// Calculate the Y positions and tessellate the text.
660fn galley_from_rows(
661    point_scale: PointScale,
662    job: Arc<LayoutJob>,
663    mut rows: Vec<PlacedRow>,
664    elided: bool,
665    intrinsic_size: Vec2,
666) -> Galley {
667    let mut first_row_min_height = job.first_row_min_height;
668    let mut cursor_y = 0.0;
669
670    for placed_row in &mut rows {
671        let mut max_row_height = first_row_min_height.at_least(placed_row.height());
672        let row = Arc::make_mut(&mut placed_row.row);
673
674        first_row_min_height = 0.0;
675        for glyph in &row.glyphs {
676            max_row_height = max_row_height.at_least(glyph.line_height);
677        }
678        max_row_height = point_scale.round_to_pixel(max_row_height);
679
680        // Now position each glyph vertically:
681        for glyph in &mut row.glyphs {
682            let format = &job.sections[glyph.section_index as usize].format;
683
684            glyph.pos.y = glyph.font_face_ascent
685
686                // Apply valign to the different in height of the entire row, and the height of this `Font`:
687                + format.valign.to_factor() * (max_row_height - glyph.line_height)
688
689                // When mixing different `FontImpl` (e.g. latin and emojis),
690                // we always center the difference:
691                + 0.5 * (glyph.font_height - glyph.font_face_height);
692
693            glyph.pos.y = point_scale.round_to_pixel(glyph.pos.y);
694        }
695
696        placed_row.pos.y = cursor_y;
697        row.size.y = max_row_height;
698
699        cursor_y += max_row_height;
700        cursor_y = point_scale.round_to_pixel(cursor_y); // TODO(emilk): it would be better to do the calculations in pixels instead.
701    }
702
703    let format_summary = format_summary(&job);
704
705    let mut rect = Rect::ZERO;
706    let mut mesh_bounds = Rect::NOTHING;
707    let mut num_vertices = 0;
708    let mut num_indices = 0;
709
710    for placed_row in &mut rows {
711        rect |= placed_row.rect();
712
713        let row = Arc::make_mut(&mut placed_row.row);
714        row.visuals = tessellate_row(point_scale, &job, &format_summary, row);
715
716        mesh_bounds |= row.visuals.mesh_bounds.translate(placed_row.pos.to_vec2());
717        num_vertices += row.visuals.mesh.vertices.len();
718        num_indices += row.visuals.mesh.indices.len();
719
720        row.section_index_at_start = u32::MAX; // No longer in use.
721        for glyph in &mut row.glyphs {
722            glyph.section_index = u32::MAX; // No longer in use.
723        }
724    }
725
726    let mut galley = Galley {
727        job,
728        rows,
729        elided,
730        rect,
731        mesh_bounds,
732        num_vertices,
733        num_indices,
734        pixels_per_point: point_scale.pixels_per_point,
735        intrinsic_size,
736    };
737
738    if galley.job.round_output_to_gui {
739        galley.round_output_to_gui();
740    }
741
742    galley
743}
744
745#[derive(Default)]
746struct FormatSummary {
747    any_background: bool,
748    any_underline: bool,
749    any_strikethrough: bool,
750}
751
752fn format_summary(job: &LayoutJob) -> FormatSummary {
753    let mut format_summary = FormatSummary::default();
754    for section in &job.sections {
755        format_summary.any_background |= section.format.background != Color32::TRANSPARENT;
756        format_summary.any_underline |= section.format.underline != Stroke::NONE;
757        format_summary.any_strikethrough |= section.format.strikethrough != Stroke::NONE;
758    }
759    format_summary
760}
761
762fn tessellate_row(
763    point_scale: PointScale,
764    job: &LayoutJob,
765    format_summary: &FormatSummary,
766    row: &mut Row,
767) -> RowVisuals {
768    if row.glyphs.is_empty() {
769        return Default::default();
770    }
771
772    let mut mesh = Mesh::default();
773
774    mesh.reserve_triangles(row.glyphs.len() * 2);
775    mesh.reserve_vertices(row.glyphs.len() * 4);
776
777    if format_summary.any_background {
778        add_row_backgrounds(point_scale, job, row, &mut mesh);
779    }
780
781    let glyph_index_start = mesh.indices.len();
782    let glyph_vertex_start = mesh.vertices.len();
783    tessellate_glyphs(point_scale, job, row, &mut mesh);
784    let glyph_vertex_end = mesh.vertices.len();
785
786    if format_summary.any_underline {
787        add_row_hline(point_scale, row, &mut mesh, |glyph| {
788            let format = &job.sections[glyph.section_index as usize].format;
789            let stroke = format.underline;
790            let y = glyph.logical_rect().bottom();
791            (stroke, y)
792        });
793    }
794
795    if format_summary.any_strikethrough {
796        add_row_hline(point_scale, row, &mut mesh, |glyph| {
797            let format = &job.sections[glyph.section_index as usize].format;
798            let stroke = format.strikethrough;
799            let y = glyph.logical_rect().center().y;
800            (stroke, y)
801        });
802    }
803
804    let mesh_bounds = mesh.calc_bounds();
805
806    RowVisuals {
807        mesh,
808        mesh_bounds,
809        glyph_index_start,
810        glyph_vertex_range: glyph_vertex_start..glyph_vertex_end,
811    }
812}
813
814/// Create background for glyphs that have them.
815/// Creates as few rectangular regions as possible.
816fn add_row_backgrounds(point_scale: PointScale, job: &LayoutJob, row: &Row, mesh: &mut Mesh) {
817    if row.glyphs.is_empty() {
818        return;
819    }
820
821    let mut end_run = |start: Option<(Color32, Rect, f32)>, stop_x: f32| {
822        if let Some((color, start_rect, expand)) = start {
823            let rect = Rect::from_min_max(start_rect.left_top(), pos2(stop_x, start_rect.bottom()));
824            let rect = rect.expand(expand);
825            let rect = rect.round_to_pixels(point_scale.pixels_per_point());
826            mesh.add_colored_rect(rect, color);
827        }
828    };
829
830    let mut run_start = None;
831    let mut last_rect = Rect::NAN;
832
833    for glyph in &row.glyphs {
834        let format = &job.sections[glyph.section_index as usize].format;
835        let color = format.background;
836        let rect = glyph.logical_rect();
837
838        if color == Color32::TRANSPARENT {
839            end_run(run_start.take(), last_rect.right());
840        } else if let Some((existing_color, start, expand)) = run_start {
841            if existing_color == color
842                && start.top() == rect.top()
843                && start.bottom() == rect.bottom()
844                && format.expand_bg == expand
845            {
846                // continue the same background rectangle
847            } else {
848                end_run(run_start.take(), last_rect.right());
849                run_start = Some((color, rect, format.expand_bg));
850            }
851        } else {
852            run_start = Some((color, rect, format.expand_bg));
853        }
854
855        last_rect = rect;
856    }
857
858    end_run(run_start.take(), last_rect.right());
859}
860
861fn tessellate_glyphs(point_scale: PointScale, job: &LayoutJob, row: &mut Row, mesh: &mut Mesh) {
862    for glyph in &mut row.glyphs {
863        glyph.first_vertex = mesh.vertices.len() as u32;
864        let uv_rect = glyph.uv_rect;
865        if !uv_rect.is_nothing() {
866            let mut left_top = glyph.pos + uv_rect.offset;
867            left_top.x = point_scale.round_to_pixel(left_top.x);
868            left_top.y = point_scale.round_to_pixel(left_top.y);
869
870            let rect = Rect::from_min_max(left_top, left_top + uv_rect.size);
871            let uv = Rect::from_min_max(
872                pos2(uv_rect.min[0] as f32, uv_rect.min[1] as f32),
873                pos2(uv_rect.max[0] as f32, uv_rect.max[1] as f32),
874            );
875
876            let format = &job.sections[glyph.section_index as usize].format;
877
878            let color = format.color;
879
880            if format.italics {
881                let idx = mesh.vertices.len() as u32;
882                mesh.add_triangle(idx, idx + 1, idx + 2);
883                mesh.add_triangle(idx + 2, idx + 1, idx + 3);
884
885                let top_offset = rect.height() * 0.25 * Vec2::X;
886
887                mesh.vertices.push(Vertex {
888                    pos: rect.left_top() + top_offset,
889                    uv: uv.left_top(),
890                    color,
891                });
892                mesh.vertices.push(Vertex {
893                    pos: rect.right_top() + top_offset,
894                    uv: uv.right_top(),
895                    color,
896                });
897                mesh.vertices.push(Vertex {
898                    pos: rect.left_bottom(),
899                    uv: uv.left_bottom(),
900                    color,
901                });
902                mesh.vertices.push(Vertex {
903                    pos: rect.right_bottom(),
904                    uv: uv.right_bottom(),
905                    color,
906                });
907            } else {
908                mesh.add_rect_with_uv(rect, uv, color);
909            }
910        }
911    }
912}
913
914/// Add a horizontal line over a row of glyphs with a stroke and y decided by a callback.
915fn add_row_hline(
916    point_scale: PointScale,
917    row: &Row,
918    mesh: &mut Mesh,
919    stroke_and_y: impl Fn(&Glyph) -> (Stroke, f32),
920) {
921    let mut path = crate::tessellator::Path::default(); // reusing path to avoid re-allocations.
922
923    let mut end_line = |start: Option<(Stroke, Pos2)>, stop_x: f32| {
924        if let Some((stroke, start)) = start {
925            let stop = pos2(stop_x, start.y);
926            path.clear();
927            path.add_line_segment([start, stop]);
928            let feathering = 1.0 / point_scale.pixels_per_point();
929            path.stroke_open(feathering, &PathStroke::from(stroke), mesh);
930        }
931    };
932
933    let mut line_start = None;
934    let mut last_right_x = f32::NAN;
935
936    for glyph in &row.glyphs {
937        let (stroke, mut y) = stroke_and_y(glyph);
938        stroke.round_center_to_pixel(point_scale.pixels_per_point, &mut y);
939
940        if stroke.is_empty() {
941            end_line(line_start.take(), last_right_x);
942        } else if let Some((existing_stroke, start)) = line_start {
943            if existing_stroke == stroke && start.y == y {
944                // continue the same line
945            } else {
946                end_line(line_start.take(), last_right_x);
947                line_start = Some((stroke, pos2(glyph.pos.x, y)));
948            }
949        } else {
950            line_start = Some((stroke, pos2(glyph.pos.x, y)));
951        }
952
953        last_right_x = glyph.max_x();
954    }
955
956    end_line(line_start.take(), last_right_x);
957}
958
959// ----------------------------------------------------------------------------
960
961/// Keeps track of good places to break a long row of text.
962/// Will focus primarily on spaces, secondarily on things like `-`
963#[derive(Clone, Copy, Default)]
964struct RowBreakCandidates {
965    /// Breaking at ` ` or other whitespace
966    /// is always the primary candidate.
967    space: Option<usize>,
968
969    /// Logograms (single character representing a whole word) or kana (Japanese hiragana and katakana) are good candidates for line break.
970    cjk: Option<usize>,
971
972    /// Breaking anywhere before a CJK character is acceptable too.
973    pre_cjk: Option<usize>,
974
975    /// Breaking at a dash is a super-
976    /// good idea.
977    dash: Option<usize>,
978
979    /// This is nicer for things like URLs, e.g. www.
980    /// example.com.
981    punctuation: Option<usize>,
982
983    /// Breaking after just random character is some
984    /// times necessary.
985    any: Option<usize>,
986}
987
988impl RowBreakCandidates {
989    fn add(&mut self, index: usize, glyphs: &[Glyph]) {
990        let chr = glyphs[0].chr;
991        const NON_BREAKING_SPACE: char = '\u{A0}';
992        if chr.is_whitespace() && chr != NON_BREAKING_SPACE {
993            self.space = Some(index);
994        } else if is_cjk(chr) && (glyphs.len() == 1 || is_cjk_break_allowed(glyphs[1].chr)) {
995            self.cjk = Some(index);
996        } else if chr == '-' {
997            self.dash = Some(index);
998        } else if chr.is_ascii_punctuation() {
999            self.punctuation = Some(index);
1000        } else if glyphs.len() > 1 && is_cjk(glyphs[1].chr) {
1001            self.pre_cjk = Some(index);
1002        }
1003        self.any = Some(index);
1004    }
1005
1006    fn word_boundary(&self) -> Option<usize> {
1007        [self.space, self.cjk, self.pre_cjk]
1008            .into_iter()
1009            .max()
1010            .flatten()
1011    }
1012
1013    fn has_good_candidate(&self, break_anywhere: bool) -> bool {
1014        if break_anywhere {
1015            self.any.is_some()
1016        } else {
1017            self.word_boundary().is_some()
1018        }
1019    }
1020
1021    fn get(&self, break_anywhere: bool) -> Option<usize> {
1022        if break_anywhere {
1023            self.any
1024        } else {
1025            self.word_boundary()
1026                .or(self.dash)
1027                .or(self.punctuation)
1028                .or(self.any)
1029        }
1030    }
1031
1032    fn forget_before_idx(&mut self, index: usize) {
1033        let Self {
1034            space,
1035            cjk,
1036            pre_cjk,
1037            dash,
1038            punctuation,
1039            any,
1040        } = self;
1041        if space.is_some_and(|s| s < index) {
1042            *space = None;
1043        }
1044        if cjk.is_some_and(|s| s < index) {
1045            *cjk = None;
1046        }
1047        if pre_cjk.is_some_and(|s| s < index) {
1048            *pre_cjk = None;
1049        }
1050        if dash.is_some_and(|s| s < index) {
1051            *dash = None;
1052        }
1053        if punctuation.is_some_and(|s| s < index) {
1054            *punctuation = None;
1055        }
1056        if any.is_some_and(|s| s < index) {
1057            *any = None;
1058        }
1059    }
1060}
1061
1062// ----------------------------------------------------------------------------
1063
1064#[cfg(test)]
1065mod tests {
1066
1067    use super::{super::*, *};
1068
1069    #[test]
1070    fn test_zero_max_width() {
1071        let pixels_per_point = 1.0;
1072        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1073        let mut layout_job = LayoutJob::single_section("W".into(), TextFormat::default());
1074        layout_job.wrap.max_width = 0.0;
1075        let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1076        assert_eq!(galley.rows.len(), 1);
1077    }
1078
1079    #[test]
1080    fn test_truncate_with_newline() {
1081        // No matter where we wrap, we should be appending the newline character.
1082
1083        let pixels_per_point = 1.0;
1084
1085        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1086        let text_format = TextFormat {
1087            font_id: FontId::monospace(12.0),
1088            ..Default::default()
1089        };
1090
1091        for text in ["Hello\nworld", "\nfoo"] {
1092            for break_anywhere in [false, true] {
1093                for max_width in [0.0, 5.0, 10.0, 20.0, f32::INFINITY] {
1094                    let mut layout_job =
1095                        LayoutJob::single_section(text.into(), text_format.clone());
1096                    layout_job.wrap.max_width = max_width;
1097                    layout_job.wrap.max_rows = 1;
1098                    layout_job.wrap.break_anywhere = break_anywhere;
1099
1100                    let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1101
1102                    assert!(galley.elided);
1103                    assert_eq!(galley.rows.len(), 1);
1104                    let row_text = galley.rows[0].text();
1105                    assert!(
1106                        row_text.ends_with('…'),
1107                        "Expected row to end with `…`, got {row_text:?} when line-breaking the text {text:?} with max_width {max_width} and break_anywhere {break_anywhere}.",
1108                    );
1109                }
1110            }
1111        }
1112
1113        {
1114            let mut layout_job = LayoutJob::single_section("Hello\nworld".into(), text_format);
1115            layout_job.wrap.max_width = 50.0;
1116            layout_job.wrap.max_rows = 1;
1117            layout_job.wrap.break_anywhere = false;
1118
1119            let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1120
1121            assert!(galley.elided);
1122            assert_eq!(galley.rows.len(), 1);
1123            let row_text = galley.rows[0].text();
1124            assert_eq!(row_text, "Hello…");
1125        }
1126    }
1127
1128    #[test]
1129    fn test_cjk() {
1130        let pixels_per_point = 1.0;
1131        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1132        let mut layout_job = LayoutJob::single_section(
1133            "日本語とEnglishの混在した文章".into(),
1134            TextFormat::default(),
1135        );
1136        layout_job.wrap.max_width = 90.0;
1137        let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1138        assert_eq!(
1139            galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
1140            vec!["日本語と", "Englishの混在", "した文章"]
1141        );
1142    }
1143
1144    #[test]
1145    fn test_pre_cjk() {
1146        let pixels_per_point = 1.0;
1147        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1148        let mut layout_job = LayoutJob::single_section(
1149            "日本語とEnglishの混在した文章".into(),
1150            TextFormat::default(),
1151        );
1152        layout_job.wrap.max_width = 110.0;
1153        let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1154        assert_eq!(
1155            galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
1156            vec!["日本語とEnglish", "の混在した文章"]
1157        );
1158    }
1159
1160    #[test]
1161    fn test_truncate_width() {
1162        let pixels_per_point = 1.0;
1163        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1164        let mut layout_job =
1165            LayoutJob::single_section("# DNA\nMore text".into(), TextFormat::default());
1166        layout_job.wrap.max_width = f32::INFINITY;
1167        layout_job.wrap.max_rows = 1;
1168        layout_job.round_output_to_gui = false;
1169        let galley = layout(&mut fonts, pixels_per_point, layout_job.into());
1170        assert!(galley.elided);
1171        assert_eq!(
1172            galley.rows.iter().map(|row| row.text()).collect::<Vec<_>>(),
1173            vec!["# DNA…"]
1174        );
1175        let row = &galley.rows[0];
1176        assert_eq!(row.pos, Pos2::ZERO);
1177        assert_eq!(row.rect().max.x, row.glyphs.last().unwrap().max_x());
1178    }
1179
1180    #[test]
1181    fn test_truncate_with_pixels_per_point() {
1182        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1183
1184        for pixels_per_point in [
1185            0.33, 0.5, 0.67, 1.0, 1.25, 1.33, 1.5, 1.75, 2.0, 3.0, 4.0, 5.0,
1186        ] {
1187            for ch in ['W', 'A', 'n', 't', 'i'] {
1188                let target_width = 50.0;
1189                let text = (0..20).map(|_| ch).collect::<String>();
1190
1191                let mut job = LayoutJob::single_section(text, TextFormat::default());
1192                job.wrap.max_width = target_width;
1193                job.wrap.max_rows = 1;
1194                let elided_galley = layout(&mut fonts, pixels_per_point, job.into());
1195                assert!(elided_galley.elided);
1196
1197                let test_galley = layout(
1198                    &mut fonts,
1199                    pixels_per_point,
1200                    Arc::new(LayoutJob::single_section(
1201                        (0..elided_galley.rows[0].char_count_excluding_newline())
1202                            .map(|_| ch)
1203                            .chain(std::iter::once('…'))
1204                            .collect::<String>(),
1205                        TextFormat::default(),
1206                    )),
1207                );
1208
1209                assert!(elided_galley.size().x >= 0.0);
1210                assert!(elided_galley.size().x <= target_width);
1211                assert!(test_galley.size().x > target_width);
1212            }
1213        }
1214    }
1215
1216    #[test]
1217    fn test_empty_row() {
1218        let pixels_per_point = 1.0;
1219        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1220
1221        let font_id = FontId::default();
1222        let font_height = fonts
1223            .font(&font_id.family)
1224            .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
1225            .row_height;
1226
1227        let job = LayoutJob::simple(String::new(), font_id, Color32::WHITE, f32::INFINITY);
1228
1229        let galley = layout(&mut fonts, pixels_per_point, job.into());
1230
1231        assert_eq!(galley.rows.len(), 1, "Expected one row");
1232        assert_eq!(
1233            galley.rows[0].row.glyphs.len(),
1234            0,
1235            "Expected no glyphs in the empty row"
1236        );
1237        assert_eq!(
1238            galley.size(),
1239            Vec2::new(0.0, font_height.round()),
1240            "Unexpected galley size"
1241        );
1242        assert_eq!(
1243            galley.intrinsic_size(),
1244            Vec2::new(0.0, font_height.round()),
1245            "Unexpected intrinsic size"
1246        );
1247    }
1248
1249    #[test]
1250    fn test_end_with_newline() {
1251        let pixels_per_point = 1.0;
1252        let mut fonts = FontsImpl::new(TextOptions::default(), FontDefinitions::default());
1253
1254        let font_id = FontId::default();
1255        let font_height = fonts
1256            .font(&font_id.family)
1257            .styled_metrics(pixels_per_point, font_id.size, &VariationCoords::default())
1258            .row_height;
1259
1260        let job = LayoutJob::simple("Hi!\n".to_owned(), font_id, Color32::WHITE, f32::INFINITY);
1261
1262        let galley = layout(&mut fonts, pixels_per_point, job.into());
1263
1264        assert_eq!(galley.rows.len(), 2, "Expected two rows");
1265        assert_eq!(
1266            galley.rows[1].row.glyphs.len(),
1267            0,
1268            "Expected no glyphs in the empty row"
1269        );
1270        assert_eq!(
1271            galley.size().round(),
1272            Vec2::new(17.0, font_height.round() * 2.0),
1273            "Unexpected galley size"
1274        );
1275        assert_eq!(
1276            galley.intrinsic_size().round(),
1277            Vec2::new(17.0, font_height.round() * 2.0),
1278            "Unexpected intrinsic size"
1279        );
1280    }
1281}