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}