use std::f32::consts::PI;
use std::sync::OnceLock;
use tiny_skia::{
BlendMode as TsBlendMode, Color, FillRule, GradientStop, LineCap, LinearGradient, LineJoin,
Mask, Paint, Path, PathBuilder, Pixmap, Point, RadialGradient, Rect, Shader, SpreadMode,
Stroke, StrokeDash, Transform,
};
use uzor::render::{
BackdropBlur, BatchPainter, BlendMode as UzorBlendMode, CircleBatch, Effects,
GradientPainter, ImagePainter, LineSegment, Masking, Painter,
RenderContext as UzorRenderContext, RenderContextExt, ShapeHelpers,
TextAlign, TextBaseline, TextBounds, TextMetrics, TextRenderer, UiEffectHelpers,
};
use uzor::fonts;
static FONT_REGULAR: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_BOLD: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_ITALIC: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_BOLD_ITALIC: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_PT_ROOT_UI: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_JB_MONO_REGULAR: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_JB_MONO_BOLD: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_NERD_FONT: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_SYMBOLS: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_COLOR_EMOJI: OnceLock<fontdue::Font> = OnceLock::new();
static FONT_EMOJI: OnceLock<fontdue::Font> = OnceLock::new();
use uzor::fonts::FontFamily;
fn make_font(bytes: &[u8]) -> fontdue::Font {
fontdue::Font::from_bytes(bytes, fontdue::FontSettings::default())
.expect("embedded font bytes are valid")
}
fn get_font(family: FontFamily, bold: bool, italic: bool) -> &'static fontdue::Font {
match family {
FontFamily::PtRootUi => FONT_PT_ROOT_UI
.get_or_init(|| make_font(fonts::font_bytes(family, bold, italic))),
FontFamily::JetBrainsMono => {
let _ = italic; if bold {
FONT_JB_MONO_BOLD
.get_or_init(|| make_font(fonts::font_bytes(family, true, false)))
} else {
FONT_JB_MONO_REGULAR
.get_or_init(|| make_font(fonts::font_bytes(family, false, false)))
}
}
FontFamily::Roboto => match (bold, italic) {
(true, true ) => FONT_BOLD_ITALIC
.get_or_init(|| make_font(fonts::font_bytes(family, true, true))),
(true, false) => FONT_BOLD
.get_or_init(|| make_font(fonts::font_bytes(family, true, false))),
(false, true ) => FONT_ITALIC
.get_or_init(|| make_font(fonts::font_bytes(family, false, true))),
(false, false) => FONT_REGULAR
.get_or_init(|| make_font(fonts::font_bytes(family, false, false))),
},
}
}
fn get_nerd_font() -> &'static fontdue::Font {
FONT_NERD_FONT.get_or_init(|| make_font(fonts::SYMBOLS_NERD_FONT_MONO))
}
fn get_symbols_font() -> &'static fontdue::Font {
FONT_SYMBOLS.get_or_init(|| make_font(fonts::NOTO_SANS_SYMBOLS2))
}
fn get_color_emoji_font() -> &'static fontdue::Font {
FONT_COLOR_EMOJI.get_or_init(|| make_font(fonts::NOTO_COLOR_EMOJI))
}
fn get_emoji_font() -> &'static fontdue::Font {
FONT_EMOJI.get_or_init(|| make_font(fonts::NOTO_EMOJI))
}
fn parse_css_color(s: &str) -> Color {
let (r, g, b, a) = uzor::render::parse_color(s);
Color::from_rgba8(r, g, b, a)
}
fn with_alpha(color: Color, alpha: f32) -> Color {
let a = (color.alpha() * alpha).clamp(0.0, 1.0);
Color::from_rgba(color.red(), color.green(), color.blue(), a)
.unwrap_or(color)
}
type FontInfo = uzor::fonts::FontInfo;
fn parse_css_font(font_str: &str) -> FontInfo {
fonts::parse_css_font(font_str)
}
fn measure_text_width(text: &str, font_info: &FontInfo) -> f64 {
let font = get_font(font_info.family, font_info.bold, font_info.italic);
let mut width = 0.0f32;
for ch in text.chars() {
let (metrics, _) = font.rasterize(ch, font_info.size);
let advance = if metrics.width == 0 && !ch.is_whitespace() {
let (nf_metrics, _) = get_nerd_font().rasterize(ch, font_info.size);
if nf_metrics.width > 0 {
nf_metrics.advance_width
} else {
let (fb_metrics, _) = get_symbols_font().rasterize(ch, font_info.size);
if fb_metrics.width > 0 {
fb_metrics.advance_width
} else {
let (cv_metrics, _) = get_color_emoji_font().rasterize(ch, font_info.size);
if cv_metrics.width > 0 {
cv_metrics.advance_width
} else {
let (em_metrics, _) = get_emoji_font().rasterize(ch, font_info.size);
em_metrics.advance_width
}
}
}
} else {
metrics.advance_width
};
width += advance;
}
width as f64
}
fn arc_to_cubics(pb: &mut PathBuilder, cx: f32, cy: f32, r: f32, start: f32, end: f32, has_current_point: bool) {
let mut sweep = end - start;
if sweep == 0.0 { return; }
if sweep > 2.0 * PI { sweep = 2.0 * PI; }
if sweep < -2.0 * PI { sweep = -2.0 * PI; }
let n_segs = ((sweep.abs() / (PI / 2.0)).ceil() as u32).max(1);
let seg_angle = sweep / n_segs as f32;
let k = (4.0 / 3.0) * ((seg_angle / 2.0).abs().tan());
let mut a = start;
let start_x = cx + r * a.cos();
let start_y = cy + r * a.sin();
if has_current_point {
pb.line_to(start_x, start_y);
} else {
pb.move_to(start_x, start_y);
}
for _ in 0..n_segs {
let a1 = a + seg_angle;
let cos_a = a.cos();
let sin_a = a.sin();
let cos_a1 = a1.cos();
let sin_a1 = a1.sin();
let p0x = cx + r * cos_a;
let p0y = cy + r * sin_a;
let p3x = cx + r * cos_a1;
let p3y = cy + r * sin_a1;
let cp1x = p0x - k * r * sin_a;
let cp1y = p0y + k * r * cos_a;
let cp2x = p3x + k * r * sin_a1;
let cp2y = p3y - k * r * cos_a1;
pb.cubic_to(cp1x, cp1y, cp2x, cp2y, p3x, p3y);
a = a1;
}
}
fn ellipse_to_cubics(
pb: &mut PathBuilder,
cx: f32,
cy: f32,
rx: f32,
ry: f32,
start: f32,
end: f32,
has_current_point: bool,
) {
let mut sweep = end - start;
if sweep == 0.0 { return; }
if sweep > 2.0 * PI { sweep = 2.0 * PI; }
if sweep < -2.0 * PI { sweep = -2.0 * PI; }
let n_segs = ((sweep.abs() / (PI / 2.0)).ceil() as u32).max(1);
let seg_angle = sweep / n_segs as f32;
let k = (4.0 / 3.0) * ((seg_angle / 2.0).abs().tan());
let mut a = start;
let start_x = cx + rx * a.cos();
let start_y = cy + ry * a.sin();
if has_current_point {
pb.line_to(start_x, start_y);
} else {
pb.move_to(start_x, start_y);
}
for _ in 0..n_segs {
let a1 = a + seg_angle;
let cos_a = a.cos();
let sin_a = a.sin();
let cos_a1 = a1.cos();
let sin_a1 = a1.sin();
let p0x = cx + rx * cos_a;
let p0y = cy + ry * sin_a;
let p3x = cx + rx * cos_a1;
let p3y = cy + ry * sin_a1;
let cp1x = p0x - k * rx * sin_a;
let cp1y = p0y + k * ry * cos_a;
let cp2x = p3x + k * rx * sin_a1;
let cp2y = p3y - k * ry * cos_a1;
pb.cubic_to(cp1x, cp1y, cp2x, cp2y, p3x, p3y);
a = a1;
}
}
fn box_blur_pixmap(pm: &mut Pixmap, radius: u32) {
if radius == 0 {
return;
}
let w = pm.width() as usize;
let h = pm.height() as usize;
let r = radius as usize;
let klen = 2 * r + 1;
let data = pm.data_mut();
for row in 0..h {
for ch in 0..4usize {
let base = row * w;
let mut sum = 0u32;
for k in 0..klen {
let col = k.saturating_sub(r).min(w.saturating_sub(1));
sum += data[(base + col) * 4 + ch] as u32;
}
for col in 0..w {
let add_col = (col + r).min(w.saturating_sub(1));
let sub_col = col.saturating_sub(r + 1).min(w.saturating_sub(1));
if col > 0 {
sum = sum + data[(base + add_col) * 4 + ch] as u32
- data[(base + sub_col) * 4 + ch] as u32;
}
data[(base + col) * 4 + ch] = (sum / klen as u32) as u8;
}
}
}
for col in 0..w {
for ch in 0..4usize {
let mut sum = 0u32;
for k in 0..klen {
let row = k.saturating_sub(r).min(h.saturating_sub(1));
sum += data[(row * w + col) * 4 + ch] as u32;
}
for row in 0..h {
let add_row = (row + r).min(h.saturating_sub(1));
let sub_row = row.saturating_sub(r + 1).min(h.saturating_sub(1));
if row > 0 {
sum = sum + data[(add_row * w + col) * 4 + ch] as u32
- data[(sub_row * w + col) * 4 + ch] as u32;
}
data[(row * w + col) * 4 + ch] = (sum / klen as u32) as u8;
}
}
}
}
fn draw_pixmap_over(dst: &mut Pixmap, src: &Pixmap) {
let len = (dst.width() * dst.height()) as usize;
let dst_data = dst.data_mut();
let src_data = src.data();
for i in 0..len {
let base = i * 4;
let sa = src_data[base + 3] as u32;
if sa == 0 {
continue;
}
if sa == 255 {
dst_data[base] = src_data[base];
dst_data[base + 1] = src_data[base + 1];
dst_data[base + 2] = src_data[base + 2];
dst_data[base + 3] = src_data[base + 3];
} else {
let inv_sa = 255 - sa;
dst_data[base] = ((src_data[base] as u32 * sa + dst_data[base] as u32 * inv_sa) / 255) as u8;
dst_data[base + 1] = ((src_data[base + 1] as u32 * sa + dst_data[base + 1] as u32 * inv_sa) / 255) as u8;
dst_data[base + 2] = ((src_data[base + 2] as u32 * sa + dst_data[base + 2] as u32 * inv_sa) / 255) as u8;
dst_data[base + 3] = (sa + dst_data[base + 3] as u32 * inv_sa / 255) as u8;
}
}
}
fn copy_pixmap_region(src: &Pixmap, src_x: i32, src_y: i32, w: u32, h: u32) -> Option<Pixmap> {
let mut dst = Pixmap::new(w, h)?;
let sw = src.width() as i32;
let sh = src.height() as i32;
let src_data = src.data();
let dst_data = dst.data_mut();
for dy in 0..h as i32 {
for dx in 0..w as i32 {
let sx = src_x + dx;
let sy = src_y + dy;
let d_base = (dy as usize * w as usize + dx as usize) * 4;
if sx >= 0 && sy >= 0 && sx < sw && sy < sh {
let s_base = (sy as usize * sw as usize + sx as usize) * 4;
dst_data[d_base] = src_data[s_base];
dst_data[d_base + 1] = src_data[s_base + 1];
dst_data[d_base + 2] = src_data[s_base + 2];
dst_data[d_base + 3] = src_data[s_base + 3];
}
}
}
Some(dst)
}
fn blit_region(dst: &mut Pixmap, scratch: &Pixmap, dst_x: i32, dst_y: i32, w: u32, h: u32, margin: i32) {
let dw = dst.width() as i32;
let dh = dst.height() as i32;
let sw = scratch.width() as i32;
let src_data = scratch.data();
let dst_data = dst.data_mut();
for row in 0..h as i32 {
for col in 0..w as i32 {
let dx = dst_x + col;
let dy = dst_y + row;
if dx < 0 || dy < 0 || dx >= dw || dy >= dh { continue; }
let sx = margin + col;
let sy = margin + row;
if sx < 0 || sy < 0 || sx >= sw || sy >= scratch.height() as i32 { continue; }
let d_base = (dy as usize * dw as usize + dx as usize) * 4;
let s_base = (sy as usize * sw as usize + sx as usize) * 4;
dst_data[d_base] = src_data[s_base];
dst_data[d_base + 1] = src_data[s_base + 1];
dst_data[d_base + 2] = src_data[s_base + 2];
dst_data[d_base + 3] = src_data[s_base + 3];
}
}
}
#[derive(Clone)]
struct ShadowState {
dx: f32,
dy: f32,
blur: f32,
color: Color,
}
#[derive(Clone)]
struct SavedState {
fill_color: Color,
stroke_color: Color,
stroke_width: f32,
line_cap: LineCap,
line_join: LineJoin,
global_alpha: f32,
font_info: FontInfo,
text_align: TextAlign,
text_baseline: TextBaseline,
transform: Transform,
clip: Option<Mask>,
}
pub struct TinySkiaCpuRenderContext {
pixmap: Pixmap,
fill_color: Color,
stroke_color: Color,
stroke_width: f32,
line_cap: LineCap,
line_join: LineJoin,
line_dash: Option<StrokeDash>,
global_alpha: f32,
font_info: FontInfo,
text_align: TextAlign,
text_baseline: TextBaseline,
transform: Transform,
path_builder: Option<PathBuilder>,
path_has_point: bool,
state_stack: Vec<SavedState>,
current_clip: Option<Mask>,
shadow: Option<ShadowState>,
blend_mode: TsBlendMode,
dpr: f64,
blur_radius: f32,
}
impl TinySkiaCpuRenderContext {
pub fn new(width: u32, height: u32, dpr: f64) -> Self {
let pixmap = Pixmap::new(width.max(1), height.max(1))
.unwrap_or_else(|| Pixmap::new(1, 1).expect("1×1 pixmap always succeeds"));
Self {
pixmap,
fill_color: Color::BLACK,
stroke_color: Color::WHITE,
stroke_width: 1.0,
line_cap: LineCap::Butt,
line_join: LineJoin::Miter,
line_dash: None,
global_alpha: 1.0,
font_info: FontInfo::default(),
text_align: TextAlign::Left,
text_baseline: TextBaseline::Middle,
transform: Transform::identity(),
path_builder: None,
path_has_point: false,
state_stack: Vec::new(),
current_clip: None,
shadow: None,
blend_mode: TsBlendMode::SourceOver,
dpr,
blur_radius: 12.0,
}
}
pub fn resize(&mut self, width: u32, height: u32) {
if let Some(pm) = Pixmap::new(width.max(1), height.max(1)) {
self.pixmap = pm;
self.current_clip = None;
}
}
pub fn set_backdrop_blur_radius(&mut self, radius: f32) {
self.blur_radius = radius.max(1.0);
}
pub fn clear(&mut self, color: Color) {
self.pixmap.fill(color);
}
pub fn pixels(&self) -> &[u8] {
self.pixmap.data()
}
pub fn pixmap(&self) -> &Pixmap {
&self.pixmap
}
pub fn width(&self) -> u32 {
self.pixmap.width()
}
pub fn height(&self) -> u32 {
self.pixmap.height()
}
fn effective_fill_color(&self) -> Color {
if self.global_alpha < 1.0 {
with_alpha(self.fill_color, self.global_alpha)
} else {
self.fill_color
}
}
fn effective_stroke_color(&self) -> Color {
if self.global_alpha < 1.0 {
with_alpha(self.stroke_color, self.global_alpha)
} else {
self.stroke_color
}
}
fn fill_paint(&self) -> Paint<'static> {
Paint {
shader: Shader::SolidColor(self.effective_fill_color()),
blend_mode: self.blend_mode,
anti_alias: true,
..Paint::default()
}
}
fn stroke_paint(&self) -> Paint<'static> {
Paint {
shader: Shader::SolidColor(self.effective_stroke_color()),
blend_mode: self.blend_mode,
anti_alias: true,
..Paint::default()
}
}
fn shadow_paint(color: Color) -> Paint<'static> {
Paint {
shader: Shader::SolidColor(color),
blend_mode: TsBlendMode::SourceOver,
anti_alias: true,
..Paint::default()
}
}
fn draw_shadow_for_path(&mut self, path: &Path) {
let Some(ref sh) = self.shadow.clone() else { return };
let w = self.pixmap.width();
let h = self.pixmap.height();
let Some(mut shadow_pm) = Pixmap::new(w, h) else { return };
let shadow_transform = self.transform.pre_translate(sh.dx, sh.dy);
shadow_pm.fill_path(
path,
&Self::shadow_paint(sh.color),
FillRule::Winding,
shadow_transform,
None,
);
let radius = (sh.blur.round() as u32).clamp(1, 32);
box_blur_pixmap(&mut shadow_pm, radius);
let shadow_data = shadow_pm.data().to_vec();
if let Some(shadow_src) = Pixmap::from_vec(shadow_data, tiny_skia::IntSize::from_wh(w, h).expect("valid size")) {
draw_pixmap_over(&mut self.pixmap, &shadow_src);
}
}
fn draw_shadow_for_rect(&mut self, rect: Rect) {
let Some(ref sh) = self.shadow.clone() else { return };
let w = self.pixmap.width();
let h = self.pixmap.height();
let Some(mut shadow_pm) = Pixmap::new(w, h) else { return };
let shadow_transform = self.transform.pre_translate(sh.dx, sh.dy);
shadow_pm.fill_rect(rect, &Self::shadow_paint(sh.color), shadow_transform, None);
let radius = (sh.blur.round() as u32).clamp(1, 32);
box_blur_pixmap(&mut shadow_pm, radius);
let shadow_data = shadow_pm.data().to_vec();
if let Some(shadow_src) = Pixmap::from_vec(shadow_data, tiny_skia::IntSize::from_wh(w, h).expect("valid size")) {
draw_pixmap_over(&mut self.pixmap, &shadow_src);
}
}
fn current_stroke(&self) -> Stroke {
let mut s = Stroke {
width: self.stroke_width,
line_cap: self.line_cap,
line_join: self.line_join,
..Stroke::default()
};
if let Some(ref dash) = self.line_dash {
s.dash = Some(dash.clone());
}
s
}
fn take_path(&mut self) -> Option<Path> {
self.path_has_point = false;
self.path_builder.take()?.finish()
}
fn builder(&mut self) -> &mut PathBuilder {
self.path_builder.get_or_insert_with(PathBuilder::new)
}
fn composite_glyph_pixel(dst: &mut [u8], cr: u8, cg: u8, cb: u8, ca: u8, coverage: u8) {
let alpha = (ca as u32 * coverage as u32 + 127) / 255;
if alpha == 0 { return; }
let inv = 255 - alpha;
dst[0] = ((cr as u32 * alpha + dst[0] as u32 * inv + 127) / 255) as u8;
dst[1] = ((cg as u32 * alpha + dst[1] as u32 * inv + 127) / 255) as u8;
dst[2] = ((cb as u32 * alpha + dst[2] as u32 * inv + 127) / 255) as u8;
dst[3] = (alpha + dst[3] as u32 * inv / 255).min(255) as u8;
}
}
impl Painter for TinySkiaCpuRenderContext {
fn save(&mut self) {
self.state_stack.push(SavedState {
fill_color: self.fill_color,
stroke_color: self.stroke_color,
stroke_width: self.stroke_width,
line_cap: self.line_cap,
line_join: self.line_join,
global_alpha: self.global_alpha,
font_info: self.font_info.clone(),
text_align: self.text_align,
text_baseline: self.text_baseline,
transform: self.transform,
clip: self.current_clip.clone(),
});
}
fn restore(&mut self) {
if let Some(s) = self.state_stack.pop() {
self.fill_color = s.fill_color;
self.stroke_color = s.stroke_color;
self.stroke_width = s.stroke_width;
self.line_cap = s.line_cap;
self.line_join = s.line_join;
self.global_alpha = s.global_alpha;
self.font_info = s.font_info;
self.text_align = s.text_align;
self.text_baseline = s.text_baseline;
self.transform = s.transform;
self.current_clip = s.clip;
}
}
fn translate(&mut self, x: f64, y: f64) {
self.transform = self.transform.pre_translate(x as f32, y as f32);
}
fn rotate(&mut self, angle: f64) {
self.transform = self.transform.pre_rotate(angle.to_degrees() as f32);
}
fn scale(&mut self, x: f64, y: f64) {
self.transform = self.transform.pre_scale(x as f32, y as f32);
}
fn set_fill_color(&mut self, color: &str) {
self.fill_color = parse_css_color(color);
}
fn set_global_alpha(&mut self, alpha: f64) {
self.global_alpha = (alpha as f32).clamp(0.0, 1.0);
}
fn set_fill_color_alpha(&mut self, color: &str, alpha: f64) {
let base = parse_css_color(color);
let a = (alpha as f32).clamp(0.0, 1.0);
self.fill_color = with_alpha(base, a);
}
fn set_stroke_color(&mut self, color: &str) {
self.stroke_color = parse_css_color(color);
}
fn set_stroke_width(&mut self, width: f64) {
self.stroke_width = width as f32;
}
fn set_line_dash(&mut self, pattern: &[f64]) {
if pattern.is_empty() {
self.line_dash = None;
} else {
let intervals: Vec<f32> = pattern.iter().map(|&v| v as f32).collect();
self.line_dash = StrokeDash::new(intervals, 0.0);
}
}
fn set_line_cap(&mut self, cap: &str) {
self.line_cap = match cap {
"round" => LineCap::Round,
"square" => LineCap::Square,
_ => LineCap::Butt,
};
}
fn set_line_join(&mut self, join: &str) {
self.line_join = match join {
"round" => LineJoin::Round,
"bevel" => LineJoin::Bevel,
_ => LineJoin::Miter,
};
}
fn begin_path(&mut self) {
self.path_builder = Some(PathBuilder::new());
self.path_has_point = false;
}
fn move_to(&mut self, x: f64, y: f64) {
self.builder().move_to(x as f32, y as f32);
self.path_has_point = true;
}
fn line_to(&mut self, x: f64, y: f64) {
self.builder().line_to(x as f32, y as f32);
self.path_has_point = true;
}
fn close_path(&mut self) {
self.builder().close();
}
fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
if let Some(r) = Rect::from_xywh(x as f32, y as f32, w as f32, h as f32) {
self.builder().push_rect(r);
}
}
fn arc(&mut self, cx: f64, cy: f64, radius: f64, start_angle: f64, end_angle: f64) {
let sweep = (end_angle - start_angle).abs();
if sweep >= std::f64::consts::TAU - 0.001 {
let r = radius as f32;
if let Some(rect) = Rect::from_xywh(
(cx - radius) as f32,
(cy - radius) as f32,
r * 2.0,
r * 2.0,
) {
self.builder().push_oval(rect);
self.path_has_point = true;
return;
}
}
let has_point = self.path_has_point;
arc_to_cubics(
self.builder(),
cx as f32, cy as f32, radius as f32,
start_angle as f32, end_angle as f32,
has_point,
);
self.path_has_point = true;
}
fn ellipse(
&mut self,
cx: f64,
cy: f64,
rx: f64,
ry: f64,
_rotation: f64,
start: f64,
end: f64,
) {
let sweep = (end - start).abs();
if sweep >= std::f64::consts::TAU - 0.001 {
if let Some(rect) = Rect::from_xywh(
(cx - rx) as f32,
(cy - ry) as f32,
(rx * 2.0) as f32,
(ry * 2.0) as f32,
) {
self.builder().push_oval(rect);
self.path_has_point = true;
return;
}
}
let has_point = self.path_has_point;
ellipse_to_cubics(
self.builder(),
cx as f32, cy as f32, rx as f32, ry as f32,
start as f32, end as f32,
has_point,
);
self.path_has_point = true;
}
fn quadratic_curve_to(&mut self, cpx: f64, cpy: f64, x: f64, y: f64) {
self.builder().quad_to(cpx as f32, cpy as f32, x as f32, y as f32);
}
fn bezier_curve_to(
&mut self,
cp1x: f64, cp1y: f64,
cp2x: f64, cp2y: f64,
x: f64, y: f64,
) {
self.builder().cubic_to(
cp1x as f32, cp1y as f32,
cp2x as f32, cp2y as f32,
x as f32, y as f32,
);
}
fn stroke(&mut self) {
let Some(path) = self.take_path() else { return };
if self.shadow.is_some() {
self.draw_shadow_for_path(&path);
}
let paint = self.stroke_paint();
let stroke = self.current_stroke();
let transform = self.transform;
let clip = self.current_clip.clone();
self.pixmap.stroke_path(&path, &paint, &stroke, transform, clip.as_ref());
}
fn fill(&mut self) {
let Some(path) = self.take_path() else { return };
if self.shadow.is_some() {
self.draw_shadow_for_path(&path);
}
let paint = self.fill_paint();
let transform = self.transform;
let clip = self.current_clip.clone();
self.pixmap.fill_path(&path, &paint, FillRule::Winding, transform, clip.as_ref());
}
}
impl TextRenderer for TinySkiaCpuRenderContext {
fn set_font(&mut self, font: &str) {
self.font_info = parse_css_font(font);
}
fn set_text_align(&mut self, align: TextAlign) {
self.text_align = align;
}
fn set_text_baseline(&mut self, baseline: TextBaseline) {
self.text_baseline = baseline;
}
fn fill_text(&mut self, text: &str, x: f64, y: f64) {
if text.is_empty() { return; }
let font_info = self.font_info.clone();
let font = get_font(font_info.family, font_info.bold, font_info.italic);
let px = font_info.size;
let total_w = measure_text_width(text, &font_info) as f32;
let x_off = match self.text_align {
TextAlign::Center => -(total_w / 2.0),
TextAlign::Right => -total_w,
TextAlign::Left => 0.0,
};
let ascent = font.horizontal_line_metrics(px)
.map(|m| m.ascent)
.unwrap_or(px * 0.75);
let y_off = match self.text_baseline {
TextBaseline::Top => ascent,
TextBaseline::Middle => ascent / 2.0,
TextBaseline::Bottom => 0.0,
TextBaseline::Alphabetic => 0.0,
};
let color = self.effective_fill_color();
let cr = (color.red() * 255.0) as u8;
let cg = (color.green() * 255.0) as u8;
let cb = (color.blue() * 255.0) as u8;
let ca = (color.alpha() * 255.0) as u8;
let pw = self.pixmap.width() as i32;
let ph = self.pixmap.height() as i32;
let stride = self.pixmap.width() as usize;
let tx = self.transform.tx;
let ty = self.transform.ty;
let sx = self.transform.sx;
let sy = self.transform.sy;
let mut pen_x = (x as f32 + x_off) * sx + tx;
let pen_y = (y as f32 + y_off) * sy + ty;
for ch in text.chars() {
let render_px = px * sx.max(sy).max(1.0);
let (primary_metrics, primary_bitmap) = font.rasterize(ch, render_px);
let (metrics, bitmap) = if primary_metrics.width == 0 && !ch.is_whitespace() {
let (nf_metrics, nf_bitmap) = get_nerd_font().rasterize(ch, render_px);
if nf_metrics.width > 0 {
(nf_metrics, nf_bitmap)
} else {
let (sym_metrics, sym_bitmap) = get_symbols_font().rasterize(ch, render_px);
if sym_metrics.width > 0 {
(sym_metrics, sym_bitmap)
} else {
let (cv_metrics, cv_bitmap) = get_color_emoji_font().rasterize(ch, render_px);
if cv_metrics.width > 0 {
(cv_metrics, cv_bitmap)
} else {
let (em_metrics, em_bitmap) = get_emoji_font().rasterize(ch, render_px);
if em_metrics.width > 0 {
(em_metrics, em_bitmap)
} else {
(primary_metrics, primary_bitmap)
}
}
}
}
} else {
(primary_metrics, primary_bitmap)
};
let gw = metrics.width as i32;
let gh = metrics.height as i32;
let gx0 = (pen_x + metrics.xmin as f32).round() as i32;
let gy0 = (pen_y - metrics.ymin as f32 - gh as f32).round() as i32;
for row in 0..gh {
let py_coord = gy0 + row;
if py_coord < 0 || py_coord >= ph { continue; }
for col in 0..gw {
let px_coord = gx0 + col;
if px_coord < 0 || px_coord >= pw { continue; }
let coverage = bitmap[(row * gw + col) as usize];
if coverage == 0 { continue; }
let dst_idx = py_coord as usize * stride + px_coord as usize;
let dst_off = dst_idx * 4;
let data = self.pixmap.data_mut();
let dst = &mut data[dst_off..dst_off + 4];
Self::composite_glyph_pixel(dst, cr, cg, cb, ca, coverage);
}
}
pen_x += metrics.advance_width * sx;
}
}
fn stroke_text(&mut self, text: &str, x: f64, y: f64) {
let saved_fill = self.fill_color;
self.fill_color = self.stroke_color;
self.fill_text(text, x, y);
self.fill_color = saved_fill;
}
}
impl TextMetrics for TinySkiaCpuRenderContext {
fn measure_text(&self, text: &str) -> f64 {
measure_text_width(text, &self.font_info)
}
fn text_bounds(&self, text: &str, font: &str) -> TextBounds {
let font_info = parse_css_font(font);
let px = font_info.size;
let w = measure_text_width(text, &font_info);
let fontdue_font = get_font(font_info.family, font_info.bold, font_info.italic);
let (ascent, descent) = fontdue_font
.horizontal_line_metrics(px)
.map(|m| (m.ascent as f64, (-m.descent) as f64))
.unwrap_or_else(|| (px as f64 * 0.9, px as f64 * 0.3));
let h = ascent + descent;
TextBounds {
x: 0.0,
y: -ascent,
w,
h,
ascent,
descent,
}
}
fn measure_text_glyphs(&self, text: &str, font: &str) -> Vec<uzor::render::GlyphMetric> {
uzor::shaper::measure_glyphs(text, font)
}
fn text_to_path(&self, text: &str, font: &str) -> String {
uzor::shaper::text_to_path(text, font)
}
}
impl Masking for TinySkiaCpuRenderContext {
fn clip(&mut self) {
let Some(path) = self.take_path() else { return };
let w = self.pixmap.width();
let h = self.pixmap.height();
if let Some(mut mask) = Mask::new(w, h) {
mask.fill_path(&path, FillRule::Winding, true, self.transform);
self.current_clip = Some(mask);
}
}
fn push_clip_svg_path_even_odd(&mut self, d: &str) {
uzor::render::emit_svg_path(self, d);
let Some(path) = self.take_path() else { return };
let w = self.pixmap.width();
let h = self.pixmap.height();
if let Some(mut mask) = Mask::new(w, h) {
mask.fill_path(&path, FillRule::EvenOdd, true, self.transform);
self.save();
self.current_clip = Some(mask);
}
}
}
impl Effects for TinySkiaCpuRenderContext {
fn set_shadow(&mut self, dx: f64, dy: f64, blur: f64, color: &str) {
let parsed = parse_css_color(color);
let color = if self.global_alpha < 1.0 {
with_alpha(parsed, self.global_alpha)
} else {
parsed
};
self.shadow = Some(ShadowState {
dx: dx as f32,
dy: dy as f32,
blur: blur.max(0.0) as f32,
color,
});
}
fn clear_shadow(&mut self) {
self.shadow = None;
}
fn set_blend_mode(&mut self, mode: UzorBlendMode) {
self.blend_mode = match mode {
UzorBlendMode::Normal => TsBlendMode::SourceOver,
UzorBlendMode::Multiply => TsBlendMode::Multiply,
UzorBlendMode::Screen => TsBlendMode::Screen,
UzorBlendMode::Overlay => TsBlendMode::Overlay,
UzorBlendMode::Darken => TsBlendMode::Darken,
UzorBlendMode::Lighten => TsBlendMode::Lighten,
UzorBlendMode::ColorDodge => TsBlendMode::ColorDodge,
UzorBlendMode::ColorBurn => TsBlendMode::ColorBurn,
UzorBlendMode::HardLight => TsBlendMode::HardLight,
UzorBlendMode::SoftLight => TsBlendMode::SoftLight,
UzorBlendMode::Difference => TsBlendMode::Difference,
UzorBlendMode::Exclusion => TsBlendMode::Exclusion,
UzorBlendMode::Plus => TsBlendMode::Plus,
};
}
}
impl ShapeHelpers for TinySkiaCpuRenderContext {
fn fill_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
let Some(rect) = Rect::from_xywh(x as f32, y as f32, w as f32, h as f32) else {
return;
};
if self.shadow.is_some() {
self.draw_shadow_for_rect(rect);
}
let paint = self.fill_paint();
let transform = self.transform;
let clip = self.current_clip.clone();
self.pixmap.fill_rect(rect, &paint, transform, clip.as_ref());
}
fn stroke_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
let Some(rect) = Rect::from_xywh(x as f32, y as f32, w as f32, h as f32) else {
return;
};
let mut pb = PathBuilder::new();
pb.push_rect(rect);
let Some(path) = pb.finish() else { return };
let paint = self.stroke_paint();
let stroke = self.current_stroke();
let transform = self.transform;
let clip = self.current_clip.clone();
self.pixmap.stroke_path(&path, &paint, &stroke, transform, clip.as_ref());
}
fn rounded_rect_corners(
&mut self,
x: f64,
y: f64,
w: f64,
h: f64,
tl: f64,
tr: f64,
br: f64,
bl: f64,
) {
let max_r = (w / 2.0).min(h / 2.0).max(0.0);
let tl = (tl as f32).min(max_r as f32).max(0.0);
let tr = (tr as f32).min(max_r as f32).max(0.0);
let br = (br as f32).min(max_r as f32).max(0.0);
let bl = (bl as f32).min(max_r as f32).max(0.0);
let x = x as f32;
let y = y as f32;
let w = w as f32;
let h = h as f32;
let pb = self.builder();
pb.move_to(x + tl, y);
pb.line_to(x + w - tr, y);
if tr > 0.0 {
arc_to_cubics(pb, x + w - tr, y + tr, tr, -PI / 2.0, 0.0, true);
}
pb.line_to(x + w, y + h - br);
if br > 0.0 {
arc_to_cubics(pb, x + w - br, y + h - br, br, 0.0, PI / 2.0, true);
}
pb.line_to(x + bl, y + h);
if bl > 0.0 {
arc_to_cubics(pb, x + bl, y + h - bl, bl, PI / 2.0, PI, true);
}
pb.line_to(x, y + tl);
if tl > 0.0 {
arc_to_cubics(pb, x + tl, y + tl, tl, PI, PI * 1.5, true);
}
pb.close();
self.path_has_point = true;
}
}
impl BatchPainter for TinySkiaCpuRenderContext {
fn draw_line_batch(&mut self, lines: &[LineSegment], color: &str, width: f64) {
if lines.is_empty() {
return;
}
self.set_stroke_color(color);
self.set_stroke_width(width);
let mut pb = PathBuilder::new();
for l in lines {
pb.move_to(l.x1 as f32, l.y1 as f32);
pb.line_to(l.x2 as f32, l.y2 as f32);
}
let Some(path) = pb.finish() else { return };
if self.shadow.is_some() {
self.draw_shadow_for_path(&path);
}
let paint = self.stroke_paint();
let stroke = self.current_stroke();
let transform = self.transform;
let clip = self.current_clip.clone();
self.pixmap.stroke_path(&path, &paint, &stroke, transform, clip.as_ref());
}
fn draw_circle_batch(&mut self, circles: &[CircleBatch], color: &str) {
if circles.is_empty() {
return;
}
self.set_fill_color(color);
let mut pb = PathBuilder::new();
for c in circles {
let r = c.r as f32;
if let Some(rect) = Rect::from_xywh(
(c.cx - c.r) as f32,
(c.cy - c.r) as f32,
r * 2.0,
r * 2.0,
) {
pb.push_oval(rect);
}
}
let Some(path) = pb.finish() else { return };
if self.shadow.is_some() {
self.draw_shadow_for_path(&path);
}
let paint = self.fill_paint();
let transform = self.transform;
let clip = self.current_clip.clone();
self.pixmap.fill_path(&path, &paint, FillRule::Winding, transform, clip.as_ref());
}
fn stroke_polyline(&mut self, pts: &[(f64, f64)], color: &str, width: f64) {
if pts.is_empty() {
return;
}
self.set_stroke_color(color);
self.set_stroke_width(width);
let mut pb = PathBuilder::new();
pb.move_to(pts[0].0 as f32, pts[0].1 as f32);
for &(x, y) in &pts[1..] {
pb.line_to(x as f32, y as f32);
}
let Some(path) = pb.finish() else { return };
if self.shadow.is_some() {
self.draw_shadow_for_path(&path);
}
let paint = self.stroke_paint();
let stroke = self.current_stroke();
let transform = self.transform;
let clip = self.current_clip.clone();
self.pixmap.stroke_path(&path, &paint, &stroke, transform, clip.as_ref());
}
}
impl GradientPainter for TinySkiaCpuRenderContext {
fn fill_linear_gradient(
&mut self,
stops: &[(f32, &str)],
x1: f64,
y1: f64,
x2: f64,
y2: f64,
) {
let Some(path) = self.take_path() else { return };
if stops.is_empty() {
return;
}
let gradient_stops: Vec<GradientStop> = stops
.iter()
.map(|&(pos, color_str)| {
let color = parse_css_color(color_str);
let color = if self.global_alpha < 1.0 {
with_alpha(color, self.global_alpha)
} else {
color
};
GradientStop::new(pos.clamp(0.0, 1.0), color)
})
.collect();
let start = Point::from_xy(x1 as f32, y1 as f32);
let end = Point::from_xy(x2 as f32, y2 as f32);
let shader = LinearGradient::new(
start,
end,
gradient_stops,
SpreadMode::Pad,
Transform::identity(),
)
.unwrap_or_else(|| {
let color = parse_css_color(stops[0].1);
let color = if self.global_alpha < 1.0 {
with_alpha(color, self.global_alpha)
} else {
color
};
Shader::SolidColor(color)
});
let paint = Paint {
shader,
anti_alias: true,
..Paint::default()
};
let transform = self.transform;
let clip = self.current_clip.clone();
self.pixmap.fill_path(&path, &paint, FillRule::Winding, transform, clip.as_ref());
}
fn fill_radial_gradient(
&mut self,
cx: f64,
cy: f64,
r: f64,
stops: &[(f32, &str)],
x: f64,
y: f64,
w: f64,
h: f64,
) {
let _ = (x, y, w, h);
let Some(path) = self.take_path() else { return };
if stops.is_empty() {
return;
}
let gradient_stops: Vec<GradientStop> = stops
.iter()
.map(|&(pos, color_str)| {
let color = parse_css_color(color_str);
let color = if self.global_alpha < 1.0 {
with_alpha(color, self.global_alpha)
} else {
color
};
GradientStop::new(pos.clamp(0.0, 1.0), color)
})
.collect();
let center = Point::from_xy(cx as f32, cy as f32);
let shader = RadialGradient::new(
center,
center,
r as f32,
gradient_stops,
SpreadMode::Pad,
Transform::identity(),
)
.unwrap_or_else(|| {
let color = parse_css_color(stops[0].1);
let color = if self.global_alpha < 1.0 {
with_alpha(color, self.global_alpha)
} else {
color
};
Shader::SolidColor(color)
});
let paint = Paint {
shader,
anti_alias: true,
..Paint::default()
};
let transform = self.transform;
let clip = self.current_clip.clone();
self.pixmap.fill_path(&path, &paint, FillRule::Winding, transform, clip.as_ref());
}
}
impl UiEffectHelpers for TinySkiaCpuRenderContext {
fn has_blur_background(&self) -> bool {
true
}
fn draw_blur_background(&mut self, x: f64, y: f64, width: f64, height: f64) {
let radius = self.blur_radius;
if radius < 1.0 || width < 1.0 || height < 1.0 {
return;
}
let margin = (radius * 3.0).round() as i32;
let w = width.round() as u32;
let h = height.round() as u32;
let sw = w.saturating_add((2 * margin as u32).min(u32::MAX - w));
let sh = h.saturating_add((2 * margin as u32).min(u32::MAX - h));
if sw == 0 || sh == 0 { return; }
let src_x = x.round() as i32 - margin;
let src_y = y.round() as i32 - margin;
let mut scratch = match copy_pixmap_region(&self.pixmap, src_x, src_y, sw, sh) {
Some(p) => p,
None => return,
};
let r = (radius.round() as u32).clamp(1, 128);
for _ in 0..3 {
box_blur_pixmap(&mut scratch, r);
}
let dst_x = x.round() as i32;
let dst_y = y.round() as i32;
blit_region(&mut self.pixmap, &scratch, dst_x, dst_y, w, h, margin);
}
}
impl BackdropBlur for TinySkiaCpuRenderContext {
fn draw_blur_background(&mut self, x: f64, y: f64, width: f64, height: f64) {
UiEffectHelpers::draw_blur_background(self, x, y, width, height);
}
fn has_blur_background(&self) -> bool {
true
}
fn use_convex_glass_buttons(&self) -> bool {
false
}
fn draw_glass_button_3d(
&mut self,
x: f64,
y: f64,
width: f64,
height: f64,
radius: f64,
_is_active: bool,
color: &str,
) {
UiEffectHelpers::draw_blur_background(self, x, y, width, height);
self.set_fill_color(color);
self.fill_rounded_rect(x, y, width, height, radius);
}
}
impl ImagePainter for TinySkiaCpuRenderContext {
fn draw_image(
&mut self,
_image_id: &str,
_x: f64,
_y: f64,
_width: f64,
_height: f64,
) -> bool {
false
}
fn draw_image_rgba(
&mut self,
_data: &[u8],
_img_width: u32,
_img_height: u32,
_x: f64,
_y: f64,
_width: f64,
_height: f64,
) {
}
}
impl UzorRenderContext for TinySkiaCpuRenderContext {
fn dpr(&self) -> f64 {
self.dpr
}
}
impl RenderContextExt for TinySkiaCpuRenderContext {
type BlurImage = ();
fn set_blur_image(&mut self, _image: Option<()>, _width: u32, _height: u32) {
}
fn set_use_convex_glass_buttons(&mut self, _use_convex: bool) {
}
}