use std::cell::RefCell;
use std::collections::HashMap;
use tiny_skia::{
FillRule, FilterQuality, GradientStop as SkStop, LineCap, LinearGradient, Mask,
Paint as SkPaint, PathBuilder, Pixmap, PixmapPaint, Point as SkPoint, RadialGradient, Shader,
SpreadMode, Stroke, Transform,
};
type ShadowKey = (i32, i32, i32, i32, u32);
thread_local! {
static SHADOW_CACHE: RefCell<HashMap<ShadowKey, Pixmap>> = RefCell::new(HashMap::new());
}
pub(crate) fn shadows_disabled() -> bool {
use std::sync::OnceLock;
static D: OnceLock<bool> = OnceLock::new();
*D.get_or_init(|| std::env::var("WINDUI_NOSHADOW").is_ok_and(|v| v != "0" && !v.is_empty()))
}
fn build_shadow_pixmap(
tw: i32,
th: i32,
margin: i32,
pw: f32,
ph: f32,
pr: f32,
pblur: f32,
color: Color,
) -> Pixmap {
let mut tmp =
Pixmap::new(tw as u32, th as u32).unwrap_or_else(|| Pixmap::new(1, 1).expect("1x1 pixmap"));
if let Some(path) = rounded_rect_path(margin as f32, margin as f32, pw, ph, pr) {
let mut sp = SkPaint::default();
sp.set_color(to_sk_color(color));
sp.anti_alias = true;
tmp.fill_path(&path, &sp, FillRule::Winding, Transform::identity(), None);
}
let r = pblur.round() as usize;
if r > 0 {
box_blur(&mut tmp, r);
}
tmp
}
use super::image::{Fit, Image};
use super::{rounded_rect_path, Canvas, Paint};
use crate::geometry::{Color, Point, Rect};
use crate::spec::Align;
use crate::text::TextEngine;
struct Clip {
rect: Rect,
mask: Mask,
}
struct Layer {
pixmap: Pixmap,
opacity: f32,
}
pub struct SkiaCanvas<'a> {
pixmap: &'a mut Pixmap,
engine: Option<&'a mut dyn TextEngine>,
clips: Vec<Clip>,
saves: Vec<usize>,
scale: f32,
offset: Point,
layers: Vec<Layer>,
}
impl<'a> SkiaCanvas<'a> {
pub fn new(pixmap: &'a mut Pixmap) -> Self {
Self {
pixmap,
engine: None,
clips: Vec::new(),
saves: Vec::new(),
scale: 1.0,
offset: Point::new(0, 0),
layers: Vec::new(),
}
}
pub fn with_text(pixmap: &'a mut Pixmap, engine: &'a mut dyn TextEngine, scale: f32) -> Self {
Self::with_text_offset(pixmap, engine, scale, Point::new(0, 0))
}
pub fn with_text_offset(
pixmap: &'a mut Pixmap,
engine: &'a mut dyn TextEngine,
scale: f32,
offset: Point,
) -> Self {
Self {
pixmap,
engine: Some(engine),
clips: Vec::new(),
saves: Vec::new(),
scale,
offset,
layers: Vec::new(),
}
}
fn sk_paint(p: &Paint) -> SkPaint<'static> {
let mut sp = SkPaint::default();
sp.set_color(to_sk_color(p.color));
sp.anti_alias = p.anti_alias;
sp
}
fn fill_paint(p: &Paint, x: f32, y: f32, w: f32, h: f32) -> SkPaint<'static> {
let mut sp = SkPaint::default();
match p.gradient.as_ref().and_then(|g| sk_shader(g, x, y, w, h)) {
Some(s) => sp.shader = s,
None => sp.set_color(to_sk_color(p.color)),
}
sp.anti_alias = p.anti_alias;
sp
}
fn fill_path_on_target(&mut self, path: &tiny_skia::Path, sp: &SkPaint, tf: Transform) {
let mask = self.clips.last().map(|c| &c.mask);
match self.layers.last_mut() {
Some(l) => l.pixmap.fill_path(path, sp, FillRule::Winding, tf, mask),
None => self.pixmap.fill_path(path, sp, FillRule::Winding, tf, mask),
};
}
fn target_pixmap(&mut self) -> &mut Pixmap {
match self.layers.last_mut() {
Some(l) => &mut l.pixmap,
None => self.pixmap,
}
}
fn stroke_path_on_target(
&mut self,
path: &tiny_skia::Path,
sp: &SkPaint,
stroke: &Stroke,
tf: Transform,
) {
let mask = self.clips.last().map(|c| &c.mask);
match self.layers.last_mut() {
Some(l) => l.pixmap.stroke_path(path, sp, stroke, tf, mask),
None => self.pixmap.stroke_path(path, sp, stroke, tf, mask),
};
}
fn tf(&self) -> Transform {
Transform::from_scale(self.scale, self.scale).post_translate(
-self.offset.x as f32 * self.scale,
-self.offset.y as f32 * self.scale,
)
}
}
impl Canvas for SkiaCanvas<'_> {
fn dpi_scale(&self) -> f32 {
self.scale
}
fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, paint: &Paint) {
self.fill_round_rect(x, y, w, h, 0.0, paint);
}
fn fill_round_rect(&mut self, x: f32, y: f32, w: f32, h: f32, radius: f32, paint: &Paint) {
let _g = super::prof::scope(super::prof::FILL);
if let Some(path) = rounded_rect_path(x, y, w, h, radius) {
let sp = Self::fill_paint(paint, x, y, w, h);
let tf = self.tf();
self.fill_path_on_target(&path, &sp, tf);
}
}
fn stroke_round_rect(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
width: f32,
paint: &Paint,
) {
let _g = super::prof::scope(super::prof::STROKE);
let s = self.scale;
let x0 = (x * s).round() / s;
let y0 = (y * s).round() / s;
let x1 = ((x + w) * s).round() / s;
let y1 = ((y + h) * s).round() / s;
let (x, y, w, h) = (x0, y0, x1 - x0, y1 - y0);
let width = width.min(w / 2.0).min(h / 2.0).max(0.0);
let half = width / 2.0;
if let Some(path) = rounded_rect_path(
x + half,
y + half,
w - width,
h - width,
(radius - half).max(0.0),
) {
let sp = Self::sk_paint(paint);
let stroke = Stroke {
width,
..Default::default()
};
let tf = self.tf();
self.stroke_path_on_target(&path, &sp, &stroke, tf);
}
}
fn draw_line(&mut self, x0: f32, y0: f32, x1: f32, y1: f32, width: f32, paint: &Paint) {
let _g = super::prof::scope(super::prof::STROKE);
let mut pb = PathBuilder::new();
pb.move_to(x0, y0);
pb.line_to(x1, y1);
if let Some(path) = pb.finish() {
let sp = Self::sk_paint(paint);
let stroke = Stroke {
width,
line_cap: LineCap::Butt,
..Default::default()
};
let tf = self.tf();
self.stroke_path_on_target(&path, &sp, &stroke, tf);
}
}
fn fill_circle(&mut self, cx: f32, cy: f32, r: f32, paint: &Paint) {
let _g = super::prof::scope(super::prof::FILL);
if let Some(path) = PathBuilder::from_circle(cx, cy, r) {
let sp = Self::fill_paint(paint, cx - r, cy - r, 2.0 * r, 2.0 * r);
let tf = self.tf();
self.fill_path_on_target(&path, &sp, tf);
}
}
fn draw_shadow(
&mut self,
x: f32,
y: f32,
w: f32,
h: f32,
radius: f32,
blur: f32,
color: Color,
) {
let _g = super::prof::scope(super::prof::SHADOW);
if color.a == 0 || w <= 0.0 || h <= 0.0 || shadows_disabled() {
return;
}
let s = self.scale;
let px = (x - self.offset.x as f32) * s;
let py = (y - self.offset.y as f32) * s;
let pw = w * s;
let ph = h * s;
let pr = (radius * s).max(0.0);
let pblur = (blur * s).max(0.0);
let margin = (pblur * 2.0).ceil() as i32 + 1;
let tw = (pw.ceil() as i32 + 2 * margin).max(1);
let th = (ph.ceil() as i32 + 2 * margin).max(1);
if tw > 8192 || th > 8192 {
return;
}
let color_key = ((color.r as u32) << 24)
| ((color.g as u32) << 16)
| ((color.b as u32) << 8)
| color.a as u32;
let key = (tw, th, pr.round() as i32, pblur.round() as i32, color_key);
let dx = px.floor() as i32 - margin;
let dy = py.floor() as i32 - margin;
let shadow_inside = match self.clips.last() {
Some(c) => {
let cr = c
.rect
.offset(-self.offset.x, -self.offset.y)
.scaled(self.scale);
dx >= cr.x && dy >= cr.y && dx + tw <= cr.x + cr.w && dy + th <= cr.y + cr.h
}
None => true,
};
SHADOW_CACHE.with(|cell| {
let mut cache = cell.borrow_mut();
if cache.len() > 128 {
cache.clear();
}
let src = cache
.entry(key)
.or_insert_with(|| build_shadow_pixmap(tw, th, margin, pw, ph, pr, pblur, color));
let mask = if shadow_inside {
None
} else {
self.clips.last().map(|c| &c.mask)
};
let pp = PixmapPaint::default();
match self.layers.last_mut() {
Some(l) => {
l.pixmap
.draw_pixmap(dx, dy, src.as_ref(), &pp, Transform::identity(), mask)
}
None => {
self.pixmap
.draw_pixmap(dx, dy, src.as_ref(), &pp, Transform::identity(), mask)
}
};
});
}
fn draw_image(&mut self, img: &Image, dst: Rect, fit: Fit, radius: f32, opacity: f32) {
let _g = super::prof::scope(super::prof::IMAGE);
let opacity = opacity.clamp(0.0, 1.0);
if opacity <= 0.0 {
return;
}
let pdst = dst
.offset(-self.offset.x, -self.offset.y)
.scaled(self.scale);
if pdst.is_empty() {
return;
}
let (iw, ih) = (img.width() as f32, img.height() as f32);
if iw <= 0.0 || ih <= 0.0 {
return;
}
let (pw, ph) = (pdst.w as f32, pdst.h as f32);
let (px, py) = (pdst.x as f32, pdst.y as f32);
let (sx, sy) = match fit {
Fit::Fill => (pw / iw, ph / ih),
Fit::Contain => {
let s = (pw / iw).min(ph / ih);
(s, s)
}
Fit::Cover => {
let s = (pw / iw).max(ph / ih);
(s, s)
}
Fit::None => (self.scale, self.scale),
};
let (dw, dh) = (iw * sx, ih * sy);
let tx = px + (pw - dw) / 2.0;
let ty = py + (ph - dh) / 2.0;
let transform = Transform::from_scale(sx, sy).post_translate(tx, ty);
let (mw, mh) = (self.pixmap.width(), self.pixmap.height());
let Some(mut mask) = Mask::new(mw, mh) else {
return;
};
let pr = (radius * self.scale).min(pw / 2.0).min(ph / 2.0).max(0.0);
let Some(path) = rounded_rect_path(px, py, pw, ph, pr) else {
return;
};
mask.fill_path(&path, FillRule::Winding, true, Transform::identity());
if let Some(c) = self.clips.last() {
let cr = c
.rect
.offset(-self.offset.x, -self.offset.y)
.scaled(self.scale);
if cr.is_empty() {
return;
}
if let Some(rect) =
tiny_skia::Rect::from_xywh(cr.x as f32, cr.y as f32, cr.w as f32, cr.h as f32)
{
let mut pb = PathBuilder::new();
pb.push_rect(rect);
if let Some(clip_path) = pb.finish() {
mask.intersect_path(
&clip_path,
FillRule::Winding,
false,
Transform::identity(),
);
}
}
}
let paint = PixmapPaint {
opacity,
quality: FilterQuality::Bilinear,
..Default::default()
};
let img_ref = img.pixmap();
self.target_pixmap()
.draw_pixmap(0, 0, img_ref.as_ref(), &paint, transform, Some(&mask));
}
fn draw_text(
&mut self,
text: &str,
rect: Rect,
color: Color,
align: Align,
family: Option<&str>,
size: f32,
) {
let _g = super::prof::scope(super::prof::TEXT);
let off = self.offset;
let rect = rect.offset(-off.x, -off.y);
let bounds = Rect::new(
0,
0,
self.pixmap.width() as i32,
self.pixmap.height() as i32,
);
if rect.scaled(self.scale).intersect(&bounds).is_empty() {
return;
}
let clip = self.clips.last().map(|c| c.rect.offset(-off.x, -off.y));
let target: &mut Pixmap = match self.layers.last_mut() {
Some(l) => &mut l.pixmap,
None => self.pixmap,
};
if let Some(engine) = self.engine.as_deref_mut() {
engine.draw(target, text, rect, color, align, family, size, clip);
}
}
fn measure_text(
&mut self,
text: &str,
family: Option<&str>,
size: f32,
) -> crate::geometry::Size {
match self.engine.as_deref_mut() {
Some(engine) => engine.measure(text, family, size, None),
None => crate::geometry::Size::new(
(text.chars().count() as f32 * size * 0.6).ceil() as i32,
size.ceil() as i32,
),
}
}
fn push_layer(&mut self, opacity: f32) {
let (w, h) = (self.pixmap.width(), self.pixmap.height());
let layer = match Pixmap::new(w, h) {
Some(pm) => Layer {
pixmap: pm,
opacity: opacity.clamp(0.0, 1.0),
},
None => Layer {
pixmap: Pixmap::new(1, 1).unwrap(),
opacity: 0.0,
},
};
self.layers.push(layer);
}
fn pop_layer(&mut self) {
if let Some(layer) = self.layers.pop() {
let pp = PixmapPaint {
opacity: layer.opacity,
..Default::default()
};
let src = layer.pixmap;
match self.layers.last_mut() {
Some(parent) => {
parent
.pixmap
.draw_pixmap(0, 0, src.as_ref(), &pp, Transform::identity(), None)
}
None => {
self.pixmap
.draw_pixmap(0, 0, src.as_ref(), &pp, Transform::identity(), None)
}
};
}
}
fn save(&mut self) {
self.saves.push(self.clips.len());
}
fn restore(&mut self) {
if let Some(depth) = self.saves.pop() {
self.clips.truncate(depth);
}
}
fn clip_rect(&mut self, r: Rect) {
let _g = super::prof::scope(super::prof::CLIP);
debug_assert!(
!self.saves.is_empty(),
"clip_rect 必须在 save() 之后调用,以与 restore() 配对"
);
let eff = match self.clips.last() {
Some(c) => c.rect.intersect(&r),
None => r,
};
let (pw, ph) = (self.pixmap.width(), self.pixmap.height());
if let Some(mut mask) = Mask::new(pw, ph) {
let peff = eff
.offset(-self.offset.x, -self.offset.y)
.scaled(self.scale);
if !peff.is_empty() {
if let Some(rect) = tiny_skia::Rect::from_xywh(
peff.x as f32,
peff.y as f32,
peff.w as f32,
peff.h as f32,
) {
let mut pb = PathBuilder::new();
pb.push_rect(rect);
if let Some(path) = pb.finish() {
mask.fill_path(&path, FillRule::Winding, false, Transform::identity());
}
}
}
self.clips.push(Clip { rect: eff, mask });
}
}
}
fn to_sk_color(c: Color) -> tiny_skia::Color {
tiny_skia::Color::from_rgba8(c.r, c.g, c.b, c.a)
}
fn box_blur(pm: &mut Pixmap, radius: usize) {
if radius == 0 {
return;
}
let (w, h) = (pm.width() as usize, pm.height() as usize);
if w == 0 || h == 0 {
return;
}
for _ in 0..3 {
let src = pm.data().to_vec();
blur_h(&src, pm.data_mut(), w, h, radius);
let src = pm.data().to_vec();
blur_v(&src, pm.data_mut(), w, h, radius);
}
}
fn blur_h(src: &[u8], dst: &mut [u8], w: usize, h: usize, r: usize) {
for y in 0..h {
let base = y * w;
let mut acc = [0u32; 4];
let mut n = 0u32;
for xx in 0..=r.min(w - 1) {
let i = (base + xx) * 4;
for c in 0..4 {
acc[c] += src[i + c] as u32;
}
n += 1;
}
for x in 0..w {
let o = (base + x) * 4;
for c in 0..4 {
dst[o + c] = (acc[c] / n) as u8;
}
let add = x + r + 1;
if add < w {
let i = (base + add) * 4;
for c in 0..4 {
acc[c] += src[i + c] as u32;
}
n += 1;
}
if x >= r {
let i = (base + (x - r)) * 4;
for c in 0..4 {
acc[c] -= src[i + c] as u32;
}
n -= 1;
}
}
}
}
fn blur_v(src: &[u8], dst: &mut [u8], w: usize, h: usize, r: usize) {
for x in 0..w {
let mut acc = [0u32; 4];
let mut n = 0u32;
for yy in 0..=r.min(h - 1) {
let i = (yy * w + x) * 4;
for c in 0..4 {
acc[c] += src[i + c] as u32;
}
n += 1;
}
for y in 0..h {
let o = (y * w + x) * 4;
for c in 0..4 {
dst[o + c] = (acc[c] / n) as u8;
}
let add = y + r + 1;
if add < h {
let i = (add * w + x) * 4;
for c in 0..4 {
acc[c] += src[i + c] as u32;
}
n += 1;
}
if y >= r {
let i = ((y - r) * w + x) * 4;
for c in 0..4 {
acc[c] -= src[i + c] as u32;
}
n -= 1;
}
}
}
}
fn sk_shader(
g: &crate::render::Gradient,
x: f32,
y: f32,
w: f32,
h: f32,
) -> Option<Shader<'static>> {
use crate::render::Gradient;
let sk_stops: Vec<SkStop> = g
.stops()
.iter()
.map(|s| SkStop::new(s.offset.clamp(0.0, 1.0), to_sk_color(s.color)))
.collect();
if sk_stops.len() < 2 {
return None;
}
match g {
Gradient::Linear { start, end, .. } => {
let p0 = SkPoint::from_xy(x + start.0 * w, y + start.1 * h);
let p1 = SkPoint::from_xy(x + end.0 * w, y + end.1 * h);
LinearGradient::new(p0, p1, sk_stops, SpreadMode::Pad, Transform::identity())
}
Gradient::Radial { center, radius, .. } => {
let c = SkPoint::from_xy(x + center.0 * w, y + center.1 * h);
let r = (radius * w.min(h)).max(0.01);
RadialGradient::new(
c,
0.0,
c,
r,
sk_stops,
SpreadMode::Pad,
Transform::identity(),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn px(pm: &Pixmap, x: u32, y: u32) -> (u8, u8, u8) {
let p = pm.pixel(x, y).unwrap();
(p.red(), p.green(), p.blue())
}
#[test]
fn thin_clip_rect_does_not_drop_fill() {
let mut pm = Pixmap::new(100, 100).unwrap();
pm.fill(tiny_skia::Color::WHITE);
{
let mut c = SkiaCanvas::new(&mut pm);
c.save();
c.clip_rect(Rect::new(10, 40, 80, 6)); c.fill_round_rect(
20.0,
40.0,
40.0,
6.0,
3.0,
&Paint::fill(Color::hex(0xFF0000)),
);
c.restore();
}
let (r, g, b) = px(&pm, 35, 43);
assert!(
r > 200 && g < 80 && b < 80,
"薄裁剪带内应被填充,实得 ({r},{g},{b})"
);
}
#[test]
fn draw_image_fills_dst_and_respects_bounds() {
let mut pm = Pixmap::new(100, 100).unwrap();
pm.fill(tiny_skia::Color::WHITE);
let red = {
let mut v = Vec::new();
for _ in 0..16 {
v.extend_from_slice(&[255, 0, 0, 255]);
}
v
};
let img = Image::from_rgba(4, 4, &red).unwrap();
{
let mut c = SkiaCanvas::new(&mut pm);
c.draw_image(&img, Rect::new(20, 20, 40, 40), Fit::Fill, 0.0, 1.0);
}
let (r, g, b) = px(&pm, 40, 40);
assert!(
r > 200 && g < 60 && b < 60,
"dst 内应被图片填充,实得 ({r},{g},{b})"
);
let (r2, g2, b2) = px(&pm, 5, 5);
assert!(
r2 > 240 && g2 > 240 && b2 > 240,
"dst 外不应被绘制,实得 ({r2},{g2},{b2})"
);
}
#[test]
fn draw_image_rounded_clips_corners() {
let mut pm = Pixmap::new(60, 60).unwrap();
pm.fill(tiny_skia::Color::WHITE);
let red = [255u8, 0, 0, 255].repeat(4 * 4);
let img = Image::from_rgba(4, 4, &red).unwrap();
{
let mut c = SkiaCanvas::new(&mut pm);
c.draw_image(&img, Rect::new(10, 10, 40, 40), Fit::Fill, 20.0, 1.0);
}
let (r, g, b) = px(&pm, 11, 11);
assert!(
r > 240 && g > 240 && b > 240,
"圆角应裁掉角落,实得 ({r},{g},{b})"
);
let (rc, gc, bc) = px(&pm, 30, 30);
assert!(
rc > 200 && gc < 60 && bc < 60,
"中心应为图片色,实得 ({rc},{gc},{bc})"
);
}
#[test]
fn draw_image_opacity_blends_lighter() {
let mut pm = Pixmap::new(40, 40).unwrap();
pm.fill(tiny_skia::Color::WHITE);
let red = [255u8, 0, 0, 255].repeat(4 * 4);
let img = Image::from_rgba(4, 4, &red).unwrap();
{
let mut c = SkiaCanvas::new(&mut pm);
c.draw_image(&img, Rect::new(5, 5, 30, 30), Fit::Fill, 0.0, 0.4);
}
let (r, g, b) = px(&pm, 20, 20);
assert!(r > 240, "红通道应仍高,实得 {r}");
assert!(g > 120 && b > 120, "低不透明应混入白底(g={g}, b={b})");
}
#[test]
fn offset_subpixmap_matches_full_region() {
let draw = |c: &mut SkiaCanvas| {
c.save();
c.clip_rect(Rect::new(20, 20, 70, 70));
c.fill_round_rect(
40.0,
40.0,
22.0,
18.0,
5.0,
&Paint::fill(Color::hex(0x3366CC)),
);
c.fill_circle(70.0, 70.0, 8.0, &Paint::fill(Color::hex(0xCC3333)));
c.restore();
};
let mut full = Pixmap::new(100, 100).unwrap();
full.fill(tiny_skia::Color::WHITE);
{
let mut c = SkiaCanvas::new(&mut full);
draw(&mut c);
}
let mut sub = Pixmap::new(40, 40).unwrap();
sub.fill(tiny_skia::Color::WHITE);
{
let mut eng = crate::text::NullTextEngine;
let mut c = SkiaCanvas::with_text_offset(&mut sub, &mut eng, 1.0, Point::new(30, 30));
draw(&mut c);
}
for y in 0..40u32 {
for x in 0..40u32 {
let f = full.pixel(30 + x, 30 + y).unwrap();
let s = sub.pixel(x, y).unwrap();
assert_eq!(
(f.red(), f.green(), f.blue(), f.alpha()),
(s.red(), s.green(), s.blue(), s.alpha()),
"局部重绘像素 ({x},{y}) 应与全窗一致"
);
}
}
}
#[test]
fn offset_subpixmap_exact_at_scale_1_5() {
let s = 1.5;
let draw = |c: &mut SkiaCanvas| {
c.fill_round_rect(
40.0,
41.0,
23.0,
17.0,
5.0,
&Paint::fill(Color::hex(0x3366CC)),
);
};
let mut full = Pixmap::new(180, 180).unwrap();
full.fill(tiny_skia::Color::WHITE);
{
let mut eng = crate::text::NullTextEngine;
let mut c = SkiaCanvas::with_text(&mut full, &mut eng, s);
draw(&mut c);
}
let mut sub = Pixmap::new(60, 60).unwrap();
sub.fill(tiny_skia::Color::WHITE);
{
let mut eng = crate::text::NullTextEngine;
let mut c = SkiaCanvas::with_text_offset(&mut sub, &mut eng, s, Point::new(12, 12));
draw(&mut c);
}
for y in 0..60u32 {
for x in 0..60u32 {
let f = full.pixel(18 + x, 18 + y).unwrap();
let g = sub.pixel(x, y).unwrap();
assert_eq!(
(f.red(), f.green(), f.blue(), f.alpha()),
(g.red(), g.green(), g.blue(), g.alpha()),
"1.5× 对齐 offset 下像素 ({x},{y}) 应逐像素一致"
);
}
}
}
#[test]
fn fill_round_rect_linear_gradient_left_to_right() {
let mut pm = Pixmap::new(100, 40).unwrap();
pm.fill(tiny_skia::Color::WHITE);
{
let mut c = SkiaCanvas::new(&mut pm);
let g = crate::render::Gradient::linear(
(0.0, 0.5),
(1.0, 0.5),
vec![(0.0, Color::hex(0x0000FF)), (1.0, Color::hex(0xFF0000))],
);
c.fill_round_rect(
0.0,
0.0,
100.0,
40.0,
8.0,
&crate::render::Paint::gradient(g),
);
}
let (lr, _lg, lb) = px(&pm, 6, 20);
let (rr, _rg, rb) = px(&pm, 93, 20);
assert!(lb > lr, "左缘应偏蓝(b>r),实得 r={lr} b={lb}");
assert!(rr > rb, "右缘应偏红(r>b),实得 r={rr} b={rb}");
}
#[test]
fn fill_rect_radial_gradient_center_to_edge() {
let mut pm = Pixmap::new(60, 60).unwrap();
pm.fill(tiny_skia::Color::BLACK);
{
let mut c = SkiaCanvas::new(&mut pm);
let g = crate::render::Gradient::radial(
(0.5, 0.5),
1.0,
vec![(0.0, Color::hex(0xFFFFFF)), (1.0, Color::hex(0x000000))],
);
c.fill_rect(0.0, 0.0, 60.0, 60.0, &crate::render::Paint::gradient(g));
}
let (cr, _, _) = px(&pm, 30, 30);
let (er, _, _) = px(&pm, 3, 3);
assert!(cr > er + 80, "圆心应明显比边角亮,实得 中心={cr} 边角={er}");
}
#[test]
fn push_pop_layer_composites_with_opacity() {
let mut pm = Pixmap::new(40, 40).unwrap();
pm.fill(tiny_skia::Color::WHITE);
{
let mut c = SkiaCanvas::new(&mut pm);
c.push_layer(0.5);
c.fill_rect(0.0, 0.0, 40.0, 40.0, &Paint::fill(Color::hex(0xFF0000)));
c.pop_layer();
}
let (r, g, b) = px(&pm, 20, 20);
assert!(r > 240, "红通道应高,实得 {r}");
assert!(
g > 100 && g < 200,
"绿应被白底抬到中段(50% 合成),实得 {g}"
);
assert!(b > 100 && b < 200, "蓝应被白底抬到中段,实得 {b}");
}
#[test]
fn draw_shadow_produces_soft_halo() {
let mut pm = Pixmap::new(120, 120).unwrap();
pm.fill(tiny_skia::Color::WHITE);
{
let mut c = SkiaCanvas::new(&mut pm);
c.draw_shadow(40.0, 40.0, 40.0, 40.0, 8.0, 10.0, Color::rgba(0, 0, 0, 180));
}
let (cr, _, _) = px(&pm, 60, 60);
assert!(cr < 120, "投影中心应变暗,实得 {cr}");
let (er, _, _) = px(&pm, 86, 60);
assert!(er > 130 && er < 252, "外缘应为柔化过渡,实得 {er}");
let (fr, _, _) = px(&pm, 4, 4);
assert!(fr > 250, "远角应保持白,实得 {fr}");
}
#[test]
fn thin_clip_rect_with_engine_and_offset() {
let mut pm = Pixmap::new(320, 280).unwrap();
pm.fill(tiny_skia::Color::WHITE);
let mut eng = crate::text::NullTextEngine;
{
let mut c = SkiaCanvas::with_text(&mut pm, &mut eng, 1.0);
c.save();
c.clip_rect(Rect::new(22, 42, 276, 6));
c.fill_round_rect(
28.37,
42.0,
96.6,
6.0,
3.0,
&Paint::fill(Color::hex(0x4C8BF5)),
);
c.restore();
}
let (r, g, b) = px(&pm, 60, 44);
assert!(
b > 180 && r < 140,
"进度滑块应在裁剪带内显现,实得 ({r},{g},{b})"
);
}
}