use cosmic_text::{FontSystem, SwashCache, SwashContent, SwashImage};
use crabshell_ui::{
CaptureView, Color, Content, Gradient, GradientShape, IconView, ImageView, InputView, NodeView,
Rect, Shadow, Size, Style, TextSystem, TextView, WindowView,
};
use tiny_skia::{
FillRule, GradientStop, LinearGradient, Mask, Paint, PathBuilder, Pixmap, PixmapMut, PixmapPaint,
RadialGradient, Shader, SpreadMode, Transform,
};
use crate::damage::{BufferAge, DamageHistory, PixelRect};
use crate::icons::Icons;
const TILE: u32 = 64;
const GLYPH_CACHE_LIMIT: usize = 2048;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PixelFormat {
Argb8888,
Abgr8888,
}
pub struct Canvas<'a> {
pub pixels: &'a mut [u8],
pub width: u32,
pub height: u32,
pub scale: f32,
pub format: PixelFormat,
pub age: &'a mut BufferAge,
}
pub struct Renderer {
glyphs: SwashCache,
icons: Icons,
scratch: Vec<u8>,
work: Work,
}
#[derive(Clone, Copy, Default)]
#[cfg_attr(not(test), allow(dead_code, reason = "only tests read it"))]
struct Work {
layer_pixels: u64,
nodes: u64,
}
impl Renderer {
pub fn new() -> Renderer {
Renderer {
glyphs: SwashCache::new(),
icons: Icons::new(),
scratch: Vec::new(),
work: Work::default(),
}
}
#[cfg(test)]
pub(crate) fn cached_glyphs(&self) -> usize {
self.glyphs.image_cache.len()
}
#[cfg(test)]
pub(crate) fn work(&self) -> (u64, u64) {
(self.work.layer_pixels, self.work.nodes)
}
#[cfg(test)]
pub(crate) fn with_icon_dirs(bases: Vec<std::path::PathBuf>) -> Renderer {
Renderer {
icons: Icons::with_themes(crate::theme::Themes::new(bases)),
..Renderer::new()
}
}
pub fn set_icon_theme(&mut self, theme: impl Into<String>) {
self.icons.set_theme(theme.into());
}
#[cfg(test)]
pub(crate) fn cached_icons(&self) -> usize {
self.icons.cached()
}
#[cfg(test)]
pub(crate) fn rasterized_icons(&self) -> usize {
self.icons.rasterized()
}
pub fn forget_captures(&mut self, gone: &[u64]) {
self.icons.forget_captures(gone);
}
#[cfg(test)]
pub(crate) fn cached_captures(&self) -> usize {
self.icons.captures()
}
pub fn draw(
&mut self,
view: &WindowView<'_>,
damage: &[Rect],
text: &mut TextSystem,
history: &mut DamageHistory,
canvas: Canvas<'_>,
) -> Result<Vec<PixelRect>, CanvasError> {
let Canvas {
pixels,
width,
height,
scale,
format,
age,
} = canvas;
if pixels.len() as u64 != u64::from(width) * u64::from(height) * 4
|| !(scale.is_finite() && scale > 0.0)
{
return Err(CanvasError {
width,
height,
scale,
bytes: pixels.len(),
});
}
let plan = history.plan(damage, scale, width, height, age);
let mut tiles: Vec<(u32, u32)> = Vec::new();
for area in plan.repaint.iter().filter(|area| area.area() > 0) {
for row in area.y / TILE..=(area.y + area.height - 1) / TILE {
tiles
.extend((area.x / TILE..=(area.x + area.width - 1) / TILE).map(|column| (row, column)));
}
}
tiles.sort_unstable();
tiles.dedup();
for (row, column) in tiles {
let (x, y) = (column * TILE, row * TILE);
let size = (width.saturating_sub(x), height.saturating_sub(y));
let area = PixelRect::new(x, y, TILE.min(size.0), TILE.min(size.1));
self.scratch.clear();
self.scratch.resize(area.area() as usize * 4, 0);
let Some(mut pixmap) = PixmapMut::from_bytes(&mut self.scratch, area.width, area.height)
else {
continue;
};
let mut painter = Painter {
grid: Grid {
scale,
window: view.size(),
width,
height,
left: area.x as f32,
top: area.y as f32,
},
glyphs: &mut self.glyphs,
icons: &mut self.icons,
fonts: text.font_system(),
work: &mut self.work,
};
painter.node(&mut pixmap, view.root());
copy(&self.scratch, area, pixels, width, format);
}
self.icons.end_frame();
if self.glyphs.image_cache.len() > GLYPH_CACHE_LIMIT {
self.glyphs.image_cache.clear();
self.glyphs.outline_command_cache.clear();
}
Ok(plan.report)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct CanvasError {
pub width: u32,
pub height: u32,
pub scale: f32,
pub bytes: usize,
}
impl std::fmt::Display for CanvasError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"a {}x{} canvas at scale {} needs {} bytes, but has {}",
self.width,
self.height,
self.scale,
u64::from(self.width) * u64::from(self.height) * 4,
self.bytes
)
}
}
impl std::error::Error for CanvasError {}
impl Default for Renderer {
fn default() -> Self {
Self::new()
}
}
fn copy(scratch: &[u8], area: PixelRect, pixels: &mut [u8], width: u32, format: PixelFormat) {
let stride = width as usize * 4;
let row_bytes = area.width as usize * 4;
for (row, source) in scratch.chunks_exact(row_bytes).enumerate() {
let start = (area.y as usize + row) * stride + area.x as usize * 4;
let target = &mut pixels[start..start + row_bytes];
match format {
PixelFormat::Abgr8888 => target.copy_from_slice(source),
PixelFormat::Argb8888 => {
let (to, _) = target.as_chunks_mut::<4>();
let (from, _) = source.as_chunks::<4>();
for (to, from) in to.iter_mut().zip(from) {
*to = [from[2], from[1], from[0], from[3]];
}
}
}
}
}
#[derive(Clone, Copy)]
struct Grid {
scale: f32,
window: Size,
width: u32,
height: u32,
left: f32,
top: f32,
}
impl Grid {
fn x(&self, x: f32) -> f32 {
let pixel = if x >= self.window.width {
self.width as f32
} else {
(x * self.scale).round()
};
pixel - self.left
}
fn y(&self, y: f32) -> f32 {
let pixel = if y >= self.window.height {
self.height as f32
} else {
(y * self.scale).round()
};
pixel - self.top
}
fn cover(&self, rect: Rect, width: u32, height: u32) -> Option<(u32, u32, u32, u32)> {
if !(rect.width > 0.0 && rect.height > 0.0) {
return None;
}
let left = ((rect.x * self.scale).floor() - self.left - 1.0).max(0.0) as u32;
let top = ((rect.y * self.scale).floor() - self.top - 1.0).max(0.0) as u32;
let right = (((rect.right() * self.scale).ceil() - self.left + 1.0).max(0.0) as u32).min(width);
let bottom =
(((rect.bottom() * self.scale).ceil() - self.top + 1.0).max(0.0) as u32).min(height);
(left < right && top < bottom).then(|| (left, top, right - left, bottom - top))
}
fn moved(&self, x: u32, y: u32) -> Grid {
Grid {
left: self.left + x as f32,
top: self.top + y as f32,
..*self
}
}
fn rect(&self, rect: Rect) -> Option<tiny_skia::Rect> {
tiny_skia::Rect::from_ltrb(
self.x(rect.x),
self.y(rect.y),
self.x(rect.right()),
self.y(rect.bottom()),
)
.filter(|rect| rect.width() >= 1.0 && rect.height() >= 1.0)
}
}
struct Painter<'a> {
grid: Grid,
glyphs: &'a mut SwashCache,
icons: &'a mut Icons,
fonts: &'a mut FontSystem,
work: &'a mut Work,
}
impl Painter<'_> {
fn node(&mut self, pixmap: &mut PixmapMut<'_>, node: NodeView<'_>) {
let Some((x, y, width, height)) =
self
.grid
.cover(node.bounds(), pixmap.width(), pixmap.height())
else {
return;
};
let style = node.style();
let opacity = style.opacity.unwrap_or(1.0);
let opacity = if opacity.is_nan() {
1.0
} else {
opacity.clamp(0.0, 1.0)
};
if opacity == 0.0 {
return;
}
if opacity < 1.0 {
let Some(mut layer) = Pixmap::new(width, height) else {
return;
};
self.work.layer_pixels += u64::from(layer.width()) * u64::from(layer.height());
let mut inner = Painter {
grid: self.grid.moved(x, y),
glyphs: &mut *self.glyphs,
icons: &mut *self.icons,
fonts: &mut *self.fonts,
work: &mut *self.work,
};
inner.node_contents(&mut layer.as_mut(), node, &style);
let paint = PixmapPaint {
opacity,
..PixmapPaint::default()
};
let (x, y) = (x as i32, y as i32);
pixmap.draw_pixmap(x, y, layer.as_ref(), &paint, Transform::identity(), None);
} else {
self.node_contents(pixmap, node, &style);
}
}
fn node_contents(&mut self, pixmap: &mut PixmapMut<'_>, node: NodeView<'_>, style: &Style) {
self.work.nodes += 1;
if let Some(shadow) = style.shadow {
self.shadow(pixmap, node.rect(), style, &shadow);
}
self.draw_box(pixmap, node.rect(), style);
match node.content() {
Content::Text(text) => self.text(pixmap, &text),
Content::Icon(icon) => self.icon(pixmap, &icon),
Content::Image(image) => self.image(pixmap, &image),
Content::Capture(capture) => self.capture(pixmap, &capture),
Content::Input(input) => self.input(pixmap, &input),
Content::None => {}
}
if node.clips() {
self.clipped_children(pixmap, node, style);
} else {
for child in node.children() {
self.node(pixmap, child);
}
}
}
fn clipped_children(&mut self, pixmap: &mut PixmapMut<'_>, node: NodeView<'_>, style: &Style) {
let Some((area, radius)) = self.box_shape(node.rect(), style) else {
return;
};
let width = self.border_width(style);
let Some(inside) = tiny_skia::Rect::from_ltrb(
area.left() + width,
area.top() + width,
area.right() - width,
area.bottom() - width,
) else {
return;
};
let radius = (radius - width).max(0.0);
self.through_mask(pixmap, node.rect(), (inside, radius), |painter, layer| {
for child in node.children() {
painter.node(layer, child);
}
});
}
fn through_mask(
&mut self,
pixmap: &mut PixmapMut<'_>,
cover: Rect,
(shape, radius): (tiny_skia::Rect, f32),
draw: impl FnOnce(&mut Painter<'_>, &mut PixmapMut<'_>),
) {
if !visible(pixmap, shape) {
return;
}
let (Some(mut mask), Some((x, y, layer_width, layer_height))) = (
Mask::new(pixmap.width(), pixmap.height()),
self.grid.cover(cover, pixmap.width(), pixmap.height()),
) else {
return;
};
let Some(mut layer) = Pixmap::new(layer_width, layer_height) else {
return;
};
self.work.layer_pixels += u64::from(layer_width) * u64::from(layer_height);
let mut inner = Painter {
grid: self.grid.moved(x, y),
glyphs: &mut *self.glyphs,
icons: &mut *self.icons,
fonts: &mut *self.fonts,
work: &mut *self.work,
};
draw(&mut inner, &mut layer.as_mut());
let mut path = PathBuilder::new();
rounded_rect(&mut path, shape, radius);
let Some(path) = path.finish() else {
return;
};
mask.fill_path(&path, FillRule::Winding, true, Transform::identity());
pixmap.draw_pixmap(
x as i32,
y as i32,
layer.as_ref(),
&PixmapPaint::default(),
Transform::identity(),
Some(&mask),
);
}
fn box_shape(&self, rect: Rect, style: &Style) -> Option<(tiny_skia::Rect, f32)> {
let area = self.grid.rect(rect)?;
let radius = (style.radius.unwrap_or(0.0) * self.grid.scale)
.clamp(0.0, area.width().min(area.height()) / 2.0);
let radius = if radius.is_nan() { 0.0 } else { radius };
Some((area, radius))
}
fn border_width(&self, style: &Style) -> f32 {
let Some(border) = style.border else {
return 0.0;
};
let width = (border.width * self.grid.scale).floor();
if border.width > 0.0 {
width.max(1.0)
} else {
width.max(0.0)
}
}
fn draw_box(&self, pixmap: &mut PixmapMut<'_>, rect: Rect, style: &Style) {
if style.background.is_none() && style.gradient.is_none() && style.border.is_none() {
return;
}
let Some((area, radius)) = self.box_shape(rect, style) else {
return;
};
if !visible(pixmap, area) {
return;
}
if let Some(color) = style.background {
let mut path = PathBuilder::new();
rounded_rect(&mut path, area, radius);
fill(pixmap, path, color, FillRule::Winding);
}
if let Some(gradient) = style.gradient {
let mut path = PathBuilder::new();
rounded_rect(&mut path, area, radius);
if let (Some(path), Some(shader)) = (path.finish(), self.shader(&gradient, area)) {
let paint = Paint {
shader,
anti_alias: true,
..Paint::default()
};
pixmap.fill_path(
&path,
&paint,
FillRule::Winding,
Transform::identity(),
None,
);
}
}
if let Some(border) = style.border {
let width = self.border_width(style);
if width >= 1.0 {
let mut path = PathBuilder::new();
rounded_rect(&mut path, area, radius);
if let Some(inner) = tiny_skia::Rect::from_ltrb(
area.left() + width,
area.top() + width,
area.right() - width,
area.bottom() - width,
) {
rounded_rect(&mut path, inner, (radius - width).max(0.0));
}
fill(pixmap, path, border.color, FillRule::EvenOdd);
}
}
}
fn shadow(&self, pixmap: &mut PixmapMut<'_>, rect: Rect, style: &Style, shadow: &Shadow) {
let Grid {
scale, left, top, ..
} = self.grid;
let numbers = [shadow.x, shadow.y, shadow.blur, shadow.spread];
if shadow.color.a == 0 || !numbers.iter().all(|n| n.is_finite()) {
return;
}
let Some((area, radius)) = self.box_shape(rect, style) else {
return;
};
let spread = shadow.spread * scale;
let Some(shape) = tiny_skia::Rect::from_ltrb(
area.left() + left + shadow.x * scale - spread,
area.top() + top + shadow.y * scale - spread,
area.right() + left + shadow.x * scale + spread,
area.bottom() + top + shadow.y * scale + spread,
) else {
return;
};
let radius = if spread > 0.0 && radius < spread {
radius + spread * (1.0 + (radius / spread - 1.0).powi(3))
} else {
radius + spread
};
let radius = radius.clamp(0.0, shape.width().min(shape.height()) / 2.0);
let cut = if shadow.cut_out {
self.box_mask(pixmap, rect, style)
} else {
None
};
let sigma = shadow.blur.max(0.0) * scale / 2.0;
if sigma < 0.25 {
let mut path = PathBuilder::new();
let Some(shape) = shape.transform(Transform::from_translate(-left, -top)) else {
return;
};
rounded_rect(&mut path, shape, radius);
let Some(path) = path.finish() else {
return;
};
let mut paint = Paint::default();
let color = shadow.color;
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
let outside = cut.map(|mut cut| {
cut.invert();
cut
});
let (rule, at) = (FillRule::Winding, Transform::identity());
pixmap.fill_path(&path, &paint, rule, at, outside.as_ref());
return;
}
let reach = 3.0 * sigma;
let columns = (shape.left() - reach - left).floor().max(0.0) as u32
..((shape.right() + reach - left).ceil().max(0.0) as u32).min(pixmap.width());
let rows = (shape.top() - reach - top).floor().max(0.0) as u32
..((shape.bottom() + reach - top).ceil().max(0.0) as u32).min(pixmap.height());
if columns.is_empty() || rows.is_empty() {
return;
}
let blurred = BlurredBox::new(shape, radius, sigma);
let mut mask = Vec::with_capacity(columns.len() * rows.len());
for row in rows.clone() {
let xs = columns.clone().map(|column| left + column as f32 + 0.5);
blurred.row(top + row as f32 + 0.5, xs, &mut mask);
}
if let Some(cut) = cut {
let stride = pixmap.width() as usize;
let under = rows.clone().flat_map(|row| {
columns
.clone()
.map(move |column| row as usize * stride + column as usize)
});
for (alpha, at) in mask.iter_mut().zip(under) {
*alpha = mul(*alpha, 255 - cut.data()[at]);
}
}
let width = columns.len() as u32;
let height = rows.len() as u32;
let (x, y) = (columns.start as i32, rows.start as i32);
blend_mask(pixmap, x, y, width, height, &mask, shadow.color);
}
fn box_mask(&self, pixmap: &PixmapMut<'_>, rect: Rect, style: &Style) -> Option<Mask> {
let (area, radius) = self.box_shape(rect, style)?;
let mut path = PathBuilder::new();
rounded_rect(&mut path, area, radius);
let mut mask = Mask::new(pixmap.width(), pixmap.height())?;
let (rule, at) = (FillRule::Winding, Transform::identity());
mask.fill_path(&path.finish()?, rule, true, at);
Some(mask)
}
fn shader(&self, gradient: &Gradient, area: tiny_skia::Rect) -> Option<Shader<'static>> {
let (left, top) = (area.left() + self.grid.left, area.top() + self.grid.top);
let (width, height) = (area.width(), area.height());
let middle = tiny_skia::Point::from_xy(left + width / 2.0, top + height / 2.0);
let given = gradient.stops();
let mut stops = Vec::with_capacity(given.len() * 2);
let stop = |at: f32, color: Color, alpha: u8| {
GradientStop::new(
at,
tiny_skia::Color::from_rgba8(color.r, color.g, color.b, alpha),
)
};
for (index, &(at, color)) in given.iter().enumerate() {
if color.a > 0 {
stops.push(stop(at, color, color.a));
continue;
}
for neighbor in [index.checked_sub(1), Some(index + 1)]
.into_iter()
.flatten()
{
if let Some(&(_, near)) = given.get(neighbor) {
stops.push(stop(at, near, 0));
}
}
}
let moved = Transform::from_translate(-self.grid.left, -self.grid.top);
match gradient.shape {
GradientShape::Linear(angle) => {
let (sin, cos) = angle.to_radians().sin_cos();
let half = (width * sin.abs() + height * cos.abs()) / 2.0;
let (dx, dy) = (sin * half, -cos * half);
let start = tiny_skia::Point::from_xy(middle.x - dx, middle.y - dy);
let end = tiny_skia::Point::from_xy(middle.x + dx, middle.y + dy);
LinearGradient::new(start, end, stops, SpreadMode::Pad, moved)
}
GradientShape::Radial => {
let reach = (width * width + height * height).sqrt() / 2.0;
RadialGradient::new(middle, 0.0, middle, reach, stops, SpreadMode::Pad, moved)
}
}
}
fn text(&mut self, pixmap: &mut PixmapMut<'_>, text: &TextView<'_>) {
let color = text.color();
if color.a == 0 {
return;
}
let Grid {
scale, left, top, ..
} = self.grid;
let origin = text.origin();
let x = origin.x * scale - left;
for run in text.runs() {
let baseline = ((origin.y + run.line_y) * scale).round() - top;
if baseline + run.line_height * scale < 0.0
|| baseline - run.line_height * scale > pixmap.height() as f32
{
continue;
}
for glyph in run.glyphs {
let physical = glyph.physical((x, baseline), scale);
let Some(image) = self.glyphs.get_image(self.fonts, physical.cache_key) else {
continue;
};
let left = physical.x + image.placement.left;
let top = physical.y - image.placement.top;
let (width, height) = (image.placement.width, image.placement.height);
match image.content {
SwashContent::Mask => {
blend_mask(pixmap, left, top, width, height, &image.data, color);
}
SwashContent::Color => {
let premultiplied = is_color_outline(image);
let rgba = (image.data.as_slice(), premultiplied);
blend_rgba(pixmap, left, top, width, height, rgba, color.a);
}
SwashContent::SubpixelMask => {}
}
}
}
}
fn input(&mut self, pixmap: &mut PixmapMut<'_>, input: &InputView<'_>) {
let Some(area) = self.grid.rect(input.area) else {
return;
};
self.through_mask(pixmap, input.area, (area, 0.0), |painter, layer| {
painter.input_contents(layer, input);
});
}
fn input_contents(&mut self, pixmap: &mut PixmapMut<'_>, input: &InputView<'_>) {
let color = input.text.color();
for selection in input.selection {
let Some(area) = self.grid.rect(*selection) else {
continue;
};
let mut path = PathBuilder::new();
path.push_rect(area);
let tint = Color::rgba(color.r, color.g, color.b, color.a / 3);
fill(pixmap, path, tint, FillRule::Winding);
}
self.text(pixmap, &input.text);
for line in input.underline {
if let Some(area) = self.grid.rect(*line) {
let mut path = PathBuilder::new();
path.push_rect(area);
fill(pixmap, path, color, FillRule::Winding);
}
}
if let Some(caret) = input.caret {
let left = self.grid.x(caret.x);
let right = self.grid.x(caret.right()).max(left + 1.0);
if let Some(area) = tiny_skia::Rect::from_ltrb(
left,
self.grid.y(caret.y),
right,
self.grid.y(caret.bottom()),
) {
let mut path = PathBuilder::new();
path.push_rect(area);
fill(pixmap, path, color, FillRule::Winding);
}
}
}
fn image(&mut self, pixmap: &mut PixmapMut<'_>, image: &ImageView<'_>) {
let Some((x, y, width, height)) = self.picture_area(pixmap, image.rect) else {
return;
};
if let Some(pixels) = self.icons.image(image.path, width, height, image.fit) {
draw_at(pixmap, x, y, pixels);
}
}
fn capture(&mut self, pixmap: &mut PixmapMut<'_>, capture: &CaptureView<'_>) {
let Some((x, y, width, height)) = self.picture_area(pixmap, capture.rect) else {
return;
};
let frame = capture.frame;
if let Some(pixels) = self
.icons
.capture(capture.id, frame, width, height, capture.fit)
{
draw_at(pixmap, x, y, pixels);
}
}
fn picture_area(&self, pixmap: &PixmapMut<'_>, rect: Rect) -> Option<(i32, i32, u32, u32)> {
let Grid {
scale, left, top, ..
} = self.grid;
let (width, height) = ((rect.width * scale).round(), (rect.height * scale).round());
let (x, y) = (
(rect.x * scale).round() - left,
(rect.y * scale).round() - top,
);
if !(width >= 1.0 && height >= 1.0) {
return None;
}
let area = tiny_skia::Rect::from_xywh(x, y, width, height)?;
visible(pixmap, area).then_some((x as i32, y as i32, width as u32, height as u32))
}
fn icon(&mut self, pixmap: &mut PixmapMut<'_>, icon: &IconView<'_>) {
let Grid {
scale, left, top, ..
} = self.grid;
let side = (icon.rect.width * scale).round();
if side.is_nan() || side < 1.0 {
return;
}
let x = (icon.rect.x * scale).round() - left;
let y = (icon.rect.y * scale).round() - top;
let size = icon.rect.width.round() as u32;
let theme_scale = scale.ceil().max(1.0) as u32;
if let Some(image) = self.icons.get(icon.source, side as u32, size, theme_scale) {
pixmap.draw_pixmap(
x as i32,
y as i32,
image.as_ref(),
&PixmapPaint::default(),
Transform::identity(),
None,
);
}
}
}
fn draw_at(pixmap: &mut PixmapMut<'_>, x: i32, y: i32, pixels: &Pixmap) {
let paint = PixmapPaint::default();
pixmap.draw_pixmap(x, y, pixels.as_ref(), &paint, Transform::identity(), None);
}
fn visible(pixmap: &PixmapMut<'_>, area: tiny_skia::Rect) -> bool {
area.right() > 0.0
&& area.bottom() > 0.0
&& area.left() < pixmap.width() as f32
&& area.top() < pixmap.height() as f32
}
fn fill(pixmap: &mut PixmapMut<'_>, path: PathBuilder, color: Color, rule: FillRule) {
let Some(path) = path.finish() else {
return;
};
let mut paint = Paint::default();
paint.set_color_rgba8(color.r, color.g, color.b, color.a);
paint.anti_alias = true;
pixmap.fill_path(&path, &paint, rule, Transform::identity(), None);
}
fn rounded_rect(path: &mut PathBuilder, rect: tiny_skia::Rect, radius: f32) {
if radius <= 0.0 {
path.push_rect(rect);
return;
}
const KAPPA: f32 = 0.552_284_8;
let (l, t, r, b) = (rect.left(), rect.top(), rect.right(), rect.bottom());
let k = radius * (1.0 - KAPPA);
path.move_to(l + radius, t);
path.line_to(r - radius, t);
path.cubic_to(r - k, t, r, t + k, r, t + radius);
path.line_to(r, b - radius);
path.cubic_to(r, b - k, r - k, b, r - radius, b);
path.line_to(l + radius, b);
path.cubic_to(l + k, b, l, b - k, l, b - radius);
path.line_to(l, t + radius);
path.cubic_to(l, t + k, l + k, t, l + radius, t);
path.close();
}
fn mul(a: u8, b: u8) -> u8 {
((u16::from(a) * u16::from(b) + 127) / 255) as u8
}
fn over(data: &mut [u8], index: usize, source: [u8; 4]) {
let rest = 255 - source[3];
for (channel, value) in data[index..index + 4].iter_mut().zip(source) {
*channel = value.saturating_add(mul(*channel, rest));
}
}
fn overlap(
pixmap: &PixmapMut<'_>,
left: i32,
top: i32,
width: u32,
height: u32,
) -> Option<(usize, usize, usize, usize)> {
let column_start = (-i64::from(left)).max(0);
let row_start = (-i64::from(top)).max(0);
let column_end = i64::from(width).min(i64::from(pixmap.width()) - i64::from(left));
let row_end = i64::from(height).min(i64::from(pixmap.height()) - i64::from(top));
(column_start < column_end && row_start < row_end).then_some((
column_start as usize,
column_end as usize,
row_start as usize,
row_end as usize,
))
}
fn blend_mask(
pixmap: &mut PixmapMut<'_>,
left: i32,
top: i32,
width: u32,
height: u32,
mask: &[u8],
color: Color,
) {
if mask.len() < width as usize * height as usize {
return;
}
let Some((columns, columns_end, rows, rows_end)) = overlap(pixmap, left, top, width, height)
else {
return;
};
let stride = pixmap.width() as usize;
let data = pixmap.data_mut();
for row in rows..rows_end {
for column in columns..columns_end {
let coverage = mask[row * width as usize + column];
if coverage == 0 {
continue;
}
let alpha = mul(color.a, coverage);
let source = [
mul(color.r, alpha),
mul(color.g, alpha),
mul(color.b, alpha),
alpha,
];
let x = (i64::from(left) + column as i64) as usize;
let y = (i64::from(top) + row as i64) as usize;
over(data, (y * stride + x) * 4, source);
}
}
}
const SAMPLES: usize = 8;
struct BlurredBox {
middle: (f32, f32),
half: (f32, f32),
radius: f32,
sigma: f32,
}
impl BlurredBox {
fn new(shape: tiny_skia::Rect, radius: f32, sigma: f32) -> BlurredBox {
BlurredBox {
middle: (
(shape.left() + shape.right()) / 2.0,
(shape.top() + shape.bottom()) / 2.0,
),
half: (shape.width() / 2.0, shape.height() / 2.0),
radius,
sigma,
}
}
fn row(&self, y: f32, xs: impl Iterator<Item = f32>, mask: &mut Vec<u8>) {
let y = y - self.middle.1;
let (half_width, half_height) = self.half;
let reach = 3.0 * self.sigma;
let down = self.along(y, half_height);
let clear_of_corners = y.abs() <= half_height - self.radius - reach;
let rows = if clear_of_corners {
[(0.0, 0.0); SAMPLES]
} else {
self.rows_near(y)
};
for x in xs {
let x = x - self.middle.0;
let coverage = if clear_of_corners || x.abs() <= half_width - self.radius - reach {
self.along(x, half_width) * down
} else {
let sum: f32 = rows
.iter()
.map(|&(span, weight)| self.along(x, span) * weight)
.sum();
sum.clamp(0.0, 1.0)
};
mask.push((coverage * 255.0).round() as u8);
}
}
fn along(&self, p: f32, half: f32) -> f32 {
if p.abs() <= half - 3.0 * self.sigma {
return 1.0;
}
let scale = std::f32::consts::FRAC_1_SQRT_2 / self.sigma;
0.5 * (erf((p + half) * scale) - erf((p - half) * scale))
}
fn rows_near(&self, y: f32) -> [(f32, f32); SAMPLES] {
let (half_width, half_height) = self.half;
let reach = 3.0 * self.sigma;
let start = (y - half_height).clamp(-reach, reach);
let end = (y + half_height).clamp(-reach, reach);
let step = (end - start) / SAMPLES as f32;
std::array::from_fn(|sample| {
let offset = start + step * (sample as f32 + 0.5);
let row = y - offset;
let into_corner = (row.abs() - (half_height - self.radius)).max(0.0);
let span = half_width - self.radius
+ (self.radius * self.radius - into_corner * into_corner)
.max(0.0)
.sqrt();
(span, gaussian(offset, self.sigma) * step)
})
}
}
fn gaussian(x: f32, sigma: f32) -> f32 {
let root_two_pi = (2.0 * std::f32::consts::PI).sqrt();
(-(x * x) / (2.0 * sigma * sigma)).exp() / (root_two_pi * sigma)
}
fn erf(x: f32) -> f32 {
let a = x.abs();
let d = 1.0 + a * (0.278_393 + a * (0.230_389 + a * (0.000_972 + a * 0.078_108)));
let d = d * d;
(1.0 - 1.0 / (d * d)).copysign(x)
}
fn is_color_outline(image: &SwashImage) -> bool {
format!("{:?}", image.source).starts_with("ColorOutline")
}
fn blend_rgba(
pixmap: &mut PixmapMut<'_>,
left: i32,
top: i32,
width: u32,
height: u32,
(rgba, premultiplied): (&[u8], bool),
opacity: u8,
) {
if rgba.len() < width as usize * height as usize * 4 {
return;
}
let Some((columns, columns_end, rows, rows_end)) = overlap(pixmap, left, top, width, height)
else {
return;
};
let stride = pixmap.width() as usize;
let data = pixmap.data_mut();
for row in rows..rows_end {
for column in columns..columns_end {
let index = (row * width as usize + column) * 4;
let alpha = mul(rgba[index + 3], opacity);
if alpha == 0 {
continue;
}
let factor = if premultiplied { opacity } else { alpha };
let source = [
mul(rgba[index], factor),
mul(rgba[index + 1], factor),
mul(rgba[index + 2], factor),
alpha,
];
let x = (i64::from(left) + column as i64) as usize;
let y = (i64::from(top) + row as i64) as usize;
over(data, (y * stride + x) * 4, source);
}
}
}