Skip to main content

floem_tiny_skia_renderer/
lib.rs

1mod recording;
2
3use anyhow::{Result, anyhow};
4use floem_renderer::Img;
5use floem_renderer::Renderer;
6use floem_renderer::text::{Glyph as ParleyGlyph, GlyphRunProps};
7use floem_renderer::tiny_skia::{
8    self, FillRule, FilterQuality, GradientStop, IntRect, LinearGradient, Mask, MaskType, Paint,
9    Path, PathBuilder, Pixmap, PixmapPaint, PremultipliedColorU8, RadialGradient, Shader,
10    SpreadMode, Stroke, Transform,
11};
12use peniko::color::{self, ColorSpaceTag, DynamicColor, HueDirection, Srgb};
13use peniko::kurbo::{PathEl, Size};
14use peniko::{
15    BlendMode, Blob, Compose, ImageAlphaType, ImageData, ImageQuality, Mix, RadialGradientPosition,
16};
17use peniko::{
18    BrushRef, Color, Extend, Gradient, GradientKind,
19    kurbo::{Affine, Point, Rect, Shape},
20};
21use recording::{RecordedCommand, RecordedLayer, Recording};
22use resvg::tiny_skia::StrokeDash;
23use rustc_hash::FxHashMap;
24use softbuffer::{Context, Surface};
25use std::cell::RefCell;
26use std::num::NonZeroU32;
27use std::sync::Arc;
28use std::time::{Duration, Instant};
29use swash::FontRef;
30use swash::scale::image::Content;
31use swash::scale::{Render, ScaleContext, Source, StrikeWith};
32use swash::zeno::Format;
33use tiny_skia::{LineCap, LineJoin};
34
35/// Cache key for rasterized glyphs, replacing cosmic-text's CacheKey.
36/// Uses Parley's font blob identity + swash-compatible glyph parameters.
37#[derive(Clone, Copy, PartialEq, Eq, Hash)]
38struct GlyphCacheKey {
39    font_blob_id: u64,
40    font_index: u32,
41    glyph_id: u16,
42    font_size_bits: u32,
43    x_bin: u8,
44    y_bin: u8,
45    hint: bool,
46    embolden: bool,
47    skew_bits: u32,
48}
49
50impl GlyphCacheKey {
51    #[allow(clippy::too_many_arguments)]
52    fn new(
53        font_blob_id: u64,
54        font_index: u32,
55        glyph_id: u16,
56        font_size: f32,
57        x: f32,
58        y: f32,
59        hint: bool,
60        embolden: bool,
61        skew: Option<f32>,
62    ) -> (Self, f32, f32) {
63        let font_size_bits = font_size.to_bits();
64        let x_floor = x.floor();
65        let y_floor = y.floor();
66        let x_fract = x - x_floor;
67        let y_fract = y - y_floor;
68        // 4 subpixel bins per axis (matching old SubpixelBin behavior)
69        let x_bin = (x_fract * 4.0).min(3.0) as u8;
70        let y_bin = (y_fract * 4.0).min(3.0) as u8;
71        let skew_bits = skew.unwrap_or(0.0).to_bits();
72
73        (
74            Self {
75                font_blob_id,
76                font_index,
77                glyph_id,
78                font_size_bits,
79                x_bin,
80                y_bin,
81                hint,
82                embolden,
83                skew_bits,
84            },
85            x_floor + (x_bin as f32) / 4.0,
86            y_floor + (y_bin as f32) / 4.0,
87        )
88    }
89}
90
91thread_local! {
92    #[allow(clippy::type_complexity)]
93    static IMAGE_CACHE: RefCell<FxHashMap<Vec<u8>, (CacheColor, Arc<Pixmap>)>> = RefCell::new(FxHashMap::default());
94    #[allow(clippy::type_complexity)]
95    static SCALED_IMAGE_CACHE: RefCell<FxHashMap<ScaledImageCacheKey, (CacheColor, Arc<Pixmap>)>> = RefCell::new(FxHashMap::default());
96    #[allow(clippy::type_complexity)]
97    // The `u32` is a color encoded as a u32 so that it is hashable and eq.
98    static GLYPH_CACHE: RefCell<FxHashMap<(GlyphCacheKey, u32), GlyphCacheEntry>> = RefCell::new(FxHashMap::default());
99    static SCALE_CONTEXT: RefCell<ScaleContext> = RefCell::new(ScaleContext::new());
100}
101
102#[allow(clippy::too_many_arguments)]
103fn cache_glyph(
104    cache_color: CacheColor,
105    cache_key: GlyphCacheKey,
106    color: Color,
107    font_ref: &FontRef<'_>,
108    font_size: f32,
109    hint: bool,
110    normalized_coords: &[i16],
111    embolden_strength: f32,
112    skew: Option<f32>,
113    offset_x: f32,
114    offset_y: f32,
115) -> Option<Arc<Glyph>> {
116    let c = color.to_rgba8();
117    let now = Instant::now();
118
119    if let Some(opt_glyph) = GLYPH_CACHE.with_borrow_mut(|gc| {
120        if let Some(entry) = gc.get_mut(&(cache_key, c.to_u32())) {
121            entry.cache_color = cache_color;
122            entry.last_touched = now;
123            Some(entry.glyph.clone())
124        } else {
125            None
126        }
127    }) {
128        return opt_glyph;
129    };
130
131    let image = SCALE_CONTEXT.with_borrow_mut(|context| {
132        let mut scaler = context
133            .builder(*font_ref)
134            .size(font_size)
135            .hint(hint)
136            .normalized_coords(normalized_coords)
137            .build();
138
139        let mut render = Render::new(&[
140            Source::ColorOutline(0),
141            Source::ColorBitmap(StrikeWith::BestFit),
142            Source::Outline,
143        ]);
144        render
145            .format(Format::Alpha)
146            .offset(swash::zeno::Vector::new(offset_x.fract(), offset_y.fract()))
147            .embolden(embolden_strength);
148        if let Some(angle) = skew {
149            render.transform(Some(swash::zeno::Transform::skew(
150                swash::zeno::Angle::from_degrees(angle),
151                swash::zeno::Angle::ZERO,
152            )));
153        }
154        render.render(&mut scaler, cache_key.glyph_id)
155    })?;
156
157    let result = if image.placement.width == 0 || image.placement.height == 0 {
158        // We can't create an empty `Pixmap`
159        None
160    } else {
161        let mut pixmap = Pixmap::new(image.placement.width, image.placement.height)?;
162
163        match image.content {
164            Content::Mask => {
165                for (a, &alpha) in pixmap.pixels_mut().iter_mut().zip(image.data.iter()) {
166                    *a = tiny_skia::Color::from_rgba8(c.r, c.g, c.b, alpha)
167                        .premultiply()
168                        .to_color_u8();
169                }
170            }
171            Content::Color => {
172                for (a, b) in pixmap.pixels_mut().iter_mut().zip(image.data.chunks(4)) {
173                    *a = tiny_skia::Color::from_rgba8(b[0], b[1], b[2], b[3])
174                        .premultiply()
175                        .to_color_u8();
176                }
177            }
178            _ => return None,
179        }
180
181        Some(Arc::new(Glyph {
182            pixmap: Arc::new(pixmap),
183            left: image.placement.left as f32,
184            top: image.placement.top as f32,
185        }))
186    };
187
188    GLYPH_CACHE.with_borrow_mut(|gc| {
189        gc.insert(
190            (cache_key, c.to_u32()),
191            GlyphCacheEntry {
192                cache_color,
193                glyph: result.clone(),
194                last_touched: now,
195            },
196        )
197    });
198
199    result
200}
201
202macro_rules! try_ret {
203    ($e:expr) => {
204        if let Some(e) = $e {
205            e
206        } else {
207            return;
208        }
209    };
210}
211
212struct Glyph {
213    pixmap: Arc<Pixmap>,
214    left: f32,
215    top: f32,
216}
217
218#[derive(Clone)]
219pub(crate) struct ClipPath {
220    path: Path,
221    rect: Rect,
222    simple_rect: Option<Rect>,
223}
224
225#[derive(PartialEq, Clone, Copy)]
226struct CacheColor(bool);
227
228const GLYPH_CACHE_MIN_TTL: Duration = Duration::from_millis(100);
229
230struct GlyphCacheEntry {
231    cache_color: CacheColor,
232    glyph: Option<Arc<Glyph>>,
233    last_touched: Instant,
234}
235
236fn should_retain_glyph_entry(
237    entry: &GlyphCacheEntry,
238    cache_color: CacheColor,
239    now: Instant,
240) -> bool {
241    entry.cache_color == cache_color || now.duration_since(entry.last_touched) < GLYPH_CACHE_MIN_TTL
242}
243
244#[derive(Hash, PartialEq, Eq)]
245struct ScaledImageCacheKey {
246    image_id: u64,
247    width: u32,
248    height: u32,
249    quality: u8,
250}
251
252struct Layer {
253    pixmap: Pixmap,
254    base_clip: Option<ClipPath>,
255    /// clip is stored with the transform at the time clip is called
256    clip: Option<Rect>,
257    simple_clip: Option<Rect>,
258    draw_bounds: Option<Rect>,
259    mask: Mask,
260    /// this transform should generally only be used when making a draw call to skia
261    transform: Affine,
262    blend_mode: BlendMode,
263    alpha: f32,
264}
265impl Layer {
266    fn new_root(width: u32, height: u32) -> Result<Self, anyhow::Error> {
267        Ok(Self {
268            pixmap: Pixmap::new(width, height).ok_or_else(|| anyhow!("unable to create pixmap"))?,
269            base_clip: None,
270            clip: None,
271            simple_clip: None,
272            draw_bounds: None,
273            mask: Mask::new(width, height).ok_or_else(|| anyhow!("unable to create mask"))?,
274            transform: Affine::IDENTITY,
275            blend_mode: Mix::Normal.into(),
276            alpha: 1.0,
277        })
278    }
279
280    fn new_with_base_clip(
281        blend_mode: BlendMode,
282        alpha: f32,
283        clip: ClipPath,
284        width: u32,
285        height: u32,
286    ) -> Result<Self, anyhow::Error> {
287        let mut layer = Self {
288            pixmap: Pixmap::new(width, height).ok_or_else(|| anyhow!("unable to create pixmap"))?,
289            base_clip: Some(clip),
290            clip: None,
291            simple_clip: None,
292            draw_bounds: None,
293            mask: Mask::new(width, height).ok_or_else(|| anyhow!("unable to create mask"))?,
294            transform: Affine::IDENTITY,
295            blend_mode,
296            alpha,
297        };
298        layer.rebuild_clip_mask(&[]);
299        Ok(layer)
300    }
301
302    fn clip_rect_to_mask_bounds(&self, rect: Rect) -> Option<(usize, usize, usize, usize)> {
303        let rect = rect_to_int_rect(rect)?;
304        let x0 = rect.x().max(0) as usize;
305        let y0 = rect.y().max(0) as usize;
306        let x1 = (rect.x() + rect.width() as i32).min(self.mask.width() as i32) as usize;
307        let y1 = (rect.y() + rect.height() as i32).min(self.mask.height() as i32) as usize;
308        (x0 < x1 && y0 < y1).then_some((x0, y0, x1, y1))
309    }
310
311    fn fill_mask_rect(&mut self, rect: Rect) {
312        self.mask.clear();
313        let Some((x0, y0, x1, y1)) = self.clip_rect_to_mask_bounds(rect) else {
314            return;
315        };
316
317        let width = self.mask.width() as usize;
318        let data = self.mask.data_mut();
319        for y in y0..y1 {
320            let row = y * width;
321            data[row + x0..row + x1].fill(255);
322        }
323    }
324
325    fn intersect_mask_rect(&mut self, rect: Rect) {
326        let Some((x0, y0, x1, y1)) = self.clip_rect_to_mask_bounds(rect) else {
327            self.mask.clear();
328            return;
329        };
330
331        let width = self.mask.width() as usize;
332        let height = self.mask.height() as usize;
333        let data = self.mask.data_mut();
334        for y in 0..height {
335            let row = y * width;
336            if y < y0 || y >= y1 {
337                data[row..row + width].fill(0);
338                continue;
339            }
340
341            data[row..row + x0].fill(0);
342            data[row + x1..row + width].fill(0);
343        }
344    }
345
346    fn intersect_clip_path(&mut self, clip: &ClipPath) {
347        let prior_simple_clip = self
348            .simple_clip
349            .or(self.base_clip.as_ref().and_then(|clip| clip.simple_rect));
350        let clip_rect = self
351            .clip
352            .map(|rect| rect.intersect(clip.rect))
353            .unwrap_or(clip.rect);
354        if clip_rect.is_zero_area() {
355            self.clip = None;
356            self.simple_clip = None;
357            self.mask.clear();
358            return;
359        }
360
361        if self.clip.is_some() {
362            if clip.simple_rect.is_some() {
363                self.intersect_mask_rect(clip.rect);
364            } else {
365                self.mask.intersect_path(
366                    &clip.path,
367                    FillRule::Winding,
368                    false,
369                    Transform::identity(),
370                );
371            }
372        } else {
373            if clip.simple_rect.is_some() {
374                self.fill_mask_rect(clip.rect);
375            } else {
376                self.mask.clear();
377                self.mask
378                    .fill_path(&clip.path, FillRule::Winding, false, Transform::identity());
379            }
380        }
381
382        self.clip = Some(clip_rect);
383        self.simple_clip = match (prior_simple_clip, clip.simple_rect) {
384            (Some(current), Some(next)) => {
385                let clipped = current.intersect(next);
386                (!clipped.is_zero_area()).then_some(clipped)
387            }
388            (None, Some(next)) if self.base_clip.is_none() && self.clip == Some(clip_rect) => {
389                Some(next)
390            }
391            _ => None,
392        };
393    }
394
395    fn rebuild_clip_mask(&mut self, clip_stack: &[ClipPath]) {
396        self.mask.clear();
397
398        let clips: Vec<ClipPath> = self
399            .base_clip
400            .iter()
401            .cloned()
402            .chain(clip_stack.iter().cloned())
403            .collect();
404        let Some(first) = clips.first() else {
405            self.clip = None;
406            self.simple_clip = None;
407            return;
408        };
409
410        let mut clip_rect = first.rect;
411        let mut simple_clip = first.simple_rect;
412        if first.simple_rect.is_some() {
413            self.fill_mask_rect(first.rect);
414        } else {
415            self.mask
416                .fill_path(&first.path, FillRule::Winding, false, Transform::identity());
417        }
418
419        for clip in clips.iter().skip(1) {
420            clip_rect = clip_rect.intersect(clip.rect);
421            simple_clip = match (simple_clip, clip.simple_rect) {
422                (Some(current), Some(next)) => {
423                    let clipped = current.intersect(next);
424                    (!clipped.is_zero_area()).then_some(clipped)
425                }
426                _ => None,
427            };
428            if clip.simple_rect.is_some() {
429                self.intersect_mask_rect(clip.rect);
430            } else {
431                self.mask.intersect_path(
432                    &clip.path,
433                    FillRule::Winding,
434                    false,
435                    Transform::identity(),
436                );
437            }
438        }
439
440        self.clip = (!clip_rect.is_zero_area()).then_some(clip_rect);
441        self.simple_clip = self.clip.and(simple_clip);
442        if self.clip.is_none() {
443            self.mask.clear();
444        }
445    }
446
447    #[cfg(test)]
448    fn set_base_clip(&mut self, clip: Option<ClipPath>) {
449        self.base_clip = clip;
450        self.rebuild_clip_mask(&[]);
451    }
452
453    fn mark_drawn_device_rect(&mut self, rect: Rect) {
454        let mut device_rect = rect;
455        if let Some(clip) = self.clip {
456            device_rect = device_rect.intersect(clip);
457        }
458
459        if device_rect.is_zero_area() {
460            return;
461        }
462
463        self.draw_bounds = Some(
464            self.draw_bounds
465                .map(|bounds| bounds.union(device_rect))
466                .unwrap_or(device_rect),
467        );
468    }
469
470    fn try_fill_solid_rect_fast(&mut self, rect: Rect, color: Color) -> bool {
471        if self.clip.is_some() && self.simple_clip.is_none() {
472            return false;
473        }
474
475        let coeffs = self.device_transform().as_coeffs();
476        if coeffs[0] != 1.0 || coeffs[1] != 0.0 || coeffs[2] != 0.0 || coeffs[3] != 1.0 {
477            return false;
478        }
479
480        let c = color.to_rgba8();
481        if c.a != 255 {
482            return false;
483        }
484
485        let Some(device_rect) = rect_to_int_rect(self.device_transform().transform_rect_bbox(rect))
486        else {
487            return false;
488        };
489
490        let mut device_rect = Rect::new(
491            device_rect.x() as f64,
492            device_rect.y() as f64,
493            (device_rect.x() + device_rect.width() as i32) as f64,
494            (device_rect.y() + device_rect.height() as i32) as f64,
495        );
496        if let Some(simple_clip) = self.simple_clip {
497            device_rect = device_rect.intersect(simple_clip);
498            if device_rect.is_zero_area() {
499                return true;
500            }
501        }
502
503        let x0 = device_rect.x0.max(0.0) as u32;
504        let y0 = device_rect.y0.max(0.0) as u32;
505        let x1 = device_rect.x1.min(self.pixmap.width() as f64) as u32;
506        let y1 = device_rect.y1.min(self.pixmap.height() as f64) as u32;
507
508        if x0 >= x1 || y0 >= y1 {
509            return true;
510        }
511
512        self.mark_drawn_device_rect(Rect::new(x0 as f64, y0 as f64, x1 as f64, y1 as f64));
513
514        let fill = tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a)
515            .premultiply()
516            .to_color_u8();
517        let width = self.pixmap.width() as usize;
518        let pixels = self.pixmap.pixels_mut();
519        for y in y0 as usize..y1 as usize {
520            let start = y * width + x0 as usize;
521            let end = y * width + x1 as usize;
522            pixels[start..end].fill(fill);
523        }
524
525        true
526    }
527
528    fn device_transform(&self) -> Affine {
529        self.transform
530    }
531
532    fn intersects_clip(&self, img_rect: Rect, transform: Affine) -> bool {
533        let device_rect = transform.transform_rect_bbox(img_rect);
534        self.clip
535            .map(|clip| to_skia_rect(clip.intersect(device_rect)).is_some())
536            .unwrap_or(true)
537    }
538
539    fn mark_drawn_rect_inflated(&mut self, rect: Rect, transform: Affine, pad: f64) {
540        self.mark_drawn_device_rect(transform.transform_rect_bbox(rect).inset(-pad));
541    }
542
543    fn mark_stroke_bounds(&mut self, shape: &impl Shape, stroke: &peniko::kurbo::Stroke) {
544        if let Some(clip) = self.clip {
545            self.mark_drawn_device_rect(clip);
546            return;
547        }
548
549        let stroke_pad = stroke.width + stroke.miter_limit.max(1.0) + 4.0;
550        self.mark_drawn_rect_inflated(
551            shape.bounding_box().inset(-stroke_pad),
552            self.device_transform(),
553            4.0,
554        );
555    }
556
557    fn try_draw_pixmap_translate_only(
558        &mut self,
559        pixmap: &Pixmap,
560        x: f64,
561        y: f64,
562        transform: Affine,
563        quality: FilterQuality,
564    ) -> bool {
565        let Some((draw_x, draw_y)) = integer_translation(transform, x, y) else {
566            return false;
567        };
568
569        let rect = Rect::from_origin_size(
570            (draw_x as f64, draw_y as f64),
571            (pixmap.width() as f64, pixmap.height() as f64),
572        );
573        if !self.intersects_clip(rect, Affine::IDENTITY) {
574            return true;
575        }
576
577        self.mark_drawn_rect_inflated(rect, Affine::IDENTITY, 2.0);
578        if quality == FilterQuality::Nearest && self.blit_pixmap_source_over(pixmap, draw_x, draw_y)
579        {
580            return true;
581        }
582
583        let paint = PixmapPaint {
584            opacity: 1.0,
585            blend_mode: tiny_skia::BlendMode::SourceOver,
586            quality,
587        };
588        self.pixmap.draw_pixmap(
589            draw_x,
590            draw_y,
591            pixmap.as_ref(),
592            &paint,
593            Transform::identity(),
594            self.clip.is_some().then_some(&self.mask),
595        );
596        true
597    }
598
599    fn blit_pixmap_source_over(&mut self, pixmap: &Pixmap, draw_x: i32, draw_y: i32) -> bool {
600        let Some((x0, y0, x1, y1)) = self.blit_bounds(pixmap, draw_x, draw_y) else {
601            return true;
602        };
603
604        let src_width = pixmap.width() as usize;
605        let dst_width = self.pixmap.width() as usize;
606        let mask_width = self.mask.width() as usize;
607        let src_pixels = pixmap.pixels();
608        let mask = self.clip.is_some().then_some(self.mask.data());
609        let dst_pixels = self.pixmap.pixels_mut();
610
611        for dst_y in y0 as usize..y1 as usize {
612            let src_y = (dst_y as i32 - draw_y) as usize;
613            let dst_row = dst_y * dst_width;
614            let src_row = src_y * src_width;
615            let mask_row = dst_y * mask_width;
616
617            for dst_x in x0 as usize..x1 as usize {
618                let src_x = (dst_x as i32 - draw_x) as usize;
619                let src = src_pixels[src_row + src_x];
620                let coverage = mask.map_or(255, |mask| mask[mask_row + dst_x]);
621                if coverage == 0 || src.alpha() == 0 {
622                    continue;
623                }
624
625                let src = scale_premultiplied_color(src, coverage);
626                let dst = dst_pixels[dst_row + dst_x];
627                dst_pixels[dst_row + dst_x] = blend_source_over(src, dst);
628            }
629        }
630
631        true
632    }
633
634    fn blit_bounds(
635        &self,
636        pixmap: &Pixmap,
637        draw_x: i32,
638        draw_y: i32,
639    ) -> Option<(i32, i32, i32, i32)> {
640        let mut x0 = draw_x.max(0);
641        let mut y0 = draw_y.max(0);
642        let mut x1 = (draw_x + pixmap.width() as i32).min(self.pixmap.width() as i32);
643        let mut y1 = (draw_y + pixmap.height() as i32).min(self.pixmap.height() as i32);
644
645        if let Some(simple_clip) = self.simple_clip {
646            let clip_rect = rect_to_int_rect(simple_clip)?;
647            x0 = x0.max(clip_rect.x());
648            y0 = y0.max(clip_rect.y());
649            x1 = x1.min(clip_rect.x() + clip_rect.width() as i32);
650            y1 = y1.min(clip_rect.y() + clip_rect.height() as i32);
651        }
652
653        (x0 < x1 && y0 < y1).then_some((x0, y0, x1, y1))
654    }
655
656    fn try_fill_rect_with_paint_fast(&mut self, rect: Rect, paint: &Paint<'static>) -> bool {
657        if !is_axis_aligned(self.device_transform()) {
658            return false;
659        }
660
661        let Some(device_rect) = to_skia_rect(self.device_transform().transform_rect_bbox(rect))
662        else {
663            return false;
664        };
665
666        let mut paint = paint.clone();
667        paint.shader.transform(self.skia_transform());
668        self.pixmap.fill_rect(
669            device_rect,
670            &paint,
671            Transform::identity(),
672            self.clip.is_some().then_some(&self.mask),
673        );
674        true
675    }
676
677    /// Renders the pixmap at the position and transforms it with the given transform.
678    /// x and y should have already been scaled by the window scale
679    fn render_pixmap_direct(
680        &mut self,
681        img_pixmap: &Pixmap,
682        x: f32,
683        y: f32,
684        transform: Affine,
685        quality: FilterQuality,
686    ) {
687        if self.try_draw_pixmap_translate_only(img_pixmap, x as f64, y as f64, transform, quality) {
688            return;
689        }
690
691        let img_rect = Rect::from_origin_size(
692            (x, y),
693            (img_pixmap.width() as f64, img_pixmap.height() as f64),
694        );
695        if !self.intersects_clip(img_rect, transform) {
696            return;
697        }
698        self.mark_drawn_rect_inflated(img_rect, transform, 2.0);
699        let paint = PixmapPaint {
700            opacity: 1.0,
701            blend_mode: tiny_skia::BlendMode::SourceOver,
702            quality,
703        };
704        let transform = affine_to_skia(transform * Affine::translate((x as f64, y as f64)));
705        self.pixmap.draw_pixmap(
706            0,
707            0,
708            img_pixmap.as_ref(),
709            &paint,
710            transform,
711            self.clip.is_some().then_some(&self.mask),
712        );
713    }
714
715    fn render_pixmap_rect(
716        &mut self,
717        pixmap: &Pixmap,
718        rect: Rect,
719        transform: Affine,
720        quality: ImageQuality,
721    ) {
722        let filter_quality = image_quality_to_filter_quality(quality);
723        let local_transform = Affine::translate((rect.x0, rect.y0)).then_scale_non_uniform(
724            rect.width() / pixmap.width() as f64,
725            rect.height() / pixmap.height() as f64,
726        );
727        let composite_transform = transform * local_transform;
728
729        if self.try_draw_pixmap_translate_only(
730            pixmap,
731            0.0,
732            0.0,
733            composite_transform,
734            filter_quality,
735        ) {
736            return;
737        }
738
739        if !self.intersects_clip(rect, transform) {
740            return;
741        }
742        self.mark_drawn_rect_inflated(rect, transform, 2.0);
743        let paint = PixmapPaint {
744            opacity: 1.0,
745            blend_mode: tiny_skia::BlendMode::SourceOver,
746            quality: filter_quality,
747        };
748
749        self.pixmap.draw_pixmap(
750            0,
751            0,
752            pixmap.as_ref(),
753            &paint,
754            affine_to_skia(composite_transform),
755            self.clip.is_some().then_some(&self.mask),
756        );
757    }
758
759    fn skia_transform(&self) -> Transform {
760        skia_transform(self.device_transform())
761    }
762}
763impl Layer {
764    #[cfg(test)]
765    fn clip(&mut self, shape: &impl Shape) {
766        let path =
767            try_ret!(shape_to_path(shape).and_then(|path| path.transform(self.skia_transform())));
768        self.set_base_clip(Some(ClipPath {
769            path,
770            rect: self
771                .device_transform()
772                .transform_rect_bbox(shape.bounding_box()),
773            simple_rect: transformed_axis_aligned_rect(shape, self.device_transform()),
774        }));
775    }
776    fn stroke_recorded_path<'b, 's>(
777        &mut self,
778        path: &Path,
779        bounds: Rect,
780        brush: impl Into<BrushRef<'b>>,
781        stroke: &'s peniko::kurbo::Stroke,
782    ) {
783        let paint = try_ret!(brush_to_paint(brush));
784        self.mark_stroke_bounds(&bounds, stroke);
785        let line_cap = match stroke.end_cap {
786            peniko::kurbo::Cap::Butt => LineCap::Butt,
787            peniko::kurbo::Cap::Square => LineCap::Square,
788            peniko::kurbo::Cap::Round => LineCap::Round,
789        };
790        let line_join = match stroke.join {
791            peniko::kurbo::Join::Bevel => LineJoin::Bevel,
792            peniko::kurbo::Join::Miter => LineJoin::Miter,
793            peniko::kurbo::Join::Round => LineJoin::Round,
794        };
795        let stroke = Stroke {
796            width: stroke.width as f32,
797            miter_limit: stroke.miter_limit as f32,
798            line_cap,
799            line_join,
800            dash: (!stroke.dash_pattern.is_empty())
801                .then_some(StrokeDash::new(
802                    stroke.dash_pattern.iter().map(|v| *v as f32).collect(),
803                    stroke.dash_offset as f32,
804                ))
805                .flatten(),
806        };
807        self.pixmap.stroke_path(
808            path,
809            &paint,
810            &stroke,
811            self.skia_transform(),
812            self.clip.is_some().then_some(&self.mask),
813        );
814    }
815
816    fn fill<'b>(&mut self, shape: &impl Shape, brush: impl Into<BrushRef<'b>>, _blur_radius: f64) {
817        // FIXME: Handle _blur_radius
818
819        let brush = brush.into();
820        if let Some(rect) = shape.as_rect()
821            && let BrushRef::Solid(color) = brush
822            && self.try_fill_solid_rect_fast(rect, color)
823        {
824            return;
825        }
826
827        let paint = try_ret!(brush_to_paint(brush));
828        self.mark_drawn_rect_inflated(shape.bounding_box(), self.device_transform(), 2.0);
829        if let Some(rect) = shape.as_rect() {
830            if !self.try_fill_rect_with_paint_fast(rect, &paint) {
831                let rect = try_ret!(to_skia_rect(rect));
832                self.pixmap.fill_rect(
833                    rect,
834                    &paint,
835                    self.skia_transform(),
836                    self.clip.is_some().then_some(&self.mask),
837                );
838            }
839        } else {
840            let path = try_ret!(shape_to_path(shape));
841            self.pixmap.fill_path(
842                &path,
843                &paint,
844                FillRule::Winding,
845                self.skia_transform(),
846                self.clip.is_some().then_some(&self.mask),
847            );
848        }
849    }
850
851    fn fill_recorded_path<'b>(
852        &mut self,
853        path: &Path,
854        bounds: Rect,
855        brush: impl Into<BrushRef<'b>>,
856        _blur_radius: f64,
857    ) {
858        let paint = try_ret!(brush_to_paint(brush));
859        self.mark_drawn_rect_inflated(bounds, self.device_transform(), 2.0);
860        self.pixmap.fill_path(
861            path,
862            &paint,
863            FillRule::Winding,
864            self.skia_transform(),
865            self.clip.is_some().then_some(&self.mask),
866        );
867    }
868}
869
870pub struct TinySkiaRenderer<W> {
871    #[allow(unused)]
872    context: Context<W>,
873    surface: Surface<W, W>,
874    cache_color: CacheColor,
875    recording: Recording,
876    transform: Affine,
877    window_scale: f64,
878    capture: bool,
879    layers: Vec<Layer>,
880    last_presented_bounds: Option<Rect>,
881    font_embolden: f32,
882}
883
884impl<W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle>
885    TinySkiaRenderer<W>
886{
887    fn current_clip_path(&self, shape: &impl Shape) -> Option<ClipPath> {
888        let path = shape_to_path(shape)?.transform(affine_to_skia(self.transform))?;
889        Some(ClipPath {
890            path,
891            rect: self.transform.transform_rect_bbox(shape.bounding_box()),
892            simple_rect: transformed_axis_aligned_rect(shape, self.transform),
893        })
894    }
895
896    fn clear_root_layer(&mut self) {
897        let first_layer = &mut self.layers[0];
898        first_layer.pixmap.fill(tiny_skia::Color::TRANSPARENT);
899        first_layer.base_clip = None;
900        first_layer.clip = None;
901        first_layer.simple_clip = None;
902        first_layer.draw_bounds = None;
903        first_layer.transform = Affine::IDENTITY;
904        first_layer.mask.clear();
905    }
906
907    fn brush_to_owned<'b>(&self, brush: impl Into<BrushRef<'b>>) -> Option<peniko::Brush> {
908        match brush.into() {
909            BrushRef::Solid(color) => Some(peniko::Brush::Solid(color)),
910            BrushRef::Gradient(gradient) => Some(peniko::Brush::Gradient(gradient.clone())),
911            BrushRef::Image(_) => None,
912        }
913    }
914
915    fn colorize_pixmap<'b>(
916        &self,
917        pixmap: &Pixmap,
918        brush: Option<impl Into<BrushRef<'b>>>,
919    ) -> Option<Arc<Pixmap>> {
920        let paint = brush.and_then(|brush| brush_to_paint(brush))?;
921        let mut colored_bg = Pixmap::new(pixmap.width(), pixmap.height())?;
922        colored_bg.fill_rect(
923            tiny_skia::Rect::from_xywh(0.0, 0.0, pixmap.width() as f32, pixmap.height() as f32)?,
924            &paint,
925            Transform::identity(),
926            None,
927        );
928
929        let mask = Mask::from_pixmap(pixmap.as_ref(), MaskType::Alpha);
930        colored_bg.apply_mask(&mask);
931        Some(Arc::new(colored_bg))
932    }
933
934    fn replay_layer(
935        recorded: &RecordedLayer,
936        raster: &mut Layer,
937        inherited_clips: &[ClipPath],
938        width: u32,
939        height: u32,
940    ) {
941        let mut active_clips = inherited_clips.to_vec();
942
943        for command in &recorded.commands {
944            match command {
945                RecordedCommand::PushClip(clip) => {
946                    active_clips.push(clip.clone());
947                    raster.intersect_clip_path(clip);
948                }
949                RecordedCommand::PopClip => {
950                    active_clips.pop();
951                    raster.rebuild_clip_mask(&active_clips);
952                }
953                RecordedCommand::FillRect {
954                    rect,
955                    brush,
956                    transform,
957                    blur_radius,
958                } => {
959                    raster.transform = *transform;
960                    raster.fill(rect, brush, *blur_radius);
961                }
962                RecordedCommand::FillPath {
963                    path,
964                    bounds,
965                    brush,
966                    transform,
967                    blur_radius,
968                } => {
969                    raster.transform = *transform;
970                    raster.fill_recorded_path(path, *bounds, brush, *blur_radius);
971                }
972                RecordedCommand::StrokePath {
973                    path,
974                    bounds,
975                    brush,
976                    stroke,
977                    transform,
978                } => {
979                    raster.transform = *transform;
980                    raster.stroke_recorded_path(path, *bounds, brush, stroke);
981                }
982                RecordedCommand::DrawPixmapDirect {
983                    pixmap,
984                    x,
985                    y,
986                    transform,
987                    quality,
988                } => {
989                    raster.render_pixmap_direct(pixmap, *x, *y, *transform, *quality);
990                }
991                RecordedCommand::DrawPixmapRect {
992                    pixmap,
993                    rect,
994                    transform,
995                    quality,
996                } => {
997                    raster.render_pixmap_rect(pixmap, *rect, *transform, *quality);
998                }
999                RecordedCommand::Layer(layer) => {
1000                    let Some(clip) = layer.clip.clone() else {
1001                        continue;
1002                    };
1003                    let Ok(mut child) = Layer::new_with_base_clip(
1004                        layer.blend_mode,
1005                        layer.alpha,
1006                        clip,
1007                        width,
1008                        height,
1009                    ) else {
1010                        continue;
1011                    };
1012                    child.rebuild_clip_mask(&active_clips);
1013                    Self::replay_layer(layer, &mut child, &active_clips, width, height);
1014                    apply_layer(&child, raster);
1015                }
1016            }
1017        }
1018    }
1019
1020    fn replay_recording(&mut self) {
1021        self.clear_root_layer();
1022        let width = self.layers[0].pixmap.width();
1023        let height = self.layers[0].pixmap.height();
1024        let raster = &mut self.layers[0];
1025        Self::replay_layer(self.recording.root(), raster, &[], width, height);
1026    }
1027
1028    pub fn new(window: W, width: u32, height: u32, scale: f64, font_embolden: f32) -> Result<Self>
1029    where
1030        W: Clone,
1031    {
1032        let context = Context::new(window.clone())
1033            .map_err(|err| anyhow!("unable to create context: {}", err))?;
1034        let mut surface = Surface::new(&context, window)
1035            .map_err(|err| anyhow!("unable to create surface: {}", err))?;
1036        surface
1037            .resize(
1038                NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()),
1039                NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()),
1040            )
1041            .map_err(|_| anyhow!("failed to resize surface"))?;
1042        let main_layer = Layer::new_root(width, height)?;
1043        Ok(Self {
1044            context,
1045            surface,
1046            recording: Recording::new(),
1047            transform: Affine::IDENTITY,
1048            window_scale: scale,
1049            capture: false,
1050            cache_color: CacheColor(false),
1051            layers: vec![main_layer],
1052            last_presented_bounds: None,
1053            font_embolden,
1054        })
1055    }
1056
1057    pub fn resize(&mut self, width: u32, height: u32, scale: f64) {
1058        if width != self.layers[0].pixmap.width() || height != self.layers[0].pixmap.height() {
1059            self.surface
1060                .resize(
1061                    NonZeroU32::new(width).unwrap_or(NonZeroU32::new(1).unwrap()),
1062                    NonZeroU32::new(height).unwrap_or(NonZeroU32::new(1).unwrap()),
1063                )
1064                .expect("failed to resize surface");
1065            self.layers[0] = Layer::new_root(width, height).expect("unable to create layer");
1066            self.last_presented_bounds = None;
1067        }
1068        self.window_scale = scale;
1069    }
1070
1071    pub fn set_scale(&mut self, scale: f64) {
1072        self.window_scale = scale;
1073    }
1074
1075    pub fn size(&self) -> Size {
1076        Size::new(
1077            self.layers[0].pixmap.width() as f64,
1078            self.layers[0].pixmap.height() as f64,
1079        )
1080    }
1081}
1082
1083fn to_color(color: Color) -> tiny_skia::Color {
1084    let c = color.to_rgba8();
1085    tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a)
1086}
1087
1088fn to_point(point: Point) -> tiny_skia::Point {
1089    tiny_skia::Point::from_xy(point.x as f32, point.y as f32)
1090}
1091
1092fn is_axis_aligned(transform: Affine) -> bool {
1093    let coeffs = transform.as_coeffs();
1094    coeffs[1] == 0.0 && coeffs[2] == 0.0
1095}
1096
1097fn affine_scale_components(transform: Affine) -> (f64, f64, f64) {
1098    let coeffs = transform.as_coeffs();
1099    let scale_x = coeffs[0].hypot(coeffs[1]);
1100    let scale_y = coeffs[2].hypot(coeffs[3]);
1101    let uniform = (scale_x + scale_y) * 0.5;
1102    (scale_x, scale_y, uniform)
1103}
1104
1105fn scaled_embolden_strength(font_embolden: f32, raster_scale: f64) -> f32 {
1106    font_embolden * raster_scale as f32
1107}
1108
1109fn normalize_affine(transform: Affine, include_translation: bool) -> Affine {
1110    let coeffs = transform.as_coeffs();
1111    let (scale_x, scale_y, _) = affine_scale_components(transform);
1112    let tx = if include_translation { coeffs[4] } else { 0.0 };
1113    let ty = if include_translation { coeffs[5] } else { 0.0 };
1114    Affine::new([
1115        if scale_x != 0.0 {
1116            coeffs[0] / scale_x
1117        } else {
1118            0.0
1119        },
1120        if scale_x != 0.0 {
1121            coeffs[1] / scale_x
1122        } else {
1123            0.0
1124        },
1125        if scale_y != 0.0 {
1126            coeffs[2] / scale_y
1127        } else {
1128            0.0
1129        },
1130        if scale_y != 0.0 {
1131            coeffs[3] / scale_y
1132        } else {
1133            0.0
1134        },
1135        tx,
1136        ty,
1137    ])
1138}
1139
1140fn transformed_axis_aligned_rect(shape: &impl Shape, transform: Affine) -> Option<Rect> {
1141    let rect = shape.as_rect()?;
1142    is_axis_aligned(transform).then(|| transform.transform_rect_bbox(rect))
1143}
1144
1145fn nearly_integral(value: f64) -> Option<i32> {
1146    let rounded = value.round();
1147    ((value - rounded).abs() <= 1e-6).then_some(rounded as i32)
1148}
1149
1150fn integer_translation(transform: Affine, x: f64, y: f64) -> Option<(i32, i32)> {
1151    let coeffs = transform.as_coeffs();
1152    (coeffs[0] == 1.0 && coeffs[1] == 0.0 && coeffs[2] == 0.0 && coeffs[3] == 1.0).then_some((
1153        nearly_integral(x + coeffs[4])?,
1154        nearly_integral(y + coeffs[5])?,
1155    ))
1156}
1157
1158fn image_quality_to_filter_quality(quality: ImageQuality) -> FilterQuality {
1159    match quality {
1160        ImageQuality::Low => FilterQuality::Nearest,
1161        ImageQuality::Medium | ImageQuality::High => FilterQuality::Bilinear,
1162    }
1163}
1164
1165fn axis_aligned_device_placement(rect: Rect, transform: Affine) -> Option<(f32, f32, u32, u32)> {
1166    if !is_axis_aligned(transform) {
1167        return None;
1168    }
1169
1170    let device_rect = transform.transform_rect_bbox(rect);
1171    let width = nearly_integral(device_rect.width())?;
1172    let height = nearly_integral(device_rect.height())?;
1173    (width > 0 && height > 0).then_some((
1174        device_rect.x0 as f32,
1175        device_rect.y0 as f32,
1176        width as u32,
1177        height as u32,
1178    ))
1179}
1180
1181fn cache_scaled_pixmap(
1182    cache_color: CacheColor,
1183    cache_key: ScaledImageCacheKey,
1184    pixmap: &Pixmap,
1185    quality: ImageQuality,
1186) -> Option<Arc<Pixmap>> {
1187    if let Some(cached) = SCALED_IMAGE_CACHE.with_borrow_mut(|cache| {
1188        cache.get_mut(&cache_key).map(|(color, pixmap)| {
1189            *color = cache_color;
1190            pixmap.clone()
1191        })
1192    }) {
1193        return Some(cached);
1194    }
1195
1196    let mut scaled = Pixmap::new(cache_key.width, cache_key.height)?;
1197    let paint = PixmapPaint {
1198        opacity: 1.0,
1199        blend_mode: tiny_skia::BlendMode::SourceOver,
1200        quality: image_quality_to_filter_quality(quality),
1201    };
1202    let transform = Transform::from_scale(
1203        cache_key.width as f32 / pixmap.width() as f32,
1204        cache_key.height as f32 / pixmap.height() as f32,
1205    );
1206    scaled.draw_pixmap(0, 0, pixmap.as_ref(), &paint, transform, None);
1207
1208    let scaled = Arc::new(scaled);
1209    SCALED_IMAGE_CACHE.with_borrow_mut(|cache| {
1210        cache.insert(cache_key, (cache_color, scaled.clone()));
1211    });
1212    Some(scaled)
1213}
1214
1215fn mul_div_255(value: u8, factor: u8) -> u8 {
1216    (((value as u16 * factor as u16) + 127) / 255) as u8
1217}
1218
1219fn scale_premultiplied_color(color: PremultipliedColorU8, alpha: u8) -> PremultipliedColorU8 {
1220    if alpha == 255 {
1221        return color;
1222    }
1223
1224    PremultipliedColorU8::from_rgba(
1225        mul_div_255(color.red(), alpha),
1226        mul_div_255(color.green(), alpha),
1227        mul_div_255(color.blue(), alpha),
1228        mul_div_255(color.alpha(), alpha),
1229    )
1230    .expect("scaled premultiplied color must remain premultiplied")
1231}
1232
1233fn blend_source_over(src: PremultipliedColorU8, dst: PremultipliedColorU8) -> PremultipliedColorU8 {
1234    if src.alpha() == 255 {
1235        return src;
1236    }
1237    if src.alpha() == 0 {
1238        return dst;
1239    }
1240
1241    let inv_alpha = 255 - src.alpha();
1242    PremultipliedColorU8::from_rgba(
1243        src.red().saturating_add(mul_div_255(dst.red(), inv_alpha)),
1244        src.green()
1245            .saturating_add(mul_div_255(dst.green(), inv_alpha)),
1246        src.blue()
1247            .saturating_add(mul_div_255(dst.blue(), inv_alpha)),
1248        src.alpha()
1249            .saturating_add(mul_div_255(dst.alpha(), inv_alpha)),
1250    )
1251    .expect("source-over premultiplied blend must remain premultiplied")
1252}
1253
1254impl<W: raw_window_handle::HasWindowHandle + raw_window_handle::HasDisplayHandle> Renderer
1255    for TinySkiaRenderer<W>
1256{
1257    fn begin(&mut self, capture: bool) {
1258        self.capture = capture;
1259        assert!(self.layers.len() == 1);
1260        self.transform = Affine::IDENTITY;
1261        self.recording.clear();
1262        self.clear_root_layer();
1263    }
1264
1265    fn stroke<'b, 's>(
1266        &mut self,
1267        shape: &impl Shape,
1268        brush: impl Into<BrushRef<'b>>,
1269        stroke: &'s peniko::kurbo::Stroke,
1270    ) {
1271        let Some(brush) = self.brush_to_owned(brush) else {
1272            return;
1273        };
1274        let Some(path) = shape_to_path(shape) else {
1275            return;
1276        };
1277        self.recording.stroke_path(
1278            path,
1279            shape.bounding_box(),
1280            brush,
1281            stroke.clone(),
1282            self.transform,
1283        );
1284    }
1285
1286    fn fill<'b>(&mut self, shape: &impl Shape, brush: impl Into<BrushRef<'b>>, blur_radius: f64) {
1287        let Some(brush) = self.brush_to_owned(brush) else {
1288            return;
1289        };
1290        if let Some(rect) = shape.as_rect() {
1291            self.recording
1292                .fill_rect(rect, brush, self.transform, blur_radius);
1293        } else if let Some(path) = shape_to_path(shape) {
1294            self.recording.fill_path(
1295                path,
1296                shape.bounding_box(),
1297                brush,
1298                self.transform,
1299                blur_radius,
1300            );
1301        }
1302    }
1303
1304    fn draw_glyphs<'a>(
1305        &mut self,
1306        origin: Point,
1307        props: &GlyphRunProps<'a>,
1308        glyphs: impl Iterator<Item = ParleyGlyph> + 'a,
1309    ) {
1310        let font = &props.font;
1311        let text_transform = self.transform * props.transform;
1312        let (_, _, raster_scale) = affine_scale_components(text_transform);
1313        let transform = normalize_affine(text_transform, false);
1314        let raster_origin = transform.inverse() * (text_transform * origin);
1315        let brush_color = match &props.brush {
1316            peniko::Brush::Solid(color) => Color::from(*color),
1317            _ => return,
1318        };
1319        let font_ref = match FontRef::from_index(font.data.data(), font.index as usize) {
1320            Some(f) => f,
1321            None => return,
1322        };
1323        let font_blob_id = font.data.id();
1324        let skew = props
1325            .glyph_transform
1326            .map(|transform| transform.as_coeffs()[0].atan().to_degrees() as f32);
1327
1328        for glyph in glyphs {
1329            let glyph_x = (raster_origin.x + glyph.x as f64 * raster_scale) as f32;
1330            let glyph_y = (raster_origin.y + glyph.y as f64 * raster_scale) as f32;
1331            let scaled_font_size = props.font_size * raster_scale as f32;
1332            let scaled_embolden = scaled_embolden_strength(self.font_embolden, raster_scale);
1333            let (cache_key, new_x, new_y) = GlyphCacheKey::new(
1334                font_blob_id,
1335                font.index,
1336                glyph.id as u16,
1337                scaled_font_size,
1338                glyph_x,
1339                glyph_y,
1340                props.hint,
1341                false,
1342                skew,
1343            );
1344
1345            let cached = cache_glyph(
1346                self.cache_color,
1347                cache_key,
1348                brush_color,
1349                &font_ref,
1350                scaled_font_size,
1351                props.hint,
1352                props.normalized_coords,
1353                scaled_embolden,
1354                skew,
1355                new_x,
1356                new_y,
1357            );
1358
1359            if let Some(cached) = cached {
1360                self.recording.draw_pixmap_direct(
1361                    cached.pixmap.clone(),
1362                    new_x.floor() + cached.left,
1363                    new_y.floor() - cached.top,
1364                    transform,
1365                    FilterQuality::Nearest,
1366                );
1367            }
1368        }
1369    }
1370
1371    fn draw_img(&mut self, img: Img<'_>, rect: Rect) {
1372        let transform = self.transform;
1373        let pixmap = if let Some(pixmap) = IMAGE_CACHE.with_borrow_mut(|ic| {
1374            ic.get_mut(img.hash).map(|(color, pixmap)| {
1375                *color = self.cache_color;
1376                pixmap.clone()
1377            })
1378        }) {
1379            pixmap
1380        } else {
1381            let image_data = img.img.image.data.data();
1382            let mut pixmap = try_ret!(Pixmap::new(img.img.image.width, img.img.image.height));
1383            for (a, b) in pixmap
1384                .pixels_mut()
1385                .iter_mut()
1386                .zip(image_data.chunks_exact(4))
1387            {
1388                *a = tiny_skia::Color::from_rgba8(b[0], b[1], b[2], b[3])
1389                    .premultiply()
1390                    .to_color_u8();
1391            }
1392
1393            let pixmap = Arc::new(pixmap);
1394            IMAGE_CACHE.with_borrow_mut(|ic| {
1395                ic.insert(img.hash.to_owned(), (self.cache_color, pixmap.clone()));
1396            });
1397            pixmap
1398        };
1399
1400        let quality = img.img.sampler.quality;
1401        if let Some((draw_x, draw_y, width, height)) =
1402            axis_aligned_device_placement(rect, transform)
1403        {
1404            let filter_quality = image_quality_to_filter_quality(quality);
1405            let device_pixmap = if width == pixmap.width() && height == pixmap.height() {
1406                pixmap.clone()
1407            } else {
1408                let cache_key = ScaledImageCacheKey {
1409                    image_id: img.img.image.data.id(),
1410                    width,
1411                    height,
1412                    quality: quality as u8,
1413                };
1414                try_ret!(cache_scaled_pixmap(
1415                    self.cache_color,
1416                    cache_key,
1417                    &pixmap,
1418                    quality
1419                ))
1420            };
1421            self.recording.draw_pixmap_direct(
1422                device_pixmap,
1423                draw_x,
1424                draw_y,
1425                Affine::IDENTITY,
1426                filter_quality,
1427            );
1428            return;
1429        }
1430
1431        self.recording
1432            .draw_pixmap_rect(pixmap, rect, transform, quality);
1433    }
1434
1435    fn draw_svg<'b>(
1436        &mut self,
1437        svg: floem_renderer::Svg<'b>,
1438        rect: Rect,
1439        brush: Option<impl Into<BrushRef<'b>>>,
1440    ) {
1441        let coeffs = self.transform.as_coeffs();
1442        let scale_x = coeffs[0].hypot(coeffs[1]);
1443        let scale_y = coeffs[2].hypot(coeffs[3]);
1444        let width = (rect.width() * scale_x.abs()).round().max(1.0) as u32;
1445        let height = (rect.height() * scale_y.abs()).round().max(1.0) as u32;
1446        let transform = self.transform;
1447
1448        if let Some(pixmap) = IMAGE_CACHE.with_borrow_mut(|ic| {
1449            ic.get_mut(svg.hash).map(|(color, pixmap)| {
1450                *color = self.cache_color;
1451                pixmap.clone()
1452            })
1453        }) {
1454            let final_pixmap = self.colorize_pixmap(&pixmap, brush).unwrap_or(pixmap);
1455            self.recording
1456                .draw_pixmap_rect(final_pixmap, rect, transform, ImageQuality::High);
1457            return;
1458        }
1459
1460        let mut non_colored_svg = try_ret!(tiny_skia::Pixmap::new(width, height));
1461        let svg_transform = tiny_skia::Transform::from_scale(
1462            width as f32 / svg.tree.size().width(),
1463            height as f32 / svg.tree.size().height(),
1464        );
1465        resvg::render(svg.tree, svg_transform, &mut non_colored_svg.as_mut());
1466
1467        let non_colored_svg = Arc::new(non_colored_svg);
1468        let final_pixmap = self
1469            .colorize_pixmap(&non_colored_svg, brush)
1470            .unwrap_or_else(|| non_colored_svg.clone());
1471        self.recording
1472            .draw_pixmap_rect(final_pixmap, rect, transform, ImageQuality::High);
1473
1474        IMAGE_CACHE.with_borrow_mut(|ic| {
1475            ic.insert(svg.hash.to_owned(), (self.cache_color, non_colored_svg));
1476        });
1477    }
1478
1479    fn set_transform(&mut self, cumulative_transform: Affine) {
1480        self.transform = cumulative_transform;
1481    }
1482
1483    fn set_z_index(&mut self, _z_index: i32) {
1484        // FIXME: Remove this method?
1485    }
1486
1487    fn clip(&mut self, shape: &impl Shape) {
1488        if let Some(clip) = self.current_clip_path(shape) {
1489            self.recording.push_clip(clip);
1490        }
1491    }
1492
1493    fn clear_clip(&mut self) {
1494        self.recording.pop_clip();
1495    }
1496
1497    fn finish(&mut self) -> Option<peniko::ImageBrush> {
1498        // Remove cache entries which were not accessed.
1499        IMAGE_CACHE.with_borrow_mut(|ic| ic.retain(|_, (c, _)| *c == self.cache_color));
1500        SCALED_IMAGE_CACHE.with_borrow_mut(|ic| ic.retain(|_, (c, _)| *c == self.cache_color));
1501        let now = Instant::now();
1502        GLYPH_CACHE.with_borrow_mut(|gc| {
1503            gc.retain(|_, entry| should_retain_glyph_entry(entry, self.cache_color, now))
1504        });
1505
1506        // Swap the cache color.
1507        self.cache_color = CacheColor(!self.cache_color.0);
1508
1509        self.replay_recording();
1510
1511        if self.capture {
1512            let pixmap = &self.layers[0].pixmap;
1513            let data = pixmap.data().to_vec();
1514            return Some(peniko::ImageBrush::new(ImageData {
1515                data: Blob::new(Arc::new(data)),
1516                format: peniko::ImageFormat::Rgba8,
1517                alpha_type: ImageAlphaType::AlphaPremultiplied,
1518                width: pixmap.width(),
1519                height: pixmap.height(),
1520            }));
1521        }
1522
1523        let mut buffer = self
1524            .surface
1525            .buffer_mut()
1526            .expect("failed to get the surface buffer");
1527
1528        let current_bounds = self.layers[0].draw_bounds;
1529        let full_bounds = Rect::new(
1530            0.0,
1531            0.0,
1532            self.layers[0].pixmap.width() as f64,
1533            self.layers[0].pixmap.height() as f64,
1534        );
1535        let copy_bounds = if buffer.age() == 0 {
1536            Some(full_bounds)
1537        } else {
1538            match (current_bounds, self.last_presented_bounds) {
1539                (Some(current), Some(previous)) => Some(current.union(previous)),
1540                (Some(current), None) => Some(current),
1541                (None, Some(previous)) => Some(previous),
1542                (None, None) => None,
1543            }
1544        };
1545
1546        if let Some(copy_bounds) = copy_bounds.and_then(rect_to_int_rect) {
1547            let x0 = copy_bounds.x().max(0) as u32;
1548            let y0 = copy_bounds.y().max(0) as u32;
1549            let x1 = (copy_bounds.x() + copy_bounds.width() as i32)
1550                .min(self.layers[0].pixmap.width() as i32) as u32;
1551            let y1 = (copy_bounds.y() + copy_bounds.height() as i32)
1552                .min(self.layers[0].pixmap.height() as i32) as u32;
1553
1554            if x0 < x1 && y0 < y1 {
1555                let pixmap = &self.layers[0].pixmap;
1556                let width = pixmap.width() as usize;
1557                for y in y0 as usize..y1 as usize {
1558                    let row_start = y * width;
1559                    let src = &pixmap.pixels()[row_start + x0 as usize..row_start + x1 as usize];
1560                    let dst = &mut buffer[row_start + x0 as usize..row_start + x1 as usize];
1561                    for (out_pixel, pixel) in dst.iter_mut().zip(src.iter()) {
1562                        *out_pixel = ((pixel.red() as u32) << 16)
1563                            | ((pixel.green() as u32) << 8)
1564                            | (pixel.blue() as u32);
1565                    }
1566                }
1567
1568                let damage = [softbuffer::Rect {
1569                    x: x0,
1570                    y: y0,
1571                    width: NonZeroU32::new(x1 - x0).unwrap(),
1572                    height: NonZeroU32::new(y1 - y0).unwrap(),
1573                }];
1574                buffer
1575                    .present_with_damage(&damage)
1576                    .expect("failed to present the surface buffer");
1577            } else {
1578                buffer
1579                    .present()
1580                    .expect("failed to present the surface buffer");
1581            }
1582        } else {
1583            buffer
1584                .present()
1585                .expect("failed to present the surface buffer");
1586        }
1587
1588        self.last_presented_bounds = current_bounds;
1589
1590        None
1591    }
1592
1593    fn push_layer(
1594        &mut self,
1595        blend: impl Into<peniko::BlendMode>,
1596        alpha: f32,
1597        transform: Affine,
1598        clip: &impl Shape,
1599    ) {
1600        let layer_transform = self.transform * transform;
1601        let Some(path) =
1602            shape_to_path(clip).and_then(|path| path.transform(affine_to_skia(layer_transform)))
1603        else {
1604            return;
1605        };
1606        self.recording.push_layer(
1607            blend.into(),
1608            alpha,
1609            ClipPath {
1610                path,
1611                rect: layer_transform.transform_rect_bbox(clip.bounding_box()),
1612                simple_rect: transformed_axis_aligned_rect(clip, layer_transform),
1613            },
1614        );
1615    }
1616
1617    fn pop_layer(&mut self) {
1618        self.recording.pop_layer();
1619    }
1620
1621    fn debug_info(&self) -> String {
1622        "name: tiny_skia".into()
1623    }
1624}
1625
1626fn shape_to_path(shape: &impl Shape) -> Option<Path> {
1627    let mut builder = PathBuilder::new();
1628    for element in shape.path_elements(0.1) {
1629        match element {
1630            PathEl::ClosePath => builder.close(),
1631            PathEl::MoveTo(p) => builder.move_to(p.x as f32, p.y as f32),
1632            PathEl::LineTo(p) => builder.line_to(p.x as f32, p.y as f32),
1633            PathEl::QuadTo(p1, p2) => {
1634                builder.quad_to(p1.x as f32, p1.y as f32, p2.x as f32, p2.y as f32)
1635            }
1636            PathEl::CurveTo(p1, p2, p3) => builder.cubic_to(
1637                p1.x as f32,
1638                p1.y as f32,
1639                p2.x as f32,
1640                p2.y as f32,
1641                p3.x as f32,
1642                p3.y as f32,
1643            ),
1644        }
1645    }
1646    builder.finish()
1647}
1648
1649fn brush_to_paint<'b>(brush: impl Into<BrushRef<'b>>) -> Option<Paint<'static>> {
1650    let shader = match brush.into() {
1651        BrushRef::Solid(c) => Shader::SolidColor(to_color(c)),
1652        BrushRef::Gradient(g) => {
1653            let stops = expand_gradient_stops(g);
1654            let spread_mode = to_spread_mode(g.extend);
1655            match g.kind {
1656                GradientKind::Linear(linear) => LinearGradient::new(
1657                    to_point(linear.start),
1658                    to_point(linear.end),
1659                    stops,
1660                    spread_mode,
1661                    Transform::identity(),
1662                )?,
1663                GradientKind::Radial(RadialGradientPosition {
1664                    start_center,
1665                    start_radius: _,
1666                    end_center,
1667                    end_radius,
1668                }) => {
1669                    // FIXME: Doesn't use `start_radius`
1670                    RadialGradient::new(
1671                        to_point(start_center),
1672                        to_point(end_center),
1673                        end_radius,
1674                        stops,
1675                        spread_mode,
1676                        Transform::identity(),
1677                    )?
1678                }
1679                GradientKind::Sweep { .. } => return None,
1680            }
1681        }
1682        BrushRef::Image(_) => return None,
1683    };
1684    Some(Paint {
1685        shader,
1686        ..Default::default()
1687    })
1688}
1689
1690const GRADIENT_TOLERANCE: f32 = 0.01;
1691
1692fn expand_gradient_stops(gradient: &Gradient) -> Vec<GradientStop> {
1693    if gradient.stops.is_empty() {
1694        return Vec::new();
1695    }
1696
1697    if gradient.stops.len() == 1 {
1698        let stop = &gradient.stops[0];
1699        let color = stop
1700            .color
1701            .to_alpha_color::<Srgb>()
1702            .convert::<peniko::color::Srgb>();
1703        return vec![GradientStop::new(
1704            stop.offset,
1705            alpha_color_to_tiny_skia(color),
1706        )];
1707    }
1708
1709    let mut expanded = Vec::new();
1710    for segment in gradient.stops.windows(2) {
1711        let start = segment[0];
1712        let end = segment[1];
1713        if start.offset == end.offset {
1714            push_gradient_stop(
1715                &mut expanded,
1716                start.offset,
1717                start.color.to_alpha_color::<Srgb>(),
1718            );
1719            push_gradient_stop(
1720                &mut expanded,
1721                end.offset,
1722                end.color.to_alpha_color::<Srgb>(),
1723            );
1724            continue;
1725        }
1726
1727        expand_gradient_segment(
1728            &mut expanded,
1729            start.offset,
1730            start.color,
1731            end.offset,
1732            end.color,
1733            gradient.interpolation_cs,
1734            gradient.hue_direction,
1735        );
1736    }
1737
1738    expanded
1739}
1740
1741fn expand_gradient_segment(
1742    expanded: &mut Vec<GradientStop>,
1743    start_offset: f32,
1744    start_color: DynamicColor,
1745    end_offset: f32,
1746    end_color: DynamicColor,
1747    interpolation_cs: ColorSpaceTag,
1748    hue_direction: HueDirection,
1749) {
1750    let push_sample =
1751        |expanded: &mut Vec<GradientStop>, t: f32, color: peniko::color::AlphaColor<Srgb>| {
1752            let offset = start_offset + (end_offset - start_offset) * t;
1753            push_gradient_stop(expanded, offset, color);
1754        };
1755
1756    for (i, (t, color)) in color::gradient::<Srgb>(
1757        start_color,
1758        end_color,
1759        interpolation_cs,
1760        hue_direction,
1761        GRADIENT_TOLERANCE,
1762    )
1763    .enumerate()
1764    {
1765        if !expanded.is_empty() && i == 0 {
1766            continue;
1767        }
1768        push_sample(expanded, t, color.un_premultiply());
1769    }
1770}
1771
1772fn push_gradient_stop(
1773    expanded: &mut Vec<GradientStop>,
1774    offset: f32,
1775    color: peniko::color::AlphaColor<Srgb>,
1776) {
1777    let tiny_color = alpha_color_to_tiny_skia(color);
1778    if let Some(previous) = expanded.last()
1779        && previous == &GradientStop::new(offset, tiny_color)
1780    {
1781        return;
1782    }
1783    expanded.push(GradientStop::new(offset, tiny_color));
1784}
1785
1786fn alpha_color_to_tiny_skia(color: peniko::color::AlphaColor<Srgb>) -> tiny_skia::Color {
1787    let color = color.to_rgba8();
1788    tiny_skia::Color::from_rgba8(color.r, color.g, color.b, color.a)
1789}
1790
1791fn to_spread_mode(extend: Extend) -> SpreadMode {
1792    match extend {
1793        Extend::Pad => SpreadMode::Pad,
1794        Extend::Repeat => SpreadMode::Repeat,
1795        Extend::Reflect => SpreadMode::Reflect,
1796    }
1797}
1798
1799fn to_skia_rect(rect: Rect) -> Option<tiny_skia::Rect> {
1800    tiny_skia::Rect::from_ltrb(
1801        rect.x0 as f32,
1802        rect.y0 as f32,
1803        rect.x1 as f32,
1804        rect.y1 as f32,
1805    )
1806}
1807
1808fn rect_to_int_rect(rect: Rect) -> Option<IntRect> {
1809    IntRect::from_ltrb(
1810        rect.x0.floor() as i32,
1811        rect.y0.floor() as i32,
1812        rect.x1.ceil() as i32,
1813        rect.y1.ceil() as i32,
1814    )
1815}
1816
1817type TinyBlendMode = tiny_skia::BlendMode;
1818
1819enum BlendStrategy {
1820    /// Can be directly mapped to a tiny-skia blend mode
1821    SinglePass(TinyBlendMode),
1822    /// Requires multiple operations
1823    MultiPass {
1824        first_pass: TinyBlendMode,
1825        second_pass: TinyBlendMode,
1826    },
1827}
1828
1829fn determine_blend_strategy(peniko_mode: &BlendMode) -> BlendStrategy {
1830    match (peniko_mode.mix, peniko_mode.compose) {
1831        (Mix::Normal, compose) => BlendStrategy::SinglePass(compose_to_tiny_blend_mode(compose)),
1832
1833        (mix, Compose::SrcOver) => BlendStrategy::SinglePass(mix_to_tiny_blend_mode(mix)),
1834
1835        (mix, compose) => BlendStrategy::MultiPass {
1836            first_pass: compose_to_tiny_blend_mode(compose),
1837            second_pass: mix_to_tiny_blend_mode(mix),
1838        },
1839    }
1840}
1841
1842fn compose_to_tiny_blend_mode(compose: Compose) -> TinyBlendMode {
1843    match compose {
1844        Compose::Clear => TinyBlendMode::Clear,
1845        Compose::Copy => TinyBlendMode::Source,
1846        Compose::Dest => TinyBlendMode::Destination,
1847        Compose::SrcOver => TinyBlendMode::SourceOver,
1848        Compose::DestOver => TinyBlendMode::DestinationOver,
1849        Compose::SrcIn => TinyBlendMode::SourceIn,
1850        Compose::DestIn => TinyBlendMode::DestinationIn,
1851        Compose::SrcOut => TinyBlendMode::SourceOut,
1852        Compose::DestOut => TinyBlendMode::DestinationOut,
1853        Compose::SrcAtop => TinyBlendMode::SourceAtop,
1854        Compose::DestAtop => TinyBlendMode::DestinationAtop,
1855        Compose::Xor => TinyBlendMode::Xor,
1856        Compose::Plus => TinyBlendMode::Plus,
1857        Compose::PlusLighter => TinyBlendMode::Plus, // ??
1858    }
1859}
1860
1861fn mix_to_tiny_blend_mode(mix: Mix) -> TinyBlendMode {
1862    match mix {
1863        Mix::Normal => TinyBlendMode::SourceOver,
1864        Mix::Multiply => TinyBlendMode::Multiply,
1865        Mix::Screen => TinyBlendMode::Screen,
1866        Mix::Overlay => TinyBlendMode::Overlay,
1867        Mix::Darken => TinyBlendMode::Darken,
1868        Mix::Lighten => TinyBlendMode::Lighten,
1869        Mix::ColorDodge => TinyBlendMode::ColorDodge,
1870        Mix::ColorBurn => TinyBlendMode::ColorBurn,
1871        Mix::HardLight => TinyBlendMode::HardLight,
1872        Mix::SoftLight => TinyBlendMode::SoftLight,
1873        Mix::Difference => TinyBlendMode::Difference,
1874        Mix::Exclusion => TinyBlendMode::Exclusion,
1875        Mix::Hue => TinyBlendMode::Hue,
1876        Mix::Saturation => TinyBlendMode::Saturation,
1877        Mix::Color => TinyBlendMode::Color,
1878        Mix::Luminosity => TinyBlendMode::Luminosity,
1879    }
1880}
1881
1882fn layer_composite_rect(layer: &Layer, parent: &Layer) -> Option<IntRect> {
1883    let mut rect = Rect::from_origin_size(
1884        Point::ZERO,
1885        Size::new(layer.pixmap.width() as f64, layer.pixmap.height() as f64),
1886    );
1887
1888    if let Some(draw_bounds) = layer.draw_bounds {
1889        rect = rect.intersect(draw_bounds);
1890    } else {
1891        return None;
1892    }
1893
1894    if let Some(layer_clip) = layer.clip {
1895        rect = rect.intersect(layer_clip);
1896    }
1897
1898    if let Some(parent_clip) = parent.clip {
1899        rect = rect.intersect(parent_clip);
1900    }
1901
1902    if rect.is_zero_area() {
1903        return None;
1904    }
1905
1906    rect_to_int_rect(rect)
1907}
1908
1909fn draw_layer_pixmap(
1910    pixmap: &Pixmap,
1911    x: i32,
1912    y: i32,
1913    parent: &mut Layer,
1914    blend_mode: TinyBlendMode,
1915    alpha: f32,
1916) {
1917    parent.mark_drawn_device_rect(Rect::new(
1918        x as f64,
1919        y as f64,
1920        (x + pixmap.width() as i32) as f64,
1921        (y + pixmap.height() as i32) as f64,
1922    ));
1923
1924    let paint = PixmapPaint {
1925        opacity: alpha,
1926        blend_mode,
1927        quality: FilterQuality::Nearest,
1928    };
1929
1930    parent.pixmap.draw_pixmap(
1931        x,
1932        y,
1933        pixmap.as_ref(),
1934        &paint,
1935        Transform::identity(),
1936        parent.clip.is_some().then_some(&parent.mask),
1937    );
1938}
1939
1940fn draw_layer_region(
1941    parent: &mut Layer,
1942    pixmap: &Pixmap,
1943    composite_rect: IntRect,
1944    blend_mode: TinyBlendMode,
1945    alpha: f32,
1946) {
1947    let Some(cropped) = pixmap.clone_rect(composite_rect) else {
1948        return;
1949    };
1950
1951    draw_layer_pixmap(
1952        &cropped,
1953        composite_rect.x(),
1954        composite_rect.y(),
1955        parent,
1956        blend_mode,
1957        alpha,
1958    );
1959}
1960
1961fn apply_layer(layer: &Layer, parent: &mut Layer) {
1962    let Some(composite_rect) = layer_composite_rect(layer, parent) else {
1963        return;
1964    };
1965
1966    match determine_blend_strategy(&layer.blend_mode) {
1967        BlendStrategy::SinglePass(blend_mode) => {
1968            draw_layer_region(
1969                parent,
1970                &layer.pixmap,
1971                composite_rect,
1972                blend_mode,
1973                layer.alpha,
1974            );
1975        }
1976        BlendStrategy::MultiPass {
1977            first_pass,
1978            second_pass,
1979        } => {
1980            let Some(original_parent) = parent.pixmap.clone_rect(composite_rect) else {
1981                return;
1982            };
1983
1984            draw_layer_region(parent, &layer.pixmap, composite_rect, first_pass, 1.0);
1985
1986            let Some(intermediate) = parent.pixmap.clone_rect(composite_rect) else {
1987                return;
1988            };
1989
1990            draw_layer_pixmap(
1991                &original_parent,
1992                composite_rect.x(),
1993                composite_rect.y(),
1994                parent,
1995                TinyBlendMode::Source,
1996                1.0,
1997            );
1998
1999            draw_layer_pixmap(
2000                &intermediate,
2001                composite_rect.x(),
2002                composite_rect.y(),
2003                parent,
2004                second_pass,
2005                1.0,
2006            );
2007        }
2008    }
2009}
2010
2011fn affine_to_skia(affine: Affine) -> Transform {
2012    let transform = affine.as_coeffs();
2013    Transform::from_row(
2014        transform[0] as f32,
2015        transform[1] as f32,
2016        transform[2] as f32,
2017        transform[3] as f32,
2018        transform[4] as f32,
2019        transform[5] as f32,
2020    )
2021}
2022
2023fn skia_transform(affine: Affine) -> Transform {
2024    affine_to_skia(affine)
2025}
2026
2027#[cfg(test)]
2028mod tests {
2029    use super::*;
2030    use peniko::color::{ColorSpaceTag, HueDirection, palette::css};
2031
2032    /// Creates a `Layer` directly without a window, for offscreen rendering.
2033    fn make_layer(width: u32, height: u32) -> Layer {
2034        Layer {
2035            pixmap: Pixmap::new(width, height).expect("failed to create pixmap"),
2036            base_clip: None,
2037            clip: None,
2038            simple_clip: None,
2039            draw_bounds: None,
2040            mask: Mask::new(width, height).expect("failed to create mask"),
2041            transform: Affine::IDENTITY,
2042            blend_mode: Mix::Normal.into(),
2043            alpha: 1.0,
2044        }
2045    }
2046
2047    fn pixel_rgba(layer: &Layer, x: u32, y: u32) -> (u8, u8, u8, u8) {
2048        let idx = (y * layer.pixmap.width() + x) as usize;
2049        let pixel = layer.pixmap.pixels()[idx];
2050        (pixel.red(), pixel.green(), pixel.blue(), pixel.alpha())
2051    }
2052
2053    fn rgba_distance(a: (u8, u8, u8, u8), b: (u8, u8, u8, u8)) -> u32 {
2054        a.0.abs_diff(b.0) as u32
2055            + a.1.abs_diff(b.1) as u32
2056            + a.2.abs_diff(b.2) as u32
2057            + a.3.abs_diff(b.3) as u32
2058    }
2059
2060    fn interpolated_midpoint(
2061        start: peniko::color::DynamicColor,
2062        end: peniko::color::DynamicColor,
2063        color_space: ColorSpaceTag,
2064        hue_direction: HueDirection,
2065    ) -> (u8, u8, u8, u8) {
2066        let color = start
2067            .interpolate(end, color_space, hue_direction)
2068            .eval(0.5)
2069            .to_alpha_color::<Srgb>()
2070            .to_rgba8();
2071        (color.r, color.g, color.b, color.a)
2072    }
2073
2074    #[test]
2075    fn render_pixmap_rect_uses_transform_and_mask() {
2076        let mut layer = make_layer(12, 12);
2077        layer.transform = Affine::translate((4.0, 0.0));
2078        layer.clip(&Rect::new(1.0, 0.0, 3.0, 4.0));
2079
2080        let mut src = Pixmap::new(2, 2).expect("failed to create src pixmap");
2081        src.fill(tiny_skia::Color::from_rgba8(255, 0, 0, 255));
2082
2083        layer.render_pixmap_rect(
2084            &src,
2085            Rect::new(0.0, 0.0, 4.0, 4.0),
2086            layer.device_transform(),
2087            ImageQuality::Medium,
2088        );
2089
2090        assert_eq!(pixel_rgba(&layer, 3, 1), (0, 0, 0, 0));
2091        assert_eq!(pixel_rgba(&layer, 4, 1), (0, 0, 0, 0));
2092        assert_eq!(pixel_rgba(&layer, 5, 1), (255, 0, 0, 255));
2093        assert_eq!(pixel_rgba(&layer, 6, 1), (255, 0, 0, 255));
2094        assert_eq!(pixel_rgba(&layer, 7, 1), (0, 0, 0, 0));
2095        assert_eq!(pixel_rgba(&layer, 8, 1), (0, 0, 0, 0));
2096    }
2097
2098    #[test]
2099    fn render_pixmap_rect_detects_exact_device_blit_when_scale_cancels() {
2100        let rect = Rect::new(1.0, 2.0, 2.0, 3.0);
2101        let pixmap = Pixmap::new(2, 2).expect("failed to create src pixmap");
2102        let local_transform = Affine::translate((rect.x0, rect.y0)).then_scale_non_uniform(
2103            rect.width() / pixmap.width() as f64,
2104            rect.height() / pixmap.height() as f64,
2105        );
2106        let composite_transform = Affine::scale(2.0) * local_transform;
2107
2108        assert_eq!(
2109            integer_translation(composite_transform, 0.0, 0.0),
2110            Some((1, 2))
2111        );
2112    }
2113
2114    #[test]
2115    fn image_quality_low_maps_to_nearest_filtering() {
2116        assert_eq!(
2117            image_quality_to_filter_quality(ImageQuality::Low),
2118            FilterQuality::Nearest
2119        );
2120        assert_eq!(
2121            image_quality_to_filter_quality(ImageQuality::Medium),
2122            FilterQuality::Bilinear
2123        );
2124        assert_eq!(
2125            image_quality_to_filter_quality(ImageQuality::High),
2126            FilterQuality::Bilinear
2127        );
2128    }
2129
2130    #[test]
2131    fn nested_layer_marks_parent_draw_bounds() {
2132        let mut root = make_layer(8, 8);
2133        let mut parent = make_layer(8, 8);
2134        let mut child = make_layer(8, 8);
2135
2136        child.fill(
2137            &Rect::new(2.0, 2.0, 4.0, 4.0),
2138            Color::from_rgb8(255, 0, 0),
2139            0.0,
2140        );
2141
2142        apply_layer(&child, &mut parent);
2143        assert!(parent.draw_bounds.is_some());
2144
2145        apply_layer(&parent, &mut root);
2146        assert_eq!(pixel_rgba(&root, 3, 3), (255, 0, 0, 255));
2147    }
2148
2149    #[test]
2150    fn render_pixmap_direct_blends_premultiplied_pixels() {
2151        let mut layer = make_layer(4, 4);
2152        layer
2153            .pixmap
2154            .fill(tiny_skia::Color::from_rgba8(0, 0, 255, 255));
2155
2156        let mut src = Pixmap::new(1, 1).expect("failed to create src pixmap");
2157        src.fill(tiny_skia::Color::from_rgba8(255, 0, 0, 128));
2158
2159        layer.render_pixmap_direct(&src, 1.0, 1.0, Affine::IDENTITY, FilterQuality::Nearest);
2160
2161        assert_eq!(pixel_rgba(&layer, 1, 1), (128, 0, 127, 255));
2162    }
2163
2164    #[test]
2165    fn normalized_text_transform_keeps_translation_and_rotation_separate() {
2166        let transform = Affine::translate((30.0, 20.0))
2167            * Affine::rotate(std::f64::consts::FRAC_PI_2)
2168            * Affine::scale(2.0);
2169
2170        let normalized = normalize_affine(transform, true);
2171        let (_, _, raster_scale) = affine_scale_components(transform);
2172        let device_origin = normalized * Point::new(5.0 * raster_scale, 0.0);
2173
2174        assert!((device_origin.x - 30.0).abs() < 1e-6);
2175        assert!((device_origin.y - 30.0).abs() < 1e-6);
2176    }
2177
2178    #[test]
2179    fn embolden_strength_scales_with_raster_scale() {
2180        assert!((scaled_embolden_strength(0.2, 1.5) - 0.3).abs() < f32::EPSILON);
2181        assert_eq!(scaled_embolden_strength(0.2, 0.0), 0.0);
2182    }
2183
2184    #[test]
2185    fn glyph_cache_entries_get_a_minimum_ttl() {
2186        let now = Instant::now();
2187        let stale_but_recent = GlyphCacheEntry {
2188            cache_color: CacheColor(true),
2189            glyph: None,
2190            last_touched: now - Duration::from_millis(50),
2191        };
2192        let stale_and_old = GlyphCacheEntry {
2193            cache_color: CacheColor(true),
2194            glyph: None,
2195            last_touched: now - Duration::from_millis(150),
2196        };
2197
2198        assert!(should_retain_glyph_entry(
2199            &stale_but_recent,
2200            CacheColor(false),
2201            now
2202        ));
2203        assert!(!should_retain_glyph_entry(
2204            &stale_and_old,
2205            CacheColor(false),
2206            now
2207        ));
2208        assert!(should_retain_glyph_entry(
2209            &stale_and_old,
2210            CacheColor(true),
2211            now
2212        ));
2213    }
2214
2215    #[test]
2216    fn linear_gradient_honors_interpolation_color_space() {
2217        let mut layer = make_layer(101, 1);
2218        let gradient = Gradient::new_linear(Point::new(0.0, 0.0), Point::new(101.0, 0.0))
2219            .with_interpolation_cs(ColorSpaceTag::Oklab)
2220            .with_stops([(0.0, css::RED), (1.0, css::BLUE)]);
2221
2222        layer.fill(&Rect::new(0.0, 0.0, 101.0, 1.0), &gradient, 0.0);
2223
2224        let rendered = pixel_rgba(&layer, 50, 0);
2225        let expected_oklab = interpolated_midpoint(
2226            css::RED.into(),
2227            css::BLUE.into(),
2228            ColorSpaceTag::Oklab,
2229            HueDirection::Shorter,
2230        );
2231        let expected_srgb = interpolated_midpoint(
2232            css::RED.into(),
2233            css::BLUE.into(),
2234            ColorSpaceTag::Srgb,
2235            HueDirection::Shorter,
2236        );
2237
2238        assert!(rgba_distance(rendered, expected_oklab) <= 10);
2239        assert!(rgba_distance(rendered, expected_srgb) >= 30);
2240    }
2241
2242    #[test]
2243    fn linear_gradient_honors_hue_direction() {
2244        let mut layer = make_layer(101, 1);
2245        let gradient = Gradient::new_linear(Point::new(0.0, 0.0), Point::new(101.0, 0.0))
2246            .with_interpolation_cs(ColorSpaceTag::Oklch)
2247            .with_hue_direction(HueDirection::Longer)
2248            .with_stops([(0.0, css::RED), (1.0, css::BLUE)]);
2249
2250        layer.fill(&Rect::new(0.0, 0.0, 101.0, 1.0), &gradient, 0.0);
2251
2252        let rendered = pixel_rgba(&layer, 50, 0);
2253        let expected_longer = interpolated_midpoint(
2254            css::RED.into(),
2255            css::BLUE.into(),
2256            ColorSpaceTag::Oklch,
2257            HueDirection::Longer,
2258        );
2259        let expected_shorter = interpolated_midpoint(
2260            css::RED.into(),
2261            css::BLUE.into(),
2262            ColorSpaceTag::Oklch,
2263            HueDirection::Shorter,
2264        );
2265
2266        assert!(rgba_distance(rendered, expected_longer) <= 10);
2267        assert!(rgba_distance(rendered, expected_shorter) >= 40);
2268    }
2269}