From 0d8cefbf2d084053b92ded4785da8083486374ea Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 23 Apr 2020 15:34:55 -0700 Subject: [PATCH 01/24] 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 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, + 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::().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, + 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> + 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>; From 7f7e803448e9706d0eec901b32eb4cf35b3ec0b0 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 23 Apr 2020 16:22:53 -0700 Subject: [PATCH 02/24] 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 } }, ) From 6bf459e068043847a0ee1e1219056d3aced3f1cb Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Thu, 14 May 2020 11:54:05 -0700 Subject: [PATCH 03/24] Rebase to master and update for api changes --- native/src/widget/image_pane.rs | 53 ++++++++------------------ wgpu/src/renderer/widget/image_pane.rs | 8 ++-- 2 files changed, 19 insertions(+), 42 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_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_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 } }, ) From 431171f975642fe96286f11fb75cd5b06827cc7f Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Fri, 15 May 2020 09:46:22 -0700 Subject: [PATCH 04/24] Rename and add to iced image module --- native/src/widget.rs | 4 +- .../widget/{image_pane.rs => image_viewer.rs} | 74 +++++++++---------- src/widget.rs | 20 ++--- wgpu/src/renderer/widget.rs | 2 +- .../widget/{image_pane.rs => image_viewer.rs} | 6 +- wgpu/src/widget.rs | 4 +- .../widget/{image_pane.rs => image_viewer.rs} | 4 +- 7 files changed, 58 insertions(+), 56 deletions(-) rename native/src/widget/{image_pane.rs => image_viewer.rs} (84%) rename wgpu/src/renderer/widget/{image_pane.rs => image_viewer.rs} (87%) rename wgpu/src/widget/{image_pane.rs => image_viewer.rs} (53%) 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_viewer.rs similarity index 84% rename from native/src/widget/image_pane.rs rename to native/src/widget/image_viewer.rs index 4f3d4877..d0f31cb4 100644 --- a/native/src/widget/image_pane.rs +++ b/native/src/widget/image_viewer.rs @@ -8,7 +8,7 @@ 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> { +pub struct ImageViewer<'a> { state: &'a mut State, padding: u16, width: Length, @@ -18,14 +18,14 @@ pub struct ImagePane<'a> { handle: image::Handle, } -impl<'a> ImagePane<'a> { - /// Creates a new [`ImagePane`] with the given [`State`] and [`Handle`]. +impl<'a> ImageViewer<'a> { + /// Creates a new [`ImageViewer`] with the given [`State`] and [`Handle`]. /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`ImageViewer`]: struct.ImageViewer.html /// [`State`]: struct.State.html /// [`Handle`]: ../image/struct.Handle.html pub fn new(state: &'a mut State, handle: image::Handle) -> Self { - ImagePane { + ImageViewer { state, padding: 0, width: Length::Shrink, @@ -36,48 +36,48 @@ impl<'a> ImagePane<'a> { } } - /// Sets the padding of the [`ImagePane`]. + /// Sets the padding of the [`ImageViewer`]. /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`ImageViewer`]: struct.ImageViewer.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } - /// Sets the width of the [`ImagePane`]. + /// Sets the width of the [`ImageViewer`]. /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`ImageViewer`]: struct.ImageViewer.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } - /// Sets the height of the [`ImagePane`]. + /// Sets the height of the [`ImageViewer`]. /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`ImageViewer`]: struct.ImageViewer.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } - /// Sets the max width of the [`ImagePane`]. + /// Sets the max width of the [`ImageViewer`]. /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`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 [`ImagePane`]. + /// Sets the max height of the [`ImageViewer`]. /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`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 for ImagePane<'a> +impl<'a, Message, Renderer> Widget for ImageViewer<'a> where Renderer: self::Renderer + image::Renderer, { @@ -263,9 +263,9 @@ where } } -/// The local state of an [`ImagePane`]. +/// The local state of an [`ImageViewer`]. /// -/// [`ImagePane`]: struct.ImagePane.html +/// [`ImageViewer`]: struct.ImageViewer.html #[derive(Debug, Clone, Copy, Default)] pub struct State { scale: Option, @@ -275,7 +275,7 @@ pub struct State { } impl State { - /// Creates a new [`State`] with the scrollbar located at the top. + /// Creates a new [`State`]. /// /// [`State`]: struct.State.html pub fn new() -> Self { @@ -283,9 +283,9 @@ impl State { } /// Apply a panning offset to the current [`State`], given the bounds of - /// the [`ImagePane`] and its image. + /// the [`ImageViewer`] and its image. /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`ImageViewer`]: struct.ImageViewer.html /// [`State`]: struct.State.html fn pan( &mut self, @@ -311,9 +311,9 @@ impl State { } /// Returns the current clipping offset of the [`State`], given the bounds - /// of the [`ImagePane`] and its contents. + /// of the [`ImageViewer`] and its contents. /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`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) @@ -331,34 +331,34 @@ impl State { } /// Returns if the left mouse button is still held down since clicking inside - /// the [`ImagePane`]. + /// the [`ImageViewer`]. /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`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 [`ImagePane`]. +/// The renderer of an [`ImageViewer`]. /// /// Your [renderer] will need to implement this trait before being -/// able to use a [`ImagePane`] in your user interface. +/// able to use a [`ImageViewer`] in your user interface. /// -/// [`ImagePane`]: struct.ImagePane.html +/// [`ImageViewer`]: struct.ImageViewer.html /// [renderer]: ../../renderer/index.html pub trait Renderer: crate::Renderer + Sized { - /// Draws the [`ImagePane`]. + /// Draws the [`ImageViewer`]. /// /// It receives: - /// - the [`State`] of the [`ImagePane`] - /// - the bounds of the [`ImagePane`] widget - /// - the bounds of the scaled [`ImagePane`] image + /// - 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 [`ImagePane`] or not + /// - whether the mouse is over the [`ImageViewer`] or not /// - /// [`ImagePane`]: struct.ImagePane.html + /// [`ImageViewer`]: struct.ImageViewer.html /// [`State`]: struct.State.html /// [`Handle`]: ../image/struct.Handle.html fn draw( @@ -372,13 +372,13 @@ pub trait Renderer: crate::Renderer + Sized { ) -> Self::Output; } -impl<'a, Message, Renderer> From> +impl<'a, Message, Renderer> From> 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) + 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_viewer.rs similarity index 87% rename from wgpu/src/renderer/widget/image_pane.rs rename to wgpu/src/renderer/widget/image_viewer.rs index b5e86913..b8546d43 100644 --- a/wgpu/src/renderer/widget/image_pane.rs +++ b/wgpu/src/renderer/widget/image_viewer.rs @@ -1,10 +1,10 @@ use crate::{Primitive, Renderer}; -use iced_native::{image, image_pane, mouse, Rectangle, Vector}; +use iced_native::{image, image_viewer, mouse, Rectangle, Vector}; -impl image_pane::Renderer for Renderer { +impl image_viewer::Renderer for Renderer { fn draw( &mut self, - state: &image_pane::State, + state: &image_viewer::State, bounds: Rectangle, image_bounds: Rectangle, offset: (u32, u32), 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_viewer.rs similarity index 53% rename from wgpu/src/widget/image_pane.rs rename to wgpu/src/widget/image_viewer.rs index aa30a8cc..ec44e30a 100644 --- a/wgpu/src/widget/image_pane.rs +++ b/wgpu/src/widget/image_viewer.rs @@ -1,6 +1,6 @@ //! Zoom and pan on an image. -pub use iced_native::image_pane::State; +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 ImagePane<'a> = iced_native::ImagePane<'a>; +pub type ImageViewer<'a> = iced_native::ImageViewer<'a>; From 5d045c2e9a639f8bbf43e68fde9091be702b3ab8 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Tue, 26 May 2020 17:15:55 -0700 Subject: [PATCH 05/24] rename to image::Viewer --- native/src/widget.rs | 3 - native/src/widget/image.rs | 3 + .../{image_viewer.rs => image/viewer.rs} | 77 +++++++++---------- src/widget.rs | 16 +--- wgpu/src/renderer/widget.rs | 2 - wgpu/src/renderer/widget/image.rs | 2 + .../{image_viewer.rs => image/viewer.rs} | 6 +- wgpu/src/widget.rs | 8 -- wgpu/src/widget/image_viewer.rs | 6 -- 9 files changed, 50 insertions(+), 73 deletions(-) rename native/src/widget/{image_viewer.rs => image/viewer.rs} (83%) rename wgpu/src/renderer/widget/{image_viewer.rs => image/viewer.rs} (87%) 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 similarity index 83% rename from native/src/widget/image_viewer.rs rename to native/src/widget/image/viewer.rs index d0f31cb4..c33f3a5e 100644 --- a/native/src/widget/image_viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -8,7 +8,7 @@ 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> { +pub struct Viewer<'a> { state: &'a mut State, padding: u16, width: Length, @@ -18,14 +18,14 @@ pub struct ImageViewer<'a> { handle: image::Handle, } -impl<'a> ImageViewer<'a> { - /// Creates a new [`ImageViewer`] with the given [`State`] and [`Handle`]. +impl<'a> Viewer<'a> { + /// Creates a new [`Viewer`] with the given [`State`] and [`Handle`]. /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`Viewer`]: struct.Viewer.html /// [`State`]: struct.State.html - /// [`Handle`]: ../image/struct.Handle.html + /// [`Handle`]: ../../image/struct.Handle.html pub fn new(state: &'a mut State, handle: image::Handle) -> Self { - ImageViewer { + Viewer { state, padding: 0, width: Length::Shrink, @@ -36,48 +36,48 @@ impl<'a> ImageViewer<'a> { } } - /// Sets the padding of the [`ImageViewer`]. + /// Sets the padding of the [`Viewer`]. /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`Viewer`]: struct.Viewer.html pub fn padding(mut self, units: u16) -> Self { self.padding = units; self } - /// Sets the width of the [`ImageViewer`]. + /// Sets the width of the [`Viewer`]. /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`Viewer`]: struct.Viewer.html pub fn width(mut self, width: Length) -> Self { self.width = width; self } - /// Sets the height of the [`ImageViewer`]. + /// Sets the height of the [`Viewer`]. /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`Viewer`]: struct.Viewer.html pub fn height(mut self, height: Length) -> Self { self.height = height; self } - /// Sets the max width of the [`ImageViewer`]. + /// Sets the max width of the [`Viewer`]. /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`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 [`ImageViewer`]. + /// Sets the max height of the [`Viewer`]. /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`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 for ImageViewer<'a> +impl<'a, Message, Renderer> Widget for Viewer<'a> where Renderer: self::Renderer + image::Renderer, { @@ -263,9 +263,9 @@ where } } -/// The local state of an [`ImageViewer`]. +/// The local state of a [`Viewer`]. /// -/// [`ImageViewer`]: struct.ImageViewer.html +/// [`Viewer`]: struct.Viewer.html #[derive(Debug, Clone, Copy, Default)] pub struct State { scale: Option, @@ -283,9 +283,9 @@ impl State { } /// Apply a panning offset to the current [`State`], given the bounds of - /// the [`ImageViewer`] and its image. + /// the [`Viewer`] and its image. /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`Viewer`]: struct.Viewer.html /// [`State`]: struct.State.html fn pan( &mut self, @@ -311,9 +311,9 @@ impl State { } /// Returns the current clipping offset of the [`State`], given the bounds - /// of the [`ImageViewer`] and its contents. + /// of the [`Viewer`] and its contents. /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`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) @@ -331,36 +331,36 @@ impl State { } /// Returns if the left mouse button is still held down since clicking inside - /// the [`ImageViewer`]. + /// the [`Viewer`]. /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`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 [`ImageViewer`]. +/// The renderer of an [`Viewer`]. /// /// Your [renderer] will need to implement this trait before being -/// able to use a [`ImageViewer`] in your user interface. +/// able to use a [`Viewer`] in your user interface. /// -/// [`ImageViewer`]: struct.ImageViewer.html -/// [renderer]: ../../renderer/index.html +/// [`Viewer`]: struct.Viewer.html +/// [renderer]: ../../../renderer/index.html pub trait Renderer: crate::Renderer + Sized { - /// Draws the [`ImageViewer`]. + /// Draws the [`Viewer`]. /// /// It receives: - /// - the [`State`] of the [`ImageViewer`] - /// - the bounds of the [`ImageViewer`] widget - /// - the bounds of the scaled [`ImageViewer`] image + /// - 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 [`ImageViewer`] or not + /// - whether the mouse is over the [`Viewer`] or not /// - /// [`ImageViewer`]: struct.ImageViewer.html + /// [`Viewer`]: struct.Viewer.html /// [`State`]: struct.State.html - /// [`Handle`]: ../image/struct.Handle.html + /// [`Handle`]: ../../image/struct.Handle.html fn draw( &mut self, state: &State, @@ -372,13 +372,12 @@ pub trait Renderer: crate::Renderer + Sized { ) -> Self::Output; } -impl<'a, Message, Renderer> From> - for Element<'a, Message, Renderer> +impl<'a, Message, Renderer> From> for Element<'a, Message, Renderer> where Renderer: 'a + self::Renderer + image::Renderer, Message: 'a, { - fn from(viewer: ImageViewer<'a>) -> Element<'a, Message, Renderer> { + fn from(viewer: Viewer<'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 similarity index 87% rename from wgpu/src/renderer/widget/image_viewer.rs rename to wgpu/src/renderer/widget/image/viewer.rs index b8546d43..72e5d93b 100644 --- a/wgpu/src/renderer/widget/image_viewer.rs +++ b/wgpu/src/renderer/widget/image/viewer.rs @@ -1,10 +1,10 @@ use crate::{Primitive, Renderer}; -use iced_native::{image, image_viewer, mouse, Rectangle, Vector}; +use iced_native::{image, mouse, Rectangle, Vector}; -impl image_viewer::Renderer for Renderer { +impl image::viewer::Renderer for Renderer { fn draw( &mut self, - state: &image_viewer::State, + state: &image::State, bounds: Rectangle, image_bounds: Rectangle, offset: (u32, u32), 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>; From de176beb282dcb2818c049957453772c6f530b69 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Wed, 27 May 2020 13:39:26 -0700 Subject: [PATCH 06/24] centered image and zoom to cursor --- native/src/widget/image/viewer.rs | 285 ++++++++++++++++------- wgpu/src/renderer/widget/image/viewer.rs | 13 +- 2 files changed, 203 insertions(+), 95 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( + &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 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 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 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, - } + 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, - starting_offset: (f32, f32), - current_offset: (f32, f32), - starting_cursor_pos: Option<(f32, f32)>, + starting_offset: Vector, + current_offset: Vector, + starting_cursor_pos: Option, } 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; + .round(); - ( - (self.current_offset.0).min(hidden_width as f32) as u32, - (self.current_offset.1).min(hidden_height as f32) as u32, + 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), } }, { From 5dd62bacd5b21d460b2e0ff22197a65cace3934b Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Wed, 27 May 2020 14:16:38 -0700 Subject: [PATCH 07/24] 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, From c7bb43411381a1bffe70ea8e684cd9e4a27739e0 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Wed, 27 May 2020 14:20:07 -0700 Subject: [PATCH 08/24] 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, From 74ee7cca811d6f462ca253bced4e3f07e52d9300 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 10:18:39 +0100 Subject: [PATCH 09/24] Format use declarations in `image::viewer` --- graphics/src/widget/image/viewer.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/graphics/src/widget/image/viewer.rs b/graphics/src/widget/image/viewer.rs index b6217ff7..0bcd09fc 100644 --- a/graphics/src/widget/image/viewer.rs +++ b/graphics/src/widget/image/viewer.rs @@ -2,10 +2,10 @@ use crate::backend::{self, Backend}; use crate::{Primitive, Renderer}; -use iced_native::{ - image::{self, viewer}, - mouse, Rectangle, Vector, -}; +use iced_native::image; +use iced_native::image::viewer; +use iced_native::mouse; +use iced_native::{Rectangle, Vector}; impl viewer::Renderer for Renderer where From 71de341684e27568c620c65e6b9e8e9ec837183c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 10:24:04 +0100 Subject: [PATCH 10/24] Turn methods into helper functions in `image::viewer` --- native/src/widget/image/viewer.rs | 46 ++++++++++++++----------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 9544beab..3ffdf2c0 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -132,7 +132,7 @@ impl<'a> Viewer<'a> { { let (width, height) = renderer.dimensions(&self.handle); - let dimensions = { + let (width, height) = { let dimensions = (width as f32, height as f32); let width_ratio = bounds.width / dimensions.0; @@ -152,33 +152,27 @@ impl<'a> Viewer<'a> { Rectangle { x: bounds.x, y: bounds.y, - width: dimensions.0, - height: dimensions.1, + width, + height, } } +} - /// 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 - } +/// Cursor position relative to the [`Viewer`] bounds. +/// +/// [`Viewer`]: struct.Viewer.html +fn relative_cursor_position( + absolute_position: Point, + bounds: Rectangle, +) -> Point { + absolute_position - Vector::new(bounds.x, bounds.y) +} - /// 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 - } +/// Center point relative to the [`Viewer`] bounds. +/// +/// [`Viewer`]: struct.Viewer.html +fn relative_center(bounds: Rectangle) -> Point { + bounds.center() - Vector::new(bounds.x, bounds.y) } impl<'a, Message, Renderer> Widget for Viewer<'a> @@ -256,10 +250,10 @@ where - 1.0; let cursor_to_center = - self.relative_cursor_position( + relative_cursor_position( cursor_position, bounds, - ) - self.relative_center(bounds); + ) - relative_center(bounds); let adjustment = cursor_to_center * factor + self.state.current_offset * factor; From 0cdf8d56ee515fb56b08ee149683a2216a535950 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 10:44:06 +0100 Subject: [PATCH 11/24] Use `image::Viewer` in `pokedex` example --- examples/pokedex/src/main.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/examples/pokedex/src/main.rs b/examples/pokedex/src/main.rs index 187e5dee..f432f0fc 100644 --- a/examples/pokedex/src/main.rs +++ b/examples/pokedex/src/main.rs @@ -1,6 +1,6 @@ use iced::{ button, futures, image, Align, Application, Button, Column, Command, - Container, Element, Image, Length, Row, Settings, Text, + Container, Element, Length, Row, Settings, Text, }; pub fn main() -> iced::Result { @@ -112,16 +112,20 @@ struct Pokemon { name: String, description: String, image: image::Handle, + image_viewer: image::viewer::State, } impl Pokemon { const TOTAL: u16 = 807; - fn view(&self) -> Element { + fn view(&mut self) -> Element { Row::new() .spacing(20) .align_items(Align::Center) - .push(Image::new(self.image.clone())) + .push(image::Viewer::new( + &mut self.image_viewer, + self.image.clone(), + )) .push( Column::new() .spacing(20) @@ -200,6 +204,7 @@ impl Pokemon { .map(|c| if c.is_control() { ' ' } else { c }) .collect(), image, + image_viewer: image::viewer::State::new(), }) } From 21b10dc103638ead2a567b3426c937f39e9addbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 10:44:24 +0100 Subject: [PATCH 12/24] Fix `layout` of `image::Viewer` --- graphics/src/widget/image/viewer.rs | 14 +++-- native/src/widget/image/viewer.rs | 88 ++++++++++++----------------- 2 files changed, 46 insertions(+), 56 deletions(-) diff --git a/graphics/src/widget/image/viewer.rs b/graphics/src/widget/image/viewer.rs index 0bcd09fc..804f2be6 100644 --- a/graphics/src/widget/image/viewer.rs +++ b/graphics/src/widget/image/viewer.rs @@ -5,7 +5,7 @@ use crate::{Primitive, Renderer}; use iced_native::image; use iced_native::image::viewer; use iced_native::mouse; -use iced_native::{Rectangle, Vector}; +use iced_native::{Rectangle, Size, Vector}; impl viewer::Renderer for Renderer where @@ -15,7 +15,7 @@ where &mut self, state: &viewer::State, bounds: Rectangle, - image_bounds: Rectangle, + image_size: Size, translation: Vector, handle: image::Handle, is_mouse_over: bool, @@ -28,7 +28,11 @@ where translation, content: Box::new(Primitive::Image { handle, - bounds: image_bounds, + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + ..Rectangle::with_size(image_size) + }, }), }), offset: Vector::new(0, 0), @@ -38,8 +42,8 @@ where if state.is_cursor_clicked() { mouse::Interaction::Grabbing } else if is_mouse_over - && (image_bounds.width > bounds.width - || image_bounds.height > bounds.height) + && (image_size.width > bounds.width + || image_size.height > bounds.height) { mouse::Interaction::Grab } else { diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 3ffdf2c0..8b8e9824 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -122,11 +122,7 @@ impl<'a> Viewer<'a> { /// will be respected. /// /// [`Viewer`]: struct.Viewer.html - fn image_bounds( - &self, - renderer: &Renderer, - bounds: Rectangle, - ) -> Rectangle + fn image_size(&self, renderer: &Renderer, bounds: Size) -> Size where Renderer: self::Renderer + image::Renderer, { @@ -149,12 +145,7 @@ impl<'a> Viewer<'a> { } }; - Rectangle { - x: bounds.x, - y: bounds.y, - width, - height, - } + Size::new(width, height) } } @@ -189,19 +180,25 @@ where fn layout( &self, - _renderer: &Renderer, + renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let padding = f32::from(self.padding); + let (width, height) = renderer.dimensions(&self.handle); - let limits = limits - .max_width(self.max_width) - .max_height(self.max_height) + let aspect_ratio = width as f32 / height as f32; + + let mut size = limits .width(self.width) .height(self.height) - .pad(padding); + .resolve(Size::new(width as f32, height as f32)); - let size = limits.resolve(Size::INFINITY); + let viewport_aspect_ratio = size.width / size.height; + + if viewport_aspect_ratio > aspect_ratio { + size.width = width as f32 * size.height / height as f32; + } else { + size.height = height as f32 * size.width / width as f32; + } layout::Node::new(size) } @@ -242,8 +239,8 @@ where .min(self.max_scale), ); - let image_bounds = - self.image_bounds(renderer, bounds); + let image_size = + self.image_size(renderer, bounds.size()); let factor = self.state.scale.unwrap() / previous_scale @@ -259,13 +256,13 @@ where + self.state.current_offset * factor; self.state.current_offset = Vector::new( - if image_bounds.width > bounds.width { + if image_size.width > bounds.width { self.state.current_offset.x + adjustment.x } else { 0.0 }, - if image_bounds.height > bounds.height { + if image_size.height > bounds.height { self.state.current_offset.y + adjustment.y } else { @@ -290,14 +287,11 @@ where } Event::Mouse(mouse::Event::CursorMoved { position }) => { if self.state.is_cursor_clicked() { - let image_bounds = self.image_bounds(renderer, bounds); + let image_size = + self.image_size(renderer, bounds.size()); - self.state.pan( - position.x, - position.y, - bounds, - image_bounds, - ); + self.state + .pan(position.x, position.y, bounds, image_size); } } _ => {} @@ -322,15 +316,15 @@ where ) -> Renderer::Output { let bounds = layout.bounds(); - let image_bounds = self.image_bounds(renderer, bounds); + let image_size = self.image_size(renderer, bounds.size()); 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, + bounds.width / 2.0 - image_size.width / 2.0, + bounds.height / 2.0 - image_size.height / 2.0, ); - image_top_left - self.state.offset(bounds, image_bounds) + image_top_left - self.state.offset(bounds, image_size) }; let is_mouse_over = bounds.contains(cursor_position); @@ -339,7 +333,7 @@ where renderer, &self.state, bounds, - image_bounds, + image_size, translation, self.handle.clone(), is_mouse_over, @@ -384,31 +378,24 @@ impl State { /// /// [`Viewer`]: struct.Viewer.html /// [`State`]: struct.State.html - fn pan( - &mut self, - x: f32, - y: f32, - bounds: Rectangle, - image_bounds: Rectangle, - ) { - let hidden_width = ((image_bounds.width - bounds.width) as f32 / 2.0) + fn pan(&mut self, x: f32, y: f32, bounds: Rectangle, image_size: Size) { + let hidden_width = ((image_size.width - bounds.width) as f32 / 2.0) .max(0.0) .round(); - let hidden_height = ((image_bounds.height - bounds.height) as f32 - / 2.0) + let hidden_height = ((image_size.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 { + if bounds.width < image_size.width { self.current_offset.x = (self.starting_offset.x - delta_x) .min(hidden_width) .max(-1.0 * hidden_width); } - if bounds.height < image_bounds.height { + if bounds.height < image_size.height { self.current_offset.y = (self.starting_offset.y - delta_y) .min(hidden_height) .max(-1.0 * hidden_height); @@ -420,12 +407,11 @@ impl State { /// /// [`Viewer`]: struct.Viewer.html /// [`State`]: struct.State.html - fn offset(&self, bounds: Rectangle, image_bounds: Rectangle) -> Vector { - let hidden_width = ((image_bounds.width - bounds.width) as f32 / 2.0) + fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector { + let hidden_width = ((image_size.width - bounds.width) as f32 / 2.0) .max(0.0) .round(); - let hidden_height = ((image_bounds.height - bounds.height) as f32 - / 2.0) + let hidden_height = ((image_size.height - bounds.height) as f32 / 2.0) .max(0.0) .round(); @@ -476,7 +462,7 @@ pub trait Renderer: crate::Renderer + Sized { &mut self, state: &State, bounds: Rectangle, - image_bounds: Rectangle, + image_size: Size, translation: Vector, handle: image::Handle, is_mouse_over: bool, From add167d6a00843fb1229ff542d411f733f916035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 10:47:29 +0100 Subject: [PATCH 13/24] Pan `image::Viewer` even if the cursor is out of bounds --- native/src/widget/image/viewer.rs | 141 ++++++++++++++---------------- 1 file changed, 66 insertions(+), 75 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 8b8e9824..6c170341 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -215,92 +215,83 @@ where let bounds = layout.bounds(); let is_mouse_over = bounds.contains(cursor_position); - if is_mouse_over { - match event { - Event::Mouse(mouse::Event::WheelScrolled { delta }) => { - match delta { - mouse::ScrollDelta::Lines { y, .. } - | mouse::ScrollDelta::Pixels { y, .. } => { - let previous_scale = - self.state.scale.unwrap_or(1.0); + match event { + Event::Mouse(mouse::Event::WheelScrolled { delta }) + if is_mouse_over => + { + match delta { + mouse::ScrollDelta::Lines { y, .. } + | mouse::ScrollDelta::Pixels { y, .. } => { + 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( - (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), - ); + if y < 0.0 && previous_scale > self.min_scale + || y > 0.0 && previous_scale < self.max_scale + { + self.state.scale = Some( + (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), + ); - let image_size = - self.image_size(renderer, bounds.size()); + let image_size = + self.image_size(renderer, bounds.size()); - let factor = self.state.scale.unwrap() - / previous_scale - - 1.0; + let factor = self.state.scale.unwrap() + / previous_scale + - 1.0; - let cursor_to_center = - relative_cursor_position( - cursor_position, - bounds, - ) - relative_center(bounds); + let cursor_to_center = relative_cursor_position( + cursor_position, + bounds, + ) - relative_center(bounds); - let adjustment = cursor_to_center * factor - + self.state.current_offset * factor; + let adjustment = cursor_to_center * factor + + self.state.current_offset * factor; - self.state.current_offset = Vector::new( - if image_size.width > bounds.width { - self.state.current_offset.x - + adjustment.x - } else { - 0.0 - }, - if image_size.height > bounds.height { - self.state.current_offset.y - + adjustment.y - } else { - 0.0 - }, - ); - } + self.state.current_offset = Vector::new( + if image_size.width > bounds.width { + self.state.current_offset.x + adjustment.x + } else { + 0.0 + }, + if image_size.height > bounds.height { + self.state.current_offset.y + adjustment.y + } else { + 0.0 + }, + ); } } } - Event::Mouse(mouse::Event::ButtonPressed(button)) => { - if button == mouse::Button::Left { - self.state.starting_cursor_pos = Some(cursor_position); - - 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 { position }) => { - if self.state.is_cursor_clicked() { - let image_size = - self.image_size(renderer, bounds.size()); - - self.state - .pan(position.x, position.y, bounds, image_size); - } - } - _ => {} } - } else if let Event::Mouse(mouse::Event::ButtonReleased(button)) = event - { - if button == mouse::Button::Left { - self.state.starting_cursor_pos = None; + Event::Mouse(mouse::Event::ButtonPressed(button)) + if is_mouse_over => + { + if button == mouse::Button::Left { + self.state.starting_cursor_pos = Some(cursor_position); + + 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 { position }) => { + if self.state.is_cursor_clicked() { + let image_size = self.image_size(renderer, bounds.size()); + + self.state.pan(position.x, position.y, bounds, image_size); + } + } + _ => {} } event::Status::Ignored From ca3e4e9f1bef9ffbe011e08c91ccb012312b71e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 10:49:10 +0100 Subject: [PATCH 14/24] Simplify pattern match in `image::Viewer::on_event` --- native/src/widget/image/viewer.rs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 6c170341..3402cf18 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -270,26 +270,21 @@ where } } } - Event::Mouse(mouse::Event::ButtonPressed(button)) + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if is_mouse_over => { - if button == mouse::Button::Left { - self.state.starting_cursor_pos = Some(cursor_position); - - self.state.starting_offset = self.state.current_offset; - } + self.state.starting_cursor_pos = Some(cursor_position); + 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::ButtonReleased(mouse::Button::Left)) => { + self.state.starting_cursor_pos = None } - Event::Mouse(mouse::Event::CursorMoved { position }) => { - if self.state.is_cursor_clicked() { - let image_size = self.image_size(renderer, bounds.size()); + Event::Mouse(mouse::Event::CursorMoved { position }) + if self.state.is_cursor_clicked() => + { + let image_size = self.image_size(renderer, bounds.size()); - self.state.pan(position.x, position.y, bounds, image_size); - } + self.state.pan(position.x, position.y, bounds, image_size); } _ => {} } From e6f23e37719ccc52e3a3802e204a9b33aeba9d2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 10:53:38 +0100 Subject: [PATCH 15/24] Rename `scale_pct` to `scale_step` in `image::Viewer` --- native/src/widget/image/viewer.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 3402cf18..c8656101 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -21,7 +21,7 @@ pub struct Viewer<'a> { max_height: u32, min_scale: f32, max_scale: f32, - scale_pct: f32, + scale_step: f32, handle: image::Handle, } @@ -41,7 +41,7 @@ impl<'a> Viewer<'a> { max_height: u32::MAX, min_scale: 0.25, max_scale: 10.0, - scale_pct: 0.10, + scale_step: 0.10, handle, } } @@ -112,8 +112,8 @@ impl<'a> Viewer<'a> { /// Default is `0.10` /// /// [`Viewer`]: struct.Viewer.html - pub fn scale_pct(mut self, scale_pct: f32) -> Self { - self.scale_pct = scale_pct; + pub fn scale_step(mut self, scale_step: f32) -> Self { + self.scale_step = scale_step; self } @@ -230,10 +230,10 @@ where self.state.scale = Some( (if y > 0.0 { self.state.scale.unwrap_or(1.0) - * (1.0 + self.scale_pct) + * (1.0 + self.scale_step) } else { self.state.scale.unwrap_or(1.0) - / (1.0 + self.scale_pct) + / (1.0 + self.scale_step) }) .max(self.min_scale) .min(self.max_scale), From 64af860ad2317d9f2e72cbda659102d96ec0f931 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 10:55:18 +0100 Subject: [PATCH 16/24] Remove unnecessary `Option` in `image::viewer::State` --- native/src/widget/image/viewer.rs | 42 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index c8656101..0f081a1a 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -136,7 +136,7 @@ impl<'a> Viewer<'a> { let ratio = width_ratio.min(height_ratio); - let scale = self.state.scale.unwrap_or(1.0); + let scale = self.state.scale; if ratio < 1.0 { (dimensions.0 * ratio * scale, dimensions.1 * ratio * scale) @@ -222,29 +222,24 @@ where match delta { mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => { - let previous_scale = self.state.scale.unwrap_or(1.0); + let previous_scale = self.state.scale; if y < 0.0 && previous_scale > self.min_scale || y > 0.0 && previous_scale < self.max_scale { - self.state.scale = Some( - (if y > 0.0 { - self.state.scale.unwrap_or(1.0) - * (1.0 + self.scale_step) - } else { - self.state.scale.unwrap_or(1.0) - / (1.0 + self.scale_step) - }) - .max(self.min_scale) - .min(self.max_scale), - ); + self.state.scale = (if y > 0.0 { + self.state.scale * (1.0 + self.scale_step) + } else { + self.state.scale / (1.0 + self.scale_step) + }) + .max(self.min_scale) + .min(self.max_scale); let image_size = self.image_size(renderer, bounds.size()); - let factor = self.state.scale.unwrap() - / previous_scale - - 1.0; + let factor = + self.state.scale / previous_scale - 1.0; let cursor_to_center = relative_cursor_position( cursor_position, @@ -343,14 +338,25 @@ where /// The local state of a [`Viewer`]. /// /// [`Viewer`]: struct.Viewer.html -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct State { - scale: Option, + scale: f32, starting_offset: Vector, current_offset: Vector, starting_cursor_pos: Option, } +impl Default for State { + fn default() -> Self { + Self { + scale: 1.0, + starting_offset: Vector::default(), + current_offset: Vector::default(), + starting_cursor_pos: None, + } + } +} + impl State { /// Creates a new [`State`]. /// From cf97982929c2c5e4487e4ae0236b24749fca3013 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 10:57:51 +0100 Subject: [PATCH 17/24] Build `todos` examples instead of `pokedex` in CI --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 9e73d3d3..bc531abf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,5 +29,5 @@ jobs: run: cargo check --package iced --target wasm32-unknown-unknown - name: Check compilation of `tour` example run: cargo build --package tour --target wasm32-unknown-unknown - - name: Check compilation of `pokedex` example - run: cargo build --package pokedex --target wasm32-unknown-unknown + - name: Check compilation of `todos` example + run: cargo build --package todos --target wasm32-unknown-unknown From 4eb5779542e3d53d2a41f30f259640bb2f8069fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 11:00:13 +0100 Subject: [PATCH 18/24] Remove redundant `f32` conversions in `image::Viewer` --- native/src/widget/image/viewer.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 0f081a1a..605298f1 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -371,12 +371,11 @@ impl State { /// [`Viewer`]: struct.Viewer.html /// [`State`]: struct.State.html fn pan(&mut self, x: f32, y: f32, bounds: Rectangle, image_size: Size) { - let hidden_width = ((image_size.width - bounds.width) as f32 / 2.0) - .max(0.0) - .round(); - let hidden_height = ((image_size.height - bounds.height) as f32 / 2.0) - .max(0.0) - .round(); + let hidden_width = + (image_size.width - bounds.width / 2.0).max(0.0).round(); + + let hidden_height = + (image_size.height - bounds.height / 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; @@ -400,12 +399,11 @@ impl State { /// [`Viewer`]: struct.Viewer.html /// [`State`]: struct.State.html fn offset(&self, bounds: Rectangle, image_size: Size) -> Vector { - let hidden_width = ((image_size.width - bounds.width) as f32 / 2.0) - .max(0.0) - .round(); - let hidden_height = ((image_size.height - bounds.height) as f32 / 2.0) - .max(0.0) - .round(); + let hidden_width = + (image_size.width - bounds.width / 2.0).max(0.0).round(); + + let hidden_height = + (image_size.height - bounds.height / 2.0).max(0.0).round(); Vector::new( self.current_offset From 8245a1176648f8df803f9820bd5ceac71d6a031a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 11:01:20 +0100 Subject: [PATCH 19/24] Negate values instead of multipling by `-1.0` in `image::Viewer` --- native/src/widget/image/viewer.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 605298f1..62745fba 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -383,13 +383,13 @@ impl State { if bounds.width < image_size.width { self.current_offset.x = (self.starting_offset.x - delta_x) .min(hidden_width) - .max(-1.0 * hidden_width); + .max(-hidden_width); } if bounds.height < image_size.height { self.current_offset.y = (self.starting_offset.y - delta_y) .min(hidden_height) - .max(-1.0 * hidden_height); + .max(-hidden_height); } } @@ -406,14 +406,8 @@ impl State { (image_size.height - bounds.height / 2.0).max(0.0).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), + self.current_offset.x.min(hidden_width).max(-hidden_width), + self.current_offset.y.min(hidden_height).max(-hidden_height), ) } From 43ef85ae5c5b8901642e0832456ed907635600ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 11:04:07 +0100 Subject: [PATCH 20/24] Rename `starting_cursor_pos` to `cursor_grabbed_at` in `image::Viewer` --- graphics/src/widget/image/viewer.rs | 2 +- native/src/widget/image/viewer.rs | 21 ++++++++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/graphics/src/widget/image/viewer.rs b/graphics/src/widget/image/viewer.rs index 804f2be6..28dffc4f 100644 --- a/graphics/src/widget/image/viewer.rs +++ b/graphics/src/widget/image/viewer.rs @@ -39,7 +39,7 @@ where } }, { - if state.is_cursor_clicked() { + if state.is_cursor_grabbed() { mouse::Interaction::Grabbing } else if is_mouse_over && (image_size.width > bounds.width diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index 62745fba..af687af0 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -268,14 +268,14 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) if is_mouse_over => { - self.state.starting_cursor_pos = Some(cursor_position); + self.state.cursor_grabbed_at = Some(cursor_position); self.state.starting_offset = self.state.current_offset; } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { - self.state.starting_cursor_pos = None + self.state.cursor_grabbed_at = None } Event::Mouse(mouse::Event::CursorMoved { position }) - if self.state.is_cursor_clicked() => + if self.state.is_cursor_grabbed() => { let image_size = self.image_size(renderer, bounds.size()); @@ -343,7 +343,7 @@ pub struct State { scale: f32, starting_offset: Vector, current_offset: Vector, - starting_cursor_pos: Option, + cursor_grabbed_at: Option, } impl Default for State { @@ -352,7 +352,7 @@ impl Default for State { scale: 1.0, starting_offset: Vector::default(), current_offset: Vector::default(), - starting_cursor_pos: None, + cursor_grabbed_at: None, } } } @@ -377,8 +377,8 @@ impl State { let hidden_height = (image_size.height - bounds.height / 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; + let delta_x = x - self.cursor_grabbed_at.unwrap().x; + let delta_y = y - self.cursor_grabbed_at.unwrap().y; if bounds.width < image_size.width { self.current_offset.x = (self.starting_offset.x - delta_x) @@ -411,13 +411,12 @@ impl State { ) } - /// Returns if the left mouse button is still held down since clicking inside - /// the [`Viewer`]. + /// Returns if the cursor is currently grabbed by the [`Viewer`]. /// /// [`Viewer`]: struct.Viewer.html /// [`State`]: struct.State.html - pub fn is_cursor_clicked(&self) -> bool { - self.starting_cursor_pos.is_some() + pub fn is_cursor_grabbed(&self) -> bool { + self.cursor_grabbed_at.is_some() } } From 149098cb686dcaad21f88adc2c646372c85a7d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 11:20:25 +0100 Subject: [PATCH 21/24] Remove use of `unwrap` in `image::Viewer` --- native/src/widget/image/viewer.rs | 65 +++++++++++++++---------------- 1 file changed, 32 insertions(+), 33 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index af687af0..ab9f3802 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -274,12 +274,39 @@ where Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { self.state.cursor_grabbed_at = None } - Event::Mouse(mouse::Event::CursorMoved { position }) - if self.state.is_cursor_grabbed() => - { - let image_size = self.image_size(renderer, bounds.size()); + Event::Mouse(mouse::Event::CursorMoved { position }) => { + if let Some(origin) = self.state.cursor_grabbed_at { + let image_size = self.image_size(renderer, bounds.size()); - self.state.pan(position.x, position.y, bounds, image_size); + let hidden_width = (image_size.width - bounds.width / 2.0) + .max(0.0) + .round(); + + let hidden_height = (image_size.height + - bounds.height / 2.0) + .max(0.0) + .round(); + + let delta = position - origin; + + let x = if bounds.width < image_size.width { + (self.state.starting_offset.x - delta.x) + .min(hidden_width) + .max(-hidden_width) + } else { + 0.0 + }; + + let y = if bounds.height < image_size.height { + (self.state.starting_offset.y - delta.y) + .min(hidden_height) + .max(-hidden_height) + } else { + 0.0 + }; + + self.state.current_offset = Vector::new(x, y); + } } _ => {} } @@ -365,34 +392,6 @@ impl State { 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_size: Size) { - let hidden_width = - (image_size.width - bounds.width / 2.0).max(0.0).round(); - - let hidden_height = - (image_size.height - bounds.height / 2.0).max(0.0).round(); - - let delta_x = x - self.cursor_grabbed_at.unwrap().x; - let delta_y = y - self.cursor_grabbed_at.unwrap().y; - - if bounds.width < image_size.width { - self.current_offset.x = (self.starting_offset.x - delta_x) - .min(hidden_width) - .max(-hidden_width); - } - - if bounds.height < image_size.height { - self.current_offset.y = (self.starting_offset.y - delta_y) - .min(hidden_height) - .max(-hidden_height); - } - } - /// Returns the current offset of the [`State`], given the bounds /// of the [`Viewer`] and its image. /// From 6a51282933ca90283c2fb9ae2088129157394d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 11:23:22 +0100 Subject: [PATCH 22/24] Simplify `cursor_to_center` in `image::Viewer` --- native/src/widget/image/viewer.rs | 23 ++--------------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index ab9f3802..d376e475 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -149,23 +149,6 @@ impl<'a> Viewer<'a> { } } -/// Cursor position relative to the [`Viewer`] bounds. -/// -/// [`Viewer`]: struct.Viewer.html -fn relative_cursor_position( - absolute_position: Point, - bounds: Rectangle, -) -> Point { - absolute_position - Vector::new(bounds.x, bounds.y) -} - -/// Center point relative to the [`Viewer`] bounds. -/// -/// [`Viewer`]: struct.Viewer.html -fn relative_center(bounds: Rectangle) -> Point { - bounds.center() - Vector::new(bounds.x, bounds.y) -} - impl<'a, Message, Renderer> Widget for Viewer<'a> where Renderer: self::Renderer + image::Renderer, @@ -241,10 +224,8 @@ where let factor = self.state.scale / previous_scale - 1.0; - let cursor_to_center = relative_cursor_position( - cursor_position, - bounds, - ) - relative_center(bounds); + let cursor_to_center = + cursor_position - bounds.center(); let adjustment = cursor_to_center * factor + self.state.current_offset * factor; From c54a6446a3a22077af651cddb37eaeb5f659e316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 11:27:43 +0100 Subject: [PATCH 23/24] Use intra-doc links in `image::viewer` --- native/src/widget/image/viewer.rs | 41 +++---------------------------- 1 file changed, 4 insertions(+), 37 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index d376e475..ba4ee087 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -28,9 +28,7 @@ pub struct Viewer<'a> { 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 + /// [`Handle`]: image::Handle pub fn new(state: &'a mut State, handle: image::Handle) -> Self { Viewer { state, @@ -47,40 +45,30 @@ impl<'a> Viewer<'a> { } /// 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 @@ -89,8 +77,6 @@ impl<'a> Viewer<'a> { /// 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 @@ -99,8 +85,6 @@ impl<'a> Viewer<'a> { /// 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 @@ -110,8 +94,6 @@ impl<'a> Viewer<'a> { /// when zoomed in / out. /// /// Default is `0.10` - /// - /// [`Viewer`]: struct.Viewer.html pub fn scale_step(mut self, scale_step: f32) -> Self { self.scale_step = scale_step; self @@ -120,8 +102,6 @@ impl<'a> Viewer<'a> { /// 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_size(&self, renderer: &Renderer, bounds: Size) -> Size where Renderer: self::Renderer + image::Renderer, @@ -344,8 +324,6 @@ where } /// The local state of a [`Viewer`]. -/// -/// [`Viewer`]: struct.Viewer.html #[derive(Debug, Clone, Copy)] pub struct State { scale: f32, @@ -367,17 +345,12 @@ impl Default for State { impl State { /// Creates a new [`State`]. - /// - /// [`State`]: struct.State.html pub fn new() -> Self { State::default() } /// 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_size: Size) -> Vector { let hidden_width = (image_size.width - bounds.width / 2.0).max(0.0).round(); @@ -392,9 +365,6 @@ impl State { } /// Returns if the cursor is currently grabbed by the [`Viewer`]. - /// - /// [`Viewer`]: struct.Viewer.html - /// [`State`]: struct.State.html pub fn is_cursor_grabbed(&self) -> bool { self.cursor_grabbed_at.is_some() } @@ -405,22 +375,19 @@ impl State { /// 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 +/// [renderer]: crate::renderer 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 [`Size`] of the scaled [`Viewer`] image /// - the translation of the clipped image /// - 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 + /// [`Handle`]: image::Handle fn draw( &mut self, state: &State, From 10d6df73e34e421cbf96d62b26c0c0701d9096ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 18 Dec 2020 11:28:55 +0100 Subject: [PATCH 24/24] Remove `max_width` and `max_height` from `image::Viewer` The support for these layout constraints is currently not ideal. We should reintroduce these methods once our layout engine improves. --- native/src/widget/image/viewer.rs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/native/src/widget/image/viewer.rs b/native/src/widget/image/viewer.rs index ba4ee087..4ec3faf6 100644 --- a/native/src/widget/image/viewer.rs +++ b/native/src/widget/image/viewer.rs @@ -8,7 +8,7 @@ use crate::{ Widget, }; -use std::{f32, hash::Hash, u32}; +use std::hash::Hash; /// A frame that displays an image with the ability to zoom in/out and pan. #[allow(missing_debug_implementations)] @@ -17,8 +17,6 @@ pub struct Viewer<'a> { padding: u16, width: Length, height: Length, - max_width: u32, - max_height: u32, min_scale: f32, max_scale: f32, scale_step: f32, @@ -35,8 +33,6 @@ impl<'a> Viewer<'a> { padding: 0, width: Length::Shrink, height: Length::Shrink, - max_width: u32::MAX, - max_height: u32::MAX, min_scale: 0.25, max_scale: 10.0, scale_step: 0.10, @@ -62,18 +58,6 @@ impl<'a> Viewer<'a> { self } - /// Sets the max width of the [`Viewer`]. - pub fn max_width(mut self, max_width: u32) -> Self { - self.max_width = max_width; - self - } - - /// Sets the max height of the [`Viewer`]. - pub fn max_height(mut self, max_height: u32) -> Self { - self.max_height = max_height; - self - } - /// Sets the max scale applied to the image of the [`Viewer`]. /// /// Default is `10.0` @@ -315,8 +299,6 @@ where 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);