From 09a6bcfffc24f5abdc8709403bab7ae1e01563f1 Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 13:15:17 +0200 Subject: Add `Image` rotation support Co-authored-by: DKolter <68352124+DKolter@users.noreply.github.com> --- widget/src/image.rs | 73 ++++++++++++++++++++++++++++++++++------------ widget/src/image/viewer.rs | 2 ++ widget/src/svg.rs | 65 +++++++++++++++++++++++++++++++---------- 3 files changed, 107 insertions(+), 33 deletions(-) (limited to 'widget') diff --git a/widget/src/image.rs b/widget/src/image.rs index 21d371b7..3a0a5e53 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,5 +1,6 @@ //! Display images in your user interface. pub mod viewer; +use iced_renderer::core::{Point, RotationLayout}; pub use viewer::Viewer; use crate::core::image; @@ -8,7 +9,7 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Rectangle, Size, Vector, Widget, + ContentFit, Element, Layout, Length, Rectangle, Size, Widget, }; pub use image::{FilterMethod, Handle}; @@ -36,6 +37,8 @@ pub struct Image { height: Length, content_fit: ContentFit, filter_method: FilterMethod, + rotation: f32, + rotation_layout: RotationLayout, } impl Image { @@ -47,6 +50,8 @@ impl Image { height: Length::Shrink, content_fit: ContentFit::Contain, filter_method: FilterMethod::default(), + rotation: 0.0, + rotation_layout: RotationLayout::Change, } } @@ -75,6 +80,18 @@ impl Image { self.filter_method = filter_method; self } + + /// Rotates the [`Image`] by the given angle in radians. + pub fn rotation(mut self, degrees: f32) -> Self { + self.rotation = degrees; + self + } + + /// Sets the [`RotationLayout`] of the [`Image`]. + pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { + self.rotation_layout = rotation_layout; + self + } } /// Computes the layout of an [`Image`]. @@ -85,22 +102,25 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, + rotation: f32, + rotation_layout: RotationLayout, ) -> layout::Node where Renderer: image::Renderer, { // The raw w/h of the underlying image - let image_size = { - let Size { width, height } = renderer.measure_image(handle); + let image_size = renderer.measure_image(handle); + let image_size = + Size::new(image_size.width as f32, image_size.height as f32); - Size::new(width as f32, height as f32) - }; + // The rotated size of the image + let rotated_size = rotation_layout.apply_to_size(image_size, rotation); // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(width, height, image_size); + let raw_size = limits.resolve(width, height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = content_fit.fit(image_size, raw_size); + let full_size = content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -124,32 +144,45 @@ pub fn draw( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, + rotation: f32, + rotation_layout: RotationLayout, ) where Renderer: image::Renderer, Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = rotation_layout.apply_to_size(image_size, rotation); let bounds = layout.bounds(); - let adjusted_fit = content_fit.fit(image_size, bounds.size()); + let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); + let scale = Size::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds + let position = match content_fit { + ContentFit::None => Point::new( + bounds.position().x + + (rotated_size.width - image_size.width) / 2.0, + bounds.position().y + + (rotated_size.height - image_size.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - image_size.width / 2.0, + bounds.center_y() - image_size.height / 2.0, + ), }; + let drawing_bounds = Rectangle::new(position, image_size); + renderer.draw_image( handle.clone(), filter_method, - drawing_bounds + offset, + drawing_bounds, + rotation, + scale, ); }; @@ -187,6 +220,8 @@ where self.width, self.height, self.content_fit, + self.rotation, + self.rotation_layout, ) } @@ -206,6 +241,8 @@ where &self.handle, self.content_fit, self.filter_method, + self.rotation, + self.rotation_layout, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 214cb996..ccdfdebb 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -341,6 +341,8 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + 0.0, + Size::UNIT, ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index eb142189..21946af8 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -5,8 +5,8 @@ use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; use crate::core::{ - Color, ContentFit, Element, Layout, Length, Rectangle, Size, Theme, Vector, - Widget, + Color, ContentFit, Element, Layout, Length, Point, Rectangle, + RotationLayout, Size, Theme, Widget, }; use std::path::PathBuf; @@ -29,6 +29,8 @@ where height: Length, content_fit: ContentFit, class: Theme::Class<'a>, + rotation: f32, + rotation_layout: RotationLayout, } impl<'a, Theme> Svg<'a, Theme> @@ -43,6 +45,8 @@ where height: Length::Shrink, content_fit: ContentFit::Contain, class: Theme::default(), + rotation: 0.0, + rotation_layout: RotationLayout::Change, } } @@ -95,6 +99,18 @@ where self.class = class.into(); self } + + /// Rotates the [`Svg`] by the given angle in radians. + pub fn rotation(mut self, degrees: f32) -> Self { + self.rotation = degrees; + self + } + + /// Sets the [`RotationLayout`] of the [`Svg`]. + pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { + self.rotation_layout = rotation_layout; + self + } } impl<'a, Message, Theme, Renderer> Widget @@ -120,11 +136,16 @@ where let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + // The rotated size of the svg + let rotated_size = self + .rotation_layout + .apply_to_size(image_size, self.rotation); + // The size to be available to the widget prior to `Shrink`ing - let raw_size = limits.resolve(self.width, self.height, image_size); + let raw_size = limits.resolve(self.width, self.height, rotated_size); // The uncropped size of the image when fit to the bounds above - let full_size = self.content_fit.fit(image_size, raw_size); + let full_size = self.content_fit.fit(rotated_size, raw_size); // Shrink the widget to fit the resized image, if requested let final_size = Size { @@ -153,23 +174,35 @@ where ) { let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); + let rotated_size = self + .rotation_layout + .apply_to_size(image_size, self.rotation); let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); + let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); + let scale = Size::new( + adjusted_fit.width / rotated_size.width, + adjusted_fit.height / rotated_size.height, + ); + let is_mouse_over = cursor.is_over(bounds); let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds + let position = match self.content_fit { + ContentFit::None => Point::new( + bounds.position().x + + (rotated_size.width - image_size.width) / 2.0, + bounds.position().y + + (rotated_size.height - image_size.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - image_size.width / 2.0, + bounds.center_y() - image_size.height / 2.0, + ), }; + let drawing_bounds = Rectangle::new(position, image_size); + let status = if is_mouse_over { Status::Hovered } else { @@ -181,7 +214,9 @@ where renderer.draw_svg( self.handle.clone(), style.color, - drawing_bounds + offset, + drawing_bounds, + self.rotation, + scale, ); }; -- cgit From a57313b23ecb9843856ca0ea08635b6121fcb2cb Mon Sep 17 00:00:00 2001 From: Héctor Ramón Jiménez Date: Thu, 2 May 2024 15:21:22 +0200 Subject: Simplify image rotation API and its internals --- widget/src/image.rs | 70 +++++++++++++++++---------------------- widget/src/image/viewer.rs | 7 ++-- widget/src/svg.rs | 81 +++++++++++++++++++--------------------------- 3 files changed, 66 insertions(+), 92 deletions(-) (limited to 'widget') diff --git a/widget/src/image.rs b/widget/src/image.rs index 3a0a5e53..45209a91 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -1,6 +1,5 @@ //! Display images in your user interface. pub mod viewer; -use iced_renderer::core::{Point, RotationLayout}; pub use viewer::Viewer; use crate::core::image; @@ -9,7 +8,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::Tree; use crate::core::{ - ContentFit, Element, Layout, Length, Rectangle, Size, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, + Vector, Widget, }; pub use image::{FilterMethod, Handle}; @@ -37,8 +37,7 @@ pub struct Image { height: Length, content_fit: ContentFit, filter_method: FilterMethod, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, } impl Image { @@ -48,10 +47,9 @@ impl Image { handle: handle.into(), width: Length::Shrink, height: Length::Shrink, - content_fit: ContentFit::Contain, + content_fit: ContentFit::default(), filter_method: FilterMethod::default(), - rotation: 0.0, - rotation_layout: RotationLayout::Change, + rotation: Rotation::default(), } } @@ -81,15 +79,9 @@ impl Image { self } - /// Rotates the [`Image`] by the given angle in radians. - pub fn rotation(mut self, degrees: f32) -> Self { - self.rotation = degrees; - self - } - - /// Sets the [`RotationLayout`] of the [`Image`]. - pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { - self.rotation_layout = rotation_layout; + /// Applies the given [`Rotation`] to the [`Image`]. + pub fn rotation(mut self, rotation: impl Into) -> Self { + self.rotation = rotation.into(); self } } @@ -102,8 +94,7 @@ pub fn layout( width: Length, height: Length, content_fit: ContentFit, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, ) -> layout::Node where Renderer: image::Renderer, @@ -114,7 +105,7 @@ where Size::new(image_size.width as f32, image_size.height as f32); // The rotated size of the image - let rotated_size = rotation_layout.apply_to_size(image_size, rotation); + let rotated_size = rotation.apply(image_size); // The size to be available to the widget prior to `Shrink`ing let raw_size = limits.resolve(width, height, rotated_size); @@ -144,45 +135,44 @@ pub fn draw( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, ) where Renderer: image::Renderer, Handle: Clone, { let Size { width, height } = renderer.measure_image(handle); let image_size = Size::new(width as f32, height as f32); - let rotated_size = rotation_layout.apply_to_size(image_size, rotation); + let rotated_size = rotation.apply(image_size); let bounds = layout.bounds(); let adjusted_fit = content_fit.fit(rotated_size, bounds.size()); - let scale = Size::new( + + let scale = Vector::new( adjusted_fit.width / rotated_size.width, adjusted_fit.height / rotated_size.height, ); - let render = |renderer: &mut Renderer| { - let position = match content_fit { - ContentFit::None => Point::new( - bounds.position().x - + (rotated_size.width - image_size.width) / 2.0, - bounds.position().y - + (rotated_size.height - image_size.height) / 2.0, - ), - _ => Point::new( - bounds.center_x() - image_size.width / 2.0, - bounds.center_y() - image_size.height / 2.0, - ), - }; + let final_size = image_size * scale; + + let position = match content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; - let drawing_bounds = Rectangle::new(position, image_size); + let drawing_bounds = Rectangle::new(position, final_size); + let render = |renderer: &mut Renderer| { renderer.draw_image( handle.clone(), filter_method, drawing_bounds, - rotation, - scale, + rotation.radians(), ); }; @@ -221,7 +211,6 @@ where self.height, self.content_fit, self.rotation, - self.rotation_layout, ) } @@ -242,7 +231,6 @@ where self.content_fit, self.filter_method, self.rotation, - self.rotation_layout, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index ccdfdebb..ee4c0fba 100644 --- a/widget/src/image/viewer.rs +++ b/widget/src/image/viewer.rs @@ -6,8 +6,8 @@ use crate::core::mouse; use crate::core::renderer; use crate::core::widget::tree::{self, Tree}; use crate::core::{ - Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle, + Shell, Size, Vector, Widget, }; /// A frame that displays an image with the ability to zoom in/out and pan. @@ -341,8 +341,7 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, - 0.0, - Size::UNIT, + Radians(0.0), ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index 21946af8..c1fccba1 100644 --- a/widget/src/svg.rs +++ b/widget/src/svg.rs @@ -5,8 +5,8 @@ use crate::core::renderer; use crate::core::svg; use crate::core::widget::Tree; use crate::core::{ - Color, ContentFit, Element, Layout, Length, Point, Rectangle, - RotationLayout, Size, Theme, Widget, + Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, + Size, Theme, Vector, Widget, }; use std::path::PathBuf; @@ -29,8 +29,7 @@ where height: Length, content_fit: ContentFit, class: Theme::Class<'a>, - rotation: f32, - rotation_layout: RotationLayout, + rotation: Rotation, } impl<'a, Theme> Svg<'a, Theme> @@ -45,8 +44,7 @@ where height: Length::Shrink, content_fit: ContentFit::Contain, class: Theme::default(), - rotation: 0.0, - rotation_layout: RotationLayout::Change, + rotation: Rotation::default(), } } @@ -100,15 +98,9 @@ where self } - /// Rotates the [`Svg`] by the given angle in radians. - pub fn rotation(mut self, degrees: f32) -> Self { - self.rotation = degrees; - self - } - - /// Sets the [`RotationLayout`] of the [`Svg`]. - pub fn rotation_layout(mut self, rotation_layout: RotationLayout) -> Self { - self.rotation_layout = rotation_layout; + /// Applies the given [`Rotation`] to the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into) -> Self { + self.rotation = rotation.into(); self } } @@ -137,9 +129,7 @@ where let image_size = Size::new(width as f32, height as f32); // The rotated size of the svg - let rotated_size = self - .rotation_layout - .apply_to_size(image_size, self.rotation); + let rotated_size = self.rotation.apply(image_size); // The size to be available to the widget prior to `Shrink`ing let raw_size = limits.resolve(self.width, self.height, rotated_size); @@ -174,49 +164,46 @@ where ) { let Size { width, height } = renderer.measure_svg(&self.handle); let image_size = Size::new(width as f32, height as f32); - let rotated_size = self - .rotation_layout - .apply_to_size(image_size, self.rotation); + let rotated_size = self.rotation.apply(image_size); let bounds = layout.bounds(); let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); - let scale = Size::new( + let scale = Vector::new( adjusted_fit.width / rotated_size.width, adjusted_fit.height / rotated_size.height, ); + let final_size = image_size * scale; + + let position = match self.content_fit { + ContentFit::None => Point::new( + bounds.x + (rotated_size.width - adjusted_fit.width) / 2.0, + bounds.y + (rotated_size.height - adjusted_fit.height) / 2.0, + ), + _ => Point::new( + bounds.center_x() - final_size.width / 2.0, + bounds.center_y() - final_size.height / 2.0, + ), + }; + + let drawing_bounds = Rectangle::new(position, final_size); + let is_mouse_over = cursor.is_over(bounds); - let render = |renderer: &mut Renderer| { - let position = match self.content_fit { - ContentFit::None => Point::new( - bounds.position().x - + (rotated_size.width - image_size.width) / 2.0, - bounds.position().y - + (rotated_size.height - image_size.height) / 2.0, - ), - _ => Point::new( - bounds.center_x() - image_size.width / 2.0, - bounds.center_y() - image_size.height / 2.0, - ), - }; - - let drawing_bounds = Rectangle::new(position, image_size); - - let status = if is_mouse_over { - Status::Hovered - } else { - Status::Idle - }; - - let style = theme.style(&self.class, status); + let status = if is_mouse_over { + Status::Hovered + } else { + Status::Idle + }; + let style = theme.style(&self.class, status); + + let render = |renderer: &mut Renderer| { renderer.draw_svg( self.handle.clone(), style.color, drawing_bounds, - self.rotation, - scale, + self.rotation.radians(), ); }; -- cgit