iced_tiny_skia/
text.rs

1use crate::core::alignment;
2use crate::core::text::{Alignment, Shaping};
3use crate::core::{Color, Font, Pixels, Point, Rectangle, Transformation};
4use crate::graphics::text::cache::{self, Cache};
5use crate::graphics::text::editor;
6use crate::graphics::text::font_system;
7use crate::graphics::text::paragraph;
8
9use rustc_hash::{FxHashMap, FxHashSet};
10use std::borrow::Cow;
11use std::cell::RefCell;
12use std::collections::hash_map;
13
14#[derive(Debug)]
15pub struct Pipeline {
16    glyph_cache: GlyphCache,
17    cache: RefCell<Cache>,
18}
19
20impl Pipeline {
21    pub fn new() -> Self {
22        Pipeline {
23            glyph_cache: GlyphCache::new(),
24            cache: RefCell::new(Cache::new()),
25        }
26    }
27
28    // TODO: Shared engine
29    #[allow(dead_code)]
30    pub fn load_font(&mut self, bytes: Cow<'static, [u8]>) {
31        font_system()
32            .write()
33            .expect("Write font system")
34            .load_font(bytes);
35
36        self.cache = RefCell::new(Cache::new());
37    }
38
39    pub fn draw_paragraph(
40        &mut self,
41        paragraph: &paragraph::Weak,
42        position: Point,
43        color: Color,
44        pixels: &mut tiny_skia::PixmapMut<'_>,
45        clip_mask: Option<&tiny_skia::Mask>,
46        transformation: Transformation,
47    ) {
48        let Some(paragraph) = paragraph.upgrade() else {
49            return;
50        };
51
52        let mut font_system = font_system().write().expect("Write font system");
53
54        draw(
55            font_system.raw(),
56            &mut self.glyph_cache,
57            paragraph.buffer(),
58            position,
59            color,
60            pixels,
61            clip_mask,
62            transformation,
63        );
64    }
65
66    pub fn draw_editor(
67        &mut self,
68        editor: &editor::Weak,
69        position: Point,
70        color: Color,
71        pixels: &mut tiny_skia::PixmapMut<'_>,
72        clip_mask: Option<&tiny_skia::Mask>,
73        transformation: Transformation,
74    ) {
75        let Some(editor) = editor.upgrade() else {
76            return;
77        };
78
79        let mut font_system = font_system().write().expect("Write font system");
80
81        draw(
82            font_system.raw(),
83            &mut self.glyph_cache,
84            editor.buffer(),
85            position,
86            color,
87            pixels,
88            clip_mask,
89            transformation,
90        );
91    }
92
93    pub fn draw_cached(
94        &mut self,
95        content: &str,
96        bounds: Rectangle,
97        color: Color,
98        size: Pixels,
99        line_height: Pixels,
100        font: Font,
101        align_x: Alignment,
102        align_y: alignment::Vertical,
103        shaping: Shaping,
104        pixels: &mut tiny_skia::PixmapMut<'_>,
105        clip_mask: Option<&tiny_skia::Mask>,
106        transformation: Transformation,
107    ) {
108        let line_height = f32::from(line_height);
109
110        let mut font_system = font_system().write().expect("Write font system");
111        let font_system = font_system.raw();
112
113        let key = cache::Key {
114            bounds: bounds.size(),
115            content,
116            font,
117            size: size.into(),
118            line_height,
119            shaping,
120            align_x,
121        };
122
123        let (_, entry) = self.cache.get_mut().allocate(font_system, key);
124
125        let width = entry.min_bounds.width;
126        let height = entry.min_bounds.height;
127
128        let x = match align_x {
129            Alignment::Default | Alignment::Left | Alignment::Justified => bounds.x,
130            Alignment::Center => bounds.x - width / 2.0,
131            Alignment::Right => bounds.x - width,
132        };
133
134        let y = match align_y {
135            alignment::Vertical::Top => bounds.y,
136            alignment::Vertical::Center => bounds.y - height / 2.0,
137            alignment::Vertical::Bottom => bounds.y - height,
138        };
139
140        draw(
141            font_system,
142            &mut self.glyph_cache,
143            &entry.buffer,
144            Point::new(x, y),
145            color,
146            pixels,
147            clip_mask,
148            transformation,
149        );
150    }
151
152    pub fn draw_raw(
153        &mut self,
154        buffer: &cosmic_text::Buffer,
155        position: Point,
156        color: Color,
157        pixels: &mut tiny_skia::PixmapMut<'_>,
158        clip_mask: Option<&tiny_skia::Mask>,
159        transformation: Transformation,
160    ) {
161        let mut font_system = font_system().write().expect("Write font system");
162
163        draw(
164            font_system.raw(),
165            &mut self.glyph_cache,
166            buffer,
167            position,
168            color,
169            pixels,
170            clip_mask,
171            transformation,
172        );
173    }
174
175    pub fn trim_cache(&mut self) {
176        self.cache.get_mut().trim();
177        self.glyph_cache.trim();
178    }
179}
180
181fn draw(
182    font_system: &mut cosmic_text::FontSystem,
183    glyph_cache: &mut GlyphCache,
184    buffer: &cosmic_text::Buffer,
185    position: Point,
186    color: Color,
187    pixels: &mut tiny_skia::PixmapMut<'_>,
188    clip_mask: Option<&tiny_skia::Mask>,
189    transformation: Transformation,
190) {
191    let position = position * transformation;
192
193    let mut swash = cosmic_text::SwashCache::new();
194
195    for run in buffer.layout_runs() {
196        for glyph in run.glyphs {
197            let physical_glyph =
198                glyph.physical((position.x, position.y), transformation.scale_factor());
199
200            if let Some((buffer, placement)) = glyph_cache.allocate(
201                physical_glyph.cache_key,
202                glyph.color_opt.map(from_color).unwrap_or(color),
203                font_system,
204                &mut swash,
205            ) {
206                let pixmap =
207                    tiny_skia::PixmapRef::from_bytes(buffer, placement.width, placement.height)
208                        .expect("Create glyph pixel map");
209
210                let opacity =
211                    color.a * glyph.color_opt.map(|c| c.a() as f32 / 255.0).unwrap_or(1.0);
212
213                pixels.draw_pixmap(
214                    physical_glyph.x + placement.left,
215                    physical_glyph.y - placement.top
216                        + (run.line_y * transformation.scale_factor()).round() as i32,
217                    pixmap,
218                    &tiny_skia::PixmapPaint {
219                        opacity,
220                        ..tiny_skia::PixmapPaint::default()
221                    },
222                    tiny_skia::Transform::identity(),
223                    clip_mask,
224                );
225            }
226        }
227    }
228}
229
230fn from_color(color: cosmic_text::Color) -> Color {
231    let [r, g, b, a] = color.as_rgba();
232
233    Color::from_rgba8(r, g, b, a as f32 / 255.0)
234}
235
236#[derive(Debug, Clone, Default)]
237struct GlyphCache {
238    entries: FxHashMap<(cosmic_text::CacheKey, [u8; 3]), (Vec<u32>, cosmic_text::Placement)>,
239    recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>,
240    trim_count: usize,
241}
242
243impl GlyphCache {
244    const TRIM_INTERVAL: usize = 300;
245    const CAPACITY_LIMIT: usize = 16 * 1024;
246
247    fn new() -> Self {
248        GlyphCache::default()
249    }
250
251    fn allocate(
252        &mut self,
253        cache_key: cosmic_text::CacheKey,
254        color: Color,
255        font_system: &mut cosmic_text::FontSystem,
256        swash: &mut cosmic_text::SwashCache,
257    ) -> Option<(&[u8], cosmic_text::Placement)> {
258        let [r, g, b, _a] = color.into_rgba8();
259        let key = (cache_key, [r, g, b]);
260
261        if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) {
262            // TODO: Outline support
263            let image = swash.get_image_uncached(font_system, cache_key)?;
264
265            let glyph_size = image.placement.width as usize * image.placement.height as usize;
266
267            if glyph_size == 0 {
268                return None;
269            }
270
271            let mut buffer = vec![0u32; glyph_size];
272
273            match image.content {
274                cosmic_text::SwashContent::Mask => {
275                    let mut i = 0;
276
277                    // TODO: Blend alpha
278
279                    for _y in 0..image.placement.height {
280                        for _x in 0..image.placement.width {
281                            buffer[i] = bytemuck::cast(
282                                tiny_skia::ColorU8::from_rgba(b, g, r, image.data[i]).premultiply(),
283                            );
284
285                            i += 1;
286                        }
287                    }
288                }
289                cosmic_text::SwashContent::Color => {
290                    let mut i = 0;
291
292                    for _y in 0..image.placement.height {
293                        for _x in 0..image.placement.width {
294                            // TODO: Blend alpha
295                            buffer[i >> 2] = bytemuck::cast(
296                                tiny_skia::ColorU8::from_rgba(
297                                    image.data[i + 2],
298                                    image.data[i + 1],
299                                    image.data[i],
300                                    image.data[i + 3],
301                                )
302                                .premultiply(),
303                            );
304
305                            i += 4;
306                        }
307                    }
308                }
309                cosmic_text::SwashContent::SubpixelMask => {
310                    // TODO
311                }
312            }
313
314            let _ = entry.insert((buffer, image.placement));
315        }
316
317        let _ = self.recently_used.insert(key);
318
319        self.entries
320            .get(&key)
321            .map(|(buffer, placement)| (bytemuck::cast_slice(buffer.as_slice()), *placement))
322    }
323
324    pub fn trim(&mut self) {
325        if self.trim_count > Self::TRIM_INTERVAL || self.recently_used.len() >= Self::CAPACITY_LIMIT
326        {
327            self.entries
328                .retain(|key, _| self.recently_used.contains(key));
329
330            self.recently_used.clear();
331
332            self.entries.shrink_to(Self::CAPACITY_LIMIT);
333            self.recently_used.shrink_to(Self::CAPACITY_LIMIT);
334
335            self.trim_count = 0;
336        } else {
337            self.trim_count += 1;
338        }
339    }
340}