iced_widget/image/
viewer.rs

1//! Zoom and pan on an image.
2use crate::core::event::{self, Event};
3use crate::core::image::{self, FilterMethod};
4use crate::core::layout;
5use crate::core::mouse;
6use crate::core::renderer;
7use crate::core::widget::tree::{self, Tree};
8use crate::core::{
9    Clipboard, ContentFit, Element, Image, Layout, Length, Pixels, Point,
10    Radians, Rectangle, Shell, Size, Vector, Widget,
11};
12
13/// A frame that displays an image with the ability to zoom in/out and pan.
14#[allow(missing_debug_implementations)]
15pub struct Viewer<Handle> {
16    padding: f32,
17    width: Length,
18    height: Length,
19    min_scale: f32,
20    max_scale: f32,
21    scale_step: f32,
22    handle: Handle,
23    filter_method: FilterMethod,
24    content_fit: ContentFit,
25}
26
27impl<Handle> Viewer<Handle> {
28    /// Creates a new [`Viewer`] with the given [`State`].
29    pub fn new<T: Into<Handle>>(handle: T) -> Self {
30        Viewer {
31            handle: handle.into(),
32            padding: 0.0,
33            width: Length::Shrink,
34            height: Length::Shrink,
35            min_scale: 0.25,
36            max_scale: 10.0,
37            scale_step: 0.10,
38            filter_method: FilterMethod::default(),
39            content_fit: ContentFit::default(),
40        }
41    }
42
43    /// Sets the [`FilterMethod`] of the [`Viewer`].
44    pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self {
45        self.filter_method = filter_method;
46        self
47    }
48
49    /// Sets the [`ContentFit`] of the [`Viewer`].
50    pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
51        self.content_fit = content_fit;
52        self
53    }
54
55    /// Sets the padding of the [`Viewer`].
56    pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
57        self.padding = padding.into().0;
58        self
59    }
60
61    /// Sets the width of the [`Viewer`].
62    pub fn width(mut self, width: impl Into<Length>) -> Self {
63        self.width = width.into();
64        self
65    }
66
67    /// Sets the height of the [`Viewer`].
68    pub fn height(mut self, height: impl Into<Length>) -> Self {
69        self.height = height.into();
70        self
71    }
72
73    /// Sets the max scale applied to the image of the [`Viewer`].
74    ///
75    /// Default is `10.0`
76    pub fn max_scale(mut self, max_scale: f32) -> Self {
77        self.max_scale = max_scale;
78        self
79    }
80
81    /// Sets the min scale applied to the image of the [`Viewer`].
82    ///
83    /// Default is `0.25`
84    pub fn min_scale(mut self, min_scale: f32) -> Self {
85        self.min_scale = min_scale;
86        self
87    }
88
89    /// Sets the percentage the image of the [`Viewer`] will be scaled by
90    /// when zoomed in / out.
91    ///
92    /// Default is `0.10`
93    pub fn scale_step(mut self, scale_step: f32) -> Self {
94        self.scale_step = scale_step;
95        self
96    }
97}
98
99impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
100    for Viewer<Handle>
101where
102    Renderer: image::Renderer<Handle = Handle>,
103    Handle: Clone,
104{
105    fn tag(&self) -> tree::Tag {
106        tree::Tag::of::<State>()
107    }
108
109    fn state(&self) -> tree::State {
110        tree::State::new(State::new())
111    }
112
113    fn size(&self) -> Size<Length> {
114        Size {
115            width: self.width,
116            height: self.height,
117        }
118    }
119
120    fn layout(
121        &self,
122        _tree: &mut Tree,
123        renderer: &Renderer,
124        limits: &layout::Limits,
125    ) -> layout::Node {
126        // The raw w/h of the underlying image
127        let image_size = renderer.measure_image(&self.handle);
128        let image_size =
129            Size::new(image_size.width as f32, image_size.height as f32);
130
131        // The size to be available to the widget prior to `Shrink`ing
132        let raw_size = limits.resolve(self.width, self.height, image_size);
133
134        // The uncropped size of the image when fit to the bounds above
135        let full_size = self.content_fit.fit(image_size, raw_size);
136
137        // Shrink the widget to fit the resized image, if requested
138        let final_size = Size {
139            width: match self.width {
140                Length::Shrink => f32::min(raw_size.width, full_size.width),
141                _ => raw_size.width,
142            },
143            height: match self.height {
144                Length::Shrink => f32::min(raw_size.height, full_size.height),
145                _ => raw_size.height,
146            },
147        };
148
149        layout::Node::new(final_size)
150    }
151
152    fn on_event(
153        &mut self,
154        tree: &mut Tree,
155        event: Event,
156        layout: Layout<'_>,
157        cursor: mouse::Cursor,
158        renderer: &Renderer,
159        _clipboard: &mut dyn Clipboard,
160        _shell: &mut Shell<'_, Message>,
161        _viewport: &Rectangle,
162    ) -> event::Status {
163        let bounds = layout.bounds();
164
165        match event {
166            Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
167                let Some(cursor_position) = cursor.position_over(bounds) else {
168                    return event::Status::Ignored;
169                };
170
171                match delta {
172                    mouse::ScrollDelta::Lines { y, .. }
173                    | mouse::ScrollDelta::Pixels { y, .. } => {
174                        let state = tree.state.downcast_mut::<State>();
175                        let previous_scale = state.scale;
176
177                        if y < 0.0 && previous_scale > self.min_scale
178                            || y > 0.0 && previous_scale < self.max_scale
179                        {
180                            state.scale = (if y > 0.0 {
181                                state.scale * (1.0 + self.scale_step)
182                            } else {
183                                state.scale / (1.0 + self.scale_step)
184                            })
185                            .clamp(self.min_scale, self.max_scale);
186
187                            let scaled_size = scaled_image_size(
188                                renderer,
189                                &self.handle,
190                                state,
191                                bounds.size(),
192                                self.content_fit,
193                            );
194
195                            let factor = state.scale / previous_scale - 1.0;
196
197                            let cursor_to_center =
198                                cursor_position - bounds.center();
199
200                            let adjustment = cursor_to_center * factor
201                                + state.current_offset * factor;
202
203                            state.current_offset = Vector::new(
204                                if scaled_size.width > bounds.width {
205                                    state.current_offset.x + adjustment.x
206                                } else {
207                                    0.0
208                                },
209                                if scaled_size.height > bounds.height {
210                                    state.current_offset.y + adjustment.y
211                                } else {
212                                    0.0
213                                },
214                            );
215                        }
216                    }
217                }
218
219                event::Status::Captured
220            }
221            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
222                let Some(cursor_position) = cursor.position_over(bounds) else {
223                    return event::Status::Ignored;
224                };
225
226                let state = tree.state.downcast_mut::<State>();
227
228                state.cursor_grabbed_at = Some(cursor_position);
229                state.starting_offset = state.current_offset;
230
231                event::Status::Captured
232            }
233            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
234                let state = tree.state.downcast_mut::<State>();
235
236                if state.cursor_grabbed_at.is_some() {
237                    state.cursor_grabbed_at = None;
238
239                    event::Status::Captured
240                } else {
241                    event::Status::Ignored
242                }
243            }
244            Event::Mouse(mouse::Event::CursorMoved { position }) => {
245                let state = tree.state.downcast_mut::<State>();
246
247                if let Some(origin) = state.cursor_grabbed_at {
248                    let scaled_size = scaled_image_size(
249                        renderer,
250                        &self.handle,
251                        state,
252                        bounds.size(),
253                        self.content_fit,
254                    );
255                    let hidden_width = (scaled_size.width - bounds.width / 2.0)
256                        .max(0.0)
257                        .round();
258
259                    let hidden_height = (scaled_size.height
260                        - bounds.height / 2.0)
261                        .max(0.0)
262                        .round();
263
264                    let delta = position - origin;
265
266                    let x = if bounds.width < scaled_size.width {
267                        (state.starting_offset.x - delta.x)
268                            .clamp(-hidden_width, hidden_width)
269                    } else {
270                        0.0
271                    };
272
273                    let y = if bounds.height < scaled_size.height {
274                        (state.starting_offset.y - delta.y)
275                            .clamp(-hidden_height, hidden_height)
276                    } else {
277                        0.0
278                    };
279
280                    state.current_offset = Vector::new(x, y);
281
282                    event::Status::Captured
283                } else {
284                    event::Status::Ignored
285                }
286            }
287            _ => event::Status::Ignored,
288        }
289    }
290
291    fn mouse_interaction(
292        &self,
293        tree: &Tree,
294        layout: Layout<'_>,
295        cursor: mouse::Cursor,
296        _viewport: &Rectangle,
297        _renderer: &Renderer,
298    ) -> mouse::Interaction {
299        let state = tree.state.downcast_ref::<State>();
300        let bounds = layout.bounds();
301        let is_mouse_over = cursor.is_over(bounds);
302
303        if state.is_cursor_grabbed() {
304            mouse::Interaction::Grabbing
305        } else if is_mouse_over {
306            mouse::Interaction::Grab
307        } else {
308            mouse::Interaction::None
309        }
310    }
311
312    fn draw(
313        &self,
314        tree: &Tree,
315        renderer: &mut Renderer,
316        _theme: &Theme,
317        _style: &renderer::Style,
318        layout: Layout<'_>,
319        _cursor: mouse::Cursor,
320        _viewport: &Rectangle,
321    ) {
322        let state = tree.state.downcast_ref::<State>();
323        let bounds = layout.bounds();
324
325        let final_size = scaled_image_size(
326            renderer,
327            &self.handle,
328            state,
329            bounds.size(),
330            self.content_fit,
331        );
332
333        let translation = {
334            let diff_w = bounds.width - final_size.width;
335            let diff_h = bounds.height - final_size.height;
336
337            let image_top_left = match self.content_fit {
338                ContentFit::None => {
339                    Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0)
340                }
341                _ => Vector::new(diff_w / 2.0, diff_h / 2.0),
342            };
343
344            image_top_left - state.offset(bounds, final_size)
345        };
346
347        let drawing_bounds = Rectangle::new(bounds.position(), final_size);
348
349        let render = |renderer: &mut Renderer| {
350            renderer.with_translation(translation, |renderer| {
351                renderer.draw_image(
352                    self.handle.clone(),
353                    self.filter_method,
354                    drawing_bounds,
355                    Radians(0.0),
356                    1.0,
357                    [0.0; 4],
358                );
359            });
360        };
361
362        renderer.with_layer(bounds, render);
363    }
364}
365
366/// The local state of a [`Viewer`].
367#[derive(Debug, Clone, Copy)]
368pub struct State {
369    scale: f32,
370    starting_offset: Vector,
371    current_offset: Vector,
372    cursor_grabbed_at: Option<Point>,
373}
374
375impl Default for State {
376    fn default() -> Self {
377        Self {
378            scale: 1.0,
379            starting_offset: Vector::default(),
380            current_offset: Vector::default(),
381            cursor_grabbed_at: None,
382        }
383    }
384}
385
386impl State {
387    /// Creates a new [`State`].
388    pub fn new() -> Self {
389        State::default()
390    }
391
392    /// Returns the current offset of the [`State`], given the bounds
393    /// of the [`Viewer`] and its image.
394    fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
395        let hidden_width =
396            (image_size.width - bounds.width / 2.0).max(0.0).round();
397
398        let hidden_height =
399            (image_size.height - bounds.height / 2.0).max(0.0).round();
400
401        Vector::new(
402            self.current_offset.x.clamp(-hidden_width, hidden_width),
403            self.current_offset.y.clamp(-hidden_height, hidden_height),
404        )
405    }
406
407    /// Returns if the cursor is currently grabbed by the [`Viewer`].
408    pub fn is_cursor_grabbed(&self) -> bool {
409        self.cursor_grabbed_at.is_some()
410    }
411}
412
413impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>>
414    for Element<'a, Message, Theme, Renderer>
415where
416    Renderer: 'a + image::Renderer<Handle = Handle>,
417    Message: 'a,
418    Handle: Clone + 'a,
419{
420    fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> {
421        Element::new(viewer)
422    }
423}
424
425/// Returns the bounds of the underlying image, given the bounds of
426/// the [`Viewer`]. Scaling will be applied and original aspect ratio
427/// will be respected.
428pub fn scaled_image_size<Renderer>(
429    renderer: &Renderer,
430    handle: &<Renderer as image::Renderer>::Handle,
431    state: &State,
432    bounds: Size,
433    content_fit: ContentFit,
434) -> Size
435where
436    Renderer: image::Renderer,
437{
438    let Size { width, height } = renderer.measure_image(handle);
439    let image_size = Size::new(width as f32, height as f32);
440
441    let adjusted_fit = content_fit.fit(image_size, bounds);
442
443    Size::new(
444        adjusted_fit.width * state.scale,
445        adjusted_fit.height * state.scale,
446    )
447}