cosmic/widget/icon/
mod.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! Lazily-generated SVG icon widget for Iced.
5
6mod named;
7use std::ffi::OsStr;
8use std::sync::Arc;
9
10pub use named::{IconFallback, Named};
11
12mod handle;
13pub use handle::{Data, Handle, from_path, from_raster_bytes, from_raster_pixels, from_svg_bytes};
14
15use crate::Element;
16use derive_setters::Setters;
17use iced::widget::{Image, Svg};
18use iced::{ContentFit, Length, Rectangle};
19use iced_core::Rotation;
20
21/// Create an [`Icon`] from a pre-existing [`Handle`]
22pub fn icon(handle: Handle) -> Icon {
23    Icon {
24        content_fit: ContentFit::Fill,
25        handle,
26        height: None,
27        size: 16,
28        class: crate::theme::Svg::default(),
29        rotation: None,
30        width: None,
31    }
32}
33
34/// Create an icon handle from its XDG icon name.
35pub fn from_name(name: impl Into<Arc<str>>) -> Named {
36    Named::new(name)
37}
38
39/// An image which may be an SVG or PNG.
40#[must_use]
41#[derive(Clone, Setters)]
42pub struct Icon {
43    #[setters(skip)]
44    handle: Handle,
45    class: crate::theme::Svg,
46    #[setters(skip)]
47    pub(super) size: u16,
48    content_fit: ContentFit,
49    #[setters(strip_option)]
50    width: Option<Length>,
51    #[setters(strip_option)]
52    height: Option<Length>,
53    #[setters(strip_option)]
54    rotation: Option<Rotation>,
55}
56
57impl Icon {
58    #[must_use]
59    pub fn into_svg_handle(self) -> Option<crate::widget::svg::Handle> {
60        match self.handle.data {
61            Data::Name(named) => {
62                if let Some(path) = named.path() {
63                    if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
64                        return Some(iced_core::svg::Handle::from_path(path));
65                    }
66                }
67            }
68
69            Data::Image(_) => (),
70            Data::Svg(handle) => return Some(handle),
71        }
72
73        None
74    }
75
76    #[must_use]
77    pub fn size(mut self, size: u16) -> Self {
78        self.size = size;
79        // ensures correct icon size variant selection
80        if let Data::Name(named) = &self.handle.data {
81            let mut new_named = named.clone();
82            new_named.size = Some(size);
83            self.handle = new_named.handle();
84        }
85        self
86    }
87
88    #[must_use]
89    fn view<'a, Message: 'a>(self) -> Element<'a, Message> {
90        let from_image = |handle| {
91            Image::new(handle)
92                .width(
93                    self.width
94                        .unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
95                )
96                .height(
97                    self.height
98                        .unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
99                )
100                .rotation(self.rotation.unwrap_or_default())
101                .content_fit(self.content_fit)
102                .into()
103        };
104
105        let from_svg = |handle| {
106            Svg::<crate::Theme>::new(handle)
107                .class(self.class.clone())
108                .width(
109                    self.width
110                        .unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
111                )
112                .height(
113                    self.height
114                        .unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
115                )
116                .rotation(self.rotation.unwrap_or_default())
117                .content_fit(self.content_fit)
118                .symbolic(self.handle.symbolic)
119                .into()
120        };
121
122        match self.handle.data {
123            Data::Name(named) => {
124                if let Some(path) = named.path() {
125                    if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
126                        from_svg(iced_core::svg::Handle::from_path(path))
127                    } else {
128                        from_image(iced_core::image::Handle::from_path(path))
129                    }
130                } else {
131                    let bytes: &'static [u8] = &[];
132                    from_svg(iced_core::svg::Handle::from_memory(bytes))
133                }
134            }
135
136            Data::Image(handle) => from_image(handle),
137            Data::Svg(handle) => from_svg(handle),
138        }
139    }
140}
141
142impl<'a, Message: 'a> From<Icon> for Element<'a, Message> {
143    fn from(icon: Icon) -> Self {
144        icon.view::<Message>()
145    }
146}
147
148/// Draw an icon in the given bounds via the runtime's renderer.
149pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectangle) {
150    enum IcedHandle {
151        Svg(iced_core::svg::Handle),
152        Image(iced_core::image::Handle),
153    }
154
155    let iced_handle = match handle.clone().data {
156        Data::Name(named) => named.path().map(|path| {
157            if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
158                IcedHandle::Svg(iced_core::svg::Handle::from_path(path))
159            } else {
160                IcedHandle::Image(iced_core::image::Handle::from_path(path))
161            }
162        }),
163
164        Data::Image(handle) => Some(IcedHandle::Image(handle)),
165        Data::Svg(handle) => Some(IcedHandle::Svg(handle)),
166    };
167
168    match iced_handle {
169        Some(IcedHandle::Svg(handle)) => iced_core::svg::Renderer::draw_svg(
170            renderer,
171            iced_core::svg::Svg::new(handle),
172            icon_bounds,
173        ),
174
175        Some(IcedHandle::Image(handle)) => {
176            iced_core::image::Renderer::draw_image(
177                renderer,
178                handle,
179                iced_core::image::FilterMethod::Linear,
180                icon_bounds,
181                iced_core::Radians::from(0),
182                1.0,
183                [0.0; 4],
184            );
185        }
186
187        None => {}
188    }
189}