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"
|
url = "2.5.8"
|
||||||
zbus = { version = "5.14.0", default-features = false, optional = true }
|
zbus = { version = "5.14.0", default-features = false, optional = true }
|
||||||
float-cmp = "0.10.0"
|
float-cmp = "0.10.0"
|
||||||
|
fast_image_resize = { version = "5.5.0", features = ["image"] }
|
||||||
|
|
||||||
# Enable DBus feature on Linux targets
|
# Enable DBus feature on Linux targets
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -666,7 +666,7 @@ where
|
||||||
};
|
};
|
||||||
|
|
||||||
bounds.x += 24.0;
|
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(
|
text::Renderer::fill_text(
|
||||||
|
|
|
||||||
|
|
@ -302,7 +302,7 @@ where
|
||||||
tree: &Tree,
|
tree: &Tree,
|
||||||
renderer: &mut crate::Renderer,
|
renderer: &mut crate::Renderer,
|
||||||
theme: &crate::Theme,
|
theme: &crate::Theme,
|
||||||
_style: &iced_core::renderer::Style,
|
style: &iced_core::renderer::Style,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
viewport: &Rectangle,
|
viewport: &Rectangle,
|
||||||
|
|
@ -311,6 +311,7 @@ where
|
||||||
draw(
|
draw(
|
||||||
renderer,
|
renderer,
|
||||||
theme,
|
theme,
|
||||||
|
style,
|
||||||
layout,
|
layout,
|
||||||
cursor,
|
cursor,
|
||||||
self.gap,
|
self.gap,
|
||||||
|
|
@ -863,6 +864,7 @@ where
|
||||||
pub fn draw<'a, S>(
|
pub fn draw<'a, S>(
|
||||||
renderer: &mut crate::Renderer,
|
renderer: &mut crate::Renderer,
|
||||||
theme: &crate::Theme,
|
theme: &crate::Theme,
|
||||||
|
renderer_style: &iced_core::renderer::Style,
|
||||||
layout: Layout<'_>,
|
layout: Layout<'_>,
|
||||||
cursor: mouse::Cursor,
|
cursor: mouse::Cursor,
|
||||||
gap: f32,
|
gap: f32,
|
||||||
|
|
@ -928,7 +930,12 @@ pub fn draw<'a, S>(
|
||||||
};
|
};
|
||||||
|
|
||||||
bounds.x += 24.0;
|
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(
|
text::Renderer::fill_text(
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,27 @@
|
||||||
mod bundle;
|
mod bundle;
|
||||||
mod named;
|
mod named;
|
||||||
use std::sync::Arc;
|
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};
|
pub use named::{IconFallback, Named};
|
||||||
|
|
||||||
mod handle;
|
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 crate::Element;
|
||||||
use derive_setters::Setters;
|
use derive_setters::Setters;
|
||||||
use iced::widget::{Image, Svg};
|
use iced::widget::{image as image_widget, Svg};
|
||||||
use iced::{ContentFit, Length, Radians, Rectangle};
|
use iced::{ContentFit, Length, Radians, Rectangle, Size};
|
||||||
use iced_core::Rotation;
|
use iced_core::widget::Tree;
|
||||||
|
use iced_core::{layout, mouse, renderer, Layout, Rotation, Widget};
|
||||||
|
|
||||||
/// Create an [`Icon`] from a pre-existing [`Handle`]
|
/// Create an [`Icon`] from a pre-existing [`Handle`]
|
||||||
pub fn icon(handle: Handle) -> Icon {
|
pub fn icon(handle: Handle) -> Icon {
|
||||||
|
|
@ -74,18 +84,18 @@ impl Icon {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
fn view<'a, Message: 'a>(self) -> Element<'a, Message> {
|
fn view<'a, Message: 'a>(self) -> Element<'a, Message> {
|
||||||
let from_image = |handle| {
|
let from_image = |handle| {
|
||||||
Image::new(handle)
|
RasterIcon {
|
||||||
.width(
|
handle,
|
||||||
self.width
|
width: self
|
||||||
.unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
|
.width
|
||||||
)
|
.unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
|
||||||
.height(
|
height: self
|
||||||
self.height
|
.height
|
||||||
.unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
|
.unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
|
||||||
)
|
content_fit: self.content_fit,
|
||||||
.rotation(self.rotation.unwrap_or_default())
|
rotation: self.rotation.unwrap_or_default(),
|
||||||
.content_fit(self.content_fit)
|
}
|
||||||
.into()
|
.into()
|
||||||
};
|
};
|
||||||
|
|
||||||
let from_svg = |handle| {
|
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.
|
/// Draw an icon in the given bounds via the runtime's renderer.
|
||||||
pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectangle) {
|
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 {
|
match handle.clone().data {
|
||||||
Data::Svg(handle) => iced_core::svg::Renderer::draw_svg(
|
Data::Svg(handle) => {
|
||||||
renderer,
|
iced_core::svg::Renderer::draw_svg(
|
||||||
iced_core::svg::Svg::new(handle),
|
renderer,
|
||||||
icon_bounds,
|
iced_core::svg::Svg::new(handle),
|
||||||
icon_bounds,
|
icon_bounds,
|
||||||
),
|
icon_bounds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Data::Image(handle) => {
|
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(
|
iced_core::image::Renderer::draw_image(
|
||||||
renderer,
|
renderer,
|
||||||
iced_core::Image {
|
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