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    pub(super) size: u16,
47    content_fit: ContentFit,
48    #[setters(strip_option)]
49    width: Option<Length>,
50    #[setters(strip_option)]
51    height: Option<Length>,
52    #[setters(strip_option)]
53    rotation: Option<Rotation>,
54}
55
56impl Icon {
57    #[must_use]
58    pub fn into_svg_handle(self) -> Option<crate::widget::svg::Handle> {
59        match self.handle.data {
60            Data::Name(named) => {
61                if let Some(path) = named.path() {
62                    if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
63                        return Some(iced_core::svg::Handle::from_path(path));
64                    }
65                }
66            }
67
68            Data::Image(_) => (),
69            Data::Svg(handle) => return Some(handle),
70        }
71
72        None
73    }
74
75    #[must_use]
76    fn view<'a, Message: 'a>(self) -> Element<'a, Message> {
77        let from_image = |handle| {
78            Image::new(handle)
79                .width(
80                    self.width
81                        .unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
82                )
83                .height(
84                    self.height
85                        .unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
86                )
87                .rotation(self.rotation.unwrap_or_default())
88                .content_fit(self.content_fit)
89                .into()
90        };
91
92        let from_svg = |handle| {
93            Svg::<crate::Theme>::new(handle)
94                .class(self.class.clone())
95                .width(
96                    self.width
97                        .unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
98                )
99                .height(
100                    self.height
101                        .unwrap_or_else(|| Length::Fixed(f32::from(self.size))),
102                )
103                .rotation(self.rotation.unwrap_or_default())
104                .content_fit(self.content_fit)
105                .symbolic(self.handle.symbolic)
106                .into()
107        };
108
109        match self.handle.data {
110            Data::Name(named) => {
111                if let Some(path) = named.path() {
112                    if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
113                        from_svg(iced_core::svg::Handle::from_path(path))
114                    } else {
115                        from_image(iced_core::image::Handle::from_path(path))
116                    }
117                } else {
118                    let bytes: &'static [u8] = &[];
119                    from_svg(iced_core::svg::Handle::from_memory(bytes))
120                }
121            }
122
123            Data::Image(handle) => from_image(handle),
124            Data::Svg(handle) => from_svg(handle),
125        }
126    }
127}
128
129impl<'a, Message: 'a> From<Icon> for Element<'a, Message> {
130    fn from(icon: Icon) -> Self {
131        icon.view::<Message>()
132    }
133}
134
135/// Draw an icon in the given bounds via the runtime's renderer.
136pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectangle) {
137    enum IcedHandle {
138        Svg(iced_core::svg::Handle),
139        Image(iced_core::image::Handle),
140    }
141
142    let iced_handle = match handle.clone().data {
143        Data::Name(named) => named.path().map(|path| {
144            if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) {
145                IcedHandle::Svg(iced_core::svg::Handle::from_path(path))
146            } else {
147                IcedHandle::Image(iced_core::image::Handle::from_path(path))
148            }
149        }),
150
151        Data::Image(handle) => Some(IcedHandle::Image(handle)),
152        Data::Svg(handle) => Some(IcedHandle::Svg(handle)),
153    };
154
155    match iced_handle {
156        Some(IcedHandle::Svg(handle)) => iced_core::svg::Renderer::draw_svg(
157            renderer,
158            iced_core::svg::Svg::new(handle),
159            icon_bounds,
160        ),
161
162        Some(IcedHandle::Image(handle)) => {
163            iced_core::image::Renderer::draw_image(
164                renderer,
165                handle,
166                iced_core::image::FilterMethod::Linear,
167                icon_bounds,
168                iced_core::Radians::from(0),
169                1.0,
170                [0.0; 4],
171            );
172        }
173
174        None => {}
175    }
176}