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