From 8a6f519166d1930afd975772c1fb091b6d12b13e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 27 Oct 2023 16:35:22 +0200 Subject: [PATCH] feat: image border radius rendering --- core/src/image.rs | 13 +++- graphics/src/backend.rs | 6 +- graphics/src/primitive.rs | 2 + graphics/src/renderer.rs | 21 ++++-- renderer/src/lib.rs | 21 ++++-- tiny_skia/src/backend.rs | 24 +++++-- tiny_skia/src/raster.rs | 144 +++++++++++++++++++++++++++++++++++-- widget/src/image.rs | 26 +++++-- widget/src/image/viewer.rs | 6 +- 9 files changed, 236 insertions(+), 27 deletions(-) diff --git a/core/src/image.rs b/core/src/image.rs index 85d9d4758c..c27570f7d6 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -174,9 +174,18 @@ pub trait Renderer: crate::Renderer { type Handle: Clone + Hash; /// Returns the dimensions of an image for the given [`Handle`]. - fn dimensions(&self, handle: &Self::Handle) -> Size; + fn dimensions( + &self, + handle: &Self::Handle, + border_radius: [f32; 4], + ) -> Size; /// Draws an image with the given [`Handle`] and inside the provided /// `bounds`. - fn draw(&mut self, handle: Self::Handle, bounds: Rectangle); + fn draw( + &mut self, + handle: Self::Handle, + bounds: Rectangle, + border_radius: [f32; 4], + ); } diff --git a/graphics/src/backend.rs b/graphics/src/backend.rs index 59e95bf8b3..8fe71c1f3c 100644 --- a/graphics/src/backend.rs +++ b/graphics/src/backend.rs @@ -81,7 +81,11 @@ pub trait Text { /// A graphics backend that supports image rendering. pub trait Image { /// Returns the dimensions of the provided image. - fn dimensions(&self, handle: &image::Handle) -> Size; + fn dimensions( + &self, + handle: &image::Handle, + border_radius: [f32; 4], + ) -> Size; } /// A graphics backend that supports SVG rendering. diff --git a/graphics/src/primitive.rs b/graphics/src/primitive.rs index 7592a41067..cca7dacec0 100644 --- a/graphics/src/primitive.rs +++ b/graphics/src/primitive.rs @@ -50,6 +50,8 @@ pub enum Primitive { handle: image::Handle, /// The bounds of the image bounds: Rectangle, + /// The border radii of the image + border_radius: [f32; 4], }, /// An SVG primitive Svg { diff --git a/graphics/src/renderer.rs b/graphics/src/renderer.rs index c0cec60aad..2374e74af8 100644 --- a/graphics/src/renderer.rs +++ b/graphics/src/renderer.rs @@ -222,12 +222,25 @@ where { type Handle = image::Handle; - fn dimensions(&self, handle: &image::Handle) -> Size { - self.backend().dimensions(handle) + fn dimensions( + &self, + handle: &image::Handle, + border_radius: [f32; 4], + ) -> Size { + self.backend().dimensions(handle, border_radius) } - fn draw(&mut self, handle: image::Handle, bounds: Rectangle) { - self.primitives.push(Primitive::Image { handle, bounds }) + fn draw( + &mut self, + handle: image::Handle, + bounds: Rectangle, + border_radius: [f32; 4], + ) { + self.primitives.push(Primitive::Image { + handle, + bounds, + border_radius, + }) } } diff --git a/renderer/src/lib.rs b/renderer/src/lib.rs index 7d1a02c245..42a2e68e70 100644 --- a/renderer/src/lib.rs +++ b/renderer/src/lib.rs @@ -212,12 +212,25 @@ impl text::Renderer for Renderer { impl crate::core::image::Renderer for Renderer { type Handle = crate::core::image::Handle; - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size { - delegate!(self, renderer, renderer.dimensions(handle)) + fn dimensions( + &self, + handle: &crate::core::image::Handle, + border_radius: [f32; 4], + ) -> Size { + delegate!(self, renderer, renderer.dimensions(handle, border_radius)) } - fn draw(&mut self, handle: crate::core::image::Handle, bounds: Rectangle) { - delegate!(self, renderer, renderer.draw(handle, bounds)); + fn draw( + &mut self, + handle: crate::core::image::Handle, + bounds: Rectangle, + border_radius: [f32; 4], + ) { + delegate!( + self, + renderer, + renderer.draw(handle, bounds, border_radius,) + ); } } diff --git a/tiny_skia/src/backend.rs b/tiny_skia/src/backend.rs index 91e57b6eda..d0d2e0e333 100644 --- a/tiny_skia/src/backend.rs +++ b/tiny_skia/src/backend.rs @@ -402,7 +402,11 @@ impl Backend { ); } #[cfg(feature = "image")] - Primitive::Image { handle, bounds } => { + Primitive::Image { + handle, + bounds, + border_radius, + } => { let physical_bounds = (*bounds + translation) * scale_factor; if !clip_bounds.intersects(&physical_bounds) { @@ -418,8 +422,14 @@ impl Backend { ) .post_scale(scale_factor, scale_factor); - self.raster_pipeline - .draw(handle, *bounds, pixels, transform, clip_mask); + self.raster_pipeline.draw( + handle, + *bounds, + pixels, + transform, + clip_mask, + *border_radius, + ); } #[cfg(not(feature = "image"))] Primitive::Image { .. } => { @@ -841,8 +851,12 @@ impl backend::Text for Backend { #[cfg(feature = "image")] impl backend::Image for Backend { - fn dimensions(&self, handle: &crate::core::image::Handle) -> Size { - self.raster_pipeline.dimensions(handle) + fn dimensions( + &self, + handle: &crate::core::image::Handle, + border_radius: [f32; 4], + ) -> Size { + self.raster_pipeline.dimensions(handle, border_radius) } } diff --git a/tiny_skia/src/raster.rs b/tiny_skia/src/raster.rs index dedb127c6a..48e444f971 100644 --- a/tiny_skia/src/raster.rs +++ b/tiny_skia/src/raster.rs @@ -2,6 +2,7 @@ use crate::core::image as raster; use crate::core::{Rectangle, Size}; use crate::graphics; +use graphics::image::image_rs::{ImageBuffer, Rgba, RgbaImage}; use rustc_hash::{FxHashMap, FxHashSet}; use std::cell::RefCell; use std::collections::hash_map; @@ -17,8 +18,14 @@ impl Pipeline { } } - pub fn dimensions(&self, handle: &raster::Handle) -> Size { - if let Some(image) = self.cache.borrow_mut().allocate(handle) { + pub fn dimensions( + &self, + handle: &raster::Handle, + border_radius: [f32; 4], + ) -> Size { + if let Some(image) = + self.cache.borrow_mut().allocate(handle, border_radius) + { Size::new(image.width(), image.height()) } else { Size::new(0, 0) @@ -32,8 +39,11 @@ impl Pipeline { pixels: &mut tiny_skia::PixmapMut<'_>, transform: tiny_skia::Transform, clip_mask: Option<&tiny_skia::Mask>, + border_radius: [f32; 4], ) { - if let Some(image) = self.cache.borrow_mut().allocate(handle) { + if let Some(image) = + self.cache.borrow_mut().allocate(handle, border_radius) + { let width_scale = bounds.width / image.width() as f32; let height_scale = bounds.height / image.height() as f32; @@ -68,11 +78,20 @@ impl Cache { pub fn allocate( &mut self, handle: &raster::Handle, + border_radius: [f32; 4], ) -> Option> { let id = handle.id(); if let hash_map::Entry::Vacant(entry) = self.entries.entry(id) { - let image = graphics::image::load(handle).ok()?.into_rgba8(); + let mut image = graphics::image::load(handle).ok()?.into_rgba8(); + + // Round the borders if a border radius is defined + if border_radius.iter().any(|&corner| corner != 0.0) { + round(&mut image, { + let [a, b, c, d] = border_radius; + [a as u32, b as u32, c as u32, d as u32] + }); + } let mut buffer = vec![0u32; image.width() as usize * image.height() as usize]; @@ -114,3 +133,120 @@ struct Entry { height: u32, pixels: Vec, } + +// https://users.rust-lang.org/t/how-to-trim-image-to-circle-image-without-jaggy/70374/2 +fn round(img: &mut ImageBuffer, Vec>, radius: [u32; 4]) { + let (width, height) = img.dimensions(); + assert!(radius[0] + radius[1] <= width); + assert!(radius[3] + radius[2] <= width); + assert!(radius[0] + radius[3] <= height); + assert!(radius[1] + radius[2] <= height); + + // top left + border_radius(img, radius[0], |x, y| (x - 1, y - 1)); + // top right + border_radius(img, radius[1], |x, y| (width - x, y - 1)); + // bottom right + border_radius(img, radius[2], |x, y| (width - x, height - y)); + // bottom left + border_radius(img, radius[3], |x, y| (x - 1, height - y)); +} + +fn border_radius( + img: &mut ImageBuffer, Vec>, + r: u32, + coordinates: impl Fn(u32, u32) -> (u32, u32), +) { + if r == 0 { + return; + } + let r0 = r; + + // 16x antialiasing: 16x16 grid creates 256 possible shades, great for u8! + let r = 16 * r; + + let mut x = 0; + let mut y = r - 1; + let mut p: i32 = 2 - r as i32; + + // ... + + let mut alpha: u16 = 0; + let mut skip_draw = true; + + let draw = |img: &mut RgbaImage, alpha, x, y| { + debug_assert!((1..=256).contains(&alpha)); + let pixel_alpha = &mut img[coordinates(r0 - x, r0 - y)].0[3]; + *pixel_alpha = ((alpha * *pixel_alpha as u16 + 128) / 256) as u8; + }; + + 'l: loop { + // (comments for bottom_right case:) + // remove contents below current position + { + let i = x / 16; + for j in y / 16 + 1..r0 { + img[coordinates(r0 - i, r0 - j)].0[3] = 0; + } + } + // remove contents right of current position mirrored + { + let j = x / 16; + for i in y / 16 + 1..r0 { + img[coordinates(r0 - i, r0 - j)].0[3] = 0; + } + } + + // draw when moving to next pixel in x-direction + if !skip_draw { + draw(img, alpha, x / 16 - 1, y / 16); + draw(img, alpha, y / 16, x / 16 - 1); + alpha = 0; + } + + for _ in 0..16 { + skip_draw = false; + + if x >= y { + break 'l; + } + + alpha += y as u16 % 16 + 1; + if p < 0 { + x += 1; + p += (2 * x + 2) as i32; + } else { + // draw when moving to next pixel in y-direction + if y % 16 == 0 { + draw(img, alpha, x / 16, y / 16); + draw(img, alpha, y / 16, x / 16); + skip_draw = true; + alpha = (x + 1) as u16 % 16 * 16; + } + + x += 1; + p -= (2 * (y - x) + 2) as i32; + y -= 1; + } + } + } + + // one corner pixel left + if x / 16 == y / 16 { + // column under current position possibly not yet accounted + if x == y { + alpha += y as u16 % 16 + 1; + } + let s = y as u16 % 16 + 1; + let alpha = 2 * alpha - s * s; + draw(img, alpha, x / 16, y / 16); + } + + // remove remaining square of content in the corner + let range = y / 16 + 1..r0; + for i in range.clone() { + for j in range.clone() { + img[coordinates(r0 - i, r0 - j)].0[3] = 0; + } + } +} diff --git a/widget/src/image.rs b/widget/src/image.rs index 406bc3c380..c9dac5654a 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -12,7 +12,6 @@ use crate::core::{ ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, }; -use std::borrow::Cow; use std::hash::Hash; pub use image::Handle; @@ -46,6 +45,7 @@ pub struct Image<'a, Handle> { width: Length, height: Length, content_fit: ContentFit, + border_radius: [f32; 4], phantom_data: std::marker::PhantomData<&'a ()>, } @@ -64,10 +64,17 @@ impl<'a, Handle> Image<'a, Handle> { width: Length::Shrink, height: Length::Shrink, content_fit: ContentFit::Contain, + border_radius: [0.0; 4], phantom_data: std::marker::PhantomData, } } + /// Sets the border radius of the image. + pub fn border_radius(mut self, border_radius: [f32; 4]) -> Self { + self.border_radius = border_radius; + self + } + /// Sets the width of the [`Image`] boundaries. pub fn width(mut self, width: impl Into) -> Self { self.width = width.into(); @@ -134,13 +141,14 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, + border_radius: [f32; 4], ) -> layout::Node where Renderer: image::Renderer, { // The raw w/h of the underlying image let image_size = { - let Size { width, height } = renderer.dimensions(handle); + let Size { width, height } = renderer.dimensions(handle, border_radius); Size::new(width as f32, height as f32) }; @@ -172,11 +180,12 @@ pub fn draw( layout: Layout<'_>, handle: &Handle, content_fit: ContentFit, + border_radius: [f32; 4], ) where Renderer: image::Renderer, Handle: Clone + Hash, { - let Size { width, height } = renderer.dimensions(handle); + let Size { width, height } = renderer.dimensions(handle, border_radius); let image_size = Size::new(width as f32, height as f32); let bounds = layout.bounds(); @@ -194,7 +203,7 @@ pub fn draw( ..bounds }; - renderer.draw(handle.clone(), drawing_bounds + offset) + renderer.draw(handle.clone(), drawing_bounds + offset, border_radius); }; if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height @@ -231,6 +240,7 @@ where self.width, self.height, self.content_fit, + self.border_radius, ) } @@ -244,7 +254,13 @@ where _cursor: mouse::Cursor, _viewport: &Rectangle, ) { - draw(renderer, layout, &self.handle, self.content_fit) + draw( + renderer, + layout, + &self.handle, + self.content_fit, + self.border_radius, + ) } #[cfg(feature = "a11y")] diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 6e09566793..692dcece70 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -108,7 +108,8 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let Size { width, height } = renderer.dimensions(&self.handle); + let Size { width, height } = + renderer.dimensions(&self.handle, [0.0; 4]); let mut size = limits .width(self.width) @@ -333,6 +334,7 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + [0.0; 4], ) }); }); @@ -410,7 +412,7 @@ pub fn image_size( where Renderer: image::Renderer, { - let Size { width, height } = renderer.dimensions(handle); + let Size { width, height } = renderer.dimensions(handle, [0.0; 4]); let (width, height) = { let dimensions = (width as f32, height as f32);