diff --git a/Cargo.toml b/Cargo.toml index bdbc141b..9534b43c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,6 +163,7 @@ unicode-segmentation = "1.12" url = "2.5.8" zbus = { version = "5.14.0", default-features = false, optional = true } float-cmp = "0.10.0" +fast_image_resize = { version = "5.5.0", features = ["image"] } # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 0c96c1c6..d6558c3c 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -666,7 +666,7 @@ where }; bounds.x += 24.0; - icon::draw(renderer, handle, icon_bounds); + icon::draw_with_scale(renderer, handle, icon_bounds, style.scale_factor as f32); } text::Renderer::fill_text( diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 2ff9c92f..7c8171f6 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -302,7 +302,7 @@ where tree: &Tree, renderer: &mut crate::Renderer, theme: &crate::Theme, - _style: &iced_core::renderer::Style, + style: &iced_core::renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, viewport: &Rectangle, @@ -311,6 +311,7 @@ where draw( renderer, theme, + style, layout, cursor, self.gap, @@ -863,6 +864,7 @@ where pub fn draw<'a, S>( renderer: &mut crate::Renderer, theme: &crate::Theme, + renderer_style: &iced_core::renderer::Style, layout: Layout<'_>, cursor: mouse::Cursor, gap: f32, @@ -928,7 +930,12 @@ pub fn draw<'a, S>( }; bounds.x += 24.0; - icon::draw(renderer, handle, icon_bounds); + icon::draw_with_scale( + renderer, + handle, + icon_bounds, + renderer_style.scale_factor as f32, + ); } text::Renderer::fill_text( diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 031b4b0c..4a24ab35 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -6,17 +6,27 @@ mod bundle; mod named; use std::sync::Arc; +use std::{ + collections::HashMap, + sync::{Mutex, OnceLock}, +}; + +use fast_image_resize::images::Image as FrImage; +use fast_image_resize::{ + create_srgb_mapper, FilterType, MulDiv, PixelType, ResizeAlg, ResizeOptions, Resizer, +}; pub use named::{IconFallback, Named}; mod handle; -pub use handle::{Data, Handle, from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes}; +pub use handle::{from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes, Data, Handle}; use crate::Element; use derive_setters::Setters; -use iced::widget::{Image, Svg}; -use iced::{ContentFit, Length, Radians, Rectangle}; -use iced_core::Rotation; +use iced::widget::{image as image_widget, Svg}; +use iced::{ContentFit, Length, Radians, Rectangle, Size}; +use iced_core::widget::Tree; +use iced_core::{layout, mouse, renderer, Layout, Rotation, Widget}; /// Create an [`Icon`] from a pre-existing [`Handle`] pub fn icon(handle: Handle) -> Icon { @@ -74,18 +84,18 @@ impl Icon { #[must_use] fn view<'a, Message: 'a>(self) -> Element<'a, Message> { let from_image = |handle| { - Image::new(handle) - .width( - self.width - .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), - ) - .height( - self.height - .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), - ) - .rotation(self.rotation.unwrap_or_default()) - .content_fit(self.content_fit) - .into() + RasterIcon { + handle, + width: self + .width + .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), + height: self + .height + .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), + content_fit: self.content_fit, + rotation: self.rotation.unwrap_or_default(), + } + .into() }; let from_svg = |handle| { @@ -120,15 +130,32 @@ impl<'a, Message: 'a> From for Element<'a, Message> { /// Draw an icon in the given bounds via the runtime's renderer. pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectangle) { + draw_with_scale(renderer, handle, icon_bounds, 1.0); +} + +/// Draw an icon in the given bounds via the runtime's renderer. +pub fn draw_with_scale( + renderer: &mut crate::Renderer, + handle: &Handle, + icon_bounds: Rectangle, + scale_factor: f32, +) { match handle.clone().data { - Data::Svg(handle) => iced_core::svg::Renderer::draw_svg( - renderer, - iced_core::svg::Svg::new(handle), - icon_bounds, - icon_bounds, - ), + Data::Svg(handle) => { + iced_core::svg::Renderer::draw_svg( + renderer, + iced_core::svg::Svg::new(handle), + icon_bounds, + icon_bounds, + ); + } Data::Image(handle) => { + let physical_width = (icon_bounds.width * scale_factor).ceil().max(1.0) as u32; + let physical_height = (icon_bounds.height * scale_factor).ceil().max(1.0) as u32; + let handle = + resized_raster_handle(&handle, physical_width, physical_height).unwrap_or(handle); + iced_core::image::Renderer::draw_image( renderer, iced_core::Image { @@ -145,3 +172,185 @@ pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectan } } } + +#[derive(Clone)] +struct RasterIcon { + handle: image_widget::Handle, + width: Length, + height: Length, + content_fit: ContentFit, + rotation: Rotation, +} + +impl Widget for RasterIcon { + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &mut self, + _tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + image_widget::layout( + renderer, + limits, + &self.handle, + self.width, + self.height, + None, + self.content_fit, + self.rotation, + false, + [0.0; 4], + ) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut crate::Renderer, + _theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let scale_factor = style.scale_factor as f32; + let physical_width = (bounds.width * scale_factor).ceil().max(1.0) as u32; + let physical_height = (bounds.height * scale_factor).ceil().max(1.0) as u32; + let handle = resized_raster_handle(&self.handle, physical_width, physical_height) + .unwrap_or_else(|| self.handle.clone()); + + image_widget::draw( + renderer, + layout, + &handle, + None, + [0.0; 4].into(), + self.content_fit, + image_widget::FilterMethod::Linear, + self.rotation, + 1.0, + 1.0, + ); + } +} + +impl From for Element<'_, Message> { + fn from(icon: RasterIcon) -> Self { + Element::new(icon) + } +} + +#[cfg(test)] +mod tests { + use super::resized_raster_handle; + + #[test] + fn lanczos_resize_produces_requested_dimensions() { + let handle = iced::advanced::image::Handle::from_rgba( + 2, + 2, + vec![ + 255, 0, 0, 255, 0, 255, 0, 255, 0, 0, 255, 255, 255, 255, 255, 255, + ], + ); + + let resized = resized_raster_handle(&handle, 6, 5).expect("resized handle"); + + match resized { + iced::advanced::image::Handle::Rgba { width, height, .. } => { + assert_eq!((width, height), (6, 5)); + } + other => panic!("unexpected handle: {other:?}"), + } + } + + #[test] + fn premultiplied_resize_avoids_transparent_color_bleed() { + let handle = iced::advanced::image::Handle::from_rgba( + 2, + 1, + vec![ + 255, 0, 0, 255, // + 0, 0, 255, 0, + ], + ); + + let resized = resized_raster_handle(&handle, 1, 1).expect("resized handle"); + let pixels = crate::iced::advanced::graphics::image::load(&resized) + .expect("resized image") + .into_raw(); + + assert_eq!(pixels.len(), 4); + assert!( + pixels[0] > pixels[2], + "expected red channel to dominate: {pixels:?}" + ); + assert!(pixels[3] > 0, "expected non-zero alpha: {pixels:?}"); + } +} + +fn resized_raster_handle( + handle: &iced::advanced::image::Handle, + width: u32, + height: u32, +) -> Option { + type CacheKey = (iced::advanced::image::Id, u32, u32); + + static CACHE: OnceLock>> = + OnceLock::new(); + + let cache = CACHE.get_or_init(|| Mutex::new(HashMap::new())); + let key = (handle.id(), width, height); + + if let Some(handle) = cache.lock().ok().and_then(|cache| cache.get(&key).cloned()) { + return Some(handle); + } + + let buffer = crate::iced::advanced::graphics::image::load(handle).ok()?; + let (src_width, src_height) = (buffer.width(), buffer.height()); + let raw = buffer.into_raw().to_vec(); + let mut source = FrImage::from_vec_u8(src_width, src_height, raw, PixelType::U8x4).ok()?; + + let mapper = create_srgb_mapper(); + mapper.forward_map_inplace(&mut source).ok()?; + + let mul_div = MulDiv::new(); + mul_div.multiply_alpha_inplace(&mut source).ok()?; + + let upscale = 4; + let hi_width = width.checked_mul(upscale)?; + let hi_height = height.checked_mul(upscale)?; + let mut hi = FrImage::new(hi_width, hi_height, PixelType::U8x4); + let mut resizer = Resizer::new(); + let upsample = + ResizeOptions::new().resize_alg(ResizeAlg::SuperSampling(FilterType::Lanczos3, 4)); + let downsample = ResizeOptions::new().resize_alg(ResizeAlg::Convolution(FilterType::Lanczos3)); + + resizer.resize(&source, &mut hi, Some(&upsample)).ok()?; + + mul_div.divide_alpha_inplace(&mut hi).ok()?; + mapper.backward_map_inplace(&mut hi).ok()?; + + let mut destination = FrImage::new(width, height, PixelType::U8x4); + resizer + .resize(&hi, &mut destination, Some(&downsample)) + .ok()?; + + let resized_pixels = destination.into_vec(); + + let resized = iced::advanced::image::Handle::from_rgba(width, height, resized_pixels); + + if let Ok(mut cache) = cache.lock() { + let _ = cache.insert(key, resized.clone()); + } + + Some(resized) +}