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.
13pub struct Viewer<Handle> {
14    padding: f32,
15    width: Length,
16    height: Length,
17    min_scale: f32,
18    max_scale: f32,
19    scale_step: f32,
20    handle: Handle,
21    filter_method: FilterMethod,
22    content_fit: ContentFit,
23}
24
25impl<Handle> Viewer<Handle> {
26    /// Creates a new [`Viewer`] with the given [`State`].
27    pub fn new<T: Into<Handle>>(handle: T) -> Self {
28        Viewer {
29            handle: handle.into(),
30            padding: 0.0,
31            width: Length::Shrink,
32            height: Length::Shrink,
33            min_scale: 0.25,
34            max_scale: 10.0,
35            scale_step: 0.10,
36            filter_method: FilterMethod::default(),
37            content_fit: ContentFit::default(),
38        }
39    }
40
41    /// Sets the [`FilterMethod`] of the [`Viewer`].
42    pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self {
43        self.filter_method = filter_method;
44        self
45    }
46
47    /// Sets the [`ContentFit`] of the [`Viewer`].
48    pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
49        self.content_fit = content_fit;
50        self
51    }
52
53    /// Sets the padding of the [`Viewer`].
54    pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
55        self.padding = padding.into().0;
56        self
57    }
58
59    /// Sets the width of the [`Viewer`].
60    pub fn width(mut self, width: impl Into<Length>) -> Self {
61        self.width = width.into();
62        self
63    }
64
65    /// Sets the height of the [`Viewer`].
66    pub fn height(mut self, height: impl Into<Length>) -> Self {
67        self.height = height.into();
68        self
69    }
70
71    /// Sets the max scale applied to the image of the [`Viewer`].
72    ///
73    /// Default is `10.0`
74    pub fn max_scale(mut self, max_scale: f32) -> Self {
75        self.max_scale = max_scale;
76        self
77    }
78
79    /// Sets the min scale applied to the image of the [`Viewer`].
80    ///
81    /// Default is `0.25`
82    pub fn min_scale(mut self, min_scale: f32) -> Self {
83        self.min_scale = min_scale;
84        self
85    }
86
87    /// Sets the percentage the image of the [`Viewer`] will be scaled by
88    /// when zoomed in / out.
89    ///
90    /// Default is `0.10`
91    pub fn scale_step(mut self, scale_step: f32) -> Self {
92        self.scale_step = scale_step;
93        self
94    }
95}
96
97impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
98    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);
126        let image_size =
127            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, .. }
171                    | mouse::ScrollDelta::Pixels { y, .. } => {
172                        let state = tree.state.downcast_mut::<State>();
173                        let previous_scale = state.scale;
174
175                        if y < 0.0 && previous_scale > self.min_scale
176                            || y > 0.0 && previous_scale < self.max_scale
177                        {
178                            state.scale = (if y > 0.0 {
179                                state.scale * (1.0 + self.scale_step)
180                            } else {
181                                state.scale / (1.0 + self.scale_step)
182                            })
183                            .clamp(self.min_scale, self.max_scale);
184
185                            let scaled_size = scaled_image_size(
186                                renderer,
187                                &self.handle,
188                                state,
189                                bounds.size(),
190                                self.content_fit,
191                            );
192
193                            let factor = state.scale / previous_scale - 1.0;
194
195                            let cursor_to_center =
196                                cursor_position - bounds.center();
197
198                            let adjustment = cursor_to_center * factor
199                                + state.current_offset * factor;
200
201                            state.current_offset = Vector::new(
202                                if scaled_size.width > bounds.width {
203                                    state.current_offset.x + adjustment.x
204                                } else {
205                                    0.0
206                                },
207                                if scaled_size.height > bounds.height {
208                                    state.current_offset.y + adjustment.y
209                                } else {
210                                    0.0
211                                },
212                            );
213                        }
214                    }
215                }
216
217                shell.request_redraw();
218                shell.capture_event();
219            }
220            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
221                let Some(cursor_position) = cursor.position_over(bounds) else {
222                    return;
223                };
224
225                let state = tree.state.downcast_mut::<State>();
226
227                state.cursor_grabbed_at = Some(cursor_position);
228                state.starting_offset = state.current_offset;
229
230                shell.request_redraw();
231                shell.capture_event();
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                    shell.request_redraw();
239                    shell.capture_event();
240                }
241            }
242            Event::Mouse(mouse::Event::CursorMoved { position }) => {
243                let state = tree.state.downcast_mut::<State>();
244
245                if let Some(origin) = state.cursor_grabbed_at {
246                    let scaled_size = scaled_image_size(
247                        renderer,
248                        &self.handle,
249                        state,
250                        bounds.size(),
251                        self.content_fit,
252                    );
253                    let hidden_width = (scaled_size.width - bounds.width / 2.0)
254                        .max(0.0)
255                        .round();
256
257                    let hidden_height = (scaled_size.height
258                        - bounds.height / 2.0)
259                        .max(0.0)
260                        .round();
261
262                    let delta = *position - origin;
263
264                    let x = if bounds.width < scaled_size.width {
265                        (state.starting_offset.x - delta.x)
266                            .clamp(-hidden_width, hidden_width)
267                    } else {
268                        0.0
269                    };
270
271                    let y = if bounds.height < scaled_size.height {
272                        (state.starting_offset.y - delta.y)
273                            .clamp(-hidden_height, hidden_height)
274                    } else {
275                        0.0
276                    };
277
278                    state.current_offset = Vector::new(x, y);
279                    shell.request_redraw();
280                    shell.capture_event();
281                }
282            }
283            _ => {}
284        }
285    }
286
287    fn mouse_interaction(
288        &self,
289        tree: &Tree,
290        layout: Layout<'_>,
291        cursor: mouse::Cursor,
292        _viewport: &Rectangle,
293        _renderer: &Renderer,
294    ) -> mouse::Interaction {
295        let state = tree.state.downcast_ref::<State>();
296        let bounds = layout.bounds();
297        let is_mouse_over = cursor.is_over(bounds);
298
299        if state.is_cursor_grabbed() {
300            mouse::Interaction::Grabbing
301        } else if is_mouse_over {
302            mouse::Interaction::Grab
303        } else {
304            mouse::Interaction::None
305        }
306    }
307
308    fn draw(
309        &self,
310        tree: &Tree,
311        renderer: &mut Renderer,
312        _theme: &Theme,
313        _style: &renderer::Style,
314        layout: Layout<'_>,
315        _cursor: mouse::Cursor,
316        _viewport: &Rectangle,
317    ) {
318        let state = tree.state.downcast_ref::<State>();
319        let bounds = layout.bounds();
320
321        let final_size = scaled_image_size(
322            renderer,
323            &self.handle,
324            state,
325            bounds.size(),
326            self.content_fit,
327        );
328
329        let translation = {
330            let diff_w = bounds.width - final_size.width;
331            let diff_h = bounds.height - final_size.height;
332
333            let image_top_left = match self.content_fit {
334                ContentFit::None => {
335                    Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0)
336                }
337                _ => Vector::new(diff_w / 2.0, diff_h / 2.0),
338            };
339
340            image_top_left - state.offset(bounds, final_size)
341        };
342
343        let drawing_bounds = Rectangle::new(bounds.position(), final_size);
344
345        let render = |renderer: &mut Renderer| {
346            renderer.with_translation(translation, |renderer| {
347                renderer.draw_image(
348                    Image {
349                        handle: self.handle.clone(),
350                        filter_method: self.filter_method,
351                        rotation: Radians(0.0),
352                        opacity: 1.0,
353                        snap: true,
354                    },
355                    drawing_bounds,
356                );
357            });
358        };
359
360        renderer.with_layer(bounds, render);
361    }
362}
363
364/// The local state of a [`Viewer`].
365#[derive(Debug, Clone, Copy)]
366pub struct State {
367    scale: f32,
368    starting_offset: Vector,
369    current_offset: Vector,
370    cursor_grabbed_at: Option<Point>,
371}
372
373impl Default for State {
374    fn default() -> Self {
375        Self {
376            scale: 1.0,
377            starting_offset: Vector::default(),
378            current_offset: Vector::default(),
379            cursor_grabbed_at: None,
380        }
381    }
382}
383
384impl State {
385    /// Creates a new [`State`].
386    pub fn new() -> Self {
387        State::default()
388    }
389
390    /// Returns the current offset of the [`State`], given the bounds
391    /// of the [`Viewer`] and its image.
392    fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector {
393        let hidden_width =
394            (image_size.width - bounds.width / 2.0).max(0.0).round();
395
396        let hidden_height =
397            (image_size.height - bounds.height / 2.0).max(0.0).round();
398
399        Vector::new(
400            self.current_offset.x.clamp(-hidden_width, hidden_width),
401            self.current_offset.y.clamp(-hidden_height, hidden_height),
402        )
403    }
404
405    /// Returns if the cursor is currently grabbed by the [`Viewer`].
406    pub fn is_cursor_grabbed(&self) -> bool {
407        self.cursor_grabbed_at.is_some()
408    }
409}
410
411impl<'a, Message, Theme, Renderer, Handle> From<Viewer<Handle>>
412    for Element<'a, Message, Theme, Renderer>
413where
414    Renderer: 'a + image::Renderer<Handle = Handle>,
415    Message: 'a,
416    Handle: Clone + 'a,
417{
418    fn from(viewer: Viewer<Handle>) -> Element<'a, Message, Theme, Renderer> {
419        Element::new(viewer)
420    }
421}
422
423/// Returns the bounds of the underlying image, given the bounds of
424/// the [`Viewer`]. Scaling will be applied and original aspect ratio
425/// will be respected.
426pub fn scaled_image_size<Renderer>(
427    renderer: &Renderer,
428    handle: &<Renderer as image::Renderer>::Handle,
429    state: &State,
430    bounds: Size,
431    content_fit: ContentFit,
432) -> Size
433where
434    Renderer: image::Renderer,
435{
436    let Size { width, height } = renderer.measure_image(handle);
437    let image_size = Size::new(width as f32, height as f32);
438
439    let adjusted_fit = content_fit.fit(image_size, bounds);
440
441    Size::new(
442        adjusted_fit.width * state.scale,
443        adjusted_fit.height * state.scale,
444    )
445}