Skip to main content

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