iced_widget/
image.rs

1//! Images display raster graphics in different formats (PNG, JPG, etc.).
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::image;
9//!
10//! enum Message {
11//!     // ...
12//! }
13//!
14//! fn view(state: &State) -> Element<'_, Message> {
15//!     image("ferris.png").into()
16//! }
17//! ```
18//! <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
19pub mod viewer;
20use iced_runtime::core::widget::Id;
21pub use viewer::Viewer;
22
23use crate::core::image;
24use crate::core::layout;
25use crate::core::mouse;
26use crate::core::renderer;
27use crate::core::widget::Tree;
28use crate::core::{
29    ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size,
30    Vector, Widget,
31};
32
33pub use image::{FilterMethod, Handle};
34
35#[cfg(feature = "a11y")]
36use std::borrow::Cow;
37
38/// Creates a new [`Viewer`] with the given image `Handle`.
39pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
40    Viewer::new(handle)
41}
42
43/// A frame that displays an image while keeping aspect ratio.
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::image;
51///
52/// enum Message {
53///     // ...
54/// }
55///
56/// fn view(state: &State) -> Element<'_, Message> {
57///     image("ferris.png").into()
58/// }
59/// ```
60/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
61#[derive(Debug)]
62pub struct Image<'a, Handle = image::Handle> {
63    id: Id,
64    #[cfg(feature = "a11y")]
65    name: Option<Cow<'a, str>>,
66    #[cfg(feature = "a11y")]
67    description: Option<iced_accessibility::Description<'a>>,
68    #[cfg(feature = "a11y")]
69    label: Option<Vec<iced_accessibility::accesskit::NodeId>>,
70    handle: Handle,
71    width: Length,
72    height: Length,
73    content_fit: ContentFit,
74    filter_method: FilterMethod,
75    rotation: Rotation,
76    opacity: f32,
77    border_radius: [f32; 4],
78    phantom_data: std::marker::PhantomData<&'a ()>,
79}
80
81impl<'a, Handle> Image<'a, Handle> {
82    /// Creates a new [`Image`] with the given path.
83    pub fn new(handle: impl Into<Handle>) -> Self {
84        Image {
85            id: Id::unique(),
86            #[cfg(feature = "a11y")]
87            name: None,
88            #[cfg(feature = "a11y")]
89            description: None,
90            #[cfg(feature = "a11y")]
91            label: None,
92            handle: handle.into(),
93            width: Length::Shrink,
94            height: Length::Shrink,
95            content_fit: ContentFit::default(),
96            filter_method: FilterMethod::default(),
97            rotation: Rotation::default(),
98            opacity: 1.0,
99            border_radius: [0.0; 4],
100            phantom_data: std::marker::PhantomData,
101        }
102    }
103
104    /// Sets the border radius of the image.
105    pub fn border_radius(mut self, border_radius: [f32; 4]) -> Self {
106        self.border_radius = border_radius;
107        self
108    }
109
110    /// Sets the width of the [`Image`] boundaries.
111    pub fn width(mut self, width: impl Into<Length>) -> Self {
112        self.width = width.into();
113        self
114    }
115
116    /// Sets the height of the [`Image`] boundaries.
117    pub fn height(mut self, height: impl Into<Length>) -> Self {
118        self.height = height.into();
119        self
120    }
121
122    /// Sets the [`ContentFit`] of the [`Image`].
123    ///
124    /// Defaults to [`ContentFit::Contain`]
125    pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
126        self.content_fit = content_fit;
127        self
128    }
129
130    /// Sets the [`FilterMethod`] of the [`Image`].
131    pub fn filter_method(mut self, filter_method: FilterMethod) -> Self {
132        self.filter_method = filter_method;
133        self
134    }
135
136    /// Applies the given [`Rotation`] to the [`Image`].
137    pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
138        self.rotation = rotation.into();
139        self
140    }
141
142    /// Sets the opacity of the [`Image`].
143    ///
144    /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
145    /// and `1.0` meaning completely opaque.
146    pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
147        self.opacity = opacity.into();
148        self
149    }
150
151    #[cfg(feature = "a11y")]
152    /// Sets the name of the [`Image`].
153    pub fn name(mut self, name: impl Into<Cow<'a, str>>) -> Self {
154        self.name = Some(name.into());
155        self
156    }
157
158    #[cfg(feature = "a11y")]
159    /// Sets the description of the [`Image`].
160    pub fn description_widget<T: iced_accessibility::Describes>(
161        mut self,
162        description: &T,
163    ) -> Self {
164        self.description = Some(iced_accessibility::Description::Id(
165            description.description(),
166        ));
167        self
168    }
169
170    #[cfg(feature = "a11y")]
171    /// Sets the description of the [`Image`].
172    pub fn description(mut self, description: impl Into<Cow<'a, str>>) -> Self {
173        self.description =
174            Some(iced_accessibility::Description::Text(description.into()));
175        self
176    }
177
178    #[cfg(feature = "a11y")]
179    /// Sets the label of the [`Image`].
180    pub fn label(mut self, label: &dyn iced_accessibility::Labels) -> Self {
181        self.label =
182            Some(label.label().into_iter().map(|l| l.into()).collect());
183        self
184    }
185}
186
187/// Computes the layout of an [`Image`].
188pub fn layout<Renderer, Handle>(
189    renderer: &Renderer,
190    limits: &layout::Limits,
191    handle: &Handle,
192    width: Length,
193    height: Length,
194    content_fit: ContentFit,
195    rotation: Rotation,
196    _border_radius: [f32; 4],
197) -> layout::Node
198where
199    Renderer: image::Renderer<Handle = Handle>,
200{
201    // The raw w/h of the underlying image
202    let image_size = renderer.measure_image(handle);
203    let image_size =
204        Size::new(image_size.width as f32, image_size.height as f32);
205
206    // The rotated size of the image
207    let rotated_size = rotation.apply(image_size);
208
209    // The size to be available to the widget prior to `Shrink`ing
210    let raw_size = limits.resolve(width, height, rotated_size);
211
212    // The uncropped size of the image when fit to the bounds above
213    let full_size = content_fit.fit(rotated_size, raw_size);
214
215    // Shrink the widget to fit the resized image, if requested
216    let final_size = Size {
217        width: match width {
218            Length::Shrink => f32::min(raw_size.width, full_size.width),
219            _ => raw_size.width,
220        },
221        height: match height {
222            Length::Shrink => f32::min(raw_size.height, full_size.height),
223            _ => raw_size.height,
224        },
225    };
226
227    layout::Node::new(final_size)
228}
229
230/// Draws an [`Image`]
231pub fn draw<Renderer, Handle>(
232    renderer: &mut Renderer,
233    layout: Layout<'_>,
234    handle: &Handle,
235    content_fit: ContentFit,
236    filter_method: FilterMethod,
237    rotation: Rotation,
238    opacity: f32,
239    border_radius: [f32; 4],
240) where
241    Renderer: image::Renderer<Handle = Handle>,
242    Handle: Clone,
243{
244    let Size { width, height } = renderer.measure_image(handle);
245    let image_size = Size::new(width as f32, height as f32);
246    let rotated_size = rotation.apply(image_size);
247
248    let bounds = layout.bounds();
249    let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
250
251    let scale = Vector::new(
252        adjusted_fit.width / rotated_size.width,
253        adjusted_fit.height / rotated_size.height,
254    );
255
256    let final_size = image_size * scale;
257
258    let position = match content_fit {
259        ContentFit::None => Point::new(
260            bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
261            bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
262        ),
263        _ => Point::new(
264            bounds.center_x() - final_size.width / 2.0,
265            bounds.center_y() - final_size.height / 2.0,
266        ),
267    };
268
269    let drawing_bounds = Rectangle::new(position, final_size);
270
271    let offset = Vector::new(
272        (bounds.width - adjusted_fit.width).max(0.0) / 2.0,
273        (bounds.height - adjusted_fit.height).max(0.0) / 2.0,
274    );
275
276    let render = |renderer: &mut Renderer| {
277        renderer.draw_image(
278            handle.clone(),
279            filter_method,
280            drawing_bounds + offset,
281            rotation.radians(),
282            opacity,
283            border_radius,
284        );
285    };
286
287    if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height
288    {
289        renderer.with_layer(bounds, render);
290    } else {
291        render(renderer);
292    }
293}
294
295impl<'a, Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
296    for Image<'a, Handle>
297where
298    Renderer: image::Renderer<Handle = Handle>,
299    Handle: Clone,
300{
301    fn size(&self) -> Size<Length> {
302        Size {
303            width: self.width,
304            height: self.height,
305        }
306    }
307
308    fn layout(
309        &self,
310        _tree: &mut Tree,
311        renderer: &Renderer,
312        limits: &layout::Limits,
313    ) -> layout::Node {
314        layout(
315            renderer,
316            limits,
317            &self.handle,
318            self.width,
319            self.height,
320            self.content_fit,
321            self.rotation,
322            self.border_radius,
323        )
324    }
325
326    fn draw(
327        &self,
328        _state: &Tree,
329        renderer: &mut Renderer,
330        _theme: &Theme,
331        _style: &renderer::Style,
332        layout: Layout<'_>,
333        _cursor: mouse::Cursor,
334        _viewport: &Rectangle,
335    ) {
336        draw(
337            renderer,
338            layout,
339            &self.handle,
340            self.content_fit,
341            self.filter_method,
342            self.rotation,
343            self.opacity,
344            self.border_radius,
345        );
346    }
347
348    #[cfg(feature = "a11y")]
349    fn a11y_nodes(
350        &self,
351        layout: Layout<'_>,
352        _state: &Tree,
353        _cursor: mouse::Cursor,
354    ) -> iced_accessibility::A11yTree {
355        use iced_accessibility::{
356            accesskit::{NodeBuilder, NodeId, Rect, Role},
357            A11yTree,
358        };
359
360        let bounds = layout.bounds();
361        let Rectangle {
362            x,
363            y,
364            width,
365            height,
366        } = bounds;
367        let bounds = Rect::new(
368            x as f64,
369            y as f64,
370            (x + width) as f64,
371            (y + height) as f64,
372        );
373        let mut node = NodeBuilder::new(Role::Image);
374        node.set_bounds(bounds);
375        if let Some(name) = self.name.as_ref() {
376            node.set_name(name.clone());
377        }
378        match self.description.as_ref() {
379            Some(iced_accessibility::Description::Id(id)) => {
380                node.set_described_by(
381                    id.iter()
382                        .cloned()
383                        .map(|id| NodeId::from(id))
384                        .collect::<Vec<_>>(),
385                );
386            }
387            Some(iced_accessibility::Description::Text(text)) => {
388                node.set_description(text.clone());
389            }
390            None => {}
391        }
392
393        if let Some(label) = self.label.as_ref() {
394            node.set_labelled_by(label.clone());
395        }
396
397        A11yTree::leaf(node, self.id.clone())
398    }
399
400    fn id(&self) -> Option<Id> {
401        Some(self.id.clone())
402    }
403
404    fn set_id(&mut self, id: Id) {
405        self.id = id;
406    }
407}
408
409impl<'a, Message, Theme, Renderer, Handle> From<Image<'a, Handle>>
410    for Element<'a, Message, Theme, Renderer>
411where
412    Renderer: image::Renderer<Handle = Handle>,
413    Handle: Clone + 'a,
414{
415    fn from(image: Image<'a, Handle>) -> Element<'a, Message, Theme, Renderer> {
416        Element::new(image)
417    }
418}