Skip to main content

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;
20pub use viewer::Viewer;
21
22use crate::core::border;
23use crate::core::image;
24use crate::core::layout;
25use crate::core::mouse;
26use crate::core::renderer;
27use crate::core::widget;
28use crate::core::widget::Tree;
29use crate::core::widget::operation::accessible::{Accessible, Role};
30use crate::core::{
31    ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, Vector, Widget,
32};
33
34pub use image::{FilterMethod, Handle};
35
36/// Creates a new [`Viewer`] with the given image `Handle`.
37pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
38    Viewer::new(handle)
39}
40
41/// A frame that displays an image while keeping aspect ratio.
42///
43/// # Example
44/// ```no_run
45/// # mod iced { pub mod widget { pub use iced_widget::*; } }
46/// # pub type State = ();
47/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
48/// use iced::widget::image;
49///
50/// enum Message {
51///     // ...
52/// }
53///
54/// fn view(state: &State) -> Element<'_, Message> {
55///     image("ferris.png").into()
56/// }
57/// ```
58/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
59pub struct Image<Handle = image::Handle> {
60    handle: Handle,
61    width: Length,
62    height: Length,
63    crop: Option<Rectangle<u32>>,
64    border_radius: border::Radius,
65    content_fit: ContentFit,
66    filter_method: FilterMethod,
67    rotation: Rotation,
68    opacity: f32,
69    scale: f32,
70    expand: bool,
71    alt: Option<String>,
72    description: Option<String>,
73}
74
75impl<Handle> Image<Handle> {
76    /// Creates a new [`Image`] with the given path.
77    pub fn new(handle: impl Into<Handle>) -> Self {
78        Image {
79            handle: handle.into(),
80            width: Length::Shrink,
81            height: Length::Shrink,
82            crop: None,
83            border_radius: border::Radius::default(),
84            content_fit: ContentFit::default(),
85            filter_method: FilterMethod::default(),
86            rotation: Rotation::default(),
87            opacity: 1.0,
88            scale: 1.0,
89            expand: false,
90            alt: None,
91            description: None,
92        }
93    }
94
95    /// Sets the width of the [`Image`] boundaries.
96    pub fn width(mut self, width: impl Into<Length>) -> Self {
97        self.width = width.into();
98        self
99    }
100
101    /// Sets the height of the [`Image`] boundaries.
102    pub fn height(mut self, height: impl Into<Length>) -> Self {
103        self.height = height.into();
104        self
105    }
106
107    /// Sets whether the [`Image`] should try to fill as much space
108    /// available as possible while keeping aspect ratio and without
109    /// allocating extra space in any axis with a [`Length::Shrink`]
110    /// sizing strategy.
111    ///
112    /// This is similar to using [`Length::Fill`] for both the
113    /// [`width`](Self::width) and the [`height`](Self::height),
114    /// but without the downside of blank space.
115    pub fn expand(mut self, expand: bool) -> Self {
116        self.expand = expand;
117        self
118    }
119
120    /// Sets the [`ContentFit`] of the [`Image`].
121    ///
122    /// Defaults to [`ContentFit::Contain`]
123    pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
124        self.content_fit = content_fit;
125        self
126    }
127
128    /// Sets the [`FilterMethod`] of the [`Image`].
129    pub fn filter_method(mut self, filter_method: FilterMethod) -> Self {
130        self.filter_method = filter_method;
131        self
132    }
133
134    /// Applies the given [`Rotation`] to the [`Image`].
135    pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self {
136        self.rotation = rotation.into();
137        self
138    }
139
140    /// Sets the opacity of the [`Image`].
141    ///
142    /// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
143    /// and `1.0` meaning completely opaque.
144    pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
145        self.opacity = opacity.into();
146        self
147    }
148
149    /// Sets the scale of the [`Image`].
150    ///
151    /// The region of the [`Image`] drawn will be scaled from the center by the given scale factor.
152    /// This can be useful to create certain effects and animations, like smooth zoom in / out.
153    pub fn scale(mut self, scale: impl Into<f32>) -> Self {
154        self.scale = scale.into();
155        self
156    }
157
158    /// Crops the [`Image`] to the given region described by the [`Rectangle`] in absolute
159    /// coordinates.
160    ///
161    /// Cropping is done before applying any transformation or [`ContentFit`]. In practice,
162    /// this means that cropping an [`Image`] with this method should produce the same result
163    /// as cropping it externally (e.g. with an image editor) and creating a new [`Handle`]
164    /// for the cropped version.
165    ///
166    /// However, this method is much more efficient; since it just leverages scissoring during
167    /// rendering and no image cropping actually takes place. Instead, it reuses the existing
168    /// image allocations and should be as efficient as not cropping at all!
169    ///
170    /// The `region` coordinates will be clamped to the image dimensions, if necessary.
171    pub fn crop(mut self, region: Rectangle<u32>) -> Self {
172        self.crop = Some(region);
173        self
174    }
175
176    /// Sets the [`border::Radius`] of the [`Image`].
177    ///
178    /// Currently, it will only be applied around the rectangular bounding box
179    /// of the [`Image`].
180    pub fn border_radius(mut self, border_radius: impl Into<border::Radius>) -> Self {
181        self.border_radius = border_radius.into();
182        self
183    }
184
185    /// Sets the alt text of the [`Image`].
186    ///
187    /// This is the accessible name announced by screen readers. It should
188    /// be a short phrase describing the image content.
189    pub fn alt(mut self, text: impl Into<String>) -> Self {
190        self.alt = Some(text.into());
191        self
192    }
193
194    /// Sets an extended description of the [`Image`].
195    ///
196    /// This supplements the alt text with additional context for
197    /// assistive technology, when the alt text alone is not sufficient.
198    pub fn description(mut self, description: impl Into<String>) -> Self {
199        self.description = Some(description.into());
200        self
201    }
202}
203
204/// Computes the layout of an [`Image`].
205pub fn layout<Renderer, Handle>(
206    renderer: &Renderer,
207    limits: &layout::Limits,
208    handle: &Handle,
209    width: Length,
210    height: Length,
211    region: Option<Rectangle<u32>>,
212    content_fit: ContentFit,
213    rotation: Rotation,
214    expand: bool,
215) -> layout::Node
216where
217    Renderer: image::Renderer<Handle = Handle>,
218{
219    // The raw w/h of the underlying image
220    let image_size = crop(renderer.measure_image(handle).unwrap_or_default(), region);
221
222    // The rotated size of the image
223    let rotated_size = rotation.apply(image_size);
224
225    // The size to be available to the widget prior to `Shrink`ing
226    let bounds = if expand {
227        limits.width(width).height(height).max()
228    } else {
229        limits.resolve(width, height, rotated_size)
230    };
231
232    // The uncropped size of the image when fit to the bounds above
233    let full_size = content_fit.fit(rotated_size, bounds);
234
235    // Shrink the widget to fit the resized image, if requested
236    let final_size = Size {
237        width: match width {
238            Length::Shrink => f32::min(bounds.width, full_size.width),
239            _ => bounds.width,
240        },
241        height: match height {
242            Length::Shrink => f32::min(bounds.height, full_size.height),
243            _ => bounds.height,
244        },
245    };
246
247    layout::Node::new(final_size)
248}
249
250fn drawing_bounds<Renderer, Handle>(
251    renderer: &Renderer,
252    bounds: Rectangle,
253    handle: &Handle,
254    region: Option<Rectangle<u32>>,
255    content_fit: ContentFit,
256    rotation: Rotation,
257    scale: f32,
258) -> Rectangle
259where
260    Renderer: image::Renderer<Handle = Handle>,
261{
262    let original_size = renderer.measure_image(handle).unwrap_or_default();
263    let image_size = crop(original_size, region);
264    let rotated_size = rotation.apply(image_size);
265    let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
266
267    let fit_scale = Vector::new(
268        adjusted_fit.width / rotated_size.width,
269        adjusted_fit.height / rotated_size.height,
270    );
271
272    let final_size = image_size * fit_scale * scale;
273
274    let (crop_offset, final_size) = if let Some(region) = region {
275        let x = region.x.min(original_size.width) as f32;
276        let y = region.y.min(original_size.height) as f32;
277        let width = image_size.width;
278        let height = image_size.height;
279
280        let ratio = Vector::new(
281            original_size.width as f32 / width,
282            original_size.height as f32 / height,
283        );
284
285        let final_size = final_size * ratio;
286
287        let scale = Vector::new(
288            final_size.width / original_size.width as f32,
289            final_size.height / original_size.height as f32,
290        );
291
292        let offset = match content_fit {
293            ContentFit::None => Vector::new(x * scale.x, y * scale.y),
294            _ => Vector::new(
295                ((original_size.width as f32 - width) / 2.0 - x) * scale.x,
296                ((original_size.height as f32 - height) / 2.0 - y) * scale.y,
297            ),
298        };
299
300        (offset, final_size)
301    } else {
302        (Vector::ZERO, final_size)
303    };
304
305    let position = match content_fit {
306        ContentFit::None => Point::new(
307            bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
308            bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
309        ),
310        _ => Point::new(
311            bounds.center_x() - final_size.width / 2.0,
312            bounds.center_y() - final_size.height / 2.0,
313        ),
314    };
315
316    Rectangle::new(position + crop_offset, final_size)
317}
318
319fn crop(size: Size<u32>, region: Option<Rectangle<u32>>) -> Size<f32> {
320    if let Some(region) = region {
321        Size::new(
322            region.width.min(size.width) as f32,
323            region.height.min(size.height) as f32,
324        )
325    } else {
326        Size::new(size.width as f32, size.height as f32)
327    }
328}
329
330/// Draws an [`Image`]
331pub fn draw<Renderer, Handle>(
332    renderer: &mut Renderer,
333    layout: Layout<'_>,
334    handle: &Handle,
335    crop: Option<Rectangle<u32>>,
336    border_radius: border::Radius,
337    content_fit: ContentFit,
338    filter_method: FilterMethod,
339    rotation: Rotation,
340    opacity: f32,
341    scale: f32,
342) where
343    Renderer: image::Renderer<Handle = Handle>,
344    Handle: Clone,
345{
346    let bounds = layout.bounds();
347    let drawing_bounds =
348        drawing_bounds(renderer, bounds, handle, crop, content_fit, rotation, scale);
349
350    renderer.draw_image(
351        image::Image {
352            handle: handle.clone(),
353            border_radius,
354            filter_method,
355            rotation: rotation.radians(),
356            opacity,
357        },
358        drawing_bounds,
359        bounds,
360    );
361}
362
363impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer> for Image<Handle>
364where
365    Renderer: image::Renderer<Handle = Handle>,
366    Handle: Clone,
367{
368    fn size(&self) -> Size<Length> {
369        Size {
370            width: self.width,
371            height: self.height,
372        }
373    }
374
375    fn layout(
376        &mut self,
377        _tree: &mut Tree,
378        renderer: &Renderer,
379        limits: &layout::Limits,
380    ) -> layout::Node {
381        layout(
382            renderer,
383            limits,
384            &self.handle,
385            self.width,
386            self.height,
387            self.crop,
388            self.content_fit,
389            self.rotation,
390            self.expand,
391        )
392    }
393
394    fn draw(
395        &self,
396        _tree: &Tree,
397        renderer: &mut Renderer,
398        _theme: &Theme,
399        _style: &renderer::Style,
400        layout: Layout<'_>,
401        _cursor: mouse::Cursor,
402        _viewport: &Rectangle,
403    ) {
404        draw(
405            renderer,
406            layout,
407            &self.handle,
408            self.crop,
409            self.border_radius,
410            self.content_fit,
411            self.filter_method,
412            self.rotation,
413            self.opacity,
414            self.scale,
415        );
416    }
417
418    fn operate(
419        &mut self,
420        _tree: &mut Tree,
421        layout: Layout<'_>,
422        _renderer: &Renderer,
423        operation: &mut dyn widget::Operation,
424    ) {
425        operation.accessible(
426            None,
427            layout.bounds(),
428            &Accessible {
429                role: Role::Image,
430                label: self.alt.as_deref(),
431                description: self.description.as_deref(),
432                ..Accessible::default()
433            },
434        );
435    }
436}
437
438impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>>
439    for Element<'a, Message, Theme, Renderer>
440where
441    Renderer: image::Renderer<Handle = Handle>,
442    Handle: Clone + 'a,
443{
444    fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> {
445        Element::new(image)
446    }
447}