Skip to main content

text_typeset/layout/
paragraph.rs

1use std::ops::Range;
2
3use unicode_linebreak::{BreakOpportunity, linebreaks};
4
5use crate::layout::line::{LayoutLine, PositionedRun, RunDecorations};
6use crate::shaping::run::ShapedRun;
7use crate::shaping::shaper::FontMetricsPx;
8
9/// Text alignment within a line.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum Alignment {
12    #[default]
13    Left,
14    Right,
15    Center,
16    Justify,
17}
18
19/// Break shaped runs into lines that fit within `available_width`.
20///
21/// Strategy: shape-first-then-break.
22/// 1. The caller has already shaped the full paragraph into one or more ShapedRuns.
23/// 2. We use unicode-linebreak to find break opportunities in the original text.
24/// 3. We map break positions to glyph boundaries via cluster values.
25/// 4. Greedy line wrapping: accumulate glyph advances, break at the last
26///    allowed opportunity before exceeding the width.
27/// 5. Apply alignment per line.
28pub fn break_into_lines(
29    runs: Vec<ShapedRun>,
30    text: &str,
31    available_width: f32,
32    alignment: Alignment,
33    first_line_indent: f32,
34    metrics: &FontMetricsPx,
35) -> Vec<LayoutLine> {
36    if runs.is_empty() || text.is_empty() {
37        // Empty paragraph: produce one empty line for the block to have height
38        return vec![make_empty_line(metrics, 0..0)];
39    }
40
41    // Flatten all glyphs into a single sequence with their run association
42    let flat = flatten_runs(&runs);
43    if flat.is_empty() {
44        return vec![make_empty_line(metrics, 0..0)];
45    }
46
47    // Get break opportunities from unicode-linebreak (byte offsets in text)
48    let breaks: Vec<(usize, BreakOpportunity)> = linebreaks(text).collect();
49
50    // Build a set of allowed break positions (glyph indices)
51    let break_points = map_breaks_to_glyph_indices(&flat, &breaks);
52
53    // Greedy line wrapping
54    let mut lines = Vec::new();
55    let mut line_start_glyph = 0usize;
56    let mut line_width = 0.0f32;
57    let mut last_break_glyph: Option<usize> = None;
58    // First line may be indented; subsequent lines use full width
59    let mut effective_width = available_width - first_line_indent;
60
61    for i in 0..flat.len() {
62        let glyph_advance = flat[i].x_advance;
63        line_width += glyph_advance;
64
65        // Check if this glyph index is a break point
66        if break_points.contains(&(i + 1)) {
67            last_break_glyph = Some(i + 1);
68        }
69
70        // Check for mandatory break
71        let is_mandatory = break_points_mandatory(&breaks, &flat, i + 1);
72
73        let exceeds_width = line_width > effective_width && line_start_glyph < i;
74
75        if is_mandatory || exceeds_width {
76            let break_at = if is_mandatory {
77                i + 1
78            } else if let Some(bp) = last_break_glyph {
79                if bp > line_start_glyph {
80                    bp
81                } else {
82                    i + 1 // emergency break -no opportunity found
83                }
84            } else {
85                i + 1 // emergency break -no break opportunities at all
86            };
87
88            let indent = if lines.is_empty() {
89                first_line_indent
90            } else {
91                0.0
92            };
93            let line = build_line(&runs, &flat, line_start_glyph, break_at, metrics, indent);
94            lines.push(line);
95
96            line_start_glyph = break_at;
97            // Subsequent lines use full available width
98            effective_width = available_width;
99            // Re-accumulate width for glyphs already scanned past the break
100            line_width = 0.0;
101            for j in break_at..=i {
102                if j < flat.len() {
103                    line_width += flat[j].x_advance;
104                }
105            }
106            last_break_glyph = None;
107        }
108    }
109
110    // Remaining glyphs form the last line
111    if line_start_glyph < flat.len() {
112        let line = build_line(
113            &runs,
114            &flat,
115            line_start_glyph,
116            flat.len(),
117            metrics,
118            if lines.is_empty() {
119                first_line_indent
120            } else {
121                0.0
122            },
123        );
124        lines.push(line);
125    }
126
127    // Apply alignment
128    let effective_width = available_width;
129    let last_idx = lines.len().saturating_sub(1);
130    for (i, line) in lines.iter_mut().enumerate() {
131        let indent = if i == 0 { first_line_indent } else { 0.0 };
132        let line_avail = effective_width - indent;
133        match alignment {
134            Alignment::Left => {} // runs already at x=0 (plus indent)
135            Alignment::Right => {
136                let shift = (line_avail - line.width).max(0.0);
137                for run in &mut line.runs {
138                    run.x += shift;
139                }
140            }
141            Alignment::Center => {
142                let shift = ((line_avail - line.width) / 2.0).max(0.0);
143                for run in &mut line.runs {
144                    run.x += shift;
145                }
146            }
147            Alignment::Justify => {
148                // Don't justify the last line
149                if i < last_idx && line.width > 0.0 {
150                    justify_line(line, line_avail, text);
151                }
152            }
153        }
154    }
155
156    if lines.is_empty() {
157        lines.push(make_empty_line(metrics, 0..0));
158    }
159
160    lines
161}
162
163/// A flattened glyph with enough info to map back to runs.
164struct FlatGlyph {
165    x_advance: f32,
166    cluster: u32,
167    run_index: usize,
168    glyph_index_in_run: usize,
169}
170
171fn flatten_runs(runs: &[ShapedRun]) -> Vec<FlatGlyph> {
172    let mut flat = Vec::new();
173    for (run_idx, run) in runs.iter().enumerate() {
174        // Offset cluster values from fragment-text space to block-text space.
175        // rustybuzz assigns clusters as byte offsets within the fragment text (0-based),
176        // but unicode-linebreak returns byte offsets in the full block text.
177        let cluster_offset = run.text_range.start as u32;
178        for (glyph_idx, glyph) in run.glyphs.iter().enumerate() {
179            flat.push(FlatGlyph {
180                x_advance: glyph.x_advance,
181                cluster: glyph.cluster + cluster_offset,
182                run_index: run_idx,
183                glyph_index_in_run: glyph_idx,
184            });
185        }
186    }
187    flat
188}
189
190/// Map unicode-linebreak byte offsets to glyph indices.
191/// A break at byte offset B maps to the glyph whose cluster >= B.
192fn map_breaks_to_glyph_indices(
193    flat: &[FlatGlyph],
194    breaks: &[(usize, BreakOpportunity)],
195) -> Vec<usize> {
196    let mut result = Vec::new();
197    for &(byte_offset, _opportunity) in breaks {
198        // Find the first glyph whose cluster starts at or after byte_offset
199        if let Some(glyph_idx) = flat.iter().position(|g| g.cluster as usize >= byte_offset) {
200            if !result.contains(&glyph_idx) {
201                result.push(glyph_idx);
202            }
203        } else {
204            // Break is at or past end of text
205            result.push(flat.len());
206        }
207    }
208    result
209}
210
211/// Check if glyph index `glyph_idx` corresponds to a mandatory break.
212fn break_points_mandatory(
213    breaks: &[(usize, BreakOpportunity)],
214    flat: &[FlatGlyph],
215    glyph_idx: usize,
216) -> bool {
217    if glyph_idx == 0 || glyph_idx > flat.len() {
218        return false;
219    }
220
221    // Find the byte offset for this glyph boundary
222    let byte_offset = if glyph_idx < flat.len() {
223        flat[glyph_idx].cluster as usize
224    } else {
225        // Past last glyph -check if there's a mandatory break at the end
226        return false;
227    };
228
229    breaks
230        .iter()
231        .any(|&(bo, op)| bo == byte_offset && op == BreakOpportunity::Mandatory)
232}
233
234/// Build a LayoutLine from a glyph range within the flat sequence.
235fn build_line(
236    runs: &[ShapedRun],
237    flat: &[FlatGlyph],
238    start: usize,
239    end: usize,
240    metrics: &FontMetricsPx,
241    indent: f32,
242) -> LayoutLine {
243    // Group consecutive glyphs by run_index to reconstruct PositionedRuns
244    let mut positioned_runs = Vec::new();
245    let mut x = indent;
246    let mut current_run_idx: Option<usize> = None;
247    let mut run_glyph_start = 0usize;
248
249    for i in start..end {
250        let fg = &flat[i];
251        if current_run_idx != Some(fg.run_index) {
252            // Emit previous run segment if any
253            if let Some(prev_run_idx) = current_run_idx {
254                // End of previous run: use the last glyph we saw from that run
255                let prev_end = if i > start {
256                    flat[i - 1].glyph_index_in_run + 1
257                } else {
258                    run_glyph_start
259                };
260                let sub_run = extract_sub_run(runs, prev_run_idx, run_glyph_start, prev_end);
261                if let Some((pr, advance)) = sub_run {
262                    positioned_runs.push(PositionedRun {
263                        decorations: RunDecorations {
264                            underline: pr.underline,
265                            overline: pr.overline,
266                            strikeout: pr.strikeout,
267                            is_link: pr.is_link,
268                        },
269                        shaped_run: pr,
270                        x,
271                    });
272                    x += advance;
273                }
274            }
275            current_run_idx = Some(fg.run_index);
276            run_glyph_start = fg.glyph_index_in_run;
277        }
278    }
279
280    // Emit final run segment
281    if let Some(run_idx) = current_run_idx {
282        let end_in_run = if end < flat.len() && flat[end].run_index == run_idx {
283            flat[end].glyph_index_in_run
284        } else if end > start {
285            flat[end - 1].glyph_index_in_run + 1
286        } else {
287            run_glyph_start
288        };
289        let sub_run = extract_sub_run(runs, run_idx, run_glyph_start, end_in_run);
290        if let Some((pr, advance)) = sub_run {
291            positioned_runs.push(PositionedRun {
292                decorations: RunDecorations {
293                    underline: pr.underline,
294                    overline: pr.overline,
295                    strikeout: pr.strikeout,
296                    is_link: pr.is_link,
297                },
298                shaped_run: pr,
299                x,
300            });
301            x += advance;
302        }
303    }
304
305    let width = x - indent;
306
307    // Compute char range from cluster values
308    let char_start = if start < flat.len() {
309        flat[start].cluster as usize
310    } else {
311        0
312    };
313    let char_end = if end > 0 && end <= flat.len() {
314        if end < flat.len() {
315            flat[end].cluster as usize
316        } else {
317            // Use end of last glyph's run text range
318            flat[end - 1].cluster as usize + 1 // approximate
319        }
320    } else {
321        char_start
322    };
323
324    let line_height = metrics.ascent + metrics.descent + metrics.leading;
325
326    LayoutLine {
327        runs: positioned_runs,
328        y: 0.0, // will be set by the caller (block layout)
329        ascent: metrics.ascent,
330        descent: metrics.descent,
331        leading: metrics.leading,
332        width,
333        char_range: char_start..char_end,
334        line_height,
335    }
336}
337
338/// Extract a sub-run (slice of glyphs) from a ShapedRun.
339/// Cluster values are offset to block-text space (adding text_range.start).
340fn extract_sub_run(
341    runs: &[ShapedRun],
342    run_index: usize,
343    glyph_start: usize,
344    glyph_end: usize,
345) -> Option<(ShapedRun, f32)> {
346    let run = &runs[run_index];
347    let end = glyph_end.min(run.glyphs.len());
348    if glyph_start >= end {
349        return None;
350    }
351    let cluster_offset = run.text_range.start as u32;
352    let mut sub_glyphs = run.glyphs[glyph_start..end].to_vec();
353    // Offset cluster values from fragment-local to block-text space
354    for g in &mut sub_glyphs {
355        g.cluster += cluster_offset;
356    }
357    let advance: f32 = sub_glyphs.iter().map(|g| g.x_advance).sum();
358
359    let sub_run = ShapedRun {
360        font_face_id: run.font_face_id,
361        size_px: run.size_px,
362        glyphs: sub_glyphs,
363        advance_width: advance,
364        text_range: run.text_range.clone(),
365        underline: run.underline,
366        overline: run.overline,
367        strikeout: run.strikeout,
368        is_link: run.is_link,
369    };
370    Some((sub_run, advance))
371}
372
373fn make_empty_line(metrics: &FontMetricsPx, char_range: Range<usize>) -> LayoutLine {
374    LayoutLine {
375        runs: Vec::new(),
376        y: 0.0,
377        ascent: metrics.ascent,
378        descent: metrics.descent,
379        leading: metrics.leading,
380        width: 0.0,
381        char_range,
382        line_height: metrics.ascent + metrics.descent + metrics.leading,
383    }
384}
385
386/// Distribute extra space among word gaps for justification.
387///
388/// Finds space glyphs (cluster mapping to ' ') across all runs and
389/// increases their x_advance proportionally. Then recomputes run x positions.
390fn justify_line(line: &mut LayoutLine, target_width: f32, text: &str) {
391    let extra = target_width - line.width;
392    if extra <= 0.0 {
393        return;
394    }
395
396    // Count space glyphs across all runs
397    let mut space_count = 0usize;
398    for run in &line.runs {
399        for glyph in &run.shaped_run.glyphs {
400            let byte_offset = glyph.cluster as usize;
401            if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
402                && ch == ' '
403            {
404                space_count += 1;
405            }
406        }
407    }
408
409    if space_count == 0 {
410        return;
411    }
412
413    let extra_per_space = extra / space_count as f32;
414
415    // Increase x_advance of space glyphs
416    for run in &mut line.runs {
417        for glyph in &mut run.shaped_run.glyphs {
418            let byte_offset = glyph.cluster as usize;
419            if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
420                && ch == ' '
421            {
422                glyph.x_advance += extra_per_space;
423            }
424        }
425        // Recompute run advance width
426        run.shaped_run.advance_width = run.shaped_run.glyphs.iter().map(|g| g.x_advance).sum();
427    }
428
429    // Recompute run x positions (runs follow each other)
430    let first_x = line.runs.first().map(|r| r.x).unwrap_or(0.0);
431    let mut x = first_x;
432    for run in &mut line.runs {
433        run.x = x;
434        x += run.shaped_run.advance_width;
435    }
436
437    line.width = target_width;
438}