Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-renderer/src/rendering/glyph_cache.rs
1use crate::fonts::{canvas, RasterizedGlyph};
2use crate::rendering::atlas::{self, AllocatedRegion, TextureId};
3use crate::{fonts::SubpixelAlignment, rendering, scene::GlyphKey};
4use anyhow::Result;
5use ordered_float::OrderedFloat;
6use pathfinder_geometry::rect::RectI;
7use pathfinder_geometry::{
8 rect::RectF,
9 vector::{Vector2F, Vector2I},
10};
11use std::collections::HashMap;
12 
13const ATLAS_SIZE: usize = 1024;
14 
15/// Callback to create a texture at a given size.
16type CreateTextureCallback<'a, T> = dyn Fn(usize) -> T + 'a;
17 
18/// Callback to insert [`RasterizedGlyph`] at a region identified by [`AllocatedRegion`] into a
19/// texture, `T`.
20type InsertIntoTextureCallback<'a, T> = dyn Fn(AllocatedRegion, &RasterizedGlyph, &mut T) + 'a;
21 
22/// Callback to compute the bounds of a glyph when rasterized.
23pub(crate) type GlyphRasterBoundsFn<'a> =
24 dyn Fn(GlyphKey, Vector2F, &rendering::GlyphConfig) -> Result<RectI> + 'a;
25 
26/// Callback to rasterize a glyph.
27pub(crate) type RasterizeGlyphFn<'a> = dyn Fn(
28 GlyphKey,
29 Vector2F,
30 SubpixelAlignment,
31 &rendering::GlyphConfig,
32 canvas::RasterFormat,
33 ) -> Result<RasterizedGlyph>
34 + 'a;
35 
36/// A cache that caches glyphs in a texture atlas.
37pub struct GlyphCache<Texture> {
38 textures: Vec<Texture>,
39 cache: HashMap<GlyphCacheKey, GlyphTextureOffset>,
40 glyph_config: rendering::GlyphConfig,
41 atlas_manager: atlas::Manager,
42}
43 
44#[derive(Hash, PartialEq, Eq)]
45struct GlyphCacheKey {
46 glyph_key: GlyphKey,
47 scale_factor: OrderedFloat<f32>,
48 subpixel_alignment: SubpixelAlignment,
49}
50 
51impl GlyphCacheKey {
52 fn new(glyph_key: GlyphKey, scale_factor: f32, subpixel_alignment: SubpixelAlignment) -> Self {
53 GlyphCacheKey {
54 glyph_key,
55 scale_factor: scale_factor.into(),
56 subpixel_alignment,
57 }
58 }
59}
60 
61/// A glyph within a texture atlas.
62#[derive(Copy, Debug, Clone)]
63pub(crate) struct GlyphTextureOffset {
64 pub texture_id: TextureId,
65 pub allocated_region: AllocatedRegion,
66 pub raster_bounds: RectF,
67 pub is_emoji: bool,
68}
69 
70impl<Texture> GlyphCache<Texture> {
71 pub(crate) fn new(glyph_config: rendering::GlyphConfig) -> Self {
72 GlyphCache {
73 textures: Vec::new(),
74 cache: HashMap::new(),
75 glyph_config,
76 atlas_manager: atlas::Manager::new(ATLAS_SIZE),
77 }
78 }
79 
80 pub(crate) fn update_config(&mut self, glyph_config: &rendering::GlyphConfig) {
81 // If the glyph rendering configuration has changed, blow away the cache
82 // and replace ourself with a new one.
83 if *glyph_config != self.glyph_config {
84 *self = GlyphCache::new(*glyph_config);
85 }
86 }
87 
88 /// Returns the texture identified by [`TextureId`].
89 pub(crate) fn texture(&self, texture_id: &TextureId) -> Option<&Texture> {
90 self.textures.get(texture_id.as_usize())
91 }
92 
93 /// Returns a [`GlyphTextureOffset`] identified by [`GlyphKey`]. If the [`GlyphKey`] has not
94 /// been previously cached, the glyph is rasterized and inserted into the texture via the
95 /// `insert_into_texture` callback. If a new texture needs to be created (since a previous
96 /// texture is now fill), the `create_texture` callback is called to construct a new texture
97 /// atlas.
98 #[allow(clippy::too_many_arguments)]
99 pub(crate) fn get(
100 &mut self,
101 glyph_key: GlyphKey,
102 scale_factor: f32,
103 subpixel_alignment: SubpixelAlignment,
104 create_texture: &CreateTextureCallback<'_, Texture>,
105 insert_into_texture: &InsertIntoTextureCallback<'_, Texture>,
106 raster_bounds_fn: &GlyphRasterBoundsFn<'_>,
107 rasterize_glyph_fn: &RasterizeGlyphFn<'_>,
108 ) -> Result<Option<GlyphTextureOffset>> {
109 let cache_key = GlyphCacheKey::new(glyph_key, scale_factor, subpixel_alignment);
110 
111 match self.cache.get(&cache_key) {
112 None => {
113 let bounds =
114 raster_bounds_fn(glyph_key, Vector2F::splat(scale_factor), &self.glyph_config)?;
115 
116 if bounds.size() == Vector2I::zero() {
117 return Ok(None);
118 }
119 
120 let rasterized_glyph = rasterize_glyph_fn(
121 glyph_key,
122 Vector2F::splat(scale_factor),
123 subpixel_alignment,
124 &self.glyph_config,
125 crate::fonts::canvas::RasterFormat::Rgba32,
126 )?;
127 
128 let texture_offset = self.atlas_manager.insert(rasterized_glyph.canvas.size)?;
129 let idx = texture_offset.texture_id.as_usize();
130 if idx >= self.textures.len() {
131 self.textures
132 .resize_with(idx + 1, || create_texture(ATLAS_SIZE));
133 }
134 let texture = &mut self.textures[idx];
135 insert_into_texture(texture_offset.allocated_region, &rasterized_glyph, texture);
136 
137 let glyph_texture_offset = GlyphTextureOffset {
138 texture_id: texture_offset.texture_id,
139 raster_bounds: bounds.to_f32(),
140 is_emoji: rasterized_glyph.is_emoji,
141 allocated_region: texture_offset.allocated_region,
142 };
143 
144 self.cache.insert(cache_key, glyph_texture_offset);
145 Ok(Some(glyph_texture_offset))
146 }
147 Some(gto) => Ok(Some(*gto)),
148 }
149 }
150}
151