iced_widget/image/
viewer.rs

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