word_aware_layout/word_aware_layout.rs
1//! Demonstrates the primary word-aware layout flow of `oxitext-layout`.
2//!
3//! [`LayoutEngine`] is the M6 entry point: it takes pre-shaped glyph runs
4//! (as produced by a shaper such as `oxitext-shape`) together with the
5//! source text they were shaped from, wraps them at UAX #14 line-break
6//! opportunities, applies horizontal alignment, and returns a
7//! [`LayoutResult`] with per-line and per-paragraph metrics.
8//!
9//! This example builds [`ShapedRun`]/[`ShapedGlyph`] values by hand (a real
10//! shaper would produce these), lays out a short paragraph that must wrap,
11//! then walks the resulting lines. It also shows the hand-off point to an
12//! SDF atlas: [`LayoutResult::unique_glyphs_for_atlas`] enumerates exactly
13//! the `(glyph_id, px_size)` pairs a rasterizer or SDF atlas needs to
14//! pre-warm before drawing this layout (see the `oxitext-sdf` crate's
15//! `glyph_to_sdf_atlas` example for the SDF side of that hand-off).
16//!
17//! Run with:
18//! ```text
19//! cargo run -p oxitext-layout --example word_aware_layout
20//! ```
21
22use oxitext_core::{LayoutConstraints, ShapedGlyph, ShapedRun, TextAlignment};
23use oxitext_layout::LayoutEngine;
24use std::sync::Arc;
25
26/// Builds a synthetic [`ShapedRun`] whose glyphs correspond 1:1 to the
27/// characters of `text`, each advancing the cursor by `advance` pixels.
28///
29/// `cluster` offsets are the UTF-8 byte offset of each character within
30/// `text`, matching the convention a real shaper (e.g. `oxitext-shape`)
31/// uses so that the layout engine can map glyphs back to source-text byte
32/// ranges for line breaking.
33fn shaped_run_from_text(text: &str, advance: f32) -> ShapedRun {
34 let glyphs: Vec<ShapedGlyph> = text
35 .char_indices()
36 .enumerate()
37 .map(|(i, (byte_idx, ch))| ShapedGlyph {
38 // Glyph 0 is usually `.notdef`; offset by one to avoid it.
39 gid: (i + 1) as u16,
40 x_advance: advance,
41 cluster: byte_idx as u32,
42 is_whitespace: ch.is_whitespace(),
43 ..Default::default()
44 })
45 .collect();
46 ShapedRun {
47 glyphs: glyphs.into(),
48 // A real pipeline stores the font bytes here so downstream stages
49 // (rasterizer, SDF atlas) can look up glyph outlines.
50 font_data: Arc::from(&[][..]),
51 }
52}
53
54fn main() {
55 let text = "The quick brown fox jumps";
56 let run = shaped_run_from_text(text, 12.0);
57
58 // Wrap at 120px — narrow enough that the paragraph spans several lines.
59 let constraints = LayoutConstraints {
60 max_width: 120.0,
61 font_size: 16.0,
62 };
63
64 let mut engine = LayoutEngine::new();
65 let result = engine
66 .layout(text, &[run], &constraints, TextAlignment::Left, None)
67 .expect("layout is currently infallible for well-formed input");
68
69 println!(
70 "laid out {} glyph(s) into {} line(s); paragraph size = {:.1} x {:.1}px",
71 result.glyphs.len(),
72 result.lines.len(),
73 result.metrics.total_width,
74 result.metrics.total_height,
75 );
76 assert!(
77 result.lines.len() > 1,
78 "narrow max_width should force wraps"
79 );
80
81 for (i, line) in result.lines.iter().enumerate() {
82 let glyphs = &result.glyphs[line.glyph_start..line.glyph_end];
83 let first_x = glyphs.first().map(|g| g.pos.0).unwrap_or(0.0);
84 println!(
85 " line {i}: {} glyph(s), starts at x={first_x:.1}, width={:.1}px",
86 line.len(),
87 line.metrics.width,
88 );
89 // Every wrapped line must restart at the left edge.
90 assert!((first_x - 0.0).abs() < 1e-3);
91 }
92
93 // Hand-off to a rasterizer / SDF atlas: the unique (glyph_id, px_size)
94 // pairs actually used by this layout, in first-occurrence order.
95 let glyph_set = result.unique_glyphs_for_atlas();
96 println!(
97 "{} unique glyph(s) needed for rasterization",
98 glyph_set.len()
99 );
100 assert!(!glyph_set.is_empty());
101}