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">
57pub struct Image<Handle = image::Handle> {
58    handle: Handle,
59    width: Length,
60    height: Length,
61    crop: Option<Rectangle<u32>>,
62    content_fit: ContentFit,
63    filter_method: FilterMethod,
64    rotation: Rotation,
65    opacity: f32,
66    scale: f32,
67    expand: bool,
68}
69
70impl<Handle> Image<Handle> {
71    /// Creates a new [`Image`] with the given path.
72    pub fn new(handle: impl Into<Handle>) -> Self {
73        Image {
74            handle: handle.into(),
75            width: Length::Shrink,
76            height: Length::Shrink,
77            crop: None,
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
169/// Computes the layout of an [`Image`].
170pub fn layout<Renderer, Handle>(
171    renderer: &Renderer,
172    limits: &layout::Limits,
173    handle: &Handle,
174    width: Length,
175    height: Length,
176    region: Option<Rectangle<u32>>,
177    content_fit: ContentFit,
178    rotation: Rotation,
179    expand: bool,
180) -> layout::Node
181where
182    Renderer: image::Renderer<Handle = Handle>,
183{
184    // The raw w/h of the underlying image
185    let image_size = crop(renderer.measure_image(handle), region);
186
187    // The rotated size of the image
188    let rotated_size = rotation.apply(image_size);
189
190    // The size to be available to the widget prior to `Shrink`ing
191    let bounds = if expand {
192        limits.width(width).height(height).max()
193    } else {
194        limits.resolve(width, height, rotated_size)
195    };
196
197    // The uncropped size of the image when fit to the bounds above
198    let full_size = content_fit.fit(rotated_size, bounds);
199
200    // Shrink the widget to fit the resized image, if requested
201    let final_size = Size {
202        width: match width {
203            Length::Shrink => f32::min(bounds.width, full_size.width),
204            _ => bounds.width,
205        },
206        height: match height {
207            Length::Shrink => f32::min(bounds.height, full_size.height),
208            _ => bounds.height,
209        },
210    };
211
212    layout::Node::new(final_size)
213}
214
215fn drawing_bounds<Renderer, Handle>(
216    renderer: &Renderer,
217    bounds: Rectangle,
218    handle: &Handle,
219    region: Option<Rectangle<u32>>,
220    content_fit: ContentFit,
221    rotation: Rotation,
222    scale: f32,
223) -> Rectangle
224where
225    Renderer: image::Renderer<Handle = Handle>,
226{
227    let original_size = renderer.measure_image(handle);
228    let image_size = crop(original_size, region);
229    let rotated_size = rotation.apply(image_size);
230    let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
231
232    let fit_scale = Vector::new(
233        adjusted_fit.width / rotated_size.width,
234        adjusted_fit.height / rotated_size.height,
235    );
236
237    let final_size = image_size * fit_scale * scale;
238
239    let (crop_offset, final_size) = if let Some(region) = region {
240        let x = region.x.min(original_size.width) as f32;
241        let y = region.y.min(original_size.height) as f32;
242        let width = image_size.width;
243        let height = image_size.height;
244
245        let ratio = Vector::new(
246            original_size.width as f32 / width,
247            original_size.height as f32 / height,
248        );
249
250        let final_size = final_size * ratio;
251
252        let scale = Vector::new(
253            final_size.width / original_size.width as f32,
254            final_size.height / original_size.height as f32,
255        );
256
257        let offset = match content_fit {
258            ContentFit::None => Vector::new(x * scale.x, y * scale.y),
259            _ => Vector::new(
260                ((original_size.width as f32 - width) / 2.0 - x) * scale.x,
261                ((original_size.height as f32 - height) / 2.0 - y) * scale.y,
262            ),
263        };
264
265        (offset, final_size)
266    } else {
267        (Vector::ZERO, final_size)
268    };
269
270    let position = match content_fit {
271        ContentFit::None => Point::new(
272            bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0,
273            bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0,
274        ),
275        _ => Point::new(
276            bounds.center_x() - final_size.width / 2.0,
277            bounds.center_y() - final_size.height / 2.0,
278        ),
279    };
280
281    Rectangle::new(position + crop_offset, final_size)
282}
283
284fn must_clip(bounds: Rectangle, drawing_bounds: Rectangle) -> bool {
285    drawing_bounds.width > bounds.width || drawing_bounds.height > bounds.height
286}
287
288fn crop(size: Size<u32>, region: Option<Rectangle<u32>>) -> Size<f32> {
289    if let Some(region) = region {
290        Size::new(
291            region.width.min(size.width) as f32,
292            region.height.min(size.height) as f32,
293        )
294    } else {
295        Size::new(size.width as f32, size.height as f32)
296    }
297}
298
299/// Draws an [`Image`]
300pub fn draw<Renderer, Handle>(
301    renderer: &mut Renderer,
302    layout: Layout<'_>,
303    viewport: &Rectangle,
304    handle: &Handle,
305    crop: Option<Rectangle<u32>>,
306    content_fit: ContentFit,
307    filter_method: FilterMethod,
308    rotation: Rotation,
309    opacity: f32,
310    scale: f32,
311) where
312    Renderer: image::Renderer<Handle = Handle>,
313    Handle: Clone,
314{
315    let bounds = layout.bounds();
316    let drawing_bounds = drawing_bounds(
317        renderer,
318        bounds,
319        handle,
320        crop,
321        content_fit,
322        rotation,
323        scale,
324    );
325
326    if must_clip(bounds, drawing_bounds) {
327        if let Some(bounds) = bounds.intersection(viewport) {
328            renderer.with_layer(bounds, |renderer| {
329                render(
330                    renderer,
331                    handle,
332                    filter_method,
333                    rotation,
334                    opacity,
335                    drawing_bounds,
336                );
337            });
338        }
339    } else {
340        render(
341            renderer,
342            handle,
343            filter_method,
344            rotation,
345            opacity,
346            drawing_bounds,
347        );
348    }
349}
350
351fn render<Renderer, Handle>(
352    renderer: &mut Renderer,
353    handle: &Handle,
354    filter_method: FilterMethod,
355    rotation: Rotation,
356    opacity: f32,
357    drawing_bounds: Rectangle,
358) where
359    Renderer: image::Renderer<Handle = Handle>,
360    Handle: Clone,
361{
362    renderer.draw_image(
363        image::Image {
364            handle: handle.clone(),
365            filter_method,
366            rotation: rotation.radians(),
367            opacity,
368            snap: true,
369        },
370        drawing_bounds,
371    );
372}
373
374impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
375    for Image<Handle>
376where
377    Renderer: image::Renderer<Handle = Handle>,
378    Handle: Clone,
379{
380    fn size(&self) -> Size<Length> {
381        Size {
382            width: self.width,
383            height: self.height,
384        }
385    }
386
387    fn layout(
388        &mut self,
389        _tree: &mut Tree,
390        renderer: &Renderer,
391        limits: &layout::Limits,
392    ) -> layout::Node {
393        layout(
394            renderer,
395            limits,
396            &self.handle,
397            self.width,
398            self.height,
399            self.crop,
400            self.content_fit,
401            self.rotation,
402            self.expand,
403        )
404    }
405
406    fn draw(
407        &self,
408        _state: &Tree,
409        renderer: &mut Renderer,
410        _theme: &Theme,
411        _style: &renderer::Style,
412        layout: Layout<'_>,
413        _cursor: mouse::Cursor,
414        viewport: &Rectangle,
415    ) {
416        draw(
417            renderer,
418            layout,
419            viewport,
420            &self.handle,
421            self.crop,
422            self.content_fit,
423            self.filter_method,
424            self.rotation,
425            self.opacity,
426            self.scale,
427        );
428    }
429}
430
431impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>>
432    for Element<'a, Message, Theme, Renderer>
433where
434    Renderer: image::Renderer<Handle = Handle>,
435    Handle: Clone + 'a,
436{
437    fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> {
438        Element::new(image)
439    }
440}