Skip to main content

text_typeset/layout/
paragraph.rs

1use std::collections::HashSet;
2use std::ops::Range;
3use std::sync::OnceLock;
4
5use icu_segmenter::LineSegmenter;
6use icu_segmenter::options::LineBreakOptions;
7
8use crate::layout::line::{LayoutLine, PositionedRun, RunDecorations};
9use crate::shaping::run::{ShapedGlyph, ShapedRun};
10use crate::shaping::shaper::FontMetricsPx;
11
12/// Whether a break opportunity *must* be taken (LB4/LB5 hard line
13/// break) or *may* be taken (regular UAX #14 break opportunity).
14///
15/// `icu_segmenter::LineSegmenter` doesn't distinguish the two — it just
16/// emits byte offsets — so we classify each emitted offset ourselves by
17/// looking at the line-break property of the preceding code point.
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19enum BreakOpportunity {
20    Allowed,
21    Mandatory,
22}
23
24/// Shared compiled-data line segmenter. `LineSegmenter::new_auto`
25/// returns a `LineSegmenterBorrowed<'static>` (Copy, statically-baked
26/// CLDR data), but constructing it still touches the option-parsing
27/// path — cache once to keep `break_into_lines` allocation-free.
28fn line_segmenter() -> icu_segmenter::LineSegmenterBorrowed<'static> {
29    static CELL: OnceLock<icu_segmenter::LineSegmenterBorrowed<'static>> = OnceLock::new();
30    *CELL.get_or_init(|| LineSegmenter::new_auto(LineBreakOptions::default()))
31}
32
33/// UAX #14 LB4/LB5: a break at `byte_offset` is mandatory iff the
34/// immediately-preceding code point is one of the hard line break
35/// characters (LF, CR, NEL, VT, FF, LS, PS). The CR+LF sequence is
36/// handled implicitly: the segmenter emits a single break after the
37/// LF, and the char before that offset is `\n` — mandatory.
38fn is_mandatory_break_at(text: &str, byte_offset: usize) -> bool {
39    if byte_offset == 0 {
40        return false;
41    }
42    let preceding = &text[..byte_offset];
43    matches!(
44        preceding.chars().next_back(),
45        Some('\n' | '\r' | '\u{0085}' | '\u{000B}' | '\u{000C}' | '\u{2028}' | '\u{2029}')
46    )
47}
48
49/// Enumerate UAX #14 break opportunities in `text` and classify each
50/// as `Allowed` or `Mandatory`. Replaces the previous
51/// `unicode_linebreak::linebreaks(text)` call site one-to-one.
52fn enumerate_breaks(text: &str) -> Vec<(usize, BreakOpportunity)> {
53    line_segmenter()
54        .segment_str(text)
55        .map(|byte_offset| {
56            let kind = if is_mandatory_break_at(text, byte_offset) {
57                BreakOpportunity::Mandatory
58            } else {
59                BreakOpportunity::Allowed
60            };
61            (byte_offset, kind)
62        })
63        .collect()
64}
65
66/// Byte offsets in `text` where a hyphenated line break may occur and a
67/// hyphen glyph should be rendered: Knuth-Liang dictionary points inside
68/// words (in `lang_code`, an ISO 639-1 code) plus soft-hyphen (U+00AD)
69/// positions.
70///
71/// Returned offsets are "break before this byte" positions, matching the
72/// UAX #14 offsets from [`enumerate_breaks`]. Soft hyphens are honored
73/// regardless of language; dictionary breaks apply only when the
74/// language's patterns are compiled in (`hypher::Lang::from_iso` resolves
75/// it) — otherwise it gracefully degrades to soft-hyphen-only.
76fn hyphenation_breaks(text: &str, lang_code: [u8; 2]) -> Vec<usize> {
77    use hypher::hyphenate;
78
79    let mut offsets = Vec::new();
80
81    // Soft hyphens: break after the U+00AD so the hyphen renders at line end.
82    for (idx, ch) in text.char_indices() {
83        if ch == '\u{00AD}' {
84            offsets.push(idx + ch.len_utf8());
85        }
86    }
87
88    // Dictionary hyphenation, word by word — only if the language resolves.
89    if let Some(lang) = hypher::Lang::from_iso(lang_code) {
90        let mut word_start: Option<usize> = None;
91        let flush = |start: usize, end: usize, offsets: &mut Vec<usize>| {
92            let word = &text[start..end];
93            // Skip trivially short words; `hyphenate` would yield no interior
94            // breaks anyway, and this avoids the call overhead.
95            if word.chars().count() < 5 {
96                return;
97            }
98            let mut pos = start;
99            let mut syllables = hyphenate(word, lang).peekable();
100            while let Some(syl) = syllables.next() {
101                pos += syl.len();
102                // A break sits between syllables, not after the last one.
103                if syllables.peek().is_some() {
104                    offsets.push(pos);
105                }
106            }
107        };
108        for (idx, ch) in text.char_indices() {
109            if ch.is_alphabetic() {
110                word_start.get_or_insert(idx);
111            } else if let Some(start) = word_start.take() {
112                flush(start, idx, &mut offsets);
113            }
114        }
115        if let Some(start) = word_start.take() {
116            flush(start, text.len(), &mut offsets);
117        }
118    }
119
120    offsets.sort_unstable();
121    offsets.dedup();
122    offsets
123}
124
125/// Map hyphenation byte offsets to glyph indices (first glyph whose
126/// cluster is `>=` the offset), mirroring [`map_breaks_to_glyph_indices`].
127fn map_offsets_to_glyph_indices(flat: &[FlatGlyph], offsets: &[usize]) -> HashSet<usize> {
128    let mut set = HashSet::new();
129    let mut cursor = 0usize;
130    for &byte_offset in offsets {
131        while cursor < flat.len() && (flat[cursor].cluster as usize) < byte_offset {
132            cursor += 1;
133        }
134        set.insert(cursor.min(flat.len()));
135    }
136    set
137}
138
139/// Append a rendered hyphen glyph to the end of a line that broke at a
140/// hyphenation point, accounting for its advance in the run and line
141/// widths. The hyphen inherits the last glyph's cluster so caret/hit
142/// math treats it as part of the final character.
143fn append_hyphen(line: &mut LayoutLine, hyphen: &ShapedGlyph) {
144    if let Some(run) = line.runs.last_mut() {
145        let mut g = hyphen.clone();
146        g.cluster = run
147            .shaped_run
148            .glyphs
149            .last()
150            .map(|gl| gl.cluster)
151            .unwrap_or(0);
152        run.shaped_run.glyphs.push(g);
153        run.shaped_run.advance_width += hyphen.x_advance;
154        line.width += hyphen.x_advance;
155    }
156}
157
158/// Convert a byte offset within a UTF-8 string to a char offset.
159///
160/// Clamps to `text.len()` and rounds down to the nearest char boundary
161/// if `byte_offset` lands inside a multi-byte character. HarfBuzz can
162/// emit cluster values that don't coincide with UTF-8 char boundaries
163/// (ligature splits, fallback shaping), so callers must never assume
164/// cluster values are well-aligned.
165fn byte_offset_to_char_offset(text: &str, byte_offset: usize) -> usize {
166    let mut off = byte_offset.min(text.len());
167    while off > 0 && !text.is_char_boundary(off) {
168        off -= 1;
169    }
170    text[..off].chars().count()
171}
172
173/// Everything line wrapping needs to hyphenate: the pre-shaped hyphen
174/// glyph to append at a break and the ISO 639-1 language for the
175/// dictionary. Passed as `Some` only when hyphenation is enabled.
176pub struct Hyphenator {
177    /// Hyphen (`-`) glyph in the run's font, appended at hyphenated breaks.
178    pub glyph: ShapedGlyph,
179    /// ISO 639-1 language code for the Knuth-Liang dictionary.
180    pub language: [u8; 2],
181}
182
183/// Text alignment within a line.
184#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
185pub enum Alignment {
186    #[default]
187    Left,
188    Right,
189    Center,
190    Justify,
191}
192
193/// Break shaped runs into lines that fit within `available_width`.
194///
195/// Strategy: shape-first-then-break.
196/// 1. The caller has already shaped the full paragraph into one or more ShapedRuns.
197/// 2. We use unicode-linebreak to find break opportunities in the original text.
198/// 3. We map break positions to glyph boundaries via cluster values.
199/// 4. Greedy line wrapping: accumulate glyph advances, break at the last
200///    allowed opportunity before exceeding the width.
201/// 5. Apply alignment per line.
202pub fn break_into_lines(
203    runs: Vec<ShapedRun>,
204    text: &str,
205    available_width: f32,
206    alignment: Alignment,
207    first_line_indent: f32,
208    metrics: &FontMetricsPx,
209    hyphenator: Option<Hyphenator>,
210) -> Vec<LayoutLine> {
211    if runs.is_empty() || text.is_empty() {
212        // Empty paragraph: produce one empty line for the block to have height
213        return vec![make_empty_line(metrics, 0..0)];
214    }
215
216    // Flatten all glyphs into a single sequence with their run association
217    let flat = flatten_runs(&runs);
218    if flat.is_empty() {
219        return vec![make_empty_line(metrics, 0..0)];
220    }
221
222    // Get UAX #14 break opportunities (byte offsets in text), each
223    // classified as Allowed or Mandatory.
224    let breaks: Vec<(usize, BreakOpportunity)> = enumerate_breaks(text);
225
226    // Build sets of allowed and mandatory break positions (glyph indices)
227    let (break_points, mandatory_breaks) = map_breaks_to_glyph_indices(&flat, &breaks);
228
229    // Hyphenation break candidates (glyph indices). A break here needs a
230    // trailing hyphen glyph and must reserve its advance in the fit check.
231    let hyphen_points = if let Some(h) = &hyphenator {
232        map_offsets_to_glyph_indices(&flat, &hyphenation_breaks(text, h.language))
233    } else {
234        HashSet::new()
235    };
236    let hyphen_adv = hyphenator
237        .as_ref()
238        .map(|h| h.glyph.x_advance)
239        .unwrap_or(0.0);
240
241    // Greedy line wrapping
242    let mut lines = Vec::new();
243    let mut line_start_glyph = 0usize;
244    let mut line_width = 0.0f32;
245    // Last break opportunity within the current line: (glyph index, whether
246    // breaking there renders a hyphen).
247    let mut last_break: Option<(usize, bool)> = None;
248    // First line may be indented; subsequent lines use full width
249    let mut effective_width = available_width - first_line_indent;
250
251    for i in 0..flat.len() {
252        let glyph_advance = flat[i].x_advance;
253        line_width += glyph_advance;
254
255        // Check for mandatory break — O(1) HashSet lookup
256        let is_mandatory = mandatory_breaks.contains(&(i + 1));
257
258        let exceeds_width = line_width > effective_width && line_start_glyph < i;
259
260        if is_mandatory || exceeds_width {
261            let (break_at, needs_hyphen) = if is_mandatory {
262                (i + 1, false)
263            } else if let Some((bp, hy)) = last_break {
264                if bp > line_start_glyph {
265                    (bp, hy)
266                } else {
267                    (i + 1, false) // emergency break -no opportunity found
268                }
269            } else {
270                (i + 1, false) // emergency break -no break opportunities at all
271            };
272
273            let indent = if lines.is_empty() {
274                first_line_indent
275            } else {
276                0.0
277            };
278            let mut line = build_line(
279                &runs,
280                &flat,
281                line_start_glyph,
282                break_at,
283                metrics,
284                indent,
285                text,
286            );
287            if needs_hyphen && let Some(h) = &hyphenator {
288                append_hyphen(&mut line, &h.glyph);
289            }
290            lines.push(line);
291
292            line_start_glyph = break_at;
293            // Subsequent lines use full available width
294            effective_width = available_width;
295            // Re-accumulate width for glyphs already scanned past the break
296            line_width = 0.0;
297            for j in break_at..=i {
298                if j < flat.len() {
299                    line_width += flat[j].x_advance;
300                }
301            }
302            last_break = None;
303        }
304
305        // Update the break opportunity AFTER the width check so that a break
306        // discovered at this glyph does not clobber the previous one when the
307        // width is already exceeded. Hyphenation points are checked first so
308        // a soft hyphen (which is also a UAX #14 opportunity) renders its
309        // hyphen; they only count when the hyphen itself still fits.
310        let at = i + 1;
311        if hyphen_points.contains(&at) && line_width + hyphen_adv <= effective_width {
312            last_break = Some((at, true));
313        } else if break_points.contains(&at) {
314            last_break = Some((at, false));
315        }
316    }
317
318    // Remaining glyphs form the last line
319    if line_start_glyph < flat.len() {
320        let line = build_line(
321            &runs,
322            &flat,
323            line_start_glyph,
324            flat.len(),
325            metrics,
326            if lines.is_empty() {
327                first_line_indent
328            } else {
329                0.0
330            },
331            text,
332        );
333        lines.push(line);
334    }
335
336    // Apply alignment
337    let effective_width = available_width;
338    let last_idx = lines.len().saturating_sub(1);
339    for (i, line) in lines.iter_mut().enumerate() {
340        let indent = if i == 0 { first_line_indent } else { 0.0 };
341        let line_avail = effective_width - indent;
342        match alignment {
343            Alignment::Left => {} // runs already at x=0 (plus indent)
344            Alignment::Right => {
345                let shift = (line_avail - line.width).max(0.0);
346                for run in &mut line.runs {
347                    run.x += shift;
348                }
349            }
350            Alignment::Center => {
351                let shift = ((line_avail - line.width) / 2.0).max(0.0);
352                for run in &mut line.runs {
353                    run.x += shift;
354                }
355            }
356            Alignment::Justify => {
357                // Don't justify the last line
358                if i < last_idx && line.width > 0.0 {
359                    justify_line(line, line_avail, text);
360                }
361            }
362        }
363    }
364
365    if lines.is_empty() {
366        lines.push(make_empty_line(metrics, 0..0));
367    }
368
369    // Convert glyph cluster values from byte offsets to char offsets.
370    // This must happen AFTER alignment because justify_line needs byte
371    // offsets to find space characters in the original text.
372    for line in &mut lines {
373        for run in &mut line.runs {
374            for glyph in &mut run.shaped_run.glyphs {
375                glyph.cluster = byte_offset_to_char_offset(text, glyph.cluster as usize) as u32;
376            }
377        }
378    }
379
380    lines
381}
382
383/// A flattened glyph with enough info to map back to runs.
384struct FlatGlyph {
385    x_advance: f32,
386    cluster: u32,
387    run_index: usize,
388    glyph_index_in_run: usize,
389}
390
391fn flatten_runs(runs: &[ShapedRun]) -> Vec<FlatGlyph> {
392    let mut flat = Vec::new();
393    for (run_idx, run) in runs.iter().enumerate() {
394        // Offset cluster values from fragment-text space to block-text space.
395        // rustybuzz assigns clusters as byte offsets within the fragment text (0-based),
396        // but unicode-linebreak returns byte offsets in the full block text.
397        let cluster_offset = run.text_range.start as u32;
398        for (glyph_idx, glyph) in run.glyphs.iter().enumerate() {
399            flat.push(FlatGlyph {
400                x_advance: glyph.x_advance,
401                cluster: glyph.cluster + cluster_offset,
402                run_index: run_idx,
403                glyph_index_in_run: glyph_idx,
404            });
405        }
406    }
407    flat
408}
409
410/// Map unicode-linebreak byte offsets to glyph indices using a merged walk.
411/// Both `flat` (by cluster) and `breaks` (by byte offset) are sorted,
412/// so a single O(b + m) pass replaces the previous O(b × m) approach.
413///
414/// Returns (break_points: HashSet<glyph_idx>, mandatory_breaks: HashSet<glyph_idx>).
415fn map_breaks_to_glyph_indices(
416    flat: &[FlatGlyph],
417    breaks: &[(usize, BreakOpportunity)],
418) -> (HashSet<usize>, HashSet<usize>) {
419    let mut break_points = HashSet::new();
420    let mut mandatory_breaks = HashSet::new();
421    let mut glyph_cursor = 0usize;
422
423    for &(byte_offset, opportunity) in breaks {
424        // Advance glyph cursor to the first glyph whose cluster >= byte_offset
425        while glyph_cursor < flat.len() && (flat[glyph_cursor].cluster as usize) < byte_offset {
426            glyph_cursor += 1;
427        }
428        let glyph_idx = if glyph_cursor < flat.len() {
429            glyph_cursor
430        } else {
431            flat.len()
432        };
433        break_points.insert(glyph_idx);
434        if opportunity == BreakOpportunity::Mandatory {
435            mandatory_breaks.insert(glyph_idx);
436        }
437    }
438
439    (break_points, mandatory_breaks)
440}
441
442/// Build a LayoutLine from a glyph range within the flat sequence.
443fn build_line(
444    runs: &[ShapedRun],
445    flat: &[FlatGlyph],
446    start: usize,
447    end: usize,
448    metrics: &FontMetricsPx,
449    indent: f32,
450    text: &str,
451) -> LayoutLine {
452    // Group consecutive glyphs by run_index to reconstruct PositionedRuns
453    let mut positioned_runs = Vec::new();
454    let mut x = indent;
455    let mut current_run_idx: Option<usize> = None;
456    let mut run_glyph_start = 0usize;
457
458    for i in start..end {
459        let fg = &flat[i];
460        if current_run_idx != Some(fg.run_index) {
461            // Emit previous run segment if any
462            if let Some(prev_run_idx) = current_run_idx {
463                // End of previous run: use the last glyph we saw from that run
464                let prev_end = if i > start {
465                    flat[i - 1].glyph_index_in_run + 1
466                } else {
467                    run_glyph_start
468                };
469                let sub_run = extract_sub_run(runs, prev_run_idx, run_glyph_start, prev_end);
470                if let Some((pr, advance)) = sub_run {
471                    positioned_runs.push(PositionedRun {
472                        decorations: RunDecorations {
473                            underline_style: pr.underline_style,
474                            overline: pr.overline,
475                            strikeout: pr.strikeout,
476                            is_link: pr.is_link,
477                            foreground_color: pr.foreground_color,
478                            underline_color: pr.underline_color,
479                            background_color: pr.background_color,
480                            anchor_href: pr.anchor_href.clone(),
481                            tooltip: pr.tooltip.clone(),
482                            vertical_alignment: pr.vertical_alignment,
483                        },
484                        shaped_run: pr,
485                        x,
486                    });
487                    x += advance;
488                }
489            }
490            current_run_idx = Some(fg.run_index);
491            run_glyph_start = fg.glyph_index_in_run;
492        }
493    }
494
495    // Emit final run segment
496    if let Some(run_idx) = current_run_idx {
497        let end_in_run = if end < flat.len() && flat[end].run_index == run_idx {
498            flat[end].glyph_index_in_run
499        } else if end > start {
500            flat[end - 1].glyph_index_in_run + 1
501        } else {
502            run_glyph_start
503        };
504        let sub_run = extract_sub_run(runs, run_idx, run_glyph_start, end_in_run);
505        if let Some((pr, advance)) = sub_run {
506            positioned_runs.push(PositionedRun {
507                decorations: RunDecorations {
508                    underline_style: pr.underline_style,
509                    overline: pr.overline,
510                    strikeout: pr.strikeout,
511                    is_link: pr.is_link,
512                    foreground_color: pr.foreground_color,
513                    underline_color: pr.underline_color,
514                    background_color: pr.background_color,
515                    anchor_href: pr.anchor_href.clone(),
516                    tooltip: pr.tooltip.clone(),
517                    vertical_alignment: pr.vertical_alignment,
518                },
519                shaped_run: pr,
520                x,
521            });
522            x += advance;
523        }
524    }
525
526    let width = x - indent;
527
528    // Compute char range from cluster values.
529    // Clusters from rustybuzz are byte offsets — convert to char offsets
530    // so that positions match text-document's character-based coordinates.
531    //
532    // Glyphs are in *visual* order, so for an RTL run `flat[start]` holds
533    // the largest cluster and `flat[end-1]` the smallest. Take the min/max
534    // over the line's glyphs instead of trusting the array ends, so the
535    // logical range is correct for both directions.
536    let byte_start = flat[start..end.min(flat.len())]
537        .iter()
538        .map(|g| g.cluster as usize)
539        .min()
540        .unwrap_or(0);
541    let byte_end = if end >= flat.len() {
542        // Last glyph reaches the end of the input text. Always snap to the
543        // full length: a trailing ligature glyph may cover several source
544        // chars, so `max_cluster + 1` would be wrong.
545        text.len()
546    } else {
547        // Next visual glyph's cluster bounds an LTR line exactly; for a
548        // wrapped RTL line take whichever is larger so the logical end
549        // still covers the line's highest cluster.
550        let line_max = flat[start..end]
551            .iter()
552            .map(|g| g.cluster as usize)
553            .max()
554            .unwrap_or(0);
555        (flat[end].cluster as usize).max(line_max)
556    };
557    let char_start = byte_offset_to_char_offset(text, byte_start);
558    let char_end = byte_offset_to_char_offset(text, byte_end);
559
560    // Expand line height for inline images taller than the font ascent
561    let mut ascent = metrics.ascent;
562    for run in &positioned_runs {
563        if run.shaped_run.image_name.is_some() && run.shaped_run.image_height > ascent {
564            ascent = run.shaped_run.image_height;
565        }
566    }
567    let line_height = ascent + metrics.descent + metrics.leading;
568
569    LayoutLine {
570        runs: positioned_runs,
571        y: 0.0, // will be set by the caller (block layout)
572        ascent,
573        descent: metrics.descent,
574        leading: metrics.leading,
575        width,
576        char_range: char_start..char_end,
577        line_height,
578    }
579}
580
581/// Extract a sub-run (slice of glyphs) from a ShapedRun.
582/// Cluster values are offset to block-text space (adding text_range.start).
583fn extract_sub_run(
584    runs: &[ShapedRun],
585    run_index: usize,
586    glyph_start: usize,
587    glyph_end: usize,
588) -> Option<(ShapedRun, f32)> {
589    let run = &runs[run_index];
590    let end = glyph_end.min(run.glyphs.len());
591    if glyph_start >= end {
592        return None;
593    }
594    let cluster_offset = run.text_range.start as u32;
595    let mut sub_glyphs = run.glyphs[glyph_start..end].to_vec();
596    // Offset cluster values from fragment-local to block-text space
597    for g in &mut sub_glyphs {
598        g.cluster += cluster_offset;
599    }
600    let advance: f32 = sub_glyphs.iter().map(|g| g.x_advance).sum();
601
602    let sub_run = ShapedRun {
603        font_face_id: run.font_face_id,
604        size_px: run.size_px,
605        weight: run.weight,
606        glyphs: sub_glyphs,
607        advance_width: advance,
608        text_range: run.text_range.clone(),
609        direction: run.direction,
610        underline_style: run.underline_style,
611        overline: run.overline,
612        strikeout: run.strikeout,
613        is_link: run.is_link,
614        foreground_color: run.foreground_color,
615        underline_color: run.underline_color,
616        background_color: run.background_color,
617        anchor_href: run.anchor_href.clone(),
618        tooltip: run.tooltip.clone(),
619        vertical_alignment: run.vertical_alignment,
620        image_name: run.image_name.clone(),
621        image_height: run.image_height,
622    };
623    Some((sub_run, advance))
624}
625
626fn make_empty_line(metrics: &FontMetricsPx, char_range: Range<usize>) -> LayoutLine {
627    LayoutLine {
628        runs: Vec::new(),
629        y: 0.0,
630        ascent: metrics.ascent,
631        descent: metrics.descent,
632        leading: metrics.leading,
633        width: 0.0,
634        char_range,
635        line_height: metrics.ascent + metrics.descent + metrics.leading,
636    }
637}
638
639/// Distribute extra space among word gaps for justification.
640///
641/// Finds space glyphs (cluster mapping to ' ') across all runs and
642/// increases their x_advance proportionally. Then recomputes run x positions.
643fn justify_line(line: &mut LayoutLine, target_width: f32, text: &str) {
644    let extra = target_width - line.width;
645    if extra <= 0.0 {
646        return;
647    }
648
649    // Count space glyphs across all runs
650    let mut space_count = 0usize;
651    for run in &line.runs {
652        for glyph in &run.shaped_run.glyphs {
653            let byte_offset = glyph.cluster as usize;
654            if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
655                && ch == ' '
656            {
657                space_count += 1;
658            }
659        }
660    }
661
662    if space_count == 0 {
663        return;
664    }
665
666    let extra_per_space = extra / space_count as f32;
667
668    // Increase x_advance of space glyphs
669    for run in &mut line.runs {
670        for glyph in &mut run.shaped_run.glyphs {
671            let byte_offset = glyph.cluster as usize;
672            if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
673                && ch == ' '
674            {
675                glyph.x_advance += extra_per_space;
676            }
677        }
678        // Recompute run advance width
679        run.shaped_run.advance_width = run.shaped_run.glyphs.iter().map(|g| g.x_advance).sum();
680    }
681
682    // Recompute run x positions (runs follow each other)
683    let first_x = line.runs.first().map(|r| r.x).unwrap_or(0.0);
684    let mut x = first_x;
685    for run in &mut line.runs {
686        run.x = x;
687        x += run.shaped_run.advance_width;
688    }
689
690    line.width = target_width;
691}