use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use cosmic_text::{Attrs, Family, Metrics, Shaping, Style, Weight};
use tiny_skia::{
FillRule, GradientStop, Paint, PathBuilder, Rect, Shader, SpreadMode, Stroke, StrokeDash,
Transform,
};
use crate::{
BackgroundLayer, BorderRadiuses, BorderStyle, Borders, Color, ColorPoint, ConicGradient,
DocumentContainer, DrawContext, FontDescription, FontHandle, FontMetrics, FontStyle,
LinearGradient, ListMarker, ListStyleType, MediaFeatures, MediaType, Position, RadialGradient,
Size, TextTransform,
};
struct FontData {
family: String,
size: f32,
weight: Weight,
style: Style,
metrics: FontMetrics,
}
pub struct PixbufContainer {
pixmap: tiny_skia::Pixmap,
font_system: Rc<RefCell<cosmic_text::FontSystem>>,
swash_cache: RefCell<cosmic_text::SwashCache>,
fonts: Rc<RefCell<HashMap<usize, FontData>>>,
next_font_id: usize,
clip_stack: Vec<(Position, BorderRadiuses)>,
cached_clip_mask: Option<tiny_skia::Mask>,
clip_mask_dirty: bool,
images: HashMap<String, tiny_skia::Pixmap>,
pending_images: Vec<(String, bool)>,
requested_images: std::collections::HashSet<String>,
viewport: Position,
base_url: String,
caption: String,
scale_factor: f32,
ignore_overflow_clips: bool,
last_anchor_click: Option<String>,
current_cursor: String,
}
impl PixbufContainer {
pub fn new(width: u32, height: u32) -> Self {
Self::new_with_scale(width, height, 1.0)
}
pub fn new_with_scale(width: u32, height: u32, scale_factor: f32) -> Self {
let phys_w = ((width as f32) * scale_factor).ceil() as u32;
let phys_h = ((height as f32) * scale_factor).ceil() as u32;
let pixmap =
tiny_skia::Pixmap::new(phys_w.max(1), phys_h.max(1)).expect("failed to create pixmap");
Self {
pixmap,
font_system: Rc::new(RefCell::new(cosmic_text::FontSystem::new())),
swash_cache: RefCell::new(cosmic_text::SwashCache::new()),
fonts: Rc::new(RefCell::new(HashMap::new())),
next_font_id: 1,
clip_stack: Vec::new(),
cached_clip_mask: None,
clip_mask_dirty: false,
images: HashMap::new(),
pending_images: Vec::new(),
requested_images: std::collections::HashSet::new(),
viewport: Position {
x: 0.0,
y: 0.0,
width: width as f32,
height: height as f32,
},
base_url: String::new(),
caption: String::new(),
scale_factor,
ignore_overflow_clips: false,
last_anchor_click: None,
current_cursor: String::new(),
}
}
pub fn pixels(&self) -> &[u8] {
self.pixmap.data()
}
pub fn width(&self) -> u32 {
self.pixmap.width()
}
pub fn height(&self) -> u32 {
self.pixmap.height()
}
pub fn load_image_data(&mut self, url: &str, data: &[u8]) {
let Ok(img) = image::load_from_memory(data) else {
return;
};
let rgba = img.to_rgba8();
let (w, h) = (rgba.width(), rgba.height());
let mut premul = rgba.into_raw();
for chunk in premul.chunks_exact_mut(4) {
let a = chunk[3] as u32;
chunk[0] = ((chunk[0] as u32 * a + 127) / 255) as u8;
chunk[1] = ((chunk[1] as u32 * a + 127) / 255) as u8;
chunk[2] = ((chunk[2] as u32 * a + 127) / 255) as u8;
}
if let Some(pm) = tiny_skia::Pixmap::from_vec(
premul,
tiny_skia::IntSize::from_wh(w, h).expect("invalid image size"),
) {
self.images.insert(url.to_string(), pm);
}
}
pub fn draw_selection_rects(&mut self, rects: &[crate::Position]) {
self.ensure_clip_mask();
let highlight = tiny_skia::Color::from_rgba8(100, 150, 255, 80);
let paint = Paint {
shader: Shader::SolidColor(highlight),
anti_alias: false,
..Paint::default()
};
let transform = Transform::from_scale(self.scale_factor, self.scale_factor);
for rect in rects {
if let Some(r) = Rect::from_xywh(rect.x, rect.y, rect.width, rect.height) {
self.pixmap
.fill_rect(r, &paint, transform, self.cached_clip_mask.as_ref());
}
}
}
pub fn scale_factor(&self) -> f32 {
self.scale_factor
}
pub fn set_ignore_overflow_clips(&mut self, ignore: bool) {
self.ignore_overflow_clips = ignore;
}
pub fn take_anchor_click(&mut self) -> Option<String> {
self.last_anchor_click.take()
}
pub fn take_pending_images(&mut self) -> Vec<(String, bool)> {
std::mem::take(&mut self.pending_images)
}
pub fn clear_pending_images(&mut self) {
self.pending_images.clear();
self.requested_images.clear();
}
pub fn cursor(&self) -> &str {
&self.current_cursor
}
pub fn resize(&mut self, width: u32, height: u32) {
self.resize_with_scale(width, height, self.scale_factor);
}
pub fn resize_with_scale(&mut self, width: u32, height: u32, scale_factor: f32) {
self.scale_factor = scale_factor;
let phys_w = ((width as f32) * scale_factor).ceil() as u32;
let phys_h = ((height as f32) * scale_factor).ceil() as u32;
self.pixmap =
tiny_skia::Pixmap::new(phys_w.max(1), phys_h.max(1)).expect("failed to create pixmap");
self.viewport.width = width as f32;
self.viewport.height = height as f32;
self.cached_clip_mask = None;
self.clip_mask_dirty = !self.clip_stack.is_empty();
}
fn ensure_clip_mask(&mut self) {
if !self.clip_mask_dirty {
return;
}
self.clip_mask_dirty = false;
if self.clip_stack.is_empty() {
self.cached_clip_mask = None;
return;
}
let w = self.pixmap.width();
let h = self.pixmap.height();
let Some(mut mask) = tiny_skia::Mask::new(w, h) else {
self.cached_clip_mask = None;
return;
};
if let Some(rect) = Rect::from_xywh(0.0, 0.0, w as f32, h as f32) {
mask.fill_path(
&PathBuilder::from_rect(rect),
FillRule::Winding,
true,
Transform::identity(),
);
}
let s = self.scale_factor;
for (pos, radii) in &self.clip_stack {
let Some(mut clip_mask) = tiny_skia::Mask::new(w, h) else {
continue;
};
let scaled_radii = BorderRadiuses {
top_left_x: radii.top_left_x * s,
top_left_y: radii.top_left_y * s,
top_right_x: radii.top_right_x * s,
top_right_y: radii.top_right_y * s,
bottom_right_x: radii.bottom_right_x * s,
bottom_right_y: radii.bottom_right_y * s,
bottom_left_x: radii.bottom_left_x * s,
bottom_left_y: radii.bottom_left_y * s,
};
let path = build_rounded_rect_path(
pos.x * s,
pos.y * s,
pos.width * s,
pos.height * s,
&scaled_radii,
);
if let Some(path) = path {
clip_mask.fill_path(&path, FillRule::Winding, true, Transform::identity());
}
intersect_masks(&mut mask, &clip_mask);
}
self.cached_clip_mask = Some(mask);
}
fn solid_paint(color: Color) -> Paint<'static> {
Paint {
shader: Shader::SolidColor(tiny_skia::Color::from_rgba8(
color.r, color.g, color.b, color.a,
)),
anti_alias: true,
..Paint::default()
}
}
fn measure_text(&self, text: &str, font: &FontData) -> f32 {
let mut fs = self.font_system.borrow_mut();
let line_height = font.metrics.height * self.scale_factor;
let metrics = Metrics::new(font.size, line_height);
let mut buffer = cosmic_text::Buffer::new(&mut fs, metrics);
buffer.set_size(&mut fs, Some(f32::MAX), Some(line_height));
let attrs = attrs_from_font(font);
buffer.set_text(&mut fs, text, &attrs, Shaping::Advanced);
buffer.shape_until_scroll(&mut fs, false);
buffer.layout_runs().map(|run| run.line_w).sum::<f32>()
}
pub fn text_measure_fn(&self) -> impl Fn(&str, FontHandle) -> f32 {
let fonts = Rc::clone(&self.fonts);
let font_system = Rc::clone(&self.font_system);
let scale_factor = self.scale_factor;
move |text: &str, font: FontHandle| -> f32 {
let fonts_ref = fonts.borrow();
let Some(font_data) = fonts_ref.get(&font.0) else {
return text.len() as f32 * 8.0;
};
let mut fs = font_system.borrow_mut();
let line_height = font_data.metrics.height * scale_factor;
let metrics = Metrics::new(font_data.size, line_height);
let mut buffer = cosmic_text::Buffer::new(&mut fs, metrics);
buffer.set_size(&mut fs, Some(f32::MAX), Some(line_height));
let attrs = attrs_from_font(font_data);
buffer.set_text(&mut fs, text, &attrs, Shaping::Advanced);
buffer.shape_until_scroll(&mut fs, false);
buffer.layout_runs().map(|run| run.line_w).sum::<f32>() / scale_factor
}
}
}
fn attrs_from_font<'a>(font: &'a FontData) -> Attrs<'a> {
let family = match font.family.as_str() {
"serif" => Family::Serif,
"sans-serif" | "sans serif" => Family::SansSerif,
"monospace" => Family::Monospace,
"cursive" => Family::Cursive,
"fantasy" => Family::Fantasy,
name => Family::Name(name),
};
Attrs::new()
.family(family)
.weight(font.weight)
.style(font.style)
}
fn intersect_masks(dst: &mut tiny_skia::Mask, src: &tiny_skia::Mask) {
let dst_data = dst.data_mut();
let src_data = src.data();
let len = dst_data.len().min(src_data.len());
for i in 0..len {
dst_data[i] = dst_data[i].min(src_data[i]);
}
}
fn build_rounded_rect_path(
x: f32,
y: f32,
w: f32,
h: f32,
radii: &BorderRadiuses,
) -> Option<tiny_skia::Path> {
if w <= 0.0 || h <= 0.0 {
return None;
}
let has_radii = radii.top_left_x > 0.0
|| radii.top_left_y > 0.0
|| radii.top_right_x > 0.0
|| radii.top_right_y > 0.0
|| radii.bottom_right_x > 0.0
|| radii.bottom_right_y > 0.0
|| radii.bottom_left_x > 0.0
|| radii.bottom_left_y > 0.0;
if !has_radii {
return Rect::from_xywh(x, y, w, h).map(PathBuilder::from_rect);
}
let max_rx = w / 2.0;
let max_ry = h / 2.0;
let tl_x = radii.top_left_x.min(max_rx);
let tl_y = radii.top_left_y.min(max_ry);
let tr_x = radii.top_right_x.min(max_rx);
let tr_y = radii.top_right_y.min(max_ry);
let br_x = radii.bottom_right_x.min(max_rx);
let br_y = radii.bottom_right_y.min(max_ry);
let bl_x = radii.bottom_left_x.min(max_rx);
let bl_y = radii.bottom_left_y.min(max_ry);
const K: f32 = 0.552_284_8;
let mut pb = PathBuilder::new();
pb.move_to(x + tl_x, y);
pb.line_to(x + w - tr_x, y);
if tr_x > 0.0 || tr_y > 0.0 {
pb.cubic_to(
x + w - tr_x * (1.0 - K),
y,
x + w,
y + tr_y * (1.0 - K),
x + w,
y + tr_y,
);
}
pb.line_to(x + w, y + h - br_y);
if br_x > 0.0 || br_y > 0.0 {
pb.cubic_to(
x + w,
y + h - br_y * (1.0 - K),
x + w - br_x * (1.0 - K),
y + h,
x + w - br_x,
y + h,
);
}
pb.line_to(x + bl_x, y + h);
if bl_x > 0.0 || bl_y > 0.0 {
pb.cubic_to(
x + bl_x * (1.0 - K),
y + h,
x,
y + h - bl_y * (1.0 - K),
x,
y + h - bl_y,
);
}
pb.line_to(x, y + tl_y);
if tl_x > 0.0 || tl_y > 0.0 {
pb.cubic_to(
x,
y + tl_y * (1.0 - K),
x + tl_x * (1.0 - K),
y,
x + tl_x,
y,
);
}
pb.close();
pb.finish()
}
fn color_points_to_stops(points: &[ColorPoint]) -> Vec<GradientStop> {
points
.iter()
.map(|cp| {
let pos = cp.offset.clamp(0.0, 1.0);
let color =
tiny_skia::Color::from_rgba8(cp.color.r, cp.color.g, cp.color.b, cp.color.a);
GradientStop::new(pos, color)
})
.collect()
}
impl DocumentContainer for PixbufContainer {
fn create_font(&mut self, descr: &FontDescription) -> (FontHandle, FontMetrics) {
let family_str = descr.family().to_string();
let size = descr.size();
let s = self.scale_factor;
let physical_size = size * s;
let weight = Weight(descr.weight() as u16);
let style = match descr.style() {
FontStyle::Italic => Style::Italic,
_ => Style::Normal,
};
let id = self.next_font_id;
self.next_font_id += 1;
let line_height = (physical_size * 1.2).ceil();
let ct_metrics = Metrics::new(physical_size, line_height);
let font_family = match family_str.as_str() {
"serif" => Family::Serif,
"sans-serif" | "sans serif" => Family::SansSerif,
"monospace" => Family::Monospace,
"cursive" => Family::Cursive,
"fantasy" => Family::Fantasy,
name => Family::Name(name),
};
let attrs = Attrs::new().family(font_family).weight(weight).style(style);
let mut fs = self.font_system.borrow_mut();
let x_height = {
let mut buf = cosmic_text::Buffer::new(&mut fs, ct_metrics);
buf.set_size(&mut fs, Some(f32::MAX), Some(line_height));
buf.set_text(&mut fs, "x", &attrs, Shaping::Advanced);
buf.shape_until_scroll(&mut fs, false);
let mut h = size * 0.5; if let Some(run) = buf.layout_runs().next() {
if let Some(glyph) = run.glyphs.iter().next() {
let physical = glyph.physical((0.0, 0.0), 1.0);
let mut sc = self.swash_cache.borrow_mut();
if let Some(img) = sc.get_image_uncached(&mut fs, physical.cache_key) {
h = img.placement.height as f32;
}
}
}
h
};
let ch_width = {
let mut buf = cosmic_text::Buffer::new(&mut fs, ct_metrics);
buf.set_size(&mut fs, Some(f32::MAX), Some(line_height));
buf.set_text(&mut fs, "0", &attrs, Shaping::Advanced);
buf.shape_until_scroll(&mut fs, false);
buf.layout_runs()
.flat_map(|run| run.glyphs.iter())
.map(|g| g.w)
.next()
.unwrap_or(size * 0.6)
};
let ascent = physical_size * 0.8;
let descent = physical_size * 0.2;
let metrics = FontMetrics {
font_size: size,
height: (size * 1.2).ceil(),
ascent: ascent / s,
descent: descent / s,
x_height: x_height / s,
ch_width: ch_width / s,
draw_spaces: true,
sub_shift: size * 0.3,
super_shift: size * 0.4,
};
self.fonts.borrow_mut().insert(
id,
FontData {
family: family_str,
size: physical_size,
weight,
style,
metrics,
},
);
(FontHandle(id), metrics)
}
fn delete_font(&mut self, font: FontHandle) {
self.fonts.borrow_mut().remove(&font.0);
}
fn text_width(&self, text: &str, font: FontHandle) -> f32 {
let fonts = self.fonts.borrow();
let Some(font_data) = fonts.get(&font.0) else {
return text.len() as f32 * 8.0;
};
self.measure_text(text, font_data) / self.scale_factor
}
fn draw_text(
&mut self,
_hdc: DrawContext,
text: &str,
font: FontHandle,
color: Color,
pos: Position,
) {
self.ensure_clip_mask();
let fonts = self.fonts.borrow();
let Some(font_data) = fonts.get(&font.0) else {
return;
};
let line_height = font_data.metrics.height * self.scale_factor;
let ct_metrics = Metrics::new(font_data.size, line_height);
let attrs = attrs_from_font(font_data);
let mut fs = self.font_system.borrow_mut();
let mut buffer = cosmic_text::Buffer::new(&mut fs, ct_metrics);
buffer.set_size(&mut fs, Some(f32::MAX), Some(line_height));
buffer.set_text(&mut fs, text, &attrs, Shaping::Advanced);
buffer.shape_until_scroll(&mut fs, false);
let mut swash = self.swash_cache.borrow_mut();
let draw_x = (pos.x * self.scale_factor) as i32;
let draw_y = (pos.y * self.scale_factor) as i32;
let pix_w = self.pixmap.width() as i32;
let pix_h = self.pixmap.height() as i32;
for run in buffer.layout_runs() {
let baseline_y = run.line_y as i32;
for glyph in run.glyphs.iter() {
let physical = glyph.physical((0.0, 0.0), 1.0);
if let Some(image) = swash.get_image_uncached(&mut fs, physical.cache_key) {
let gx = draw_x + physical.x + image.placement.left;
let gy = draw_y + baseline_y + physical.y - image.placement.top;
match image.content {
cosmic_text::SwashContent::Mask => {
let mut i = 0;
for off_y in 0..image.placement.height as i32 {
for off_x in 0..image.placement.width as i32 {
let px = gx + off_x;
let py = gy + off_y;
if px >= 0 && px < pix_w && py >= 0 && py < pix_h {
let alpha = image.data[i];
if alpha > 0 {
let a = (alpha as u32 * color.a as u32 + 127) / 255;
blend_pixel(
self.pixmap.data_mut(),
pix_w as u32,
px as u32,
py as u32,
color.r,
color.g,
color.b,
a as u8,
self.cached_clip_mask.as_ref(),
);
}
}
i += 1;
}
}
}
cosmic_text::SwashContent::Color => {
let mut i = 0;
for off_y in 0..image.placement.height as i32 {
for off_x in 0..image.placement.width as i32 {
let px = gx + off_x;
let py = gy + off_y;
if px >= 0 && px < pix_w && py >= 0 && py < pix_h {
let r = image.data[i];
let g = image.data[i + 1];
let b = image.data[i + 2];
let a = image.data[i + 3];
if a > 0 {
blend_pixel(
self.pixmap.data_mut(),
pix_w as u32,
px as u32,
py as u32,
r,
g,
b,
a,
self.cached_clip_mask.as_ref(),
);
}
}
i += 4;
}
}
}
cosmic_text::SwashContent::SubpixelMask => {
let mut i = 0;
for off_y in 0..image.placement.height as i32 {
for off_x in 0..image.placement.width as i32 {
let px = gx + off_x;
let py = gy + off_y;
if px >= 0 && px < pix_w && py >= 0 && py < pix_h {
let alpha = if i + 2 < image.data.len() {
image.data[i + 1]
} else {
0
};
if alpha > 0 {
let a = (alpha as u32 * color.a as u32 + 127) / 255;
blend_pixel(
self.pixmap.data_mut(),
pix_w as u32,
px as u32,
py as u32,
color.r,
color.g,
color.b,
a as u8,
self.cached_clip_mask.as_ref(),
);
}
}
i += 3;
}
}
}
}
}
}
}
}
fn draw_list_marker(&mut self, _hdc: DrawContext, marker: &ListMarker) {
let marker_type = marker.marker_type();
match marker_type {
ListStyleType::None
| ListStyleType::Circle
| ListStyleType::Disc
| ListStyleType::Square => {}
_ => {
let idx = marker.index();
let text = format!("{}.", idx);
let font_id = marker.font();
let color = marker.color();
let pos = marker.pos();
self.draw_text(DrawContext::default(), &text, font_id, color, pos);
return;
}
}
self.ensure_clip_mask();
let pos = marker.pos();
let color = marker.color();
let paint = Self::solid_paint(color);
let transform = Transform::from_scale(self.scale_factor, self.scale_factor);
match marker_type {
ListStyleType::Disc => {
let cx = pos.x + pos.width / 2.0;
let cy = pos.y + pos.height / 2.0;
let r = pos.width.min(pos.height) / 2.0;
if let Some(path) = build_circle_path(cx, cy, r) {
self.pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
transform,
self.cached_clip_mask.as_ref(),
);
}
}
ListStyleType::Circle => {
let cx = pos.x + pos.width / 2.0;
let cy = pos.y + pos.height / 2.0;
let r = pos.width.min(pos.height) / 2.0;
if let Some(path) = build_circle_path(cx, cy, r) {
let stroke = Stroke {
width: 1.0,
..Stroke::default()
};
self.pixmap.stroke_path(
&path,
&paint,
&stroke,
transform,
self.cached_clip_mask.as_ref(),
);
}
}
ListStyleType::Square => {
if let Some(rect) = Rect::from_xywh(pos.x, pos.y, pos.width, pos.height) {
self.pixmap
.fill_rect(rect, &paint, transform, self.cached_clip_mask.as_ref());
}
}
_ => {}
}
}
fn load_image(&mut self, src: &str, _baseurl: &str, redraw_on_ready: bool) {
if src.is_empty() || self.images.contains_key(src) || self.requested_images.contains(src) {
return;
}
self.requested_images.insert(src.to_string());
self.pending_images.push((src.to_string(), redraw_on_ready));
}
fn get_image_size(&self, src: &str, _baseurl: &str) -> Size {
if let Some(pm) = self.images.get(src) {
Size {
width: pm.width() as f32,
height: pm.height() as f32,
}
} else {
Size::default()
}
}
fn draw_image(
&mut self,
_hdc: DrawContext,
layer: &BackgroundLayer,
url: &str,
_base_url: &str,
) {
self.ensure_clip_mask();
let Some(img) = self.images.get(url) else {
return;
};
let clip = layer.clip_box();
let border = layer.border_box();
let s = self.scale_factor;
let dst_x = (border.x * s) as i32;
let dst_y = (border.y * s) as i32;
let img_paint = tiny_skia::PixmapPaint {
opacity: 1.0,
blend_mode: tiny_skia::BlendMode::SourceOver,
quality: tiny_skia::FilterQuality::Bilinear,
};
if clip.width > 0.0 && clip.height > 0.0 {
let w = self.pixmap.width();
let h = self.pixmap.height();
if let Some(mut m) = tiny_skia::Mask::new(w, h) {
if let Some(rect) =
Rect::from_xywh(clip.x * s, clip.y * s, clip.width * s, clip.height * s)
{
m.fill_path(
&PathBuilder::from_rect(rect),
FillRule::Winding,
true,
Transform::identity(),
);
}
if let Some(ref existing) = self.cached_clip_mask {
intersect_masks(&mut m, existing);
}
self.pixmap.draw_pixmap(
dst_x,
dst_y,
img.as_ref(),
&img_paint,
Transform::identity(),
Some(&m),
);
}
} else {
self.pixmap.draw_pixmap(
dst_x,
dst_y,
img.as_ref(),
&img_paint,
Transform::identity(),
self.cached_clip_mask.as_ref(),
);
}
}
fn draw_solid_fill(&mut self, _hdc: DrawContext, layer: &BackgroundLayer, color: Color) {
if color.a == 0 {
return;
}
let border = layer.border_box();
let radii = layer.border_radius();
let paint = Self::solid_paint(color);
self.ensure_clip_mask();
let transform = Transform::from_scale(self.scale_factor, self.scale_factor);
if let Some(path) =
build_rounded_rect_path(border.x, border.y, border.width, border.height, &radii)
{
self.pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
transform,
self.cached_clip_mask.as_ref(),
);
}
}
fn draw_linear_gradient(
&mut self,
_hdc: DrawContext,
layer: &BackgroundLayer,
gradient: &LinearGradient,
) {
let points = gradient.color_points();
let stops = color_points_to_stops(&points);
if stops.len() < 2 {
if let Some(cp) = points.first() {
self.draw_solid_fill(DrawContext::default(), layer, cp.color);
}
return;
}
self.ensure_clip_mask();
let border = layer.border_box();
let radii = layer.border_radius();
let start = gradient.start();
let end = gradient.end();
let transform = Transform::from_scale(self.scale_factor, self.scale_factor);
let shader = tiny_skia::LinearGradient::new(
tiny_skia::Point::from_xy(border.x + start.x, border.y + start.y),
tiny_skia::Point::from_xy(border.x + end.x, border.y + end.y),
stops,
SpreadMode::Pad,
transform,
);
if let Some(shader) = shader {
let paint = Paint {
shader,
anti_alias: true,
..Paint::default()
};
if let Some(path) =
build_rounded_rect_path(border.x, border.y, border.width, border.height, &radii)
{
self.pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
transform,
self.cached_clip_mask.as_ref(),
);
}
}
}
fn draw_radial_gradient(
&mut self,
_hdc: DrawContext,
layer: &BackgroundLayer,
gradient: &RadialGradient,
) {
let points = gradient.color_points();
let stops = color_points_to_stops(&points);
if stops.len() < 2 {
if let Some(cp) = points.first() {
self.draw_solid_fill(DrawContext::default(), layer, cp.color);
}
return;
}
self.ensure_clip_mask();
let border = layer.border_box();
let radii = layer.border_radius();
let center = gradient.position();
let radius = gradient.radius();
let transform = Transform::from_scale(self.scale_factor, self.scale_factor);
let cx = border.x + center.x;
let cy = border.y + center.y;
let r = radius.x.max(radius.y).max(0.001);
let shader = tiny_skia::RadialGradient::new(
tiny_skia::Point::from_xy(cx, cy),
tiny_skia::Point::from_xy(cx, cy),
r,
stops,
SpreadMode::Pad,
transform,
);
if let Some(shader) = shader {
let paint = Paint {
shader,
anti_alias: true,
..Paint::default()
};
if let Some(path) =
build_rounded_rect_path(border.x, border.y, border.width, border.height, &radii)
{
self.pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
transform,
self.cached_clip_mask.as_ref(),
);
}
}
}
fn draw_conic_gradient(
&mut self,
_hdc: DrawContext,
layer: &BackgroundLayer,
gradient: &ConicGradient,
) {
let points = gradient.color_points();
if let Some(cp) = points.first() {
self.draw_solid_fill(DrawContext::default(), layer, cp.color);
}
}
fn draw_borders(
&mut self,
_hdc: DrawContext,
borders: &Borders,
draw_pos: Position,
_root: bool,
) {
self.ensure_clip_mask();
let transform = Transform::from_scale(self.scale_factor, self.scale_factor);
let x = draw_pos.x;
let y = draw_pos.y;
let w = draw_pos.width;
let h = draw_pos.height;
draw_border_side(
&mut self.pixmap,
self.cached_clip_mask.as_ref(),
&borders.top,
x,
y,
w,
borders.top.width,
true,
transform,
);
draw_border_side(
&mut self.pixmap,
self.cached_clip_mask.as_ref(),
&borders.bottom,
x,
y + h - borders.bottom.width,
w,
borders.bottom.width,
true,
transform,
);
draw_border_side(
&mut self.pixmap,
self.cached_clip_mask.as_ref(),
&borders.left,
x,
y,
borders.left.width,
h,
false,
transform,
);
draw_border_side(
&mut self.pixmap,
self.cached_clip_mask.as_ref(),
&borders.right,
x + w - borders.right.width,
y,
borders.right.width,
h,
false,
transform,
);
}
fn set_caption(&mut self, caption: &str) {
self.caption = caption.to_string();
}
fn set_base_url(&mut self, base_url: &str) {
self.base_url = base_url.to_string();
}
fn on_anchor_click(&mut self, url: &str) {
self.last_anchor_click = Some(url.to_string());
}
fn set_cursor(&mut self, cursor: &str) {
self.current_cursor = cursor.to_string();
}
fn set_clip(&mut self, pos: Position, radius: BorderRadiuses) {
if self.ignore_overflow_clips {
return;
}
self.clip_stack.push((pos, radius));
self.clip_mask_dirty = true;
}
fn del_clip(&mut self) {
if self.ignore_overflow_clips {
return;
}
self.clip_stack.pop();
self.clip_mask_dirty = true;
}
fn get_viewport(&self) -> Position {
self.viewport
}
fn get_media_features(&self) -> MediaFeatures {
MediaFeatures {
media_type: MediaType::Screen,
width: self.viewport.width,
height: self.viewport.height,
device_width: self.viewport.width * self.scale_factor,
device_height: self.viewport.height * self.scale_factor,
color: 8,
color_index: 0,
monochrome: 0,
resolution: 96.0 * self.scale_factor,
}
}
fn transform_text(&self, text: &str, tt: TextTransform) -> String {
match tt {
TextTransform::Uppercase => text.to_uppercase(),
TextTransform::Lowercase => text.to_lowercase(),
TextTransform::Capitalize => {
let mut result = String::with_capacity(text.len());
let mut capitalize_next = true;
for ch in text.chars() {
if capitalize_next && ch.is_alphabetic() {
for upper in ch.to_uppercase() {
result.push(upper);
}
capitalize_next = false;
} else {
result.push(ch);
if ch.is_whitespace() {
capitalize_next = true;
}
}
}
result
}
TextTransform::None => text.to_string(),
}
}
}
#[allow(clippy::too_many_arguments)]
fn blend_pixel(
data: &mut [u8],
width: u32,
x: u32,
y: u32,
r: u8,
g: u8,
b: u8,
a: u8,
mask: Option<&tiny_skia::Mask>,
) {
if a == 0 {
return;
}
let pixel_offset = y as usize * width as usize + x as usize;
let effective_a = if let Some(mask) = mask {
let mask_data = mask.data();
if pixel_offset >= mask_data.len() {
return;
}
let mask_val = mask_data[pixel_offset];
if mask_val == 0 {
return;
}
((a as u32 * mask_val as u32 + 127) / 255) as u8
} else {
a
};
if effective_a == 0 {
return;
}
let idx = pixel_offset * 4;
if idx + 3 >= data.len() {
return;
}
let sa = effective_a as u32;
let sr = (r as u32 * sa + 127) / 255;
let sg = (g as u32 * sa + 127) / 255;
let sb = (b as u32 * sa + 127) / 255;
let dr = data[idx] as u32;
let dg = data[idx + 1] as u32;
let db = data[idx + 2] as u32;
let da = data[idx + 3] as u32;
let inv_sa = 255 - sa;
data[idx] = (sr + (dr * inv_sa + 127) / 255).min(255) as u8;
data[idx + 1] = (sg + (dg * inv_sa + 127) / 255).min(255) as u8;
data[idx + 2] = (sb + (db * inv_sa + 127) / 255).min(255) as u8;
data[idx + 3] = (sa + (da * inv_sa + 127) / 255).min(255) as u8;
}
#[allow(clippy::too_many_arguments)]
fn draw_border_side(
pixmap: &mut tiny_skia::Pixmap,
mask: Option<&tiny_skia::Mask>,
border: &crate::Border,
x: f32,
y: f32,
w: f32,
h: f32,
horizontal: bool,
transform: Transform,
) {
if border.width <= 0.0 || matches!(border.style, BorderStyle::None | BorderStyle::Hidden) {
return;
}
let paint = Paint {
shader: Shader::SolidColor(tiny_skia::Color::from_rgba8(
border.color.r,
border.color.g,
border.color.b,
border.color.a,
)),
anti_alias: true,
..Paint::default()
};
match border.style {
BorderStyle::Solid
| BorderStyle::Double
| BorderStyle::Groove
| BorderStyle::Ridge
| BorderStyle::Inset
| BorderStyle::Outset => {
if let Some(rect) = Rect::from_xywh(x, y, w.max(0.001), h.max(0.001)) {
pixmap.fill_rect(rect, &paint, transform, mask);
}
}
BorderStyle::Dashed => {
let mut pb = PathBuilder::new();
if horizontal {
let mid_y = y + h / 2.0;
pb.move_to(x, mid_y);
pb.line_to(x + w, mid_y);
} else {
let mid_x = x + w / 2.0;
pb.move_to(mid_x, y);
pb.line_to(mid_x, y + h);
}
if let Some(path) = pb.finish() {
let dash_len = border.width * 3.0;
let stroke = Stroke {
width: border.width,
dash: StrokeDash::new(vec![dash_len, dash_len], 0.0),
..Stroke::default()
};
pixmap.stroke_path(&path, &paint, &stroke, transform, mask);
}
}
BorderStyle::Dotted => {
let mut pb = PathBuilder::new();
if horizontal {
let mid_y = y + h / 2.0;
pb.move_to(x, mid_y);
pb.line_to(x + w, mid_y);
} else {
let mid_x = x + w / 2.0;
pb.move_to(mid_x, y);
pb.line_to(mid_x, y + h);
}
if let Some(path) = pb.finish() {
let dot = border.width;
let stroke = Stroke {
width: border.width,
line_cap: tiny_skia::LineCap::Round,
dash: StrokeDash::new(vec![0.001, dot * 2.0], 0.0),
..Stroke::default()
};
pixmap.stroke_path(&path, &paint, &stroke, transform, mask);
}
}
BorderStyle::None | BorderStyle::Hidden => {}
}
}
fn build_circle_path(cx: f32, cy: f32, r: f32) -> Option<tiny_skia::Path> {
if r <= 0.0 {
return None;
}
const K: f32 = 0.552_284_8;
let mut pb = PathBuilder::new();
pb.move_to(cx + r, cy);
pb.cubic_to(cx + r, cy + r * K, cx + r * K, cy + r, cx, cy + r);
pb.cubic_to(cx - r * K, cy + r, cx - r, cy + r * K, cx - r, cy);
pb.cubic_to(cx - r, cy - r * K, cx - r * K, cy - r, cx, cy - r);
pb.cubic_to(cx + r * K, cy - r, cx + r, cy - r * K, cx + r, cy);
pb.close();
pb.finish()
}
pub fn render_to_rgba(html: &str, width: u32, height: u32) -> Vec<u8> {
let mut container = PixbufContainer::new(width, height);
if let Ok(mut doc) = crate::Document::from_html(html, &mut container, None, None) {
let _ = doc.render(width as f32);
doc.draw(
crate::DrawContext::default(),
0.0,
0.0,
Some(Position {
x: 0.0,
y: 0.0,
width: width as f32,
height: height as f32,
}),
);
}
container.pixels().to_vec()
}
pub fn render_to_rgba_scaled(html: &str, width: u32, height: u32, scale_factor: f32) -> Vec<u8> {
let mut container = PixbufContainer::new_with_scale(width, height, scale_factor);
if let Ok(mut doc) = crate::Document::from_html(html, &mut container, None, None) {
let _ = doc.render(width as f32);
doc.draw(
crate::DrawContext::default(),
0.0,
0.0,
Some(Position {
x: 0.0,
y: 0.0,
width: width as f32,
height: height as f32,
}),
);
}
container.pixels().to_vec()
}