From 0d8cefbf2d084053b92ded4785da8083486374ea Mon Sep 17 00:00:00 2001
From: Cory Forsstrom <cforsstrom18@gmail.com>
Date: Thu, 23 Apr 2020 15:34:55 -0700
Subject: Add `ImagePane` widget

---
 native/src/widget.rs                   |   3 +
 native/src/widget/image_pane.rs        | 407 +++++++++++++++++++++++++++++++++
 src/widget.rs                          |  10 +-
 wgpu/src/renderer/widget.rs            |   2 +
 wgpu/src/renderer/widget/image_pane.rs |  36 +++
 wgpu/src/widget.rs                     |   8 +
 wgpu/src/widget/image_pane.rs          |   6 +
 7 files changed, 470 insertions(+), 2 deletions(-)
 create mode 100644 native/src/widget/image_pane.rs
 create mode 100644 wgpu/src/renderer/widget/image_pane.rs
 create mode 100644 wgpu/src/widget/image_pane.rs

diff --git a/native/src/widget.rs b/native/src/widget.rs
index 4453145b..23194545 100644
--- a/native/src/widget.rs
+++ b/native/src/widget.rs
@@ -25,6 +25,7 @@ pub mod checkbox;
 pub mod column;
 pub mod container;
 pub mod image;
+pub mod image_pane;
 pub mod pane_grid;
 pub mod progress_bar;
 pub mod radio;
@@ -47,6 +48,8 @@ pub use container::Container;
 #[doc(no_inline)]
 pub use image::Image;
 #[doc(no_inline)]
+pub use image_pane::ImagePane;
+#[doc(no_inline)]
 pub use pane_grid::PaneGrid;
 #[doc(no_inline)]
 pub use progress_bar::ProgressBar;
