Skip to main content

alpha2_engine/
text.rs

1//! Text rendering: caption tracks' cues and text generators, rasterized by
2//! the portable text host.
3//!
4//! The engine draws text itself, from font bytes its host registers once,
5//! so a browser preview and a native export of one project, given the same
6//! fonts, draw the same text pixels. A host keeps one [`TextTextures`]
7//! beside its frame provider, calls [`TextTextures::prepare`] for each frame
8//! before rendering it, and answers [`crate::FrameProvider::text`] from it.
9//! A caption is composited as a layer between tracks; a text generator's
10//! texture is placed on the transparent picture the generator draws. A
11//! layout generator is resolved here too, with the same fonts, and its
12//! texts rasterized beside the others ([`crate::layout`]).
13
14use std::collections::HashMap;
15use std::fmt;
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18
19use alpha2_eval::{FrameDesc, GeneratorPlan, LayoutPlan, StepOp};
20use alpha2_media::EmojiPolicy;
21use alpha2_model::{Generator, Rgba, TextAlign, TextStyle};
22use alpha2_render::{
23    CpuImage, CpuRenderer, CpuSource, RenderCacheKey, RenderError, RenderSize, Renderer,
24    SourceView, UploadTexture,
25};
26use alpha2_text::{ShapedText, TextFont, TextRasterError, TextRasterRequest, TextRasterizer};
27
28use crate::layout::PreparedLayout;
29
30/// The high 64 bits of a text's result cache key, before its font set is
31/// mixed in: font sets are counted from 1 and stay far below this, so those
32/// bits are never zero, as a host's source keys ([`RenderCacheKey::source`])
33/// leave them, and the two never meet.
34const TEXT_CACHE_DOMAIN: u64 = 0x6361_7074_696f_6e73;
35
36/// The next font set's identity: each store, and each font a store adds,
37/// takes one, so no two font sets that might draw text differently in one
38/// process share a text cache key.
39static NEXT_FONT_SET: AtomicU64 = AtomicU64::new(1);
40
41/// How many rasterized texts a host keeps between frames; the current
42/// frame's are kept however many it shows.
43const RETAINED_TEXTS: usize = 16;
44
45/// How many resolved layouts a host keeps between frames; the current
46/// frame's are kept however many it shows.
47const RETAINED_LAYOUTS: usize = 8;
48
49/// How many shaped texts a host keeps for layout, least recently used
50/// first out.
51const RETAINED_SHAPED: usize = 256;
52
53/// The largest font, outline, and padding the text host draws, in pixels.
54pub(crate) const MAX_FONT_PIXELS: f32 = 2_048.0;
55pub(crate) const MAX_OUTLINE_PIXELS: f32 = 64.0;
56const MAX_PADDING_PIXELS: f32 = 256.0;
57
58/// Where [`TextTextures`] keeps the pixels of a rasterized text: the GPU
59/// renderer uploads them to a texture, the CPU renderer keeps them.
60pub trait TextUpload {
61    /// Takes `rgba8`, premultiplied sRGB-encoded RGBA of `size` with rows
62    /// packed, as the renderer's source for that text.
63    ///
64    /// # Errors
65    /// Returns [`RenderError::InvalidImageData`] when `rgba8` does not hold
66    /// `size`, or the GPU's upload failure.
67    fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError>;
68}
69
70/// The pixels of one prepared text, as the renderer that drew it keeps
71/// them.
72#[derive(Debug)]
73pub struct TextPixels(TextStorage);
74
75#[derive(Debug)]
76enum TextStorage {
77    Gpu(UploadTexture),
78    Cpu(CpuSource),
79}
80
81impl TextPixels {
82    /// The text as a graph source.
83    fn view(&self) -> SourceView<'_> {
84        match &self.0 {
85            TextStorage::Gpu(texture) => SourceView::from(texture),
86            TextStorage::Cpu(source) => SourceView::cpu(source),
87        }
88    }
89}
90
91impl TextUpload for Renderer {
92    fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError> {
93        self.upload_rgba8(size, rgba8)
94            .map(|texture| TextPixels(TextStorage::Gpu(texture)))
95    }
96}
97
98impl TextUpload for CpuRenderer {
99    fn upload_text(&self, size: RenderSize, rgba8: &[u8]) -> Result<TextPixels, RenderError> {
100        let picture = CpuImage::from_premultiplied_rgba8(size, rgba8)?;
101        Ok(TextPixels(TextStorage::Cpu(CpuSource::image(Arc::new(
102            picture,
103        )))))
104    }
105}
106
107/// One prepared text: its texture, pixel size, and where its layout box
108/// lies in it.
109#[derive(Clone, Copy, Debug)]
110pub struct TextTexture<'a> {
111    /// Premultiplied, sRGB-encoded RGBA, as every source texture.
112    pub texture: SourceView<'a>,
113    /// Width in pixels.
114    pub width: u32,
115    /// Height in pixels.
116    pub height: u32,
117    /// The margin, in pixels, the same on both sides of each axis, that the
118    /// texture adds around the text and its decorations for ink reaching
119    /// past them: an accent above the first line, say. Placement leaves it
120    /// out, so such ink never moves the text.
121    pub margin: [u32; 2],
122    /// The key the renderer's result cache holds results drawn from this
123    /// texture under, or `None` to keep every frame showing it out of that
124    /// cache. It is the provider's promise that the texture's pixels depend
125    /// on nothing it leaves out, as [`crate::FrameProvider::source_key`] is
126    /// for a source: [`TextTextures`] keys each text by its [`text_key`] and
127    /// the fonts it was drawn with.
128    pub cache_key: Option<RenderCacheKey>,
129}
130
131/// Where a text lies, as fractions of the canvas, `(0, 0)` top-left to
132/// `(1, 1)` bottom-right, as its style's position is: on the picture a text
133/// generator draws, before its clip's transform and effects, and on the
134/// canvas for a caption. [`measure_text`] measures it.
135#[derive(Copy, Clone, Debug, PartialEq)]
136pub struct TextBounds {
137    /// The box the style's anchor and position place: the wrapping width
138    /// wide, every line's em box tall, and the whole pixels of outline and
139    /// background padding around that.
140    pub layout: CanvasRect,
141    /// The lines' advances: from the start of the line that starts farthest
142    /// left to the end of the one that ends farthest right, as tall as the
143    /// layout, with the same decorations around it. It moves only as the
144    /// advances do, not as glyphs with more or less ink are typed.
145    pub content: CanvasRect,
146    /// What the text draws: its glyphs, their outline, and the background's
147    /// boxes, as [`alpha2_text::TextMeasure::ink`] bounds them at the measured
148    /// size. Every pixel the text gives any alpha there meets it, give or
149    /// take the pixel sampling the texture spreads it by. `None` when the text
150    /// draws nothing.
151    pub ink: Option<CanvasRect>,
152    /// Number of laid-out lines.
153    pub line_count: u32,
154}
155
156/// A rectangle as fractions of the canvas, y down.
157#[derive(Copy, Clone, Debug, PartialEq)]
158pub struct CanvasRect {
159    /// Left edge, a fraction of the canvas width.
160    pub left: f32,
161    /// Top edge, a fraction of the canvas height.
162    pub top: f32,
163    /// Right edge, a fraction of the canvas width.
164    pub right: f32,
165    /// Bottom edge, a fraction of the canvas height.
166    pub bottom: f32,
167}
168
169/// Why text could not be prepared.
170#[derive(Clone, Debug, Eq, PartialEq)]
171#[non_exhaustive]
172pub enum TextError {
173    /// The text host refused: no fonts registered, a malformed font, or a
174    /// text too large to rasterize.
175    Raster(TextRasterError),
176    /// The rasterized text could not be uploaded.
177    Render(RenderError),
178}
179
180impl fmt::Display for TextError {
181    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
182        match self {
183            Self::Raster(TextRasterError::NoFonts) => {
184                formatter.write_str("the project shows text, but no fonts were registered")
185            }
186            Self::Raster(error) => write!(formatter, "text failed: {error}"),
187            Self::Render(error) => write!(formatter, "text upload failed: {error}"),
188        }
189    }
190}
191
192impl std::error::Error for TextError {}
193
194struct TextEntry {
195    texture: TextPixels,
196    width: u32,
197    height: u32,
198    margin: [u32; 2],
199    /// Where the layout box's top-left corner lies in the texture.
200    corner: [u32; 2],
201    last_used: u64,
202}
203
204/// A layout resolved at one size, with the requests of its texts, to
205/// rasterize again one the text store has let go.
206struct LayoutEntry {
207    prepared: Arc<PreparedLayout>,
208    requests: Vec<(u64, TextRasterRequest)>,
209    last_used: u64,
210}
211
212/// A text shaped for layout, and when it was last asked for.
213struct ShapedEntry {
214    shaped: Arc<ShapedText>,
215    last_used: u64,
216}
217
218/// Fonts and the texts rasterized with them, keyed by content and render
219/// size.
220pub struct TextTextures {
221    rasterizer: TextRasterizer,
222    /// The identity of the fonts the texts are drawn with, from
223    /// [`NEXT_FONT_SET`].
224    font_set: u64,
225    entries: HashMap<u64, TextEntry>,
226    /// Resolved layouts by [`layout_key`].
227    layouts: HashMap<u128, LayoutEntry>,
228    /// Texts shaped for layout, by [`shaped_key`].
229    shaped: HashMap<u64, ShapedEntry>,
230    /// How many layouts have been resolved: a static layout resolves once.
231    resolves: u64,
232    frame: u64,
233}
234
235impl fmt::Debug for TextTextures {
236    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
237        formatter
238            .debug_struct("TextTextures")
239            .field("fonts", &self.rasterizer.font_count())
240            .field("texts", &self.entries.len())
241            .field("layouts", &self.layouts.len())
242            .finish_non_exhaustive()
243    }
244}
245
246impl Default for TextTextures {
247    fn default() -> Self {
248        Self::new()
249    }
250}
251
252impl TextTextures {
253    /// No fonts and no texts.
254    #[must_use]
255    pub fn new() -> Self {
256        Self {
257            rasterizer: TextRasterizer::empty(),
258            font_set: NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed),
259            entries: HashMap::new(),
260            layouts: HashMap::new(),
261            shaped: HashMap::new(),
262            resolves: 0,
263            frame: 0,
264        }
265    }
266
267    /// Appends an OpenType font to the fallback stack text draws with. Texts
268    /// already rasterized are dropped.
269    ///
270    /// # Errors
271    /// Returns [`TextError::Raster`] for malformed font bytes.
272    pub fn add_font(
273        &mut self,
274        family: impl Into<String>,
275        bytes: impl Into<Arc<[u8]>>,
276        face_index: u32,
277    ) -> Result<(), TextError> {
278        self.rasterizer
279            .add_font(TextFont {
280                family: family.into(),
281                bytes: bytes.into(),
282                face_index,
283            })
284            .map_err(TextError::Raster)?;
285        self.entries.clear();
286        self.layouts.clear();
287        self.shaped.clear();
288        // The same text may now draw other glyphs.
289        self.font_set = NEXT_FONT_SET.fetch_add(1, Ordering::Relaxed);
290        Ok(())
291    }
292
293    /// Fonts in the fallback stack.
294    #[must_use]
295    pub fn font_count(&self) -> usize {
296        self.rasterizer.font_count()
297    }
298
299    /// Rasterizes and uploads every text `frame` shows at `size` that is not
300    /// already held: its captions and the text generators its clips draw.
301    /// Resolves every layout it draws that is not already held, and
302    /// rasterizes its texts. Forgets the least recently shown beyond a small
303    /// number.
304    ///
305    /// # Errors
306    /// Returns [`TextError`] when a text cannot be rasterized or uploaded;
307    /// with no fonts registered, only if the frame shows one.
308    pub fn prepare(
309        &mut self,
310        renderer: &dyn TextUpload,
311        frame: &FrameDesc,
312        size: RenderSize,
313    ) -> Result<(), TextError> {
314        self.frame += 1;
315        for (text, style) in frame_texts(frame) {
316            let key = text_key(text, style, size);
317            if let Some(entry) = self.entries.get_mut(&key) {
318                entry.last_used = self.frame;
319                continue;
320            }
321            let request = text_request(text, style, size, key);
322            self.rasterize(renderer, key, &request)?;
323        }
324        for (plan, time) in frame_layouts(frame) {
325            self.prepare_layout(renderer, plan, time, size)?;
326        }
327        while self.layouts.len() > RETAINED_LAYOUTS {
328            let Some(oldest) = self
329                .layouts
330                .iter()
331                .filter(|(_, entry)| entry.last_used < self.frame)
332                .min_by_key(|(_, entry)| entry.last_used)
333                .map(|(key, _)| *key)
334            else {
335                break;
336            };
337            self.layouts.remove(&oldest);
338        }
339        while self.entries.len() > RETAINED_TEXTS {
340            let Some(oldest) = self
341                .entries
342                .iter()
343                .filter(|(_, entry)| entry.last_used < self.frame)
344                .min_by_key(|(_, entry)| entry.last_used)
345                .map(|(key, _)| *key)
346            else {
347                break;
348            };
349            self.entries.remove(&oldest);
350        }
351        Ok(())
352    }
353
354    /// Rasterizes `request` and uploads it as the text with `key`.
355    fn rasterize(
356        &mut self,
357        renderer: &dyn TextUpload,
358        key: u64,
359        request: &TextRasterRequest,
360    ) -> Result<(), TextError> {
361        let raster = self
362            .rasterizer
363            .rasterize(request)
364            .map_err(TextError::Raster)?;
365        // Each text is rasterized once per key; keep no copy.
366        self.rasterizer.invalidate(key);
367        let texture_size = &raster.descriptor.texture;
368        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
369        let corner =
370            [raster.descriptor.layout_x, raster.descriptor.layout_y].map(|corner| corner as u32);
371        // The anchor places the decorated text; the margin, which holds only ink reaching past
372        // it, takes no part in placement.
373        let margin = corner.map(|corner| corner.saturating_sub(request.decoration_margin()));
374        let texture = renderer
375            .upload_text(
376                RenderSize::new(texture_size.width, texture_size.height)
377                    .map_err(TextError::Render)?,
378                &raster.pixels.rgba8,
379            )
380            .map_err(TextError::Render)?;
381        self.entries.insert(
382            key,
383            TextEntry {
384                texture,
385                width: texture_size.width,
386                height: texture_size.height,
387                margin,
388                corner,
389                last_used: self.frame,
390            },
391        );
392        Ok(())
393    }
394
395    /// Resolves `plan` at picture time `time` and `size`, and rasterizes its
396    /// texts, unless that layout is already held: then only texts the store
397    /// let go are rasterized again.
398    fn prepare_layout(
399        &mut self,
400        renderer: &dyn TextUpload,
401        plan: &LayoutPlan,
402        time: alpha2_time::MediaTime,
403        size: RenderSize,
404    ) -> Result<(), TextError> {
405        let evaluated = plan.evaluate(time);
406        let key = layout_key(evaluated.key, size);
407        if let Some(entry) = self.layouts.get_mut(&key) {
408            entry.last_used = self.frame;
409            let requests = entry.requests.clone();
410            for (text, request) in &requests {
411                match self.entries.get_mut(text) {
412                    Some(held) => held.last_used = self.frame,
413                    None => self.rasterize(renderer, *text, request)?,
414                }
415            }
416            return Ok(());
417        }
418        self.resolves += 1;
419        let frame = self.frame;
420        let shaped =
421            crate::layout::shape_texts(&evaluated, size, |request| self.shape(request, frame))?;
422        let resolved = crate::layout::resolve(&evaluated, size, &shaped);
423        let (texts, requests) = crate::layout::layout_texts(&evaluated, &resolved, size);
424        let mut boxes = Vec::with_capacity(texts.len());
425        for text in &texts {
426            boxes.push(match text {
427                None => None,
428                Some(text_key) => {
429                    let request = &requests
430                        .iter()
431                        .find(|(key, _)| key == text_key)
432                        .expect("a request per text")
433                        .1;
434                    match self.entries.get_mut(text_key) {
435                        Some(held) => held.last_used = self.frame,
436                        None => self.rasterize(renderer, *text_key, request)?,
437                    }
438                    let entry = &self.entries[text_key];
439                    Some(crate::layout::paint::TextBox {
440                        corner: entry.corner,
441                        size: [entry.width, entry.height],
442                    })
443                }
444            });
445        }
446        let items = crate::layout::paint::paint_list(&evaluated, &resolved, size, &boxes);
447        self.layouts.insert(
448            key,
449            LayoutEntry {
450                prepared: Arc::new(PreparedLayout { items, texts }),
451                requests,
452                last_used: self.frame,
453            },
454        );
455        Ok(())
456    }
457
458    /// `request`'s text shaped for layout, from the cache when it holds it.
459    fn shape(
460        &mut self,
461        request: &TextRasterRequest,
462        frame: u64,
463    ) -> Result<Arc<ShapedText>, TextRasterError> {
464        let key = shaped_key(request);
465        if let Some(entry) = self.shaped.get_mut(&key) {
466            entry.last_used = frame;
467            return Ok(Arc::clone(&entry.shaped));
468        }
469        let shaped = Arc::new(self.rasterizer.shape(request)?);
470        if self.shaped.len() >= RETAINED_SHAPED
471            && let Some(oldest) = self
472                .shaped
473                .iter()
474                .min_by_key(|(_, entry)| entry.last_used)
475                .map(|(key, _)| *key)
476        {
477            self.shaped.remove(&oldest);
478        }
479        self.shaped.insert(
480            key,
481            ShapedEntry {
482                shaped: Arc::clone(&shaped),
483                last_used: frame,
484            },
485        );
486        Ok(shaped)
487    }
488
489    /// How many layouts have been resolved, held or not.
490    #[cfg(test)]
491    #[must_use]
492    pub(crate) const fn layout_resolves(&self) -> u64 {
493        self.resolves
494    }
495
496    /// The prepared layout with `key`, as [`layout_key`] names it.
497    #[must_use]
498    pub fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
499        self.layouts
500            .get(&key)
501            .map(|entry| Arc::clone(&entry.prepared))
502    }
503
504    /// Where each node of `root` lies on a picture of `size` at picture
505    /// time `time`, its pixels those of a `canvas` canvas, laid out with
506    /// these fonts: [`crate::measure_layout`].
507    ///
508    /// # Errors
509    /// Returns [`TextError::Raster`] as [`Self::prepare`] would for its
510    /// texts.
511    pub fn measure_layout(
512        &self,
513        root: &alpha2_model::LayoutNode,
514        time: alpha2_time::MediaTime,
515        size: RenderSize,
516        canvas: (u32, u32),
517    ) -> Result<crate::LayoutBounds, TextError> {
518        crate::measure_layout(&self.rasterizer, root, time, size, canvas)
519    }
520
521    /// [`Self::prepare`] for the frame of `project` at `time`, rendered at
522    /// `size`; nothing to do, and no evaluation, when the project draws no
523    /// text ([`crate::EngineProject::draws_text`]).
524    ///
525    /// # Errors
526    /// Returns [`crate::EngineError::Evaluate`] or
527    /// [`crate::EngineError::Text`].
528    pub fn prepare_at(
529        &mut self,
530        renderer: &dyn TextUpload,
531        project: &crate::EngineProject,
532        time: alpha2_time::MediaTime,
533        size: RenderSize,
534    ) -> Result<(), crate::EngineError> {
535        if !project.draws_text() {
536            return Ok(());
537        }
538        let frame = project.evaluate(time)?;
539        self.prepare(renderer, &frame, size)
540            .map_err(crate::EngineError::Text)
541    }
542
543    /// Where `text` in `style` lies on a canvas of `size`, drawn with these
544    /// fonts: [`measure_text`].
545    ///
546    /// # Errors
547    /// Returns [`TextError::Raster`] as [`Self::prepare`] would for the text.
548    pub fn measure(
549        &self,
550        text: &str,
551        style: &TextStyle,
552        size: RenderSize,
553    ) -> Result<TextBounds, TextError> {
554        measure_text(&self.rasterizer, text, style, size)
555    }
556
557    /// The prepared text with `key`, as [`text_key`] names it.
558    #[must_use]
559    pub fn get(&self, key: u64) -> Option<TextTexture<'_>> {
560        self.entries.get(&key).map(|entry| TextTexture {
561            texture: entry.texture.view(),
562            width: entry.width,
563            height: entry.height,
564            margin: entry.margin,
565            cache_key: Some(self.cache_key(key)),
566        })
567    }
568
569    /// The result cache key of the text with `key`, drawn with these fonts:
570    /// the font set in the high half, beside the text domain, and the text
571    /// in the low half, so two keys are equal only for the same text drawn
572    /// with the same fonts.
573    fn cache_key(&self, key: u64) -> RenderCacheKey {
574        let high = TEXT_CACHE_DOMAIN ^ self.font_set;
575        RenderCacheKey::from_value((u128::from(high) << 64) | u128::from(key))
576    }
577}
578
579/// Every text `frame` may draw, with its style: each caption, then each text
580/// generator of a clip it draws, the clip's own source or a step of its
581/// compositions. A text drawn twice is named twice.
582pub(crate) fn frame_texts<'f>(
583    frame: &'f FrameDesc<'_>,
584) -> impl Iterator<Item = (&'f str, &'f TextStyle)> {
585    let captions = frame
586        .captions
587        .iter()
588        .map(|caption| (caption.text, caption.style));
589    let generators = frame
590        .visual
591        .iter()
592        .filter(|clip| frame.draws(clip))
593        .flat_map(|clip| {
594            let steps = clip
595                .composition
596                .iter()
597                .chain(&clip.source_composition)
598                .flat_map(|composition| &composition.plan().steps)
599                .filter_map(|step| match &step.op {
600                    StepOp::Generator { generator, .. } => Some(generator),
601                    _ => None,
602                });
603            clip.generator.into_iter().chain(steps)
604        })
605        .filter_map(|generator| match generator {
606            Generator::Text { text, style } => Some((text.as_str(), style)),
607            _ => None,
608        });
609    captions.chain(generators)
610}
611
612/// A host's frame provider together with its text store: frames come from
613/// the one and text from the other.
614pub struct FramesWithText<'a> {
615    frames: &'a dyn crate::FrameProvider,
616    texts: &'a TextTextures,
617}
618
619impl<'a> FramesWithText<'a> {
620    /// `frames` with the texts `texts` holds.
621    #[must_use]
622    pub fn new(frames: &'a dyn crate::FrameProvider, texts: &'a TextTextures) -> Self {
623        Self { frames, texts }
624    }
625}
626
627impl crate::FrameProvider for FramesWithText<'_> {
628    fn texture_for(
629        &self,
630        source: &alpha2_media::SourceId,
631        time: alpha2_time::MediaTime,
632    ) -> Option<SourceView<'_>> {
633        self.frames.texture_for(source, time)
634    }
635
636    fn pending_source_work(
637        &self,
638        source: &alpha2_media::SourceId,
639        time: alpha2_time::MediaTime,
640    ) -> Option<&alpha2_render::PendingSourceWork> {
641        self.frames.pending_source_work(source, time)
642    }
643
644    fn source_key(
645        &self,
646        source: &alpha2_media::SourceId,
647        time: alpha2_time::MediaTime,
648    ) -> Option<alpha2_render::RenderCacheKey> {
649        self.frames.source_key(source, time)
650    }
651
652    fn text(&self, key: u64) -> Option<TextTexture<'_>> {
653        self.texts.get(key)
654    }
655
656    fn layout(&self, key: u128) -> Option<Arc<PreparedLayout>> {
657        self.texts.layout(key)
658    }
659}
660
661/// Every layout `frame` may draw, with its picture time: each drawn clip's
662/// layout source at its source time, then each layout step of its
663/// compositions at the step's time plus its start.
664pub(crate) fn frame_layouts<'f>(
665    frame: &'f FrameDesc<'_>,
666) -> impl Iterator<Item = (&'f LayoutPlan, alpha2_time::MediaTime)> {
667    frame
668        .visual
669        .iter()
670        .filter(|clip| frame.draws(clip))
671        .flat_map(|clip| {
672            let own = clip
673                .plan
674                .and_then(GeneratorPlan::layout)
675                .map(|plan| (plan, clip.source_time));
676            let steps = clip
677                .composition
678                .iter()
679                .chain(&clip.source_composition)
680                .flat_map(|composition| {
681                    let time = composition.time();
682                    composition
683                        .plan()
684                        .steps
685                        .iter()
686                        .filter_map(move |step| match &step.op {
687                            StepOp::Generator {
688                                plan: Some(plan),
689                                start,
690                                ..
691                            } => plan.layout().map(|plan| {
692                                (
693                                    plan,
694                                    alpha2_time::MediaTime(
695                                        step.time(time).ticks().saturating_add(start.ticks()),
696                                    ),
697                                )
698                            }),
699                            _ => None,
700                        })
701                });
702            own.into_iter().chain(steps)
703        })
704}
705
706/// A layout's identity at a render size: its evaluated key and the size.
707/// Equal keys lay out and draw alike with the same fonts.
708#[must_use]
709pub fn layout_key(evaluated: u128, size: RenderSize) -> u128 {
710    let mut hash = alpha2_geometry::StableHash::new();
711    hash.bytes(b"layout");
712    hash.bytes(&evaluated.to_le_bytes());
713    hash.u32(size.width);
714    hash.u32(size.height);
715    hash.finish()
716}
717
718/// A layout text's raster request as a text store key: FNV-1a over a domain
719/// byte and every field of the request. Never zero.
720pub(crate) fn layout_text_key(request: &TextRasterRequest) -> u64 {
721    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
722    let mut feed = |bytes: &[u8]| {
723        for byte in bytes {
724            hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
725        }
726    };
727    // Layout texts take keys apart from captions' and text generators'.
728    feed(&[0x4c]);
729    feed_request(&mut feed, request);
730    hash.max(1)
731}
732
733/// What a request is shaped from, as a shaped-text cache key.
734fn shaped_key(request: &TextRasterRequest) -> u64 {
735    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
736    let mut feed = |bytes: &[u8]| {
737        for byte in bytes {
738            hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
739        }
740    };
741    feed(request.text.as_bytes());
742    feed(&[0xff]);
743    feed(
744        request
745            .font_family
746            .as_deref()
747            .unwrap_or_default()
748            .as_bytes(),
749    );
750    feed(&[0xff, u8::from(request.font_family.is_some())]);
751    feed(&request.font_size.to_bits().to_le_bytes());
752    feed(&request.line_height.to_bits().to_le_bytes());
753    hash
754}
755
756fn feed_request(feed: &mut impl FnMut(&[u8]), request: &TextRasterRequest) {
757    feed(request.text.as_bytes());
758    feed(&[0xff]);
759    feed(
760        request
761            .font_family
762            .as_deref()
763            .unwrap_or_default()
764            .as_bytes(),
765    );
766    feed(&[0xff, u8::from(request.font_family.is_some())]);
767    for value in [request.font_size, request.max_width, request.line_height] {
768        feed(&value.to_bits().to_le_bytes());
769    }
770    feed(&request.color);
771    feed(&[match request.align {
772        alpha2_media::TextAlign::Start => 0,
773        alpha2_media::TextAlign::Center => 1,
774        alpha2_media::TextAlign::End => 2,
775    }]);
776    match request.outline {
777        Some(outline) => {
778            feed(&[1]);
779            feed(&outline.color);
780            feed(&outline.width.to_bits().to_le_bytes());
781        }
782        None => feed(&[0]),
783    }
784}
785
786/// A text's identity at a render size: the text, its style, and the size,
787/// hashed (FNV-1a). Never zero.
788#[must_use]
789pub fn text_key(text: &str, style: &TextStyle, size: RenderSize) -> u64 {
790    let mut hash = 0xcbf2_9ce4_8422_2325_u64;
791    let mut feed = |bytes: &[u8]| {
792        for byte in bytes {
793            hash = (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3);
794        }
795    };
796    feed(text.as_bytes());
797    feed(&[0xff]);
798    // Family names cannot hold 0xff (they are UTF-8), so the text and family stay apart.
799    feed(style.font_family.as_deref().unwrap_or_default().as_bytes());
800    feed(&[0xff, u8::from(style.font_family.is_some())]);
801    for value in [
802        style.font_size,
803        style.line_height,
804        style.max_width,
805        style.position.x,
806        style.position.y,
807        style.anchor.x,
808        style.anchor.y,
809    ] {
810        feed(&value.to_bits().to_le_bytes());
811    }
812    feed(&rgba8(style.color));
813    feed(&[match style.align {
814        TextAlign::Start => 0,
815        TextAlign::Center => 1,
816        TextAlign::End => 2,
817    }]);
818    match style.outline {
819        Some(outline) => {
820            feed(&[1]);
821            feed(&rgba8(outline.color));
822            feed(&outline.width.to_bits().to_le_bytes());
823        }
824        None => feed(&[0]),
825    }
826    match style.background {
827        Some(background) => {
828            feed(&[1]);
829            feed(&rgba8(background.color));
830            feed(&background.padding.to_bits().to_le_bytes());
831        }
832        None => feed(&[0]),
833    }
834    feed(&size.width.to_le_bytes());
835    feed(&size.height.to_le_bytes());
836    hash.max(1)
837}
838
839/// Where `text` in `style` lies on a canvas of `size` ([`TextBounds`]), laid
840/// out with `rasterizer`'s fonts as [`TextTextures::prepare`] lays it out and
841/// placed as a frame places it, without drawing it. Measured at the size a
842/// frame is drawn at, it agrees with that frame to the pixel; text laid out
843/// at another size can break its lines elsewhere.
844///
845/// # Errors
846/// Returns [`TextError::Raster`] when the text host refuses the text: no
847/// fonts, or a style out of range.
848#[allow(clippy::cast_precision_loss)]
849pub fn measure_text(
850    rasterizer: &TextRasterizer,
851    text: &str,
852    style: &TextStyle,
853    size: RenderSize,
854) -> Result<TextBounds, TextError> {
855    let request = text_request(text, style, size, text_key(text, style, size));
856    let measure = rasterizer.measure(&request).map_err(TextError::Raster)?;
857    let (width, height) = (size.width as f32, size.height as f32);
858    let margin = measure.decoration_margin as f32;
859    let (box_width, box_height) = (measure.box_width as f32, measure.box_height as f32);
860    // The layout box's top-left corner on the canvas: the anchor of the box with its decorations
861    // lies at the position, as `text_placement` places the texture.
862    let origin = [
863        style.position.x * width - style.anchor.x * (box_width + 2.0 * margin) + margin,
864        style.position.y * height - style.anchor.y * (box_height + 2.0 * margin) + margin,
865    ];
866    let rect = |[left, top, right, bottom]: [f32; 4]| CanvasRect {
867        left: (origin[0] + left) / width,
868        top: (origin[1] + top) / height,
869        right: (origin[0] + right) / width,
870        bottom: (origin[1] + bottom) / height,
871    };
872    Ok(TextBounds {
873        layout: rect([-margin, -margin, box_width + margin, box_height + margin]),
874        content: rect(measure.content),
875        ink: measure.ink.map(rect),
876        line_count: measure.line_count,
877    })
878}
879
880/// A straight, sRGB-encoded color as bytes.
881#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
882pub(crate) fn rgba8(color: Rgba) -> [u8; 4] {
883    [color.red, color.green, color.blue, color.alpha]
884        .map(|value| (value.clamp(0.0, 1.0) * 255.0).round() as u8)
885}
886
887/// The text host request for `text` at `size`: sizes scaled from canvas
888/// fractions to pixels, within what the text host draws.
889#[allow(clippy::cast_precision_loss)]
890fn text_request(text: &str, style: &TextStyle, size: RenderSize, key: u64) -> TextRasterRequest {
891    let font_size = (style.font_size * size.height as f32).clamp(1.0, MAX_FONT_PIXELS);
892    TextRasterRequest {
893        text: text.to_owned(),
894        font_size,
895        max_width: (style.max_width * size.width as f32).max(1.0),
896        line_height: style.line_height,
897        align: match style.align {
898            TextAlign::Start => alpha2_media::TextAlign::Start,
899            TextAlign::Center => alpha2_media::TextAlign::Center,
900            TextAlign::End => alpha2_media::TextAlign::End,
901        },
902        color: rgba8(style.color),
903        emoji_policy: EmojiPolicy::Color,
904        content_version: key,
905        outline: style.outline.map(|outline| alpha2_text::TextOutline {
906            color: rgba8(outline.color),
907            width: (outline.width * font_size).clamp(f32::MIN_POSITIVE, MAX_OUTLINE_PIXELS),
908        }),
909        background: style
910            .background
911            .map(|background| alpha2_text::TextBackground {
912                color: rgba8(background.color),
913                padding: (background.padding * font_size).clamp(0.0, MAX_PADDING_PIXELS),
914            }),
915        font_family: style.font_family.clone(),
916    }
917}
918
919#[cfg(test)]
920#[path = "text_tests.rs"]
921mod tests;