epaint/text/
text_layout.rs

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