1use crate::display::types::Color;
4use cosmic_text::{fontdb, Attrs, Buffer, Family, FontSystem, Metrics, Shaping, SwashCache};
5use tiny_skia::Pixmap;
6
7pub const MAX_FONT_SIZE: u32 = 512;
8const MAX_GLYPH_CACHE_BYTES: usize = 32 * 1024 * 1024;
9
10pub struct TextRenderer {
12 font_system: FontSystem,
13 swash_cache: SwashCache,
14 cache_bytes: usize,
15}
16
17impl TextRenderer {
18 pub fn new() -> Self {
20 let mut font_db = fontdb::Database::new();
21 font_db.load_system_fonts();
22
23 let font_system = FontSystem::new_with_locale_and_db(
24 sys_locale::get_locale().unwrap_or_else(|| String::from("en-US")),
25 font_db,
26 );
27
28 Self {
29 font_system,
30 swash_cache: SwashCache::new(),
31 cache_bytes: 0,
32 }
33 }
34
35 pub fn draw_text(
37 &mut self,
38 pixmap: &mut Pixmap,
39 x: u32,
40 y: u32,
41 text: &str,
42 font_size: u32,
43 color: Color,
44 ) {
45 self.draw_with_family(pixmap, x, y, text, font_size, color, Family::SansSerif);
46 }
47
48 #[allow(clippy::too_many_arguments)]
50 pub(crate) fn draw_with_family(
51 &mut self,
52 pixmap: &mut Pixmap,
53 x: u32,
54 y: u32,
55 text: &str,
56 font_size: u32,
57 color: Color,
58 family: Family<'_>,
59 ) {
60 if font_size == 0
61 || font_size > MAX_FONT_SIZE
62 || text.len() > super::canvas::MAX_CANVAS_TEXT_BYTES
63 || text.is_empty()
64 || color.a == 0
65 || x >= pixmap.width()
66 || y >= pixmap.height()
67 {
68 return;
69 }
70 let mut buffer = Buffer::new(
71 &mut self.font_system,
72 Metrics::new(font_size as f32, font_size as f32),
73 );
74 buffer.set_wrap(&mut self.font_system, cosmic_text::Wrap::None);
75 buffer.set_size(
76 &mut self.font_system,
77 Some((pixmap.width() - x) as f32),
78 Some((pixmap.height() - y) as f32),
79 );
80 buffer.set_text(
81 &mut self.font_system,
82 text,
83 Attrs::new().family(family),
84 Shaping::Advanced,
85 );
86
87 for run in buffer.layout_runs() {
90 for glyph in run.glyphs {
91 if glyph.x >= (pixmap.width() - x) as f32 || glyph.x + glyph.w < 0.0 {
92 continue;
93 }
94 if self.cache_bytes > MAX_GLYPH_CACHE_BYTES {
95 self.swash_cache.image_cache.clear();
96 self.swash_cache.outline_command_cache.clear();
97 self.cache_bytes = 0;
98 }
99 let physical = glyph.physical((0.0, 0.0), 1.0);
100 let cached = self
101 .swash_cache
102 .image_cache
103 .contains_key(&physical.cache_key);
104 let base = glyph
105 .color_opt
106 .unwrap_or_else(|| cosmic_text::Color::rgb(color.r, color.g, color.b));
107 self.swash_cache.with_pixels(
108 &mut self.font_system,
109 physical.cache_key,
110 base,
111 |glyph_x, glyph_y, pixel| {
112 let px = i64::from(x) + i64::from(physical.x) + i64::from(glyph_x);
113 let py = i64::from(y)
114 + i64::from(run.line_y as i32)
115 + i64::from(physical.y)
116 + i64::from(glyph_y);
117 if px < 0
118 || py < 0
119 || px >= i64::from(pixmap.width())
120 || py >= i64::from(pixmap.height())
121 {
122 return;
123 }
124 let index = py as usize * pixmap.width() as usize + px as usize;
125 let existing = pixmap.pixels()[index];
126 let scale =
127 |v: u8, a: u8| ((u32::from(v) * u32::from(a) + 127) / 255) as u8;
128 let alpha = scale(pixel.a(), color.a);
129 let inverse = 255 - alpha;
130 let blended = tiny_skia::PremultipliedColorU8::from_rgba(
131 scale(pixel.r(), alpha) + scale(existing.red(), inverse),
132 scale(pixel.g(), alpha) + scale(existing.green(), inverse),
133 scale(pixel.b(), alpha) + scale(existing.blue(), inverse),
134 alpha + scale(existing.alpha(), inverse),
135 );
136 if let Some(blended) = blended {
137 pixmap.pixels_mut()[index] = blended;
138 }
139 },
140 );
141 if !cached {
142 self.cache_bytes = self.cache_bytes.saturating_add(
143 self.swash_cache
144 .image_cache
145 .get(&physical.cache_key)
146 .and_then(Option::as_ref)
147 .map_or(0, |image| image.data.len()),
148 );
149 }
150 }
151 }
152 }
153}
154
155impl Default for TextRenderer {
156 fn default() -> Self {
157 Self::new()
158 }
159}