diff --git a/native/src/widget/image_pane.rs b/native/src/widget/image_pane.rs
new file mode 100644
index 00000000..4d07f228
--- /dev/null
+++ b/native/src/widget/image_pane.rs
@@ -0,0 +1,407 @@
+//! Zoom and pan on an image.
+use crate::{
+    image,
+    input::{self, mouse},
+    layout, Clipboard, Element, Event, Hasher, Layout, Length, Point,
+    Rectangle, Size, Widget,
+};
+
+use std::{f32, hash::Hash, u32};
+
+/// A widget that can display an image with the ability to zoom in/out and pan.
+#[allow(missing_debug_implementations)]
+pub struct ImagePane<'a> {
+    state: &'a mut State,
+    padding: u16,
+    width: Length,
+    height: Length,
+    max_width: u32,
+    max_height: u32,
+    handle: image::Handle,
+}
+
+impl<'a> ImagePane<'a> {
+    /// Creates a new [`ImagePane`] with the given [`State`] and [`Handle`].
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    /// [`State`]: struct.State.html
+    /// [`Handle`]: ../image/struct.Handle.html
+    pub fn new(state: &'a mut State, handle: image::Handle) -> Self {
+        ImagePane {
+            state,
+            padding: 0,
+            width: Length::Shrink,
+            height: Length::Shrink,
+            max_width: u32::MAX,
+            max_height: u32::MAX,
+            handle,
+        }
+    }
+
+    /// Sets the padding of the [`ImagePane`].
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    pub fn padding(mut self, units: u16) -> Self {
+        self.padding = units;
+        self
+    }
+
+    /// Sets the width of the [`ImagePane`].
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    pub fn width(mut self, width: Length) -> Self {
+        self.width = width;
+        self
+    }
+
+    /// Sets the height of the [`ImagePane`].
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    pub fn height(mut self, height: Length) -> Self {
+        self.height = height;
+        self
+    }
+
+    /// Sets the max width of the [`ImagePane`].
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    pub fn max_width(mut self, max_width: u32) -> Self {
+        self.max_width = max_width;
+        self
+    }
+
+    /// Sets the max height of the [`ImagePane`].
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    pub fn max_height(mut self, max_height: u32) -> Self {
+        self.max_height = max_height;
+        self
+    }
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer> for ImagePane<'a>
+where
+    Renderer: self::Renderer + image::Renderer,
+{
+    fn width(&self) -> Length {
+        self.width
+    }
+
+    fn height(&self) -> Length {
+        self.height
+    }
+
+    fn layout(
+        &self,
+        _renderer: &Renderer,
+        limits: &layout::Limits,
+    ) -> layout::Node {
+        let padding = f32::from(self.padding);
+
+        let limits = limits
+            .max_width(self.max_width)
+            .max_height(self.max_height)
+            .width(self.width)
+            .height(self.height)
+            .pad(padding);
+
+        let size = limits.resolve(Size::INFINITY);
+
+        layout::Node::new(size)
+    }
+
+    fn on_event(
+        &mut self,
+        event: Event,
+        layout: Layout<'_>,
+        cursor_position: Point,
+        _messages: &mut Vec<Message>,
+        renderer: &Renderer,
+        _clipboard: Option<&dyn Clipboard>,
+    ) {
+        let bounds = layout.bounds();
+        let is_mouse_over = bounds.contains(cursor_position);
+
+        let image_bounds = {
+            let (width, height) = renderer.dimensions(&self.handle);
+
+            let dimensions = if let Some(scale) = self.state.scale {
+                (width as f32 * scale, height as f32 * scale)
+            } else {
+                let dimensions = (width as f32, height as f32);
+
+                let width_scale = bounds.width / dimensions.0;
+                let height_scale = bounds.height / dimensions.1;
+
+                let scale = width_scale.min(height_scale);
+
+                if scale < 1.0 {
+                    (dimensions.0 * scale, dimensions.1 * scale)
+                } else {
+                    (dimensions.0, dimensions.1)
+                }
+            };
+
+            Rectangle {
+                x: bounds.x,
+                y: bounds.y,
+                width: dimensions.0,
+                height: dimensions.1,
+            }
+        };
+
+        if is_mouse_over {
+            match event {
+                Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
+                    match delta {
+                        mouse::ScrollDelta::Lines { y, .. } => {
+                            // TODO: Configurable step and limits
+                            if y > 0.0 {
+                                self.state.scale = Some(
+                                    (self.state.scale.unwrap_or(1.0) + 0.25)
+                                        .min(10.0),
+                                );
+                            } else {
+                                self.state.scale = Some(
+                                    (self.state.scale.unwrap_or(1.0) - 0.25)
+                                        .max(0.25),
+                                );
+                            }
+                        }
+                        mouse::ScrollDelta::Pixels { y, .. } => {
+                            // TODO: Configurable step and limits
+                            if y > 0.0 {
+                                self.state.scale = Some(
+                                    (self.state.scale.unwrap_or(1.0) + 0.25)
+                                        .min(10.0),
+                                );
+                            } else {
+                                self.state.scale = Some(
+                                    (self.state.scale.unwrap_or(1.0) - 0.25)
+                                        .max(0.25),
+                                );
+                            }
+                        }
+                    }
+                }
+                Event::Mouse(mouse::Event::Input { button, state }) => {
+                    if button == mouse::Button::Left {
+                        match state {
+                            input::ButtonState::Pressed => {
+                                self.state.starting_cursor_pos = Some((
+                                    cursor_position.x,
+                                    cursor_position.y,
+                                ));
+
+                                self.state.starting_offset =
+                                    self.state.current_offset;
+                            }
+                            input::ButtonState::Released => {
+                                self.state.starting_cursor_pos = None
+                            }
+                        }
+                    }
+                }
+                Event::Mouse(mouse::Event::CursorMoved { x, y }) => {
+                    if self.state.is_cursor_clicked() {
+                        self.state.pan(x, y, bounds, image_bounds);
+                    }
+                }
+                _ => {}
+            }
+        } else if let Event::Mouse(mouse::Event::Input { button, state }) =
+            event
+        {
+            if button == mouse::Button::Left
+                && state == input::ButtonState::Released
+            {
+                self.state.starting_cursor_pos = None;
+            }
+        }
+    }
+
+    fn draw(
+        &self,
+        renderer: &mut Renderer,
+        _defaults: &Renderer::Defaults,
+        layout: Layout<'_>,
+        cursor_position: Point,
+    ) -> Renderer::Output {
+        let bounds = layout.bounds();
+
+        let image_bounds = {
+            let (width, height) = renderer.dimensions(&self.handle);
+
+            let dimensions = if let Some(scale) = self.state.scale {
+                (width as f32 * scale, height as f32 * scale)
+            } else {
+                let dimensions = (width as f32, height as f32);
+
+                let width_scale = bounds.width / dimensions.0;
+                let height_scale = bounds.height / dimensions.1;
+
+                let scale = width_scale.min(height_scale);
+
+                if scale < 1.0 {
+                    (dimensions.0 * scale, dimensions.1 * scale)
+                } else {
+                    (dimensions.0, dimensions.1)
+                }
+            };
+
+            Rectangle {
+                x: bounds.x,
+                y: bounds.y,
+                width: dimensions.0,
+                height: dimensions.1,
+            }
+        };
+
+        let offset = self.state.offset(bounds, image_bounds);
+
+        let is_mouse_over = bounds.contains(cursor_position);
+
+        self::Renderer::draw(
+            renderer,
+            &self.state,
+            bounds,
+            image_bounds,
+            offset,
+            self.handle.clone(),
+            is_mouse_over,
+        )
+    }
+
+    fn hash_layout(&self, state: &mut Hasher) {
+        struct Marker;
+        std::any::TypeId::of::<Marker>().hash(state);
+
+        self.width.hash(state);
+        self.height.hash(state);
+        self.max_width.hash(state);
+        self.max_height.hash(state);
+        self.padding.hash(state);
+
+        self.handle.hash(state);
+    }
+}
+
+/// The local state of an [`ImagePane`].
+///
+/// [`ImagePane`]: struct.ImagePane.html
+#[derive(Debug, Clone, Copy, Default)]
+pub struct State {
+    scale: Option<f32>,
+    starting_offset: (f32, f32),
+    current_offset: (f32, f32),
+    starting_cursor_pos: Option<(f32, f32)>,
+}
+
+impl State {
+    /// Creates a new [`State`] with the scrollbar located at the top.
+    ///
+    /// [`State`]: struct.State.html
+    pub fn new() -> Self {
+        State::default()
+    }
+
+    /// Apply a panning offset to the current [`State`], given the bounds of
+    /// the [`ImagePane`] and its image.
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    /// [`State`]: struct.State.html
+    fn pan(
+        &mut self,
+        x: f32,
+        y: f32,
+        bounds: Rectangle,
+        image_bounds: Rectangle,
+    ) {
+        let delta_x = x - self.starting_cursor_pos.unwrap().0;
+        let delta_y = y - self.starting_cursor_pos.unwrap().1;
+
+        if bounds.width < image_bounds.width {
+            self.current_offset.0 = (self.starting_offset.0 - delta_x)
+                .max(0.0)
+                .min((image_bounds.width - bounds.width) as f32);
+        }
+
+        if bounds.height < image_bounds.height {
+            self.current_offset.1 = (self.starting_offset.1 - delta_y)
+                .max(0.0)
+                .min((image_bounds.height - bounds.height) as f32);
+        }
+    }
+
+    /// Returns the current clipping offset of the [`State`], given the bounds
+    /// of the [`ImagePane`] and its contents.
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    /// [`State`]: struct.State.html
+    fn offset(&self, bounds: Rectangle, image_bounds: Rectangle) -> (u32, u32) {
+        let hidden_width = ((image_bounds.width - bounds.width) as f32)
+            .max(0.0)
+            .round() as u32;
+
+        let hidden_height = ((image_bounds.height - bounds.height) as f32)
+            .max(0.0)
+            .round() as u32;
+
+        (
+            (self.current_offset.0).min(hidden_width as f32) as u32,
+            (self.current_offset.1).min(hidden_height as f32) as u32,
+        )
+    }
+
+    /// Returns if the left mouse button is still held down since clicking inside
+    /// the [`ImagePane`].
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    /// [`State`]: struct.State.html
+    pub fn is_cursor_clicked(&self) -> bool {
+        self.starting_cursor_pos.is_some()
+    }
+}
+
+/// The renderer of an [`ImagePane`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`ImagePane`] in your user interface.
+///
+/// [`ImagePane`]: struct.ImagePane.html
+/// [renderer]: ../../renderer/index.html
+pub trait Renderer: crate::Renderer + Sized {
+    /// Draws the [`ImagePane`].
+    ///
+    /// It receives:
+    /// - the [`State`] of the [`ImagePane`]
+    /// - the bounds of the [`ImagePane`] widget
+    /// - the bounds of the scaled [`ImagePane`] image
+    /// - the clipping x,y offset
+    /// - the [`Handle`] to the underlying image
+    /// - whether the mouse is over the [`ImagePane`] or not
+    ///
+    /// [`ImagePane`]: struct.ImagePane.html
+    /// [`State`]: struct.State.html
+    /// [`Handle`]: ../image/struct.Handle.html
+    fn draw(
+        &mut self,
+        state: &State,
+        bounds: Rectangle,
+        image_bounds: Rectangle,
+        offset: (u32, u32),
+        handle: image::Handle,
+        is_mouse_over: bool,
+    ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<ImagePane<'a>>
+    for Element<'a, Message, Renderer>
+where
+    Renderer: 'a + self::Renderer + image::Renderer,
+    Message: 'a,
+{
+    fn from(image_pane: ImagePane<'a>) -> Element<'a, Message, Renderer> {
+        Element::new(image_pane)
+    }
+}
diff --git a/src/widget.rs b/src/widget.rs
index e33a6b2c..a1bc8f5b 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -33,6 +33,12 @@ mod platform {
         pub use iced_winit::image::{Handle, Image};
     }
 
+    #[cfg_attr(docsrs, doc(cfg(feature = "image")))]
+    pub mod image_pane {
+        //! Zoom and pan on an image.
+        pub use iced_wgpu::image_pane::{ImagePane, State};
+    }
+
     #[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
     pub mod svg {
         //! Display vector graphics in your user interface.
@@ -44,8 +50,8 @@ mod platform {
     #[doc(no_inline)]
     pub use {
         button::Button, checkbox::Checkbox, container::Container, image::Image,
-        pane_grid::PaneGrid, progress_bar::ProgressBar, radio::Radio,
-        scrollable::Scrollable, slider::Slider, svg::Svg,
+        image_pane::ImagePane, pane_grid::PaneGrid, progress_bar::ProgressBar,
+        radio::Radio, scrollable::Scrollable, slider::Slider, svg::Svg,
         text_input::TextInput,
     };
 
diff --git a/wgpu/src/renderer/widget.rs b/wgpu/src/renderer/widget.rs
index 37421fbe..6e1b7fe9 100644
--- a/wgpu/src/renderer/widget.rs
+++ b/wgpu/src/renderer/widget.rs
@@ -17,3 +17,5 @@ mod svg;
 
 #[cfg(feature = "image")]
 mod image;
+#[cfg(feature = "image")]
+mod image_pane;
diff --git a/wgpu/src/renderer/widget/image_pane.rs b/wgpu/src/renderer/widget/image_pane.rs
new file mode 100644
index 00000000..8b032250
--- /dev/null
+++ b/wgpu/src/renderer/widget/image_pane.rs
@@ -0,0 +1,36 @@
+use crate::{Primitive, Renderer};
+use iced_native::{image, image_pane, MouseCursor, Rectangle, Vector};
+
+impl image_pane::Renderer for Renderer {
+    fn draw(
+        &mut self,
+        state: &image_pane::State,
+        bounds: Rectangle,
+        image_bounds: Rectangle,
+        offset: (u32, u32),
+        handle: image::Handle,
+        is_mouse_over: bool,
+    ) -> Self::Output {
+        (
+            {
+                Primitive::Clip {
+                    bounds,
+                    offset: Vector::new(offset.0, offset.1),
+                    content: Box::new(Primitive::Image {
+                        handle,
+                        bounds: image_bounds,
+                    }),
+                }
+            },
+            {
+                if state.is_cursor_clicked() {
+                    MouseCursor::Grabbing
+                } else if is_mouse_over {
+                    MouseCursor::Grab
+                } else {
+                    MouseCursor::OutOfBounds
+                }
+            },
+        )
+    }
+}
diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs
index 32ccad17..a62a610d 100644
--- a/wgpu/src/widget.rs
+++ b/wgpu/src/widget.rs
@@ -47,3 +47,11 @@ pub mod canvas;
 #[cfg(feature = "canvas")]
 #[doc(no_inline)]
 pub use canvas::Canvas;
+
+#[cfg(feature = "image")]
+#[doc(no_inline)]
+pub mod image_pane;
+
+#[cfg(feature = "image")]
+#[doc(no_inline)]
+pub use image_pane::ImagePane;
diff --git a/wgpu/src/widget/image_pane.rs b/wgpu/src/widget/image_pane.rs
new file mode 100644
index 00000000..aa30a8cc
--- /dev/null
+++ b/wgpu/src/widget/image_pane.rs
@@ -0,0 +1,6 @@
+//! Zoom and pan on an image.
+
+pub use iced_native::image_pane::State;
+
+/// A widget that can display an image with the ability to zoom in/out and pan.
+pub type ImagePane<'a> = iced_native::ImagePane<'a>;
-- 
cgit 


From 7f7e803448e9706d0eec901b32eb4cf35b3ec0b0 Mon Sep 17 00:00:00 2001
From: Cory Forsstrom <cforsstrom18@gmail.com>
Date: Thu, 23 Apr 2020 16:22:53 -0700
Subject: Show idle cursor if image can't be panned

---
 wgpu/src/renderer/widget/image_pane.rs | 7 +++++--
 1 file changed, 5 insertions(+), 2 deletions(-)

diff --git a/wgpu/src/renderer/widget/image_pane.rs b/wgpu/src/renderer/widget/image_pane.rs
index 8b032250..a7cee6ac 100644
--- a/wgpu/src/renderer/widget/image_pane.rs
+++ b/wgpu/src/renderer/widget/image_pane.rs
@@ -25,10 +25,13 @@ impl image_pane::Renderer for Renderer {
             {
                 if state.is_cursor_clicked() {
                     MouseCursor::Grabbing
-                } else if is_mouse_over {
+                } else if is_mouse_over
+                    && (image_bounds.width > bounds.width
+                        || image_bounds.height > bounds.height)
+                {
                     MouseCursor::Grab
                 } else {
-                    MouseCursor::OutOfBounds
+                    MouseCursor::Idle
                 }
             },
         )
-- 
cgit 


From 6bf459e068043847a0ee1e1219056d3aced3f1cb Mon Sep 17 00:00:00 2001
From: Cory Forsstrom <cforsstrom18@gmail.com>
Date: Thu, 14 May 2020 11:54:05 -0700
Subject: Rebase to master and update for api changes

---
 native/src/widget/image_pane.rs        | 55 ++++++++++------------------------
 wgpu/src/renderer/widget/image_pane.rs |  8 ++---
 2 files changed, 20 insertions(+), 43 deletions(-)

diff --git a/native/src/widget/image_pane.rs b/native/src/widget/image_pane.rs
index 4d07f228..4f3d4877 100644
--- a/native/src/widget/image_pane.rs
+++ b/native/src/widget/image_pane.rs
@@ -1,9 +1,7 @@
 //! Zoom and pan on an image.
 use crate::{
-    image,
-    input::{self, mouse},
-    layout, Clipboard, Element, Event, Hasher, Layout, Length, Point,
-    Rectangle, Size, Widget,
+    image, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length,
+    Point, Rectangle, Size, Widget,
 };
 
 use std::{f32, hash::Hash, u32};
@@ -154,21 +152,8 @@ where
             match event {
                 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
                     match delta {
-                        mouse::ScrollDelta::Lines { y, .. } => {
-                            // TODO: Configurable step and limits
-                            if y > 0.0 {
-                                self.state.scale = Some(
-                                    (self.state.scale.unwrap_or(1.0) + 0.25)
-                                        .min(10.0),
-                                );
-                            } else {
-                                self.state.scale = Some(
-                                    (self.state.scale.unwrap_or(1.0) - 0.25)
-                                        .max(0.25),
-                                );
-                            }
-                        }
-                        mouse::ScrollDelta::Pixels { y, .. } => {
+                        mouse::ScrollDelta::Lines { y, .. }
+                        | mouse::ScrollDelta::Pixels { y, .. } => {
                             // TODO: Configurable step and limits
                             if y > 0.0 {
                                 self.state.scale = Some(
@@ -184,22 +169,17 @@ where
                         }
                     }
                 }
-                Event::Mouse(mouse::Event::Input { button, state }) => {
+                Event::Mouse(mouse::Event::ButtonPressed(button)) => {
                     if button == mouse::Button::Left {
-                        match state {
-                            input::ButtonState::Pressed => {
-                                self.state.starting_cursor_pos = Some((
-                                    cursor_position.x,
-                                    cursor_position.y,
-                                ));
-
-                                self.state.starting_offset =
-                                    self.state.current_offset;
-                            }
-                            input::ButtonState::Released => {
-                                self.state.starting_cursor_pos = None
-                            }
-                        }
+                        self.state.starting_cursor_pos =
+                            Some((cursor_position.x, cursor_position.y));
+
+                        self.state.starting_offset = self.state.current_offset;
+                    }
+                }
+                Event::Mouse(mouse::Event::ButtonReleased(button)) => {
+                    if button == mouse::Button::Left {
+                        self.state.starting_cursor_pos = None
                     }
                 }
                 Event::Mouse(mouse::Event::CursorMoved { x, y }) => {
@@ -209,12 +189,9 @@ where
                 }
                 _ => {}
             }
-        } else if let Event::Mouse(mouse::Event::Input { button, state }) =
-            event
+        } else if let Event::Mouse(mouse::Event::ButtonReleased(button)) = event
         {
-            if button == mouse::Button::Left
-                && state == input::ButtonState::Released
-            {
+            if button == mouse::Button::Left {
                 self.state.starting_cursor_pos = None;
             }
         }
diff --git a/wgpu/src/renderer/widget/image_pane.rs b/wgpu/src/renderer/widget/image_pane.rs
index a7cee6ac..b5e86913 100644
--- a/wgpu/src/renderer/widget/image_pane.rs
+++ b/wgpu/src/renderer/widget/image_pane.rs
@@ -1,5 +1,5 @@
 use crate::{Primitive, Renderer};
-use iced_native::{image, image_pane, MouseCursor, Rectangle, Vector};
+use iced_native::{image, image_pane, mouse, Rectangle, Vector};
 
 impl image_pane::Renderer for Renderer {
     fn draw(
@@ -24,14 +24,14 @@ impl image_pane::Renderer for Renderer {
             },
             {
                 if state.is_cursor_clicked() {
-                    MouseCursor::Grabbing
+                    mouse::Interaction::Grabbing
                 } else if is_mouse_over
                     && (image_bounds.width > bounds.width
                         || image_bounds.height > bounds.height)
                 {
-                    MouseCursor::Grab
+                    mouse::Interaction::Grab
                 } else {
-                    MouseCursor::Idle
+                    mouse::Interaction::Idle
                 }
             },
         )
-- 
cgit 


From 431171f975642fe96286f11fb75cd5b06827cc7f Mon Sep 17 00:00:00 2001
From: Cory Forsstrom <cforsstrom18@gmail.com>
Date: Fri, 15 May 2020 09:46:22 -0700
Subject: Rename and add to iced image module

---
 native/src/widget.rs                     |   4 +-
 native/src/widget/image_pane.rs          | 384 -------------------------------
 native/src/widget/image_viewer.rs        | 384 +++++++++++++++++++++++++++++++
 src/widget.rs                            |  20 +-
 wgpu/src/renderer/widget.rs              |   2 +-
 wgpu/src/renderer/widget/image_pane.rs   |  39 ----
 wgpu/src/renderer/widget/image_viewer.rs |  39 ++++
 wgpu/src/widget.rs                       |   4 +-
 wgpu/src/widget/image_pane.rs            |   6 -
 wgpu/src/widget/image_viewer.rs          |   6 +
 10 files changed, 445 insertions(+), 443 deletions(-)
 delete mode 100644 native/src/widget/image_pane.rs
 create mode 100644 native/src/widget/image_viewer.rs
 delete mode 100644 wgpu/src/renderer/widget/image_pane.rs
 create mode 100644 wgpu/src/renderer/widget/image_viewer.rs
 delete mode 100644 wgpu/src/widget/image_pane.rs
 create mode 100644 wgpu/src/widget/image_viewer.rs

diff --git a/native/src/widget.rs b/native/src/widget.rs
index 23194545..46d41367 100644
--- a/native/src/widget.rs
+++ b/native/src/widget.rs
@@ -25,7 +25,7 @@ pub mod checkbox;
 pub mod column;
 pub mod container;
 pub mod image;
-pub mod image_pane;
+pub mod image_viewer;
 pub mod pane_grid;
 pub mod progress_bar;
 pub mod radio;
@@ -48,7 +48,7 @@ pub use container::Container;
 #[doc(no_inline)]
 pub use image::Image;
 #[doc(no_inline)]
-pub use image_pane::ImagePane;
+pub use image_viewer::ImageViewer;
 #[doc(no_inline)]
 pub use pane_grid::PaneGrid;
 #[doc(no_inline)]
diff --git a/native/src/widget/image_pane.rs b/native/src/widget/image_pane.rs
deleted file mode 100644
index 4f3d4877..00000000
--- a/native/src/widget/image_pane.rs
+++ /dev/null
@@ -1,384 +0,0 @@
-//! Zoom and pan on an image.
-use crate::{
-    image, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length,
-    Point, Rectangle, Size, Widget,
-};
-
-use std::{f32, hash::Hash, u32};
-
-/// A widget that can display an image with the ability to zoom in/out and pan.
-#[allow(missing_debug_implementations)]
-pub struct ImagePane<'a> {
-    state: &'a mut State,
-    padding: u16,
-    width: Length,
-    height: Length,
-    max_width: u32,
-    max_height: u32,
-    handle: image::Handle,
-}
-
-impl<'a> ImagePane<'a> {
-    /// Creates a new [`ImagePane`] with the given [`State`] and [`Handle`].
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    /// [`State`]: struct.State.html
-    /// [`Handle`]: ../image/struct.Handle.html
-    pub fn new(state: &'a mut State, handle: image::Handle) -> Self {
-        ImagePane {
-            state,
-            padding: 0,
-            width: Length::Shrink,
-            height: Length::Shrink,
-            max_width: u32::MAX,
-            max_height: u32::MAX,
-            handle,
-        }
-    }
-
-    /// Sets the padding of the [`ImagePane`].
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    pub fn padding(mut self, units: u16) -> Self {
-        self.padding = units;
-        self
-    }
-
-    /// Sets the width of the [`ImagePane`].
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    pub fn width(mut self, width: Length) -> Self {
-        self.width = width;
-        self
-    }
-
-    /// Sets the height of the [`ImagePane`].
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    pub fn height(mut self, height: Length) -> Self {
-        self.height = height;
-        self
-    }
-
-    /// Sets the max width of the [`ImagePane`].
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    pub fn max_width(mut self, max_width: u32) -> Self {
-        self.max_width = max_width;
-        self
-    }
-
-    /// Sets the max height of the [`ImagePane`].
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    pub fn max_height(mut self, max_height: u32) -> Self {
-        self.max_height = max_height;
-        self
-    }
-}
-
-impl<'a, Message, Renderer> Widget<Message, Renderer> for ImagePane<'a>
-where
-    Renderer: self::Renderer + image::Renderer,
-{
-    fn width(&self) -> Length {
-        self.width
-    }
-
-    fn height(&self) -> Length {
-        self.height
-    }
-
-    fn layout(
-        &self,
-        _renderer: &Renderer,
-        limits: &layout::Limits,
-    ) -> layout::Node {
-        let padding = f32::from(self.padding);
-
-        let limits = limits
-            .max_width(self.max_width)
-            .max_height(self.max_height)
-            .width(self.width)
-            .height(self.height)
-            .pad(padding);
-
-        let size = limits.resolve(Size::INFINITY);
-
-        layout::Node::new(size)
-    }
-
-    fn on_event(
-        &mut self,
-        event: Event,
-        layout: Layout<'_>,
-        cursor_position: Point,
-        _messages: &mut Vec<Message>,
-        renderer: &Renderer,
-        _clipboard: Option<&dyn Clipboard>,
-    ) {
-        let bounds = layout.bounds();
-        let is_mouse_over = bounds.contains(cursor_position);
-
-        let image_bounds = {
-            let (width, height) = renderer.dimensions(&self.handle);
-
-            let dimensions = if let Some(scale) = self.state.scale {
-                (width as f32 * scale, height as f32 * scale)
-            } else {
-                let dimensions = (width as f32, height as f32);
-
-                let width_scale = bounds.width / dimensions.0;
-                let height_scale = bounds.height / dimensions.1;
-
-                let scale = width_scale.min(height_scale);
-
-                if scale < 1.0 {
-                    (dimensions.0 * scale, dimensions.1 * scale)
-                } else {
-                    (dimensions.0, dimensions.1)
-                }
-            };
-
-            Rectangle {
-                x: bounds.x,
-                y: bounds.y,
-                width: dimensions.0,
-                height: dimensions.1,
-            }
-        };
-
-        if is_mouse_over {
-            match event {
-                Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
-                    match delta {
-                        mouse::ScrollDelta::Lines { y, .. }
-                        | mouse::ScrollDelta::Pixels { y, .. } => {
-                            // TODO: Configurable step and limits
-                            if y > 0.0 {
-                                self.state.scale = Some(
-                                    (self.state.scale.unwrap_or(1.0) + 0.25)
-                                        .min(10.0),
-                                );
-                            } else {
-                                self.state.scale = Some(
-                                    (self.state.scale.unwrap_or(1.0) - 0.25)
-                                        .max(0.25),
-                                );
-                            }
-                        }
-                    }
-                }
-                Event::Mouse(mouse::Event::ButtonPressed(button)) => {
-                    if button == mouse::Button::Left {
-                        self.state.starting_cursor_pos =
-                            Some((cursor_position.x, cursor_position.y));
-
-                        self.state.starting_offset = self.state.current_offset;
-                    }
-                }
-                Event::Mouse(mouse::Event::ButtonReleased(button)) => {
-                    if button == mouse::Button::Left {
-                        self.state.starting_cursor_pos = None
-                    }
-                }
-                Event::Mouse(mouse::Event::CursorMoved { x, y }) => {
-                    if self.state.is_cursor_clicked() {
-                        self.state.pan(x, y, bounds, image_bounds);
-                    }
-                }
-                _ => {}
-            }
-        } else if let Event::Mouse(mouse::Event::ButtonReleased(button)) = event
-        {
-            if button == mouse::Button::Left {
-                self.state.starting_cursor_pos = None;
-            }
-        }
-    }
-
-    fn draw(
-        &self,
-        renderer: &mut Renderer,
-        _defaults: &Renderer::Defaults,
-        layout: Layout<'_>,
-        cursor_position: Point,
-    ) -> Renderer::Output {
-        let bounds = layout.bounds();
-
-        let image_bounds = {
-            let (width, height) = renderer.dimensions(&self.handle);
-
-            let dimensions = if let Some(scale) = self.state.scale {
-                (width as f32 * scale, height as f32 * scale)
-            } else {
-                let dimensions = (width as f32, height as f32);
-
-                let width_scale = bounds.width / dimensions.0;
-                let height_scale = bounds.height / dimensions.1;
-
-                let scale = width_scale.min(height_scale);
-
-                if scale < 1.0 {
-                    (dimensions.0 * scale, dimensions.1 * scale)
-                } else {
-                    (dimensions.0, dimensions.1)
-                }
-            };
-
-            Rectangle {
-                x: bounds.x,
-                y: bounds.y,
-                width: dimensions.0,
-                height: dimensions.1,
-            }
-        };
-
-        let offset = self.state.offset(bounds, image_bounds);
-
-        let is_mouse_over = bounds.contains(cursor_position);
-
-        self::Renderer::draw(
-            renderer,
-            &self.state,
-            bounds,
-            image_bounds,
-            offset,
-            self.handle.clone(),
-            is_mouse_over,
-        )
-    }
-
-    fn hash_layout(&self, state: &mut Hasher) {
-        struct Marker;
-        std::any::TypeId::of::<Marker>().hash(state);
-
-        self.width.hash(state);
-        self.height.hash(state);
-        self.max_width.hash(state);
-        self.max_height.hash(state);
-        self.padding.hash(state);
-
-        self.handle.hash(state);
-    }
-}
-
-/// The local state of an [`ImagePane`].
-///
-/// [`ImagePane`]: struct.ImagePane.html
-#[derive(Debug, Clone, Copy, Default)]
-pub struct State {
-    scale: Option<f32>,
-    starting_offset: (f32, f32),
-    current_offset: (f32, f32),
-    starting_cursor_pos: Option<(f32, f32)>,
-}
-
-impl State {
-    /// Creates a new [`State`] with the scrollbar located at the top.
-    ///
-    /// [`State`]: struct.State.html
-    pub fn new() -> Self {
-        State::default()
-    }
-
-    /// Apply a panning offset to the current [`State`], given the bounds of
-    /// the [`ImagePane`] and its image.
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    /// [`State`]: struct.State.html
-    fn pan(
-        &mut self,
-        x: f32,
-        y: f32,
-        bounds: Rectangle,
-        image_bounds: Rectangle,
-    ) {
-        let delta_x = x - self.starting_cursor_pos.unwrap().0;
-        let delta_y = y - self.starting_cursor_pos.unwrap().1;
-
-        if bounds.width < image_bounds.width {
-            self.current_offset.0 = (self.starting_offset.0 - delta_x)
-                .max(0.0)
-                .min((image_bounds.width - bounds.width) as f32);
-        }
-
-        if bounds.height < image_bounds.height {
-            self.current_offset.1 = (self.starting_offset.1 - delta_y)
-                .max(0.0)
-                .min((image_bounds.height - bounds.height) as f32);
-        }
-    }
-
-    /// Returns the current clipping offset of the [`State`], given the bounds
-    /// of the [`ImagePane`] and its contents.
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    /// [`State`]: struct.State.html
-    fn offset(&self, bounds: Rectangle, image_bounds: Rectangle) -> (u32, u32) {
-        let hidden_width = ((image_bounds.width - bounds.width) as f32)
-            .max(0.0)
-            .round() as u32;
-
-        let hidden_height = ((image_bounds.height - bounds.height) as f32)
-            .max(0.0)
-            .round() as u32;
-
-        (
-            (self.current_offset.0).min(hidden_width as f32) as u32,
-            (self.current_offset.1).min(hidden_height as f32) as u32,
-        )
-    }
-
-    /// Returns if the left mouse button is still held down since clicking inside
-    /// the [`ImagePane`].
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    /// [`State`]: struct.State.html
-    pub fn is_cursor_clicked(&self) -> bool {
-        self.starting_cursor_pos.is_some()
-    }
-}
-
-/// The renderer of an [`ImagePane`].
-///
-/// Your [renderer] will need to implement this trait before being
-/// able to use a [`ImagePane`] in your user interface.
-///
-/// [`ImagePane`]: struct.ImagePane.html
-/// [renderer]: ../../renderer/index.html
-pub trait Renderer: crate::Renderer + Sized {
-    /// Draws the [`ImagePane`].
-    ///
-    /// It receives:
-    /// - the [`State`] of the [`ImagePane`]
-    /// - the bounds of the [`ImagePane`] widget
-    /// - the bounds of the scaled [`ImagePane`] image
-    /// - the clipping x,y offset
-    /// - the [`Handle`] to the underlying image
-    /// - whether the mouse is over the [`ImagePane`] or not
-    ///
-    /// [`ImagePane`]: struct.ImagePane.html
-    /// [`State`]: struct.State.html
-    /// [`Handle`]: ../image/struct.Handle.html
-    fn draw(
-        &mut self,
-        state: &State,
-        bounds: Rectangle,
-        image_bounds: Rectangle,
-        offset: (u32, u32),
-        handle: image::Handle,
-        is_mouse_over: bool,
-    ) -> Self::Output;
-}
-
-impl<'a, Message, Renderer> From<ImagePane<'a>>
-    for Element<'a, Message, Renderer>
-where
-    Renderer: 'a + self::Renderer + image::Renderer,
-    Message: 'a,
-{
-    fn from(image_pane: ImagePane<'a>) -> Element<'a, Message, Renderer> {
-        Element::new(image_pane)
-    }
-}
diff --git a/native/src/widget/image_viewer.rs b/native/src/widget/image_viewer.rs
new file mode 100644
index 00000000..d0f31cb4
--- /dev/null
+++ b/native/src/widget/image_viewer.rs
@@ -0,0 +1,384 @@
+//! Zoom and pan on an image.
+use crate::{
+    image, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length,
+    Point, Rectangle, Size, Widget,
+};
+
+use std::{f32, hash::Hash, u32};
+
+/// A widget that can display an image with the ability to zoom in/out and pan.
+#[allow(missing_debug_implementations)]
+pub struct ImageViewer<'a> {
+    state: &'a mut State,
+    padding: u16,
+    width: Length,
+    height: Length,
+    max_width: u32,
+    max_height: u32,
+    handle: image::Handle,
+}
+
+impl<'a> ImageViewer<'a> {
+    /// Creates a new [`ImageViewer`] with the given [`State`] and [`Handle`].
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    /// [`State`]: struct.State.html
+    /// [`Handle`]: ../image/struct.Handle.html
+    pub fn new(state: &'a mut State, handle: image::Handle) -> Self {
+        ImageViewer {
+            state,
+            padding: 0,
+            width: Length::Shrink,
+            height: Length::Shrink,
+            max_width: u32::MAX,
+            max_height: u32::MAX,
+            handle,
+        }
+    }
+
+    /// Sets the padding of the [`ImageViewer`].
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    pub fn padding(mut self, units: u16) -> Self {
+        self.padding = units;
+        self
+    }
+
+    /// Sets the width of the [`ImageViewer`].
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    pub fn width(mut self, width: Length) -> Self {
+        self.width = width;
+        self
+    }
+
+    /// Sets the height of the [`ImageViewer`].
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    pub fn height(mut self, height: Length) -> Self {
+        self.height = height;
+        self
+    }
+
+    /// Sets the max width of the [`ImageViewer`].
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    pub fn max_width(mut self, max_width: u32) -> Self {
+        self.max_width = max_width;
+        self
+    }
+
+    /// Sets the max height of the [`ImageViewer`].
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    pub fn max_height(mut self, max_height: u32) -> Self {
+        self.max_height = max_height;
+        self
+    }
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer> for ImageViewer<'a>
+where
+    Renderer: self::Renderer + image::Renderer,
+{
+    fn width(&self) -> Length {
+        self.width
+    }
+
+    fn height(&self) -> Length {
+        self.height
+    }
+
+    fn layout(
+        &self,
+        _renderer: &Renderer,
+        limits: &layout::Limits,
+    ) -> layout::Node {
+        let padding = f32::from(self.padding);
+
+        let limits = limits
+            .max_width(self.max_width)
+            .max_height(self.max_height)
+            .width(self.width)
+            .height(self.height)
+            .pad(padding);
+
+        let size = limits.resolve(Size::INFINITY);
+
+        layout::Node::new(size)
+    }
+
+    fn on_event(
+        &mut self,
+        event: Event,
+        layout: Layout<'_>,
+        cursor_position: Point,
+        _messages: &mut Vec<Message>,
+        renderer: &Renderer,
+        _clipboard: Option<&dyn Clipboard>,
+    ) {
+        let bounds = layout.bounds();
+        let is_mouse_over = bounds.contains(cursor_position);
+
+        let image_bounds = {
+            let (width, height) = renderer.dimensions(&self.handle);
+
+            let dimensions = if let Some(scale) = self.state.scale {
+                (width as f32 * scale, height as f32 * scale)
+            } else {
+                let dimensions = (width as f32, height as f32);
+
+                let width_scale = bounds.width / dimensions.0;
+                let height_scale = bounds.height / dimensions.1;
+
+                let scale = width_scale.min(height_scale);
+
+                if scale < 1.0 {
+                    (dimensions.0 * scale, dimensions.1 * scale)
+                } else {
+                    (dimensions.0, dimensions.1)
+                }
+            };
+
+            Rectangle {
+                x: bounds.x,
+                y: bounds.y,
+                width: dimensions.0,
+                height: dimensions.1,
+            }
+        };
+
+        if is_mouse_over {
+            match event {
+                Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
+                    match delta {
+                        mouse::ScrollDelta::Lines { y, .. }
+                        | mouse::ScrollDelta::Pixels { y, .. } => {
+                            // TODO: Configurable step and limits
+                            if y > 0.0 {
+                                self.state.scale = Some(
+                                    (self.state.scale.unwrap_or(1.0) + 0.25)
+                                        .min(10.0),
+                                );
+                            } else {
+                                self.state.scale = Some(
+                                    (self.state.scale.unwrap_or(1.0) - 0.25)
+                                        .max(0.25),
+                                );
+                            }
+                        }
+                    }
+                }
+                Event::Mouse(mouse::Event::ButtonPressed(button)) => {
+                    if button == mouse::Button::Left {
+                        self.state.starting_cursor_pos =
+                            Some((cursor_position.x, cursor_position.y));
+
+                        self.state.starting_offset = self.state.current_offset;
+                    }
+                }
+                Event::Mouse(mouse::Event::ButtonReleased(button)) => {
+                    if button == mouse::Button::Left {
+                        self.state.starting_cursor_pos = None
+                    }
+                }
+                Event::Mouse(mouse::Event::CursorMoved { x, y }) => {
+                    if self.state.is_cursor_clicked() {
+                        self.state.pan(x, y, bounds, image_bounds);
+                    }
+                }
+                _ => {}
+            }
+        } else if let Event::Mouse(mouse::Event::ButtonReleased(button)) = event
+        {
+            if button == mouse::Button::Left {
+                self.state.starting_cursor_pos = None;
+            }
+        }
+    }
+
+    fn draw(
+        &self,
+        renderer: &mut Renderer,
+        _defaults: &Renderer::Defaults,
+        layout: Layout<'_>,
+        cursor_position: Point,
+    ) -> Renderer::Output {
+        let bounds = layout.bounds();
+
+        let image_bounds = {
+            let (width, height) = renderer.dimensions(&self.handle);
+
+            let dimensions = if let Some(scale) = self.state.scale {
+                (width as f32 * scale, height as f32 * scale)
+            } else {
+                let dimensions = (width as f32, height as f32);
+
+                let width_scale = bounds.width / dimensions.0;
+                let height_scale = bounds.height / dimensions.1;
+
+                let scale = width_scale.min(height_scale);
+
+                if scale < 1.0 {
+                    (dimensions.0 * scale, dimensions.1 * scale)
+                } else {
+                    (dimensions.0, dimensions.1)
+                }
+            };
+
+            Rectangle {
+                x: bounds.x,
+                y: bounds.y,
+                width: dimensions.0,
+                height: dimensions.1,
+            }
+        };
+
+        let offset = self.state.offset(bounds, image_bounds);
+
+        let is_mouse_over = bounds.contains(cursor_position);
+
+        self::Renderer::draw(
+            renderer,
+            &self.state,
+            bounds,
+            image_bounds,
+            offset,
+            self.handle.clone(),
+            is_mouse_over,
+        )
+    }
+
+    fn hash_layout(&self, state: &mut Hasher) {
+        struct Marker;
+        std::any::TypeId::of::<Marker>().hash(state);
+
+        self.width.hash(state);
+        self.height.hash(state);
+        self.max_width.hash(state);
+        self.max_height.hash(state);
+        self.padding.hash(state);
+
+        self.handle.hash(state);
+    }
+}
+
+/// The local state of an [`ImageViewer`].
+///
+/// [`ImageViewer`]: struct.ImageViewer.html
+#[derive(Debug, Clone, Copy, Default)]
+pub struct State {
+    scale: Option<f32>,
+    starting_offset: (f32, f32),
+    current_offset: (f32, f32),
+    starting_cursor_pos: Option<(f32, f32)>,
+}
+
+impl State {
+    /// Creates a new [`State`].
+    ///
+    /// [`State`]: struct.State.html
+    pub fn new() -> Self {
+        State::default()
+    }
+
+    /// Apply a panning offset to the current [`State`], given the bounds of
+    /// the [`ImageViewer`] and its image.
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    /// [`State`]: struct.State.html
+    fn pan(
+        &mut self,
+        x: f32,
+        y: f32,
+        bounds: Rectangle,
+        image_bounds: Rectangle,
+    ) {
+        let delta_x = x - self.starting_cursor_pos.unwrap().0;
+        let delta_y = y - self.starting_cursor_pos.unwrap().1;
+
+        if bounds.width < image_bounds.width {
+            self.current_offset.0 = (self.starting_offset.0 - delta_x)
+                .max(0.0)
+                .min((image_bounds.width - bounds.width) as f32);
+        }
+
+        if bounds.height < image_bounds.height {
+            self.current_offset.1 = (self.starting_offset.1 - delta_y)
+                .max(0.0)
+                .min((image_bounds.height - bounds.height) as f32);
+        }
+    }
+
+    /// Returns the current clipping offset of the [`State`], given the bounds
+    /// of the [`ImageViewer`] and its contents.
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    /// [`State`]: struct.State.html
+    fn offset(&self, bounds: Rectangle, image_bounds: Rectangle) -> (u32, u32) {
+        let hidden_width = ((image_bounds.width - bounds.width) as f32)
+            .max(0.0)
+            .round() as u32;
+
+        let hidden_height = ((image_bounds.height - bounds.height) as f32)
+            .max(0.0)
+            .round() as u32;
+
+        (
+            (self.current_offset.0).min(hidden_width as f32) as u32,
+            (self.current_offset.1).min(hidden_height as f32) as u32,
+        )
+    }
+
+    /// Returns if the left mouse button is still held down since clicking inside
+    /// the [`ImageViewer`].
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    /// [`State`]: struct.State.html
+    pub fn is_cursor_clicked(&self) -> bool {
+        self.starting_cursor_pos.is_some()
+    }
+}
+
+/// The renderer of an [`ImageViewer`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`ImageViewer`] in your user interface.
+///
+/// [`ImageViewer`]: struct.ImageViewer.html
+/// [renderer]: ../../renderer/index.html
+pub trait Renderer: crate::Renderer + Sized {
+    /// Draws the [`ImageViewer`].
+    ///
+    /// It receives:
+    /// - the [`State`] of the [`ImageViewer`]
+    /// - the bounds of the [`ImageViewer`] widget
+    /// - the bounds of the scaled [`ImageViewer`] image
+    /// - the clipping x,y offset
+    /// - the [`Handle`] to the underlying image
+    /// - whether the mouse is over the [`ImageViewer`] or not
+    ///
+    /// [`ImageViewer`]: struct.ImageViewer.html
+    /// [`State`]: struct.State.html
+    /// [`Handle`]: ../image/struct.Handle.html
+    fn draw(
+        &mut self,
+        state: &State,
+        bounds: Rectangle,
+        image_bounds: Rectangle,
+        offset: (u32, u32),
+        handle: image::Handle,
+        is_mouse_over: bool,
+    ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<ImageViewer<'a>>
+    for Element<'a, Message, Renderer>
+where
+    Renderer: 'a + self::Renderer + image::Renderer,
+    Message: 'a,
+{
+    fn from(viewer: ImageViewer<'a>) -> Element<'a, Message, Renderer> {
+        Element::new(viewer)
+    }
+}
diff --git a/src/widget.rs b/src/widget.rs
index a1bc8f5b..92303277 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -31,12 +31,7 @@ mod platform {
     pub mod image {
         //! Display images in your user interface.
         pub use iced_winit::image::{Handle, Image};
-    }
-
-    #[cfg_attr(docsrs, doc(cfg(feature = "image")))]
-    pub mod image_pane {
-        //! Zoom and pan on an image.
-        pub use iced_wgpu::image_pane::{ImagePane, State};
+        pub use iced_winit::image_viewer::{ImageViewer, State};
     }
 
     #[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
@@ -49,9 +44,16 @@ mod platform {
 
     #[doc(no_inline)]
     pub use {
-        button::Button, checkbox::Checkbox, container::Container, image::Image,
-        image_pane::ImagePane, pane_grid::PaneGrid, progress_bar::ProgressBar,
-        radio::Radio, scrollable::Scrollable, slider::Slider, svg::Svg,
+        button::Button,
+        checkbox::Checkbox,
+        container::Container,
+        image::{Image, ImageViewer},
+        pane_grid::PaneGrid,
+        progress_bar::ProgressBar,
+        radio::Radio,
+        scrollable::Scrollable,
+        slider::Slider,
+        svg::Svg,
         text_input::TextInput,
     };
 
diff --git a/wgpu/src/renderer/widget.rs b/wgpu/src/renderer/widget.rs
index 6e1b7fe9..c9f288c7 100644
--- a/wgpu/src/renderer/widget.rs
+++ b/wgpu/src/renderer/widget.rs
@@ -18,4 +18,4 @@ mod svg;
 #[cfg(feature = "image")]
 mod image;
 #[cfg(feature = "image")]
-mod image_pane;
+mod image_viewer;
diff --git a/wgpu/src/renderer/widget/image_pane.rs b/wgpu/src/renderer/widget/image_pane.rs
deleted file mode 100644
index b5e86913..00000000
--- a/wgpu/src/renderer/widget/image_pane.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-use crate::{Primitive, Renderer};
-use iced_native::{image, image_pane, mouse, Rectangle, Vector};
-
-impl image_pane::Renderer for Renderer {
-    fn draw(
-        &mut self,
-        state: &image_pane::State,
-        bounds: Rectangle,
-        image_bounds: Rectangle,
-        offset: (u32, u32),
-        handle: image::Handle,
-        is_mouse_over: bool,
-    ) -> Self::Output {
-        (
-            {
-                Primitive::Clip {
-                    bounds,
-                    offset: Vector::new(offset.0, offset.1),
-                    content: Box::new(Primitive::Image {
-                        handle,
-                        bounds: image_bounds,
-                    }),
-                }
-            },
-            {
-                if state.is_cursor_clicked() {
-                    mouse::Interaction::Grabbing
-                } else if is_mouse_over
-                    && (image_bounds.width > bounds.width
-                        || image_bounds.height > bounds.height)
-                {
-                    mouse::Interaction::Grab
-                } else {
-                    mouse::Interaction::Idle
-                }
-            },
-        )
-    }
-}
diff --git a/wgpu/src/renderer/widget/image_viewer.rs b/wgpu/src/renderer/widget/image_viewer.rs
new file mode 100644
index 00000000..b8546d43
--- /dev/null
+++ b/wgpu/src/renderer/widget/image_viewer.rs
@@ -0,0 +1,39 @@
+use crate::{Primitive, Renderer};
+use iced_native::{image, image_viewer, mouse, Rectangle, Vector};
+
+impl image_viewer::Renderer for Renderer {
+    fn draw(
+        &mut self,
+        state: &image_viewer::State,
+        bounds: Rectangle,
+        image_bounds: Rectangle,
+        offset: (u32, u32),
+        handle: image::Handle,
+        is_mouse_over: bool,
+    ) -> Self::Output {
+        (
+            {
+                Primitive::Clip {
+                    bounds,
+                    offset: Vector::new(offset.0, offset.1),
+                    content: Box::new(Primitive::Image {
+                        handle,
+                        bounds: image_bounds,
+                    }),
+                }
+            },
+            {
+                if state.is_cursor_clicked() {
+                    mouse::Interaction::Grabbing
+                } else if is_mouse_over
+                    && (image_bounds.width > bounds.width
+                        || image_bounds.height > bounds.height)
+                {
+                    mouse::Interaction::Grab
+                } else {
+                    mouse::Interaction::Idle
+                }
+            },
+        )
+    }
+}
diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs
index a62a610d..e968c366 100644
--- a/wgpu/src/widget.rs
+++ b/wgpu/src/widget.rs
@@ -50,8 +50,8 @@ pub use canvas::Canvas;
 
 #[cfg(feature = "image")]
 #[doc(no_inline)]
-pub mod image_pane;
+pub mod image_viewer;
 
 #[cfg(feature = "image")]
 #[doc(no_inline)]
-pub use image_pane::ImagePane;
+pub use image_viewer::ImageViewer;
diff --git a/wgpu/src/widget/image_pane.rs b/wgpu/src/widget/image_pane.rs
deleted file mode 100644
index aa30a8cc..00000000
--- a/wgpu/src/widget/image_pane.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-//! Zoom and pan on an image.
-
-pub use iced_native::image_pane::State;
-
-/// A widget that can display an image with the ability to zoom in/out and pan.
-pub type ImagePane<'a> = iced_native::ImagePane<'a>;
diff --git a/wgpu/src/widget/image_viewer.rs b/wgpu/src/widget/image_viewer.rs
new file mode 100644
index 00000000..ec44e30a
--- /dev/null
+++ b/wgpu/src/widget/image_viewer.rs
@@ -0,0 +1,6 @@
+//! Zoom and pan on an image.
+
+pub use iced_native::image_viewer::State;
+
+/// A widget that can display an image with the ability to zoom in/out and pan.
+pub type ImageViewer<'a> = iced_native::ImageViewer<'a>;
-- 
cgit 


From 5d045c2e9a639f8bbf43e68fde9091be702b3ab8 Mon Sep 17 00:00:00 2001
From: Cory Forsstrom <cforsstrom18@gmail.com>
Date: Tue, 26 May 2020 17:15:55 -0700
Subject: rename to image::Viewer

---
 native/src/widget.rs                     |   3 -
 native/src/widget/image.rs               |   3 +
 native/src/widget/image/viewer.rs        | 383 ++++++++++++++++++++++++++++++
 native/src/widget/image_viewer.rs        | 384 -------------------------------
 src/widget.rs                            |  16 +-
 wgpu/src/renderer/widget.rs              |   2 -
 wgpu/src/renderer/widget/image.rs        |   2 +
 wgpu/src/renderer/widget/image/viewer.rs |  39 ++++
 wgpu/src/renderer/widget/image_viewer.rs |  39 ----
 wgpu/src/widget.rs                       |   8 -
 wgpu/src/widget/image_viewer.rs          |   6 -
 11 files changed, 431 insertions(+), 454 deletions(-)
 create mode 100644 native/src/widget/image/viewer.rs
 delete mode 100644 native/src/widget/image_viewer.rs
 create mode 100644 wgpu/src/renderer/widget/image/viewer.rs
 delete mode 100644 wgpu/src/renderer/widget/image_viewer.rs
 delete mode 100644 wgpu/src/widget/image_viewer.rs

diff --git a/native/src/widget.rs b/native/src/widget.rs
index 46d41367..4453145b 100644
--- a/native/src/widget.rs
+++ b/native/src/widget.rs
@@ -25,7 +25,6 @@ pub mod checkbox;
 pub mod column;
 pub mod container;
 pub mod image;
-pub mod image_viewer;
 pub mod pane_grid;
 pub mod progress_bar;
 pub mod radio;
@@ -48,8 +47,6 @@ pub use container::Container;
 #[doc(no_inline)]
 pub use image::Image;
 #[doc(no_inline)]
-pub use image_viewer::ImageViewer;
-#[doc(no_inline)]
 pub use pane_grid::PaneGrid;
 #[doc(no_inline)]
 pub use progress_bar::ProgressBar;
diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs
index 132f249d..685cb81a 100644
--- a/native/src/widget/image.rs
+++ b/native/src/widget/image.rs
@@ -1,4 +1,7 @@
 //! Display images in your user interface.
+pub mod viewer;
+pub use viewer::{State, Viewer};
+
 use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget};
 
 use std::{
diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs
new file mode 100644
index 00000000..c33f3a5e
--- /dev/null
+++ b/native/src/widget/image/viewer.rs
@@ -0,0 +1,383 @@
+//! Zoom and pan on an image.
+use crate::{
+    image, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length,
+    Point, Rectangle, Size, Widget,
+};
+
+use std::{f32, hash::Hash, u32};
+
+/// A widget that can display an image with the ability to zoom in/out and pan.
+#[allow(missing_debug_implementations)]
+pub struct Viewer<'a> {
+    state: &'a mut State,
+    padding: u16,
+    width: Length,
+    height: Length,
+    max_width: u32,
+    max_height: u32,
+    handle: image::Handle,
+}
+
+impl<'a> Viewer<'a> {
+    /// Creates a new [`Viewer`] with the given [`State`] and [`Handle`].
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    /// [`State`]: struct.State.html
+    /// [`Handle`]: ../../image/struct.Handle.html
+    pub fn new(state: &'a mut State, handle: image::Handle) -> Self {
+        Viewer {
+            state,
+            padding: 0,
+            width: Length::Shrink,
+            height: Length::Shrink,
+            max_width: u32::MAX,
+            max_height: u32::MAX,
+            handle,
+        }
+    }
+
+    /// Sets the padding of the [`Viewer`].
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    pub fn padding(mut self, units: u16) -> Self {
+        self.padding = units;
+        self
+    }
+
+    /// Sets the width of the [`Viewer`].
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    pub fn width(mut self, width: Length) -> Self {
+        self.width = width;
+        self
+    }
+
+    /// Sets the height of the [`Viewer`].
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    pub fn height(mut self, height: Length) -> Self {
+        self.height = height;
+        self
+    }
+
+    /// Sets the max width of the [`Viewer`].
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    pub fn max_width(mut self, max_width: u32) -> Self {
+        self.max_width = max_width;
+        self
+    }
+
+    /// Sets the max height of the [`Viewer`].
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    pub fn max_height(mut self, max_height: u32) -> Self {
+        self.max_height = max_height;
+        self
+    }
+}
+
+impl<'a, Message, Renderer> Widget<Message, Renderer> for Viewer<'a>
+where
+    Renderer: self::Renderer + image::Renderer,
+{
+    fn width(&self) -> Length {
+        self.width
+    }
+
+    fn height(&self) -> Length {
+        self.height
+    }
+
+    fn layout(
+        &self,
+        _renderer: &Renderer,
+        limits: &layout::Limits,
+    ) -> layout::Node {
+        let padding = f32::from(self.padding);
+
+        let limits = limits
+            .max_width(self.max_width)
+            .max_height(self.max_height)
+            .width(self.width)
+            .height(self.height)
+            .pad(padding);
+
+        let size = limits.resolve(Size::INFINITY);
+
+        layout::Node::new(size)
+    }
+
+    fn on_event(
+        &mut self,
+        event: Event,
+        layout: Layout<'_>,
+        cursor_position: Point,
+        _messages: &mut Vec<Message>,
+        renderer: &Renderer,
+        _clipboard: Option<&dyn Clipboard>,
+    ) {
+        let bounds = layout.bounds();
+        let is_mouse_over = bounds.contains(cursor_position);
+
+        let image_bounds = {
+            let (width, height) = renderer.dimensions(&self.handle);
+
+            let dimensions = if let Some(scale) = self.state.scale {
+                (width as f32 * scale, height as f32 * scale)
+            } else {
+                let dimensions = (width as f32, height as f32);
+
+                let width_scale = bounds.width / dimensions.0;
+                let height_scale = bounds.height / dimensions.1;
+
+                let scale = width_scale.min(height_scale);
+
+                if scale < 1.0 {
+                    (dimensions.0 * scale, dimensions.1 * scale)
+                } else {
+                    (dimensions.0, dimensions.1)
+                }
+            };
+
+            Rectangle {
+                x: bounds.x,
+                y: bounds.y,
+                width: dimensions.0,
+                height: dimensions.1,
+            }
+        };
+
+        if is_mouse_over {
+            match event {
+                Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
+                    match delta {
+                        mouse::ScrollDelta::Lines { y, .. }
+                        | mouse::ScrollDelta::Pixels { y, .. } => {
+                            // TODO: Configurable step and limits
+                            if y > 0.0 {
+                                self.state.scale = Some(
+                                    (self.state.scale.unwrap_or(1.0) + 0.25)
+                                        .min(10.0),
+                                );
+                            } else {
+                                self.state.scale = Some(
+                                    (self.state.scale.unwrap_or(1.0) - 0.25)
+                                        .max(0.25),
+                                );
+                            }
+                        }
+                    }
+                }
+                Event::Mouse(mouse::Event::ButtonPressed(button)) => {
+                    if button == mouse::Button::Left {
+                        self.state.starting_cursor_pos =
+                            Some((cursor_position.x, cursor_position.y));
+
+                        self.state.starting_offset = self.state.current_offset;
+                    }
+                }
+                Event::Mouse(mouse::Event::ButtonReleased(button)) => {
+                    if button == mouse::Button::Left {
+                        self.state.starting_cursor_pos = None
+                    }
+                }
+                Event::Mouse(mouse::Event::CursorMoved { x, y }) => {
+                    if self.state.is_cursor_clicked() {
+                        self.state.pan(x, y, bounds, image_bounds);
+                    }
+                }
+                _ => {}
+            }
+        } else if let Event::Mouse(mouse::Event::ButtonReleased(button)) = event
+        {
+            if button == mouse::Button::Left {
+                self.state.starting_cursor_pos = None;
+            }
+        }
+    }
+
+    fn draw(
+        &self,
+        renderer: &mut Renderer,
+        _defaults: &Renderer::Defaults,
+        layout: Layout<'_>,
+        cursor_position: Point,
+    ) -> Renderer::Output {
+        let bounds = layout.bounds();
+
+        let image_bounds = {
+            let (width, height) = renderer.dimensions(&self.handle);
+
+            let dimensions = if let Some(scale) = self.state.scale {
+                (width as f32 * scale, height as f32 * scale)
+            } else {
+                let dimensions = (width as f32, height as f32);
+
+                let width_scale = bounds.width / dimensions.0;
+                let height_scale = bounds.height / dimensions.1;
+
+                let scale = width_scale.min(height_scale);
+
+                if scale < 1.0 {
+                    (dimensions.0 * scale, dimensions.1 * scale)
+                } else {
+                    (dimensions.0, dimensions.1)
+                }
+            };
+
+            Rectangle {
+                x: bounds.x,
+                y: bounds.y,
+                width: dimensions.0,
+                height: dimensions.1,
+            }
+        };
+
+        let offset = self.state.offset(bounds, image_bounds);
+
+        let is_mouse_over = bounds.contains(cursor_position);
+
+        self::Renderer::draw(
+            renderer,
+            &self.state,
+            bounds,
+            image_bounds,
+            offset,
+            self.handle.clone(),
+            is_mouse_over,
+        )
+    }
+
+    fn hash_layout(&self, state: &mut Hasher) {
+        struct Marker;
+        std::any::TypeId::of::<Marker>().hash(state);
+
+        self.width.hash(state);
+        self.height.hash(state);
+        self.max_width.hash(state);
+        self.max_height.hash(state);
+        self.padding.hash(state);
+
+        self.handle.hash(state);
+    }
+}
+
+/// The local state of a [`Viewer`].
+///
+/// [`Viewer`]: struct.Viewer.html
+#[derive(Debug, Clone, Copy, Default)]
+pub struct State {
+    scale: Option<f32>,
+    starting_offset: (f32, f32),
+    current_offset: (f32, f32),
+    starting_cursor_pos: Option<(f32, f32)>,
+}
+
+impl State {
+    /// Creates a new [`State`].
+    ///
+    /// [`State`]: struct.State.html
+    pub fn new() -> Self {
+        State::default()
+    }
+
+    /// Apply a panning offset to the current [`State`], given the bounds of
+    /// the [`Viewer`] and its image.
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    /// [`State`]: struct.State.html
+    fn pan(
+        &mut self,
+        x: f32,
+        y: f32,
+        bounds: Rectangle,
+        image_bounds: Rectangle,
+    ) {
+        let delta_x = x - self.starting_cursor_pos.unwrap().0;
+        let delta_y = y - self.starting_cursor_pos.unwrap().1;
+
+        if bounds.width < image_bounds.width {
+            self.current_offset.0 = (self.starting_offset.0 - delta_x)
+                .max(0.0)
+                .min((image_bounds.width - bounds.width) as f32);
+        }
+
+        if bounds.height < image_bounds.height {
+            self.current_offset.1 = (self.starting_offset.1 - delta_y)
+                .max(0.0)
+                .min((image_bounds.height - bounds.height) as f32);
+        }
+    }
+
+    /// Returns the current clipping offset of the [`State`], given the bounds
+    /// of the [`Viewer`] and its contents.
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    /// [`State`]: struct.State.html
+    fn offset(&self, bounds: Rectangle, image_bounds: Rectangle) -> (u32, u32) {
+        let hidden_width = ((image_bounds.width - bounds.width) as f32)
+            .max(0.0)
+            .round() as u32;
+
+        let hidden_height = ((image_bounds.height - bounds.height) as f32)
+            .max(0.0)
+            .round() as u32;
+
+        (
+            (self.current_offset.0).min(hidden_width as f32) as u32,
+            (self.current_offset.1).min(hidden_height as f32) as u32,
+        )
+    }
+
+    /// Returns if the left mouse button is still held down since clicking inside
+    /// the [`Viewer`].
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    /// [`State`]: struct.State.html
+    pub fn is_cursor_clicked(&self) -> bool {
+        self.starting_cursor_pos.is_some()
+    }
+}
+
+/// The renderer of an [`Viewer`].
+///
+/// Your [renderer] will need to implement this trait before being
+/// able to use a [`Viewer`] in your user interface.
+///
+/// [`Viewer`]: struct.Viewer.html
+/// [renderer]: ../../../renderer/index.html
+pub trait Renderer: crate::Renderer + Sized {
+    /// Draws the [`Viewer`].
+    ///
+    /// It receives:
+    /// - the [`State`] of the [`Viewer`]
+    /// - the bounds of the [`Viewer`] widget
+    /// - the bounds of the scaled [`Viewer`] image
+    /// - the clipping x,y offset
+    /// - the [`Handle`] to the underlying image
+    /// - whether the mouse is over the [`Viewer`] or not
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    /// [`State`]: struct.State.html
+    /// [`Handle`]: ../../image/struct.Handle.html
+    fn draw(
+        &mut self,
+        state: &State,
+        bounds: Rectangle,
+        image_bounds: Rectangle,
+        offset: (u32, u32),
+        handle: image::Handle,
+        is_mouse_over: bool,
+    ) -> Self::Output;
+}
+
+impl<'a, Message, Renderer> From<Viewer<'a>> for Element<'a, Message, Renderer>
+where
+    Renderer: 'a + self::Renderer + image::Renderer,
+    Message: 'a,
+{
+    fn from(viewer: Viewer<'a>) -> Element<'a, Message, Renderer> {
+        Element::new(viewer)
+    }
+}
diff --git a/native/src/widget/image_viewer.rs b/native/src/widget/image_viewer.rs
deleted file mode 100644
index d0f31cb4..00000000
--- a/native/src/widget/image_viewer.rs
+++ /dev/null
@@ -1,384 +0,0 @@
-//! Zoom and pan on an image.
-use crate::{
-    image, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length,
-    Point, Rectangle, Size, Widget,
-};
-
-use std::{f32, hash::Hash, u32};
-
-/// A widget that can display an image with the ability to zoom in/out and pan.
-#[allow(missing_debug_implementations)]
-pub struct ImageViewer<'a> {
-    state: &'a mut State,
-    padding: u16,
-    width: Length,
-    height: Length,
-    max_width: u32,
-    max_height: u32,
-    handle: image::Handle,
-}
-
-impl<'a> ImageViewer<'a> {
-    /// Creates a new [`ImageViewer`] with the given [`State`] and [`Handle`].
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    /// [`State`]: struct.State.html
-    /// [`Handle`]: ../image/struct.Handle.html
-    pub fn new(state: &'a mut State, handle: image::Handle) -> Self {
-        ImageViewer {
-            state,
-            padding: 0,
-            width: Length::Shrink,
-            height: Length::Shrink,
-            max_width: u32::MAX,
-            max_height: u32::MAX,
-            handle,
-        }
-    }
-
-    /// Sets the padding of the [`ImageViewer`].
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    pub fn padding(mut self, units: u16) -> Self {
-        self.padding = units;
-        self
-    }
-
-    /// Sets the width of the [`ImageViewer`].
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    pub fn width(mut self, width: Length) -> Self {
-        self.width = width;
-        self
-    }
-
-    /// Sets the height of the [`ImageViewer`].
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    pub fn height(mut self, height: Length) -> Self {
-        self.height = height;
-        self
-    }
-
-    /// Sets the max width of the [`ImageViewer`].
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    pub fn max_width(mut self, max_width: u32) -> Self {
-        self.max_width = max_width;
-        self
-    }
-
-    /// Sets the max height of the [`ImageViewer`].
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    pub fn max_height(mut self, max_height: u32) -> Self {
-        self.max_height = max_height;
-        self
-    }
-}
-
-impl<'a, Message, Renderer> Widget<Message, Renderer> for ImageViewer<'a>
-where
-    Renderer: self::Renderer + image::Renderer,
-{
-    fn width(&self) -> Length {
-        self.width
-    }
-
-    fn height(&self) -> Length {
-        self.height
-    }
-
-    fn layout(
-        &self,
-        _renderer: &Renderer,
-        limits: &layout::Limits,
-    ) -> layout::Node {
-        let padding = f32::from(self.padding);
-
-        let limits = limits
-            .max_width(self.max_width)
-            .max_height(self.max_height)
-            .width(self.width)
-            .height(self.height)
-            .pad(padding);
-
-        let size = limits.resolve(Size::INFINITY);
-
-        layout::Node::new(size)
-    }
-
-    fn on_event(
-        &mut self,
-        event: Event,
-        layout: Layout<'_>,
-        cursor_position: Point,
-        _messages: &mut Vec<Message>,
-        renderer: &Renderer,
-        _clipboard: Option<&dyn Clipboard>,
-    ) {
-        let bounds = layout.bounds();
-        let is_mouse_over = bounds.contains(cursor_position);
-
-        let image_bounds = {
-            let (width, height) = renderer.dimensions(&self.handle);
-
-            let dimensions = if let Some(scale) = self.state.scale {
-                (width as f32 * scale, height as f32 * scale)
-            } else {
-                let dimensions = (width as f32, height as f32);
-
-                let width_scale = bounds.width / dimensions.0;
-                let height_scale = bounds.height / dimensions.1;
-
-                let scale = width_scale.min(height_scale);
-
-                if scale < 1.0 {
-                    (dimensions.0 * scale, dimensions.1 * scale)
-                } else {
-                    (dimensions.0, dimensions.1)
-                }
-            };
-
-            Rectangle {
-                x: bounds.x,
-                y: bounds.y,
-                width: dimensions.0,
-                height: dimensions.1,
-            }
-        };
-
-        if is_mouse_over {
-            match event {
-                Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
-                    match delta {
-                        mouse::ScrollDelta::Lines { y, .. }
-                        | mouse::ScrollDelta::Pixels { y, .. } => {
-                            // TODO: Configurable step and limits
-                            if y > 0.0 {
-                                self.state.scale = Some(
-                                    (self.state.scale.unwrap_or(1.0) + 0.25)
-                                        .min(10.0),
-                                );
-                            } else {
-                                self.state.scale = Some(
-                                    (self.state.scale.unwrap_or(1.0) - 0.25)
-                                        .max(0.25),
-                                );
-                            }
-                        }
-                    }
-                }
-                Event::Mouse(mouse::Event::ButtonPressed(button)) => {
-                    if button == mouse::Button::Left {
-                        self.state.starting_cursor_pos =
-                            Some((cursor_position.x, cursor_position.y));
-
-                        self.state.starting_offset = self.state.current_offset;
-                    }
-                }
-                Event::Mouse(mouse::Event::ButtonReleased(button)) => {
-                    if button == mouse::Button::Left {
-                        self.state.starting_cursor_pos = None
-                    }
-                }
-                Event::Mouse(mouse::Event::CursorMoved { x, y }) => {
-                    if self.state.is_cursor_clicked() {
-                        self.state.pan(x, y, bounds, image_bounds);
-                    }
-                }
-                _ => {}
-            }
-        } else if let Event::Mouse(mouse::Event::ButtonReleased(button)) = event
-        {
-            if button == mouse::Button::Left {
-                self.state.starting_cursor_pos = None;
-            }
-        }
-    }
-
-    fn draw(
-        &self,
-        renderer: &mut Renderer,
-        _defaults: &Renderer::Defaults,
-        layout: Layout<'_>,
-        cursor_position: Point,
-    ) -> Renderer::Output {
-        let bounds = layout.bounds();
-
-        let image_bounds = {
-            let (width, height) = renderer.dimensions(&self.handle);
-
-            let dimensions = if let Some(scale) = self.state.scale {
-                (width as f32 * scale, height as f32 * scale)
-            } else {
-                let dimensions = (width as f32, height as f32);
-
-                let width_scale = bounds.width / dimensions.0;
-                let height_scale = bounds.height / dimensions.1;
-
-                let scale = width_scale.min(height_scale);
-
-                if scale < 1.0 {
-                    (dimensions.0 * scale, dimensions.1 * scale)
-                } else {
-                    (dimensions.0, dimensions.1)
-                }
-            };
-
-            Rectangle {
-                x: bounds.x,
-                y: bounds.y,
-                width: dimensions.0,
-                height: dimensions.1,
-            }
-        };
-
-        let offset = self.state.offset(bounds, image_bounds);
-
-        let is_mouse_over = bounds.contains(cursor_position);
-
-        self::Renderer::draw(
-            renderer,
-            &self.state,
-            bounds,
-            image_bounds,
-            offset,
-            self.handle.clone(),
-            is_mouse_over,
-        )
-    }
-
-    fn hash_layout(&self, state: &mut Hasher) {
-        struct Marker;
-        std::any::TypeId::of::<Marker>().hash(state);
-
-        self.width.hash(state);
-        self.height.hash(state);
-        self.max_width.hash(state);
-        self.max_height.hash(state);
-        self.padding.hash(state);
-
-        self.handle.hash(state);
-    }
-}
-
-/// The local state of an [`ImageViewer`].
-///
-/// [`ImageViewer`]: struct.ImageViewer.html
-#[derive(Debug, Clone, Copy, Default)]
-pub struct State {
-    scale: Option<f32>,
-    starting_offset: (f32, f32),
-    current_offset: (f32, f32),
-    starting_cursor_pos: Option<(f32, f32)>,
-}
-
-impl State {
-    /// Creates a new [`State`].
-    ///
-    /// [`State`]: struct.State.html
-    pub fn new() -> Self {
-        State::default()
-    }
-
-    /// Apply a panning offset to the current [`State`], given the bounds of
-    /// the [`ImageViewer`] and its image.
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    /// [`State`]: struct.State.html
-    fn pan(
-        &mut self,
-        x: f32,
-        y: f32,
-        bounds: Rectangle,
-        image_bounds: Rectangle,
-    ) {
-        let delta_x = x - self.starting_cursor_pos.unwrap().0;
-        let delta_y = y - self.starting_cursor_pos.unwrap().1;
-
-        if bounds.width < image_bounds.width {
-            self.current_offset.0 = (self.starting_offset.0 - delta_x)
-                .max(0.0)
-                .min((image_bounds.width - bounds.width) as f32);
-        }
-
-        if bounds.height < image_bounds.height {
-            self.current_offset.1 = (self.starting_offset.1 - delta_y)
-                .max(0.0)
-                .min((image_bounds.height - bounds.height) as f32);
-        }
-    }
-
-    /// Returns the current clipping offset of the [`State`], given the bounds
-    /// of the [`ImageViewer`] and its contents.
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    /// [`State`]: struct.State.html
-    fn offset(&self, bounds: Rectangle, image_bounds: Rectangle) -> (u32, u32) {
-        let hidden_width = ((image_bounds.width - bounds.width) as f32)
-            .max(0.0)
-            .round() as u32;
-
-        let hidden_height = ((image_bounds.height - bounds.height) as f32)
-            .max(0.0)
-            .round() as u32;
-
-        (
-            (self.current_offset.0).min(hidden_width as f32) as u32,
-            (self.current_offset.1).min(hidden_height as f32) as u32,
-        )
-    }
-
-    /// Returns if the left mouse button is still held down since clicking inside
-    /// the [`ImageViewer`].
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    /// [`State`]: struct.State.html
-    pub fn is_cursor_clicked(&self) -> bool {
-        self.starting_cursor_pos.is_some()
-    }
-}
-
-/// The renderer of an [`ImageViewer`].
-///
-/// Your [renderer] will need to implement this trait before being
-/// able to use a [`ImageViewer`] in your user interface.
-///
-/// [`ImageViewer`]: struct.ImageViewer.html
-/// [renderer]: ../../renderer/index.html
-pub trait Renderer: crate::Renderer + Sized {
-    /// Draws the [`ImageViewer`].
-    ///
-    /// It receives:
-    /// - the [`State`] of the [`ImageViewer`]
-    /// - the bounds of the [`ImageViewer`] widget
-    /// - the bounds of the scaled [`ImageViewer`] image
-    /// - the clipping x,y offset
-    /// - the [`Handle`] to the underlying image
-    /// - whether the mouse is over the [`ImageViewer`] or not
-    ///
-    /// [`ImageViewer`]: struct.ImageViewer.html
-    /// [`State`]: struct.State.html
-    /// [`Handle`]: ../image/struct.Handle.html
-    fn draw(
-        &mut self,
-        state: &State,
-        bounds: Rectangle,
-        image_bounds: Rectangle,
-        offset: (u32, u32),
-        handle: image::Handle,
-        is_mouse_over: bool,
-    ) -> Self::Output;
-}
-
-impl<'a, Message, Renderer> From<ImageViewer<'a>>
-    for Element<'a, Message, Renderer>
-where
-    Renderer: 'a + self::Renderer + image::Renderer,
-    Message: 'a,
-{
-    fn from(viewer: ImageViewer<'a>) -> Element<'a, Message, Renderer> {
-        Element::new(viewer)
-    }
-}
diff --git a/src/widget.rs b/src/widget.rs
index 92303277..0b0b25db 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -30,8 +30,7 @@ mod platform {
     #[cfg_attr(docsrs, doc(cfg(feature = "image")))]
     pub mod image {
         //! Display images in your user interface.
-        pub use iced_winit::image::{Handle, Image};
-        pub use iced_winit::image_viewer::{ImageViewer, State};
+        pub use iced_winit::image::{Handle, Image, State, Viewer};
     }
 
     #[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
@@ -44,16 +43,9 @@ mod platform {
 
     #[doc(no_inline)]
     pub use {
-        button::Button,
-        checkbox::Checkbox,
-        container::Container,
-        image::{Image, ImageViewer},
-        pane_grid::PaneGrid,
-        progress_bar::ProgressBar,
-        radio::Radio,
-        scrollable::Scrollable,
-        slider::Slider,
-        svg::Svg,
+        button::Button, checkbox::Checkbox, container::Container, image::Image,
+        pane_grid::PaneGrid, progress_bar::ProgressBar, radio::Radio,
+        scrollable::Scrollable, slider::Slider, svg::Svg,
         text_input::TextInput,
     };
 
diff --git a/wgpu/src/renderer/widget.rs b/wgpu/src/renderer/widget.rs
index c9f288c7..37421fbe 100644
--- a/wgpu/src/renderer/widget.rs
+++ b/wgpu/src/renderer/widget.rs
@@ -17,5 +17,3 @@ mod svg;
 
 #[cfg(feature = "image")]
 mod image;
-#[cfg(feature = "image")]
-mod image_viewer;
diff --git a/wgpu/src/renderer/widget/image.rs b/wgpu/src/renderer/widget/image.rs
index c4c04984..d32c078a 100644
--- a/wgpu/src/renderer/widget/image.rs
+++ b/wgpu/src/renderer/widget/image.rs
@@ -1,3 +1,5 @@
+mod viewer;
+
 use crate::{Primitive, Renderer};
 use iced_native::{image, mouse, Layout};
 
diff --git a/wgpu/src/renderer/widget/image/viewer.rs b/wgpu/src/renderer/widget/image/viewer.rs
new file mode 100644
index 00000000..72e5d93b
--- /dev/null
+++ b/wgpu/src/renderer/widget/image/viewer.rs
@@ -0,0 +1,39 @@
+use crate::{Primitive, Renderer};
+use iced_native::{image, mouse, Rectangle, Vector};
+
+impl image::viewer::Renderer for Renderer {
+    fn draw(
+        &mut self,
+        state: &image::State,
+        bounds: Rectangle,
+        image_bounds: Rectangle,
+        offset: (u32, u32),
+        handle: image::Handle,
+        is_mouse_over: bool,
+    ) -> Self::Output {
+        (
+            {
+                Primitive::Clip {
+                    bounds,
+                    offset: Vector::new(offset.0, offset.1),
+                    content: Box::new(Primitive::Image {
+                        handle,
+                        bounds: image_bounds,
+                    }),
+                }
+            },
+            {
+                if state.is_cursor_clicked() {
+                    mouse::Interaction::Grabbing
+                } else if is_mouse_over
+                    && (image_bounds.width > bounds.width
+                        || image_bounds.height > bounds.height)
+                {
+                    mouse::Interaction::Grab
+                } else {
+                    mouse::Interaction::Idle
+                }
+            },
+        )
+    }
+}
diff --git a/wgpu/src/renderer/widget/image_viewer.rs b/wgpu/src/renderer/widget/image_viewer.rs
deleted file mode 100644
index b8546d43..00000000
--- a/wgpu/src/renderer/widget/image_viewer.rs
+++ /dev/null
@@ -1,39 +0,0 @@
-use crate::{Primitive, Renderer};
-use iced_native::{image, image_viewer, mouse, Rectangle, Vector};
-
-impl image_viewer::Renderer for Renderer {
-    fn draw(
-        &mut self,
-        state: &image_viewer::State,
-        bounds: Rectangle,
-        image_bounds: Rectangle,
-        offset: (u32, u32),
-        handle: image::Handle,
-        is_mouse_over: bool,
-    ) -> Self::Output {
-        (
-            {
-                Primitive::Clip {
-                    bounds,
-                    offset: Vector::new(offset.0, offset.1),
-                    content: Box::new(Primitive::Image {
-                        handle,
-                        bounds: image_bounds,
-                    }),
-                }
-            },
-            {
-                if state.is_cursor_clicked() {
-                    mouse::Interaction::Grabbing
-                } else if is_mouse_over
-                    && (image_bounds.width > bounds.width
-                        || image_bounds.height > bounds.height)
-                {
-                    mouse::Interaction::Grab
-                } else {
-                    mouse::Interaction::Idle
-                }
-            },
-        )
-    }
-}
diff --git a/wgpu/src/widget.rs b/wgpu/src/widget.rs
index e968c366..32ccad17 100644
--- a/wgpu/src/widget.rs
+++ b/wgpu/src/widget.rs
@@ -47,11 +47,3 @@ pub mod canvas;
 #[cfg(feature = "canvas")]
 #[doc(no_inline)]
 pub use canvas::Canvas;
-
-#[cfg(feature = "image")]
-#[doc(no_inline)]
-pub mod image_viewer;
-
-#[cfg(feature = "image")]
-#[doc(no_inline)]
-pub use image_viewer::ImageViewer;
diff --git a/wgpu/src/widget/image_viewer.rs b/wgpu/src/widget/image_viewer.rs
deleted file mode 100644
index ec44e30a..00000000
--- a/wgpu/src/widget/image_viewer.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-//! Zoom and pan on an image.
-
-pub use iced_native::image_viewer::State;
-
-/// A widget that can display an image with the ability to zoom in/out and pan.
-pub type ImageViewer<'a> = iced_native::ImageViewer<'a>;
-- 
cgit 


From de176beb282dcb2818c049957453772c6f530b69 Mon Sep 17 00:00:00 2001
From: Cory Forsstrom <cforsstrom18@gmail.com>
Date: Wed, 27 May 2020 13:39:26 -0700
Subject: centered image and zoom to cursor

---
 native/src/widget/image/viewer.rs        | 287 +++++++++++++++++++++----------
 wgpu/src/renderer/widget/image/viewer.rs |  13 +-
 2 files changed, 204 insertions(+), 96 deletions(-)

diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs
index c33f3a5e..af6d960b 100644
--- a/native/src/widget/image/viewer.rs
+++ b/native/src/widget/image/viewer.rs
@@ -1,7 +1,7 @@
 //! Zoom and pan on an image.
 use crate::{
     image, layout, mouse, Clipboard, Element, Event, Hasher, Layout, Length,
-    Point, Rectangle, Size, Widget,
+    Point, Rectangle, Size, Vector, Widget,
 };
 
 use std::{f32, hash::Hash, u32};
@@ -15,6 +15,9 @@ pub struct Viewer<'a> {
     height: Length,
     max_width: u32,
     max_height: u32,
+    min_scale: f32,
+    max_scale: f32,
+    scale_pct: f32,
     handle: image::Handle,
 }
 
@@ -32,6 +35,9 @@ impl<'a> Viewer<'a> {
             height: Length::Shrink,
             max_width: u32::MAX,
             max_height: u32::MAX,
+            min_scale: 0.25,
+            max_scale: 10.0,
+            scale_pct: 0.10,
             handle,
         }
     }
@@ -75,6 +81,100 @@ impl<'a> Viewer<'a> {
         self.max_height = max_height;
         self
     }
+
+    /// Sets the max scale applied to the image of the [`Viewer`].
+    ///
+    /// Default is `10.0`
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    pub fn max_scale(mut self, max_scale: f32) -> Self {
+        self.max_scale = max_scale;
+        self
+    }
+
+    /// Sets the min scale applied to the image of the [`Viewer`].
+    ///
+    /// Default is `0.25`
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    pub fn min_scale(mut self, min_scale: f32) -> Self {
+        self.min_scale = min_scale;
+        self
+    }
+
+    /// Sets the percentage the image of the [`Viewer`] will be scaled by
+    /// when zoomed in / out.
+    ///
+    /// Default is `0.10`
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    pub fn scale_pct(mut self, scale_pct: f32) -> Self {
+        self.scale_pct = scale_pct;
+        self
+    }
+
+    /// Returns the bounds of the underlying image, given the bounds of
+    /// the [`Viewer`]. Scaling will be applied and original aspect ratio
+    /// will be respected.
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    fn image_bounds<Renderer>(
+        &self,
+        renderer: &Renderer,
+        bounds: Rectangle,
+    ) -> Rectangle
+    where
+        Renderer: self::Renderer + image::Renderer,
+    {
+        let (width, height) = renderer.dimensions(&self.handle);
+
+        let dimensions = {
+            let dimensions = (width as f32, height as f32);
+
+            let width_ratio = bounds.width / dimensions.0;
+            let height_ratio = bounds.height / dimensions.1;
+
+            let ratio = width_ratio.min(height_ratio);
+
+            let scale = self.state.scale.unwrap_or(1.0);
+
+            if ratio < 1.0 {
+                (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale)
+            } else {
+                (dimensions.0 * scale, dimensions.1 * scale)
+            }
+        };
+
+        Rectangle {
+            x: bounds.x,
+            y: bounds.y,
+            width: dimensions.0,
+            height: dimensions.1,
+        }
+    }
+
+    /// Cursor position relative to the [`Viewer`] bounds.
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    fn relative_cursor_position(
+        &self,
+        mut absolute_position: Point,
+        bounds: Rectangle,
+    ) -> Point {
+        absolute_position.x -= bounds.x;
+        absolute_position.y -= bounds.y;
+        absolute_position
+    }
+
+    /// Center point relative to the [`Viewer`] bounds.
+    ///
+    /// [`Viewer`]: struct.Viewer.html
+    fn relative_center(&self, bounds: Rectangle) -> Point {
+        let mut center = bounds.center();
+        center.x -= bounds.x;
+        center.y -= bounds.y;
+        center
+    }
 }
 
 impl<'a, Message, Renderer> Widget<Message, Renderer> for Viewer<'a>
@@ -120,50 +220,59 @@ where
         let bounds = layout.bounds();
         let is_mouse_over = bounds.contains(cursor_position);
 
-        let image_bounds = {
-            let (width, height) = renderer.dimensions(&self.handle);
-
-            let dimensions = if let Some(scale) = self.state.scale {
-                (width as f32 * scale, height as f32 * scale)
-            } else {
-                let dimensions = (width as f32, height as f32);
-
-                let width_scale = bounds.width / dimensions.0;
-                let height_scale = bounds.height / dimensions.1;
-
-                let scale = width_scale.min(height_scale);
-
-                if scale < 1.0 {
-                    (dimensions.0 * scale, dimensions.1 * scale)
-                } else {
-                    (dimensions.0, dimensions.1)
-                }
-            };
-
-            Rectangle {
-                x: bounds.x,
-                y: bounds.y,
-                width: dimensions.0,
-                height: dimensions.1,
-            }
-        };
-
         if is_mouse_over {
             match event {
                 Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
                     match delta {
                         mouse::ScrollDelta::Lines { y, .. }
                         | mouse::ScrollDelta::Pixels { y, .. } => {
-                            // TODO: Configurable step and limits
-                            if y > 0.0 {
+                            let previous_scale =
+                                self.state.scale.unwrap_or(1.0);
+
+                            if y < 0.0 && previous_scale > self.min_scale
+                                || y > 0.0 && previous_scale < self.max_scale
+                            {
                                 self.state.scale = Some(
-                                    (self.state.scale.unwrap_or(1.0) + 0.25)
-                                        .min(10.0),
+                                    (if y > 0.0 {
+                                        self.state.scale.unwrap_or(1.0)
+                                            * (1.0 + self.scale_pct)
+                                    } else {
+                                        self.state.scale.unwrap_or(1.0)
+                                            / (1.0 + self.scale_pct)
+                                    })
+                                    .max(self.min_scale)
+                                    .min(self.max_scale),
                                 );
-                            } else {
-                                self.state.scale = Some(
-                                    (self.state.scale.unwrap_or(1.0) - 0.25)
-                                        .max(0.25),
+
+                                let image_bounds =
+                                    self.image_bounds(renderer, bounds);
+
+                                let factor = self.state.scale.unwrap()
+                                    / previous_scale
+                                    - 1.0;
+
+                                let cursor_to_center =
+                                    self.relative_cursor_position(
+                                        cursor_position,
+                                        bounds,
+                                    ) - self.relative_center(bounds);
+
+                                let adjustment = cursor_to_center * factor
+                                    + self.state.current_offset * factor;
+
+                                self.state.current_offset = Vector::new(
+                                    if image_bounds.width > bounds.width {
+                                        self.state.current_offset.x
+                                            + adjustment.x
+                                    } else {
+                                        0.0
+                                    },
+                                    if image_bounds.height > bounds.height {
+                                        self.state.current_offset.y
+                                            + adjustment.y
+                                    } else {
+                                        0.0
+                                    },
                                 );
                             }
                         }
@@ -171,8 +280,7 @@ where
                 }
                 Event::Mouse(mouse::Event::ButtonPressed(button)) => {
                     if button == mouse::Button::Left {
-                        self.state.starting_cursor_pos =
-                            Some((cursor_position.x, cursor_position.y));
+                        self.state.starting_cursor_pos = Some(cursor_position);
 
                         self.state.starting_offset = self.state.current_offset;
                     }
@@ -184,6 +292,8 @@ where
                 }
                 Event::Mouse(mouse::Event::CursorMoved { x, y }) => {
                     if self.state.is_cursor_clicked() {
+                        let image_bounds = self.image_bounds(renderer, bounds);
+
                         self.state.pan(x, y, bounds, image_bounds);
                     }
                 }
@@ -206,36 +316,17 @@ where
     ) -> Renderer::Output {
         let bounds = layout.bounds();
 
-        let image_bounds = {
-            let (width, height) = renderer.dimensions(&self.handle);
+        let image_bounds = self.image_bounds(renderer, bounds);
 
-            let dimensions = if let Some(scale) = self.state.scale {
-                (width as f32 * scale, height as f32 * scale)
-            } else {
-                let dimensions = (width as f32, height as f32);
-
-                let width_scale = bounds.width / dimensions.0;
-                let height_scale = bounds.height / dimensions.1;
+        let translation = {
+            let image_top_left = Vector::new(
+                bounds.width / 2.0 - image_bounds.width / 2.0,
+                bounds.height / 2.0 - image_bounds.height / 2.0,
+            );
 
-                let scale = width_scale.min(height_scale);
-
-                if scale < 1.0 {
-                    (dimensions.0 * scale, dimensions.1 * scale)
-                } else {
-                    (dimensions.0, dimensions.1)
-                }
-            };
-
-            Rectangle {
-                x: bounds.x,
-                y: bounds.y,
-                width: dimensions.0,
-                height: dimensions.1,
-            }
+            image_top_left - self.state.offset(bounds, image_bounds)
         };
 
-        let offset = self.state.offset(bounds, image_bounds);
-
         let is_mouse_over = bounds.contains(cursor_position);
 
         self::Renderer::draw(
@@ -243,7 +334,7 @@ where
             &self.state,
             bounds,
             image_bounds,
-            offset,
+            translation,
             self.handle.clone(),
             is_mouse_over,
         )
@@ -269,9 +360,9 @@ where
 #[derive(Debug, Clone, Copy, Default)]
 pub struct State {
     scale: Option<f32>,
-    starting_offset: (f32, f32),
-    current_offset: (f32, f32),
-    starting_cursor_pos: Option<(f32, f32)>,
+    starting_offset: Vector,
+    current_offset: Vector,
+    starting_cursor_pos: Option<Point>,
 }
 
 impl State {
@@ -294,39 +385,53 @@ impl State {
         bounds: Rectangle,
         image_bounds: Rectangle,
     ) {
-        let delta_x = x - self.starting_cursor_pos.unwrap().0;
-        let delta_y = y - self.starting_cursor_pos.unwrap().1;
+        let hidden_width = ((image_bounds.width - bounds.width) as f32 / 2.0)
+            .max(0.0)
+            .round();
+        let hidden_height = ((image_bounds.height - bounds.height) as f32
+            / 2.0)
+            .max(0.0)
+            .round();
+
+        let delta_x = x - self.starting_cursor_pos.unwrap().x;
+        let delta_y = y - self.starting_cursor_pos.unwrap().y;
 
         if bounds.width < image_bounds.width {
-            self.current_offset.0 = (self.starting_offset.0 - delta_x)
-                .max(0.0)
-                .min((image_bounds.width - bounds.width) as f32);
+            self.current_offset.x = (self.starting_offset.x - delta_x)
+                .min(hidden_width)
+                .max(-1.0 * hidden_width);
         }
 
         if bounds.height < image_bounds.height {
-            self.current_offset.1 = (self.starting_offset.1 - delta_y)
-                .max(0.0)
-                .min((image_bounds.height - bounds.height) as f32);
+            self.current_offset.y = (self.starting_offset.y - delta_y)
+                .min(hidden_height)
+                .max(-1.0 * hidden_height);
         }
     }
 
-    /// Returns the current clipping offset of the [`State`], given the bounds
-    /// of the [`Viewer`] and its contents.
+    /// Returns the current offset of the [`State`], given the bounds
+    /// of the [`Viewer`] and its image.
     ///
     /// [`Viewer`]: struct.Viewer.html
     /// [`State`]: struct.State.html
-    fn offset(&self, bounds: Rectangle, image_bounds: Rectangle) -> (u32, u32) {
-        let hidden_width = ((image_bounds.width - bounds.width) as f32)
+    fn offset(&self, bounds: Rectangle, image_bounds: Rectangle) -> Vector {
+        let hidden_width = ((image_bounds.width - bounds.width) as f32 / 2.0)
             .max(0.0)
-            .round() as u32;
-
-        let hidden_height = ((image_bounds.height - bounds.height) as f32)
+            .round();
+        let hidden_height = ((image_bounds.height - bounds.height) as f32
+            / 2.0)
             .max(0.0)
-            .round() as u32;
-
-        (
-            (self.current_offset.0).min(hidden_width as f32) as u32,
-            (self.current_offset.1).min(hidden_height as f32) as u32,
+            .round();
+
+        Vector::new(
+            self.current_offset
+                .x
+                .min(hidden_width)
+                .max(-1.0 * hidden_width),
+            self.current_offset
+                .y
+                .min(hidden_height)
+                .max(-1.0 * hidden_height),
         )
     }
 
@@ -354,7 +459,7 @@ pub trait Renderer: crate::Renderer + Sized {
     /// - the [`State`] of the [`Viewer`]
     /// - the bounds of the [`Viewer`] widget
     /// - the bounds of the scaled [`Viewer`] image
-    /// - the clipping x,y offset
+    /// - the translation of the clipped image
     /// - the [`Handle`] to the underlying image
     /// - whether the mouse is over the [`Viewer`] or not
     ///
@@ -366,7 +471,7 @@ pub trait Renderer: crate::Renderer + Sized {
         state: &State,
         bounds: Rectangle,
         image_bounds: Rectangle,
-        offset: (u32, u32),
+        translation: Vector,
         handle: image::Handle,
         is_mouse_over: bool,
     ) -> Self::Output;
diff --git a/wgpu/src/renderer/widget/image/viewer.rs b/wgpu/src/renderer/widget/image/viewer.rs
index 72e5d93b..f71ca6fb 100644
--- a/wgpu/src/renderer/widget/image/viewer.rs
+++ b/wgpu/src/renderer/widget/image/viewer.rs
@@ -7,7 +7,7 @@ impl image::viewer::Renderer for Renderer {
         state: &image::State,
         bounds: Rectangle,
         image_bounds: Rectangle,
-        offset: (u32, u32),
+        translation: Vector,
         handle: image::Handle,
         is_mouse_over: bool,
     ) -> Self::Output {
@@ -15,11 +15,14 @@ impl image::viewer::Renderer for Renderer {
             {
                 Primitive::Clip {
                     bounds,
-                    offset: Vector::new(offset.0, offset.1),
-                    content: Box::new(Primitive::Image {
-                        handle,
-                        bounds: image_bounds,
+                    content: Box::new(Primitive::Translate {
+                        translation,
+                        content: Box::new(Primitive::Image {
+                            handle,
+                            bounds: image_bounds,
+                        }),
                     }),
+                    offset: Vector::new(0, 0),
                 }
             },
             {
-- 
cgit 


From 5dd62bacd5b21d460b2e0ff22197a65cace3934b Mon Sep 17 00:00:00 2001
From: Cory Forsstrom <cforsstrom18@gmail.com>
Date: Wed, 27 May 2020 14:16:38 -0700
Subject: update docs

---
 native/src/widget/image/viewer.rs | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs
index af6d960b..b129924b 100644
--- a/native/src/widget/image/viewer.rs
+++ b/native/src/widget/image/viewer.rs
@@ -6,7 +6,7 @@ use crate::{
 
 use std::{f32, hash::Hash, u32};
 
-/// A widget that can display an image with the ability to zoom in/out and pan.
+/// A frame that displays an image with the ability to zoom in/out and pan.
 #[allow(missing_debug_implementations)]
 pub struct Viewer<'a> {
     state: &'a mut State,
-- 
cgit 


From c7bb43411381a1bffe70ea8e684cd9e4a27739e0 Mon Sep 17 00:00:00 2001
From: Cory Forsstrom <cforsstrom18@gmail.com>
Date: Wed, 27 May 2020 14:20:07 -0700
Subject: remove re-export on viewer::State

---
 native/src/widget/image.rs               | 2 +-
 src/widget.rs                            | 4 +++-
 wgpu/src/renderer/widget/image/viewer.rs | 9 ++++++---
 3 files changed, 10 insertions(+), 5 deletions(-)

diff --git a/native/src/widget/image.rs b/native/src/widget/image.rs
index 685cb81a..49905830 100644
--- a/native/src/widget/image.rs
+++ b/native/src/widget/image.rs
@@ -1,6 +1,6 @@
 //! Display images in your user interface.
 pub mod viewer;
-pub use viewer::{State, Viewer};
+pub use viewer::Viewer;
 
 use crate::{layout, Element, Hasher, Layout, Length, Point, Size, Widget};
 
diff --git a/src/widget.rs b/src/widget.rs
index 0b0b25db..932a8cf6 100644
--- a/src/widget.rs
+++ b/src/widget.rs
@@ -30,7 +30,9 @@ mod platform {
     #[cfg_attr(docsrs, doc(cfg(feature = "image")))]
     pub mod image {
         //! Display images in your user interface.
-        pub use iced_winit::image::{Handle, Image, State, Viewer};
+        pub use iced_winit::image::{Handle, Image, Viewer};
+
+        pub use iced_winit::image::viewer;
     }
 
     #[cfg_attr(docsrs, doc(cfg(feature = "svg")))]
diff --git a/wgpu/src/renderer/widget/image/viewer.rs b/wgpu/src/renderer/widget/image/viewer.rs
index f71ca6fb..2599bfa5 100644
--- a/wgpu/src/renderer/widget/image/viewer.rs
+++ b/wgpu/src/renderer/widget/image/viewer.rs
@@ -1,10 +1,13 @@
 use crate::{Primitive, Renderer};
-use iced_native::{image, mouse, Rectangle, Vector};
+use iced_native::{
+    image::{self, viewer},
+    mouse, Rectangle, Vector,
+};
 
-impl image::viewer::Renderer for Renderer {
+impl viewer::Renderer for Renderer {
     fn draw(
         &mut self,
-        state: &image::State,
+        state: &viewer::State,
         bounds: Rectangle,
         image_bounds: Rectangle,
         translation: Vector,
-- 
cgit