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