Resize raster icons to physical draw size
This commit is contained in:
parent
cdd825b953
commit
1c5171aff2
4 changed files with 242 additions and 25 deletions
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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<Icon> 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<Message> Widget<Message, crate::Theme, crate::Renderer> for RasterIcon {
|
||||
fn size(&self) -> Size<Length> {
|
||||
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<Message> From<RasterIcon> 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<iced::advanced::image::Handle> {
|
||||
type CacheKey = (iced::advanced::image::Id, u32, u32);
|
||||
|
||||
static CACHE: OnceLock<Mutex<HashMap<CacheKey, iced::advanced::image::Handle>>> =
|
||||
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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue