ailloli_ui_text/prepared.rs
1//! One-shot prepared text layout: glyphs without per-instance font byte copies.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use ailloli_ui_core::{style::TextStyle, Color, Rect};
7use parley::PositionedLayoutItem;
8use parley::{Affinity, Cursor};
9
10use crate::engine_parley::ParleyEngine;
11use crate::glyph::GlyphInstance;
12use crate::params::{LaidOutLine, StyledTextLayoutParams, TextLayoutParams, TextMetrics, WrapMode};
13
14/// Laid-out text plus glyph instances ready for GPU atlas rasterization.
15///
16/// The source text, internal Parley layout, and glyph slice are owned through
17/// reference-counted allocations. Glyph instances identify font data stored in
18/// the `face_blobs` map supplied to [`prepare_layout`] or
19/// [`prepare_styled_layout`]; they do not copy font bytes individually.
20/// Dimensions and glyph positions are logical pixels.
21///
22/// # Examples
23///
24/// ```
25/// use std::collections::HashMap;
26/// use ailloli_ui_core::{Color, FontId, TextStyle};
27/// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
28/// #[allow(deprecated)] let mut engine = ParleyEngine::new();
29/// let mut faces = HashMap::new();
30/// let prepared = prepare_layout(
31/// &mut engine,
32/// TextLayoutParams::new("hello", TextStyle::new(FontId::Ui, 16, Color::WHITE)),
33/// &mut faces,
34/// );
35/// assert_eq!(prepared.text(), "hello");
36/// assert!(!prepared.lines.is_empty());
37/// ```
38#[derive(Debug)]
39pub struct PreparedTextLayout {
40 /// Owned source text referenced by byte ranges and caret methods.
41 text: Arc<str>,
42 /// Base style used for the layout.
43 style: TextStyle,
44 /// Uniform or styled Parley layout retained for caret hit testing.
45 layout: Arc<ParleyLayoutDebug>,
46 /// Per-line summaries in source order.
47 pub lines: Vec<LaidOutLine>,
48 /// Overall layout extent in logical pixels.
49 pub metrics: TextMetrics,
50 /// Flattened renderer-facing glyphs.
51 glyphs: Arc<[GlyphInstance]>,
52}
53
54/// Local wrapper to implement `Debug` without exposing Parley brush types.
55///
56/// The enum remains public because it appears in generated private-item
57/// documentation, but [`PreparedTextLayout`] keeps its instance private. Its
58/// `Debug` implementation intentionally prints only the layout brush kind and
59/// does not dump shaped text or font data.
60///
61/// # Examples
62///
63/// ```
64/// use ailloli_ui_core::{Color, FontId, TextStyle};
65/// use ailloli_ui_text::{ParleyEngine, prepared::ParleyLayoutDebug};
66/// #[allow(deprecated)] let mut engine = ParleyEngine::new();
67/// let layout = engine.layout_text("x", TextStyle::new(FontId::Ui, 12, Color::WHITE), None);
68/// let wrapped = ParleyLayoutDebug::Uniform(layout);
69/// assert!(format!("{wrapped:?}").contains("Layout"));
70/// ```
71pub enum ParleyLayoutDebug {
72 /// Layout whose glyph runs carry unit brushes and use a uniform paint color.
73 Uniform(parley::Layout<()>),
74 /// Layout whose glyph runs carry per-span linear-RGBA brushes.
75 Styled(parley::Layout<[f32; 4]>),
76}
77
78impl std::fmt::Debug for ParleyLayoutDebug {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 match self {
81 Self::Uniform(_) => f.debug_struct("parley::Layout<()>").finish(),
82 Self::Styled(_) => f.debug_struct("parley::Layout<[f32;4]>").finish(),
83 }
84 }
85}
86
87impl PreparedTextLayout {
88 /// Returns the source text owned by this prepared layout.
89 ///
90 /// # Examples
91 ///
92 /// ```
93 /// use std::collections::HashMap;
94 /// use ailloli_ui_core::{Color, FontId, TextStyle};
95 /// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
96 /// #[allow(deprecated)] let mut engine = ParleyEngine::new();
97 /// let mut faces = HashMap::new();
98 /// let prepared = prepare_layout(&mut engine, TextLayoutParams::new("é", TextStyle::new(FontId::Ui, 14, Color::WHITE)), &mut faces);
99 /// assert_eq!(prepared.text().len(), 2);
100 /// ```
101 pub fn text(&self) -> &str {
102 self.text.as_ref()
103 }
104
105 /// Returns the copyable base style used to build the layout.
106 ///
107 /// For styled layouts this is the style outside overrides; individual glyph
108 /// colors are available through [`Self::glyphs`].
109 ///
110 /// # Examples
111 ///
112 /// ```
113 /// use std::collections::HashMap;
114 /// use ailloli_ui_core::{Color, FontId, TextStyle};
115 /// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
116 /// #[allow(deprecated)] let mut engine = ParleyEngine::new();
117 /// let style = TextStyle::new(FontId::Mono, 13, Color::WHITE);
118 /// let mut faces = HashMap::new();
119 /// let prepared = prepare_layout(&mut engine, TextLayoutParams::new("x", style), &mut faces);
120 /// assert_eq!(prepared.style(), style);
121 /// ```
122 pub fn style(&self) -> TextStyle {
123 self.style
124 }
125
126 /// Returns the overall layout width in logical pixels.
127 ///
128 /// # Examples
129 ///
130 /// ```
131 /// use std::collections::HashMap;
132 /// use ailloli_ui_core::{Color, FontId, TextStyle};
133 /// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
134 /// #[allow(deprecated)] let mut engine = ParleyEngine::new();
135 /// let mut faces = HashMap::new();
136 /// let prepared = prepare_layout(&mut engine, TextLayoutParams::new("width", TextStyle::new(FontId::Ui, 16, Color::WHITE)), &mut faces);
137 /// assert_eq!(prepared.width(), prepared.metrics.width);
138 /// ```
139 pub fn width(&self) -> f32 {
140 self.metrics.width
141 }
142
143 /// Returns the overall layout height in logical pixels.
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// use std::collections::HashMap;
149 /// use ailloli_ui_core::{Color, FontId, TextStyle};
150 /// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
151 /// #[allow(deprecated)] let mut engine = ParleyEngine::new();
152 /// let mut faces = HashMap::new();
153 /// let prepared = prepare_layout(&mut engine, TextLayoutParams::new("height", TextStyle::new(FontId::Ui, 16, Color::WHITE)), &mut faces);
154 /// assert_eq!(prepared.height(), prepared.metrics.height);
155 /// assert!(prepared.height() > 0.0);
156 /// ```
157 pub fn height(&self) -> f32 {
158 self.metrics.height
159 }
160
161 /// Borrows flattened glyph instances without allocating.
162 ///
163 /// Uniform layouts use `color == None`; styled layouts attach the effective
164 /// linear-RGBA run color. An empty input can produce an empty slice.
165 ///
166 /// # Examples
167 ///
168 /// ```
169 /// use std::collections::HashMap;
170 /// use ailloli_ui_core::{Color, FontId, TextStyle};
171 /// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
172 /// #[allow(deprecated)] let mut engine = ParleyEngine::new();
173 /// let mut faces = HashMap::new();
174 /// let prepared = prepare_layout(&mut engine, TextLayoutParams::new("Hi", TextStyle::new(FontId::Ui, 16, Color::WHITE)), &mut faces);
175 /// assert!(!prepared.glyphs().is_empty());
176 /// assert!(prepared.glyphs().iter().all(|glyph| glyph.color.is_none()));
177 /// ```
178 pub fn glyphs(&self) -> &[GlyphInstance] {
179 &self.glyphs
180 }
181
182 /// Maps a UTF-8 byte index to the downstream caret's logical-pixel X.
183 ///
184 /// Shaped clusters, combining sequences, and ligatures may make multiple
185 /// byte indices share an X coordinate.
186 ///
187 /// # Examples
188 ///
189 /// ```
190 /// use std::collections::HashMap;
191 /// use ailloli_ui_core::{Color, FontId, TextStyle};
192 /// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
193 /// #[allow(deprecated)] let mut engine = ParleyEngine::new();
194 /// let mut faces = HashMap::new();
195 /// let prepared = prepare_layout(&mut engine, TextLayoutParams::new("abc", TextStyle::new(FontId::Ui, 16, Color::WHITE)), &mut faces);
196 /// assert!(prepared.caret_x_at(3) >= prepared.caret_x_at(0));
197 /// ```
198 pub fn caret_x_at(&self, byte_idx: usize) -> f32 {
199 self.caret_rect_at(byte_idx, 0.0).x
200 }
201
202 /// Maps a UTF-8 byte index to a logical-pixel caret rectangle.
203 ///
204 /// `width` is normalized with a zero lower bound; negative and NaN values
205 /// behave as zero. A downstream affinity resolves ambiguous boundaries.
206 /// The result height is never negative.
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// use std::collections::HashMap;
212 /// use ailloli_ui_core::{Color, FontId, TextStyle};
213 /// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
214 /// #[allow(deprecated)] let mut engine = ParleyEngine::new();
215 /// let mut faces = HashMap::new();
216 /// let prepared = prepare_layout(&mut engine, TextLayoutParams::new("abc", TextStyle::new(FontId::Ui, 16, Color::WHITE)), &mut faces);
217 /// let rect = prepared.caret_rect_at(0, 1.5);
218 /// assert!(rect.w >= 1.5 && rect.h >= 0.0);
219 /// ```
220 pub fn caret_rect_at(&self, byte_idx: usize, width: f32) -> Rect {
221 match self.layout.as_ref() {
222 ParleyLayoutDebug::Uniform(layout) => caret_rect_at_layout(layout, byte_idx, width),
223 ParleyLayoutDebug::Styled(layout) => caret_rect_at_layout(layout, byte_idx, width),
224 }
225 }
226
227 /// Maps a logical layout-space point to a cluster-aware UTF-8 byte index.
228 ///
229 /// Parley resolves points outside glyph bounds to a valid cursor in the
230 /// layout. No explicit coordinate clamping is performed here.
231 ///
232 /// # Examples
233 ///
234 /// ```
235 /// use std::collections::HashMap;
236 /// use ailloli_ui_core::{Color, FontId, TextStyle};
237 /// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
238 /// #[allow(deprecated)] let mut engine = ParleyEngine::new();
239 /// let mut faces = HashMap::new();
240 /// let prepared = prepare_layout(&mut engine, TextLayoutParams::new("abc", TextStyle::new(FontId::Ui, 16, Color::WHITE)), &mut faces);
241 /// assert_eq!(prepared.caret_index_at_point(-100.0, 0.0), 0);
242 /// ```
243 pub fn caret_index_at_point(&self, x: f32, y: f32) -> usize {
244 match self.layout.as_ref() {
245 ParleyLayoutDebug::Uniform(layout) => Cursor::from_point(layout, x, y).index(),
246 ParleyLayoutDebug::Styled(layout) => Cursor::from_point(layout, x, y).index(),
247 }
248 }
249}
250
251/// Builds a Parley layout and glyphs; registers font blobs by `face_id`.
252///
253/// `face_blobs` is an append-only caller-owned registry during this call:
254/// existing IDs are not replaced, and each newly encountered face receives one
255/// reference-counted copy of its complete font bytes. The map has no eviction
256/// policy. [`WrapMode::NoWrap`] ignores `params.max_width`; other modes treat it
257/// as an optional logical-pixel constraint. Uniform glyph colors are `None`
258/// because painting supplies `params.style.color` separately.
259///
260/// # Examples
261///
262/// ```
263/// use std::collections::HashMap;
264/// use ailloli_ui_core::{Color, FontId, TextStyle};
265/// use ailloli_ui_text::{prepare_layout, ParleyEngine, TextLayoutParams};
266/// #[allow(deprecated)] let mut engine = ParleyEngine::new();
267/// let mut faces = HashMap::new();
268/// let prepared = prepare_layout(
269/// &mut engine,
270/// TextLayoutParams::new("render", TextStyle::new(FontId::Mono, 14, Color::WHITE)),
271/// &mut faces,
272/// );
273/// assert!(!prepared.glyphs().is_empty());
274/// assert!(!faces.is_empty());
275/// ```
276pub fn prepare_layout(
277 engine: &mut ParleyEngine,
278 params: TextLayoutParams<'_>,
279 face_blobs: &mut HashMap<u64, Arc<[u8]>>,
280) -> PreparedTextLayout {
281 let max_width = match params.wrap_mode {
282 WrapMode::NoWrap => None,
283 WrapMode::Word | WrapMode::WordOrAnywhere => params.max_width,
284 };
285
286 let layout =
287 engine.layout_text_with_wrap(params.text, params.style, max_width, params.wrap_mode);
288 let metrics = TextMetrics {
289 width: layout.width(),
290 height: layout.height(),
291 };
292 let lines = laid_out_lines(&layout);
293 let glyphs = glyphs_from_layout(&layout, face_blobs, None);
294
295 PreparedTextLayout {
296 text: Arc::from(params.text),
297 style: params.style,
298 layout: Arc::new(ParleyLayoutDebug::Uniform(layout)),
299 lines,
300 metrics,
301 glyphs: glyphs.into(),
302 }
303}
304
305/// Builds a colored, ranged-style layout and registers its font blobs.
306///
307/// Each output glyph carries the effective run color. Span ranges must be
308/// ordered, in bounds, and on UTF-8 boundaries; they replace font family, size,
309/// and color, while text decoration remains paint-only. `face_blobs` has the
310/// same append-only, no-eviction behavior as in [`prepare_layout`].
311///
312/// # Panics
313///
314/// Parley may panic if a span has an invalid UTF-8 byte range.
315///
316/// # Examples
317///
318/// ```
319/// use std::collections::HashMap;
320/// use ailloli_ui_core::{Color, FontId, TextStyle};
321/// use ailloli_ui_text::{ParleyEngine, StyledTextLayoutParams, StyledTextSpan, WrapMode};
322/// use ailloli_ui_text::prepared::prepare_styled_layout;
323/// #[allow(deprecated)] let mut engine = ParleyEngine::new();
324/// let base = TextStyle::new(FontId::Mono, 14, Color::WHITE);
325/// let spans = [StyledTextSpan { range: 0..2, style: TextStyle::new(FontId::Mono, 14, Color::BLACK) }];
326/// let mut faces = HashMap::new();
327/// let prepared = prepare_styled_layout(&mut engine, StyledTextLayoutParams {
328/// text: "fn main", base_style: base, spans: &spans,
329/// max_width: None, wrap_mode: WrapMode::NoWrap,
330/// }, &mut faces);
331/// assert!(prepared.glyphs().iter().all(|glyph| glyph.color.is_some()));
332/// ```
333pub fn prepare_styled_layout(
334 engine: &mut ParleyEngine,
335 params: StyledTextLayoutParams<'_>,
336 face_blobs: &mut HashMap<u64, Arc<[u8]>>,
337) -> PreparedTextLayout {
338 let max_width = match params.wrap_mode {
339 WrapMode::NoWrap => None,
340 WrapMode::Word | WrapMode::WordOrAnywhere => params.max_width,
341 };
342
343 let layout = engine.layout_styled_text_with_wrap(
344 params.text,
345 params.base_style,
346 params.spans,
347 max_width,
348 params.wrap_mode,
349 );
350 let metrics = TextMetrics {
351 width: layout.width(),
352 height: layout.height(),
353 };
354 let lines = laid_out_lines(&layout);
355 let glyphs = styled_glyphs_from_layout(&layout, face_blobs);
356
357 PreparedTextLayout {
358 text: Arc::from(params.text),
359 style: params.base_style,
360 layout: Arc::new(ParleyLayoutDebug::Styled(layout)),
361 lines,
362 metrics,
363 glyphs: glyphs.into(),
364 }
365}
366
367/// Extracts source ranges and logical-pixel metrics from all Parley lines.
368fn laid_out_lines<B: parley::style::Brush>(layout: &parley::Layout<B>) -> Vec<LaidOutLine> {
369 let mut lines = Vec::new();
370 for line in layout.lines() {
371 let m = *line.metrics();
372 let width = (m.advance - m.trailing_whitespace).max(0.0);
373 lines.push(LaidOutLine {
374 text_range: line.text_range(),
375 width,
376 ascent: m.ascent,
377 descent: m.descent,
378 baseline_y: m.baseline,
379 });
380 }
381 lines
382}
383
384/// Flattens uniform-brush glyph runs and records their font blobs.
385fn glyphs_from_layout(
386 layout: &parley::Layout<()>,
387 face_blobs: &mut HashMap<u64, Arc<[u8]>>,
388 color: Option<Color>,
389) -> Vec<GlyphInstance> {
390 let mut out = Vec::new();
391 for line in layout.lines() {
392 for item in line.items() {
393 let PositionedLayoutItem::GlyphRun(run) = item else {
394 continue;
395 };
396 push_glyph_run(&mut out, face_blobs, &run, color);
397 }
398 }
399 out
400}
401
402/// Flattens colored glyph runs and converts brushes to Ailloli colors.
403fn styled_glyphs_from_layout(
404 layout: &parley::Layout<[f32; 4]>,
405 face_blobs: &mut HashMap<u64, Arc<[u8]>>,
406) -> Vec<GlyphInstance> {
407 let mut out = Vec::new();
408 for line in layout.lines() {
409 for item in line.items() {
410 let PositionedLayoutItem::GlyphRun(run) = item else {
411 continue;
412 };
413 let [r, g, b, a] = run.style().brush;
414 push_glyph_run(
415 &mut out,
416 face_blobs,
417 &run,
418 Some(Color::from_f32_const(r, g, b, a)),
419 );
420 }
421 }
422 out
423}
424
425/// Registers one run's font once and appends all of its positioned glyphs.
426fn push_glyph_run<B: parley::style::Brush>(
427 out: &mut Vec<GlyphInstance>,
428 face_blobs: &mut HashMap<u64, Arc<[u8]>>,
429 run: &parley::layout::GlyphRun<'_, B>,
430 color: Option<Color>,
431) {
432 let font = run.run().font();
433 let face_id = font.data.id();
434 let font_index = font.index;
435 face_blobs
436 .entry(face_id)
437 .or_insert_with(|| Arc::from(font.data.data()));
438
439 let px_size = run.run().font_size().round().max(1.0) as u16;
440 for g in run.positioned_glyphs() {
441 out.push(GlyphInstance {
442 face_id,
443 font_index,
444 glyph_id: g.id,
445 px_size,
446 x: g.x,
447 y: g.y,
448 color,
449 });
450 }
451}
452
453/// Computes downstream caret geometry for either Parley brush type.
454fn caret_rect_at_layout<B: parley::style::Brush>(
455 layout: &parley::Layout<B>,
456 byte_idx: usize,
457 width: f32,
458) -> Rect {
459 let cur = Cursor::from_byte_index(layout, byte_idx, Affinity::Downstream);
460 let rect = cur.geometry(layout, width.max(0.0));
461 Rect::new(
462 rect.x0 as f32,
463 rect.y0 as f32,
464 (rect.x1 - rect.x0).max(width.max(0.0) as f64) as f32,
465 (rect.y1 - rect.y0).max(0.0) as f32,
466 )
467}