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