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