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#[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 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 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 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: Option<Rect>,
257 simple_clip: Option<Rect>,
258 draw_bounds: Option<Rect>,
259 mask: Mask,
260 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 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 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 }
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 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 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 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 SinglePass(TinyBlendMode),
1822 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, }
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 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}