epaint/text/
text_layout.rs

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