libcosmic/src/widget/icon/mod.rs
2026-04-03 07:47:48 -07:00

356 lines
10 KiB
Rust

// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Lazily-generated SVG icon widget for Iced.
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::{from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes, Data, Handle};
use crate::Element;
use derive_setters::Setters;
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 {
Icon {
content_fit: ContentFit::Fill,
handle,
height: None,
size: 16,
class: crate::theme::Svg::default(),
rotation: None,
width: None,
}
}
/// Create an icon handle from its XDG icon name.
pub fn from_name(name: impl Into<Arc<str>>) -> Named {
Named::new(name)
}
/// An image which may be an SVG or PNG.
#[must_use]
#[derive(Clone, Setters)]
pub struct Icon {
#[setters(skip)]
handle: Handle,
class: crate::theme::Svg,
#[setters(skip)]
pub(super) size: u16,
content_fit: ContentFit,
#[setters(strip_option)]
width: Option<Length>,
#[setters(strip_option)]
height: Option<Length>,
#[setters(strip_option)]
rotation: Option<Rotation>,
}
impl Icon {
#[must_use]
pub fn into_svg_handle(self) -> Option<crate::widget::svg::Handle> {
match self.handle.data {
Data::Image(_) => (),
Data::Svg(handle) => return Some(handle),
}
None
}
#[must_use]
pub fn size(mut self, size: u16) -> Self {
self.size = size;
self
}
#[must_use]
fn view<'a, Message: 'a>(self) -> Element<'a, Message> {
let from_image = |handle| {
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| {
Svg::<crate::Theme>::new(handle)
.class(self.class.clone())
.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)
.symbolic(self.handle.symbolic)
.into()
};
match self.handle.data {
Data::Image(handle) => from_image(handle),
Data::Svg(handle) => from_svg(handle),
}
}
}
impl<'a, Message: 'a> From<Icon> for Element<'a, Message> {
fn from(icon: Icon) -> Self {
icon.view::<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::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 {
handle,
filter_method: iced_core::image::FilterMethod::Linear,
rotation: Radians(0.),
border_radius: [0.0; 4].into(),
opacity: 1.0,
snap: true,
},
icon_bounds,
icon_bounds,
);
}
}
}
#[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)
}