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 => {
130                bounds.x
131            }
132            Alignment::Center => bounds.x - width / 2.0,
133            Alignment::Right => bounds.x - width,
134        };
135
136        let y = match align_y {
137            alignment::Vertical::Top => bounds.y,
138            alignment::Vertical::Center => bounds.y - height / 2.0,
139            alignment::Vertical::Bottom => bounds.y - height,
140        };
141
142        draw(
143            font_system,
144            &mut self.glyph_cache,
145            &entry.buffer,
146            Point::new(x, y),
147            color,
148            pixels,
149            clip_mask,
150            transformation,
151        );
152    }
153
154    pub fn draw_raw(
155        &mut self,
156        buffer: &cosmic_text::Buffer,
157        position: Point,
158        color: Color,
159        pixels: &mut tiny_skia::PixmapMut<'_>,
160        clip_mask: Option<&tiny_skia::Mask>,
161        transformation: Transformation,
162    ) {
163        let mut font_system = font_system().write().expect("Write font system");
164
165        draw(
166            font_system.raw(),
167            &mut self.glyph_cache,
168            buffer,
169            position,
170            color,
171            pixels,
172            clip_mask,
173            transformation,
174        );
175    }
176
177    pub fn trim_cache(&mut self) {
178        self.cache.get_mut().trim();
179        self.glyph_cache.trim();
180    }
181}
182
183fn draw(
184    font_system: &mut cosmic_text::FontSystem,
185    glyph_cache: &mut GlyphCache,
186    buffer: &cosmic_text::Buffer,
187    position: Point,
188    color: Color,
189    pixels: &mut tiny_skia::PixmapMut<'_>,
190    clip_mask: Option<&tiny_skia::Mask>,
191    transformation: Transformation,
192) {
193    let position = position * transformation;
194
195    let mut swash = cosmic_text::SwashCache::new();
196
197    for run in buffer.layout_runs() {
198        for glyph in run.glyphs {
199            let physical_glyph = glyph.physical(
200                (position.x, position.y),
201                transformation.scale_factor(),
202            );
203
204            if let Some((buffer, placement)) = glyph_cache.allocate(
205                physical_glyph.cache_key,
206                glyph.color_opt.map(from_color).unwrap_or(color),
207                font_system,
208                &mut swash,
209            ) {
210                let pixmap = tiny_skia::PixmapRef::from_bytes(
211                    buffer,
212                    placement.width,
213                    placement.height,
214                )
215                .expect("Create glyph pixel map");
216
217                let opacity = color.a
218                    * glyph
219                        .color_opt
220                        .map(|c| c.a() as f32 / 255.0)
221                        .unwrap_or(1.0);
222
223                pixels.draw_pixmap(
224                    physical_glyph.x + placement.left,
225                    physical_glyph.y - placement.top
226                        + (run.line_y * transformation.scale_factor()).round()
227                            as i32,
228                    pixmap,
229                    &tiny_skia::PixmapPaint {
230                        opacity,
231                        ..tiny_skia::PixmapPaint::default()
232                    },
233                    tiny_skia::Transform::identity(),
234                    clip_mask,
235                );
236            }
237        }
238    }
239}
240
241fn from_color(color: cosmic_text::Color) -> Color {
242    let [r, g, b, a] = color.as_rgba();
243
244    Color::from_rgba8(r, g, b, a as f32 / 255.0)
245}
246
247#[derive(Debug, Clone, Default)]
248struct GlyphCache {
249    entries: FxHashMap<
250        (cosmic_text::CacheKey, [u8; 3]),
251        (Vec<u32>, cosmic_text::Placement),
252    >,
253    recently_used: FxHashSet<(cosmic_text::CacheKey, [u8; 3])>,
254    trim_count: usize,
255}
256
257impl GlyphCache {
258    const TRIM_INTERVAL: usize = 300;
259    const CAPACITY_LIMIT: usize = 16 * 1024;
260
261    fn new() -> Self {
262        GlyphCache::default()
263    }
264
265    fn allocate(
266        &mut self,
267        cache_key: cosmic_text::CacheKey,
268        color: Color,
269        font_system: &mut cosmic_text::FontSystem,
270        swash: &mut cosmic_text::SwashCache,
271    ) -> Option<(&[u8], cosmic_text::Placement)> {
272        let [r, g, b, _a] = color.into_rgba8();
273        let key = (cache_key, [r, g, b]);
274
275        if let hash_map::Entry::Vacant(entry) = self.entries.entry(key) {
276            // TODO: Outline support
277            let image = swash.get_image_uncached(font_system, cache_key)?;
278
279            let glyph_size = image.placement.width as usize
280                * image.placement.height as usize;
281
282            if glyph_size == 0 {
283                return None;
284            }
285
286            let mut buffer = vec![0u32; glyph_size];
287
288            match image.content {
289                cosmic_text::SwashContent::Mask => {
290                    let mut i = 0;
291
292                    // TODO: Blend alpha
293
294                    for _y in 0..image.placement.height {
295                        for _x in 0..image.placement.width {
296                            buffer[i] = bytemuck::cast(
297                                tiny_skia::ColorU8::from_rgba(
298                                    b,
299                                    g,
300                                    r,
301                                    image.data[i],
302                                )
303                                .premultiply(),
304                            );
305
306                            i += 1;
307                        }
308                    }
309                }
310                cosmic_text::SwashContent::Color => {
311                    let mut i = 0;
312
313                    for _y in 0..image.placement.height {
314                        for _x in 0..image.placement.width {
315                            // TODO: Blend alpha
316                            buffer[i >> 2] = bytemuck::cast(
317                                tiny_skia::ColorU8::from_rgba(
318                                    image.data[i + 2],
319                                    image.data[i + 1],
320                                    image.data[i],
321                                    image.data[i + 3],
322                                )
323                                .premultiply(),
324                            );
325
326                            i += 4;
327                        }
328                    }
329                }
330                cosmic_text::SwashContent::SubpixelMask => {
331                    // TODO
332                }
333            }
334
335            let _ = entry.insert((buffer, image.placement));
336        }
337
338        let _ = self.recently_used.insert(key);
339
340        self.entries.get(&key).map(|(buffer, placement)| {
341            (bytemuck::cast_slice(buffer.as_slice()), *placement)
342        })
343    }
344
345    pub fn trim(&mut self) {
346        if self.trim_count > Self::TRIM_INTERVAL
347            || self.recently_used.len() >= Self::CAPACITY_LIMIT
348        {
349            self.entries
350                .retain(|key, _| self.recently_used.contains(key));
351
352            self.recently_used.clear();
353
354            self.entries.shrink_to(Self::CAPACITY_LIMIT);
355            self.recently_used.shrink_to(Self::CAPACITY_LIMIT);
356
357            self.trim_count = 0;
358        } else {
359            self.trim_count += 1;
360        }
361    }
362}