iced_widget/
svg.rs

1//! Svg widgets display vector graphics in your application.
2//!
3//! # Example
4//! ```no_run
5//! # mod iced { pub mod widget { pub use iced_widget::*; } }
6//! # pub type State = ();
7//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
8//! use iced::widget::svg;
9//!
10//! enum Message {
11//!     // ...
12//! }
13//!
14//! fn view(state: &State) -> Element<'_, Message> {
15//!     svg("tiger.svg").into()
16//! }
17//! ```
18//! Display vector graphics in your application.
19use iced_runtime::core::widget::Id;
20
21use crate::core::layout;
22use crate::core::mouse;
23use crate::core::renderer;
24use crate::core::svg;
25use crate::core::widget::Tree;
26use crate::core::{
27    Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation,
28    Size, Theme, Vector, Widget,
29};
30
31#[cfg(feature = "a11y")]
32use std::borrow::Cow;
33use std::marker::PhantomData;
34use std::path::PathBuf;
35
36pub use crate::core::svg::Handle;
37
38/// A vector graphics image.
39///
40/// An [`Svg`] image resizes smoothly without losing any quality.
41///
42/// [`Svg`] images can have a considerable rendering cost when resized,
43/// specially when they are complex.
44///
45/// # Example
46/// ```no_run
47/// # mod iced { pub mod widget { pub use iced_widget::*; } }
48/// # pub type State = ();
49/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
50/// use iced::widget::svg;
51///
52/// enum Message {
53///     // ...
54/// }
55///
56/// fn view(state: &State) -> Element<'_, Message> {
57///     svg("tiger.svg").into()
58/// }
59/// ```
60#[allow(missing_debug_implementations)]
61pub struct Svg<'a, Theme = crate::Theme>
62where
63    Theme: Catalog,
64{
65    id: Id,
66    #[cfg(feature = "a11y")]
67    name: Option<Cow<'a, str>>,
68    #[cfg(feature = "a11y")]
69    description: Option<iced_accessibility::Description<'a>>,
70    #[cfg(feature = "a11y")]
71    label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
72    handle: Handle,
73    width: Length,
74    height: Length,
75    content_fit: ContentFit,
76    class: Theme::Class<'a>,
77    rotation: Rotation,
78    opacity: f32,
79    symbolic: bool,
80    _phantom_data: PhantomData<&'a ()>,
81}
82
83impl<'a, Theme> Svg<'a, Theme>
84where
85    Theme: Catalog,
86{
87    /// Creates a new [`Svg`] from the given [`Handle`].
88    pub fn new(handle: impl Into<Handle>) -> Self {
89        Svg {
90            id: Id::unique(),
91            #[cfg(feature = "a11y")]
92            name: None,
93            #[cfg(feature = "a11y")]
94            description: None,
95            #[cfg(feature = "a11y")]
96            label: None,
97            handle: handle.into(),
98            width: Length::Fill,
99            height: Length::Shrink,
100            content_fit: ContentFit::Contain,
101            class: Theme::default(),
102            rotation: Rotation::default(),
103            opacity: 1.0,
104            symbolic: false,
105            _phantom_data: PhantomData::default(),
106        }
107    }
108
109    /// Creates a new [`Svg`] that will display the contents of the file at the
110    /// provided path.
111    #[must_use]
112    pub fn from_path(path: impl Into<PathBuf>) -> Self {
113        Self::new(Handle::from_path(path))
114    }
115
116    /// Sets the width of the [`Svg`].
117    #[must_use]
118    pub fn width(mut self, width: impl Into<Length>) -> Self {
119        self.width = width.into();
120        self
121    }
122
123    /// Sets the height of the [`Svg`].
124    #[must_use]
125    pub fn height(mut self, height: impl Into<Length>) -> Self {
126        self.height = height.into();
127        self
128    }
129
130    /// Sets the [`ContentFit`] of the [`Svg`].
131    ///
132    /// Defaults to [`ContentFit::Contain`]
133    #[must_use]
134    pub fn content_fit(self, content_fit: ContentFit) -> Self {
135        Self {
136            content_fit,
137            ..self
138        }
139    }
140
141    /// Symbolic icons inherit their color from the renderer if a color is not defined.
142    #[must_use]
143    pub fn symbolic(mut self, symbolic: bool) -> Self {
144        self.symbolic = symbolic;
145        self
146    }
147
148    /// Sets the style of the [`Svg`].
149    #[must_use]
150    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
151    where
152        Theme::Class<'a>: From<StyleFn<'a, Theme>>,
153    {
154        self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
155        self
156    }
157
158    /// Sets the style class of the [`Svg`].
159    #[cfg(feature = "advanced")]
160    #[must_use]
161    pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
162        self.class = class.into();
163        self
164    }
165
166    /// Applies the given [`Rotation`] to the [`Svg`].
167    pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
168        self.rotation = rotation.into();
169        self
170    }
171
172    /// Sets the opacity of the [`Svg`].
173    ///
174    /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
175    /// and `1.0` meaning completely opaque.
176    pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
177        self.opacity = opacity.into();
178        self
179    }
180
181    #[cfg(feature = "a11y")]
182    /// Sets the name of the [`Svg`].
183    pub fn name(mut self, name: impl Into<Cow<'a, str>>) -> Self {
184        self.name = Some(name.into());
185        self
186    }
187
188    #[cfg(feature = "a11y")]
189    /// Sets the description of the [`Svg`].
190    pub fn description_widget<T: iced_accessibility::Describes>(
191        mut self,
192        description: &T,
193    ) -> Self {
194        self.description = Some(iced_accessibility::Description::Id(
195            description.description(),
196        ));
197        self
198    }
199
200    #[cfg(feature = "a11y")]
201    /// Sets the description of the [`Svg`].
202    pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
203        self.description =
204            Some(iced_accessibility::Description::Text(description.into()));
205        self
206    }
207
208    #[cfg(feature = "a11y")]
209    /// Sets the label of the [`Svg`].
210    pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
211        self.label =
212            Some(label.label().into_iter().map(|l| l.into()).collect());
213        self
214    }
215}
216
217impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
218    for Svg<'a, Theme>
219where
220    Renderer: svg::Renderer,
221    Theme: Catalog,
222{
223    fn size(&self) -> Size<Length> {
224        Size {
225            width: self.width,
226            height: self.height,
227        }
228    }
229
230    fn layout(
231        &self,
232        _tree: &mut Tree,
233        renderer: &Renderer,
234        limits: &layout::Limits,
235    ) -> layout::Node {
236        // The raw w/h of the underlying image
237        let Size { width, height } = renderer.measure_svg(&self.handle);
238        let image_size = Size::new(width as f32, height as f32);
239
240        // The rotated size of the svg
241        let rotated_size = self.rotation.apply(image_size);
242
243        // The size to be available to the widget prior to `Shrink`ing
244        let raw_size = limits.resolve(self.width, self.height, rotated_size);
245
246        // The uncropped size of the image when fit to the bounds above
247        let full_size = self.content_fit.fit(rotated_size, raw_size);
248
249        // Shrink the widget to fit the resized image, if requested
250        let final_size = Size {
251            width: match self.width {
252                Length::Shrink => f32::min(raw_size.width, full_size.width),
253                _ => raw_size.width,
254            },
255            height: match self.height {
256                Length::Shrink => f32::min(raw_size.height, full_size.height),
257                _ => raw_size.height,
258            },
259        };
260
261        layout::Node::new(final_size)
262    }
263
264    fn draw(
265        &self,
266        _state: &Tree,
267        renderer: &mut Renderer,
268        theme: &Theme,
269        renderer_style: &renderer::Style,
270        layout: Layout<'_>,
271        cursor: mouse::Cursor,
272        _viewport: &Rectangle,
273    ) {
274        let Size { width, height } = renderer.measure_svg(&self.handle);
275        let image_size = Size::new(width as f32, height as f32);
276        let rotated_size = self.rotation.apply(image_size);
277
278        let bounds = layout.bounds();
279        let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size());
280        let scale = Vector::new(
281            adjusted_fit.width / rotated_size.width,
282            adjusted_fit.height / rotated_size.height,
283        );
284
285        let final_size = image_size * scale;
286
287        let position = match self.content_fit {
288            ContentFit::None => Point::new(
289                bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
290                bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
291            ),
292            _ => Point::new(
293                bounds.center_x() - final_size.width / 2.0,
294                bounds.center_y() - final_size.height / 2.0,
295            ),
296        };
297
298        let drawing_bounds = Rectangle::new(position, final_size);
299
300        let is_mouse_over = cursor.is_over(bounds);
301
302        let status = if is_mouse_over {
303            Status::Hovered
304        } else {
305            Status::Idle
306        };
307
308        let mut style = theme.style(&self.class, status);
309        if self.symbolic && style.color.is_none() {
310            style.color = Some(renderer_style.icon_color);
311        }
312
313        let render = |renderer: &mut Renderer| {
314            renderer.draw_svg(
315                svg::Svg {
316                    handle: self.handle.clone(),
317                    color: style.color,
318                    rotation: self.rotation.radians(),
319                    opacity: self.opacity,
320                    border_radius: [0.0; 4],
321                },
322                drawing_bounds,
323            );
324        };
325
326        if adjusted_fit.width > bounds.width
327            || adjusted_fit.height > bounds.height
328        {
329            renderer.with_layer(bounds, render);
330        } else {
331            render(renderer);
332        }
333    }
334
335    #[cfg(feature = "a11y")]
336    fn a11y_nodes(
337        &self,
338        layout: Layout<'_>,
339        _state: &Tree,
340        _cursor: mouse::Cursor,
341    ) -> iced_accessibility::A11yTree {
342        use iced_accessibility::{
343            accesskit::{NodeBuilder, NodeId, Rect, Role},
344            A11yTree,
345        };
346
347        let bounds = layout.bounds();
348        let Rectangle {
349            x,
350            y,
351            width,
352            height,
353        } = bounds;
354        let bounds = Rect::new(
355            x as f64,
356            y as f64,
357            (x + width) as f64,
358            (y + height) as f64,
359        );
360        let mut node = NodeBuilder::new(Role::Image);
361        node.set_bounds(bounds);
362        if let Some(name) = self.name.as_ref() {
363            node.set_name(name.clone());
364        }
365        match self.description.as_ref() {
366            Some(iced_accessibility::Description::Id(id)) => {
367                node.set_described_by(
368                    id.iter()
369                        .cloned()
370                        .map(|id| NodeId::from(id))
371                        .collect::<Vec<_>>(),
372                );
373            }
374            Some(iced_accessibility::Description::Text(text)) => {
375                node.set_description(text.clone());
376            }
377            None => {}
378        }
379
380        if let Some(label) = self.label.as_ref() {
381            node.set_labelled_by(label.clone());
382        }
383
384        A11yTree::leaf(node, self.id.clone())
385    }
386
387    fn id(&self) -> Option<Id> {
388        Some(self.id.clone())
389    }
390
391    fn set_id(&mut self, id: Id) {
392        self.id = id;
393    }
394}
395
396impl<'a, Message, Theme, Renderer> From<Svg<'a, Theme>>
397    for Element<'a, Message, Theme, Renderer>
398where
399    Theme: Catalog + 'a,
400    Renderer: svg::Renderer + 'a,
401{
402    fn from(icon: Svg<'a, Theme>) -> Element<'a, Message, Theme, Renderer> {
403        Element::new(icon)
404    }
405}
406
407/// The possible status of an [`Svg`].
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub enum Status {
410    /// The [`Svg`] is idle.
411    Idle,
412    /// The [`Svg`] is being hovered.
413    Hovered,
414}
415
416/// The appearance of an [`Svg`].
417#[derive(Debug, Clone, Copy, PartialEq, Default)]
418pub struct Style {
419    /// The [`Color`] filter of an [`Svg`].
420    ///
421    /// Useful for coloring a symbolic icon.
422    ///
423    /// `None` keeps the original color.
424    pub color: Option<Color>,
425}
426
427/// The theme catalog of an [`Svg`].
428pub trait Catalog {
429    /// The item class of the [`Catalog`].
430    type Class<'a>;
431
432    /// The default class produced by the [`Catalog`].
433    fn default<'a>() -> Self::Class<'a>;
434
435    /// The [`Style`] of a class with the given status.
436    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
437}
438
439impl Catalog for Theme {
440    type Class<'a> = StyleFn<'a, Self>;
441
442    fn default<'a>() -> Self::Class<'a> {
443        Box::new(|_theme, _status| Style::default())
444    }
445
446    fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
447        class(self, status)
448    }
449}
450
451/// A styling function for an [`Svg`].
452///
453/// This is just a boxed closure: `Fn(&Theme, Status) -> Style`.
454pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
455
456impl<'a, Theme> From<Style> for StyleFn<'a, Theme> {
457    fn from(style: Style) -> Self {
458        Box::new(move |_theme, _status| style)
459    }
460}