Skip to main content

netget/display/
text.rs

1//! Text rendering using cosmic-text
2
3use 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
10/// Text renderer using cosmic-text for advanced text handling
11pub struct TextRenderer {
12    font_system: FontSystem,
13    swash_cache: SwashCache,
14    cache_bytes: usize,
15}
16
17impl TextRenderer {
18    /// Create a new text renderer with system fonts
19    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    /// Draw text on a pixmap at the specified position
36    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    /// Render fixed-width text, for ASCII art whose columns must align.
49    #[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        // Rasterize visible glyphs only, retaining cosmic-text's baseline and
88        // RGBA/mask handling. Bound the shared cache between glyphs.
89        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}