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