iced_widget/image/
viewer.rs

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