iced_tiny_skia/
text.rs

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