diff options
author | 2024-05-03 07:31:34 +0200 | |
---|---|---|
committer | 2024-05-03 07:31:34 +0200 | |
commit | 1cefe6be210cdae8c6769673e8d23c6781a988f1 (patch) | |
tree | 7d4a383412e6bd69d0cc1b32f996ba7cf6ef892e | |
parent | fe240a93aacd15bd3fa75876054753a53bda9054 (diff) | |
parent | 4010e3983d40e24a5d7b590d8fec9801651199ce (diff) | |
download | iced-1cefe6be210cdae8c6769673e8d23c6781a988f1.tar.gz iced-1cefe6be210cdae8c6769673e8d23c6781a988f1.tar.bz2 iced-1cefe6be210cdae8c6769673e8d23c6781a988f1.zip |
Merge pull request #2334 from DKolter/image-rotation
Adding feature: Image rotation
Diffstat (limited to '')
-rw-r--r-- | core/src/angle.rs | 75 | ||||
-rw-r--r-- | core/src/content_fit.rs | 17 | ||||
-rw-r--r-- | core/src/image.rs | 3 | ||||
-rw-r--r-- | core/src/lib.rs | 2 | ||||
-rw-r--r-- | core/src/rectangle.rs | 32 | ||||
-rw-r--r-- | core/src/renderer/null.rs | 5 | ||||
-rw-r--r-- | core/src/rotation.rs | 72 | ||||
-rw-r--r-- | core/src/size.rs | 29 | ||||
-rw-r--r-- | core/src/svg.rs | 3 | ||||
-rw-r--r-- | core/src/vector.rs | 3 | ||||
-rw-r--r-- | examples/ferris/Cargo.toml | 10 | ||||
-rw-r--r-- | examples/ferris/src/main.rs | 201 | ||||
-rw-r--r-- | graphics/src/image.rs | 17 | ||||
-rw-r--r-- | renderer/src/fallback.rs | 12 | ||||
-rw-r--r-- | src/lib.rs | 4 | ||||
-rw-r--r-- | tiny_skia/src/engine.rs | 51 | ||||
-rw-r--r-- | tiny_skia/src/layer.rs | 12 | ||||
-rw-r--r-- | tiny_skia/src/lib.rs | 14 | ||||
-rw-r--r-- | tiny_skia/src/vector.rs | 4 | ||||
-rw-r--r-- | wgpu/src/image/mod.rs | 41 | ||||
-rw-r--r-- | wgpu/src/layer.rs | 9 | ||||
-rw-r--r-- | wgpu/src/lib.rs | 16 | ||||
-rw-r--r-- | wgpu/src/shader/image.wgsl | 32 | ||||
-rw-r--r-- | widget/src/image.rs | 67 | ||||
-rw-r--r-- | widget/src/image/viewer.rs | 5 | ||||
-rw-r--r-- | widget/src/svg.rs | 68 |
26 files changed, 695 insertions, 109 deletions
diff --git a/core/src/angle.rs b/core/src/angle.rs index dc3c0e93..9c8a9b24 100644 --- a/core/src/angle.rs +++ b/core/src/angle.rs @@ -1,12 +1,17 @@ use crate::{Point, Rectangle, Vector}; use std::f32::consts::{FRAC_PI_2, PI}; -use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Sub, SubAssign}; +use std::ops::{Add, AddAssign, Div, Mul, RangeInclusive, Rem, Sub, SubAssign}; /// Degrees #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Degrees(pub f32); +impl Degrees { + /// The range of degrees of a circle. + pub const RANGE: RangeInclusive<Self> = Self(0.0)..=Self(360.0); +} + impl PartialEq<f32> for Degrees { fn eq(&self, other: &f32) -> bool { self.0.eq(other) @@ -19,6 +24,52 @@ impl PartialOrd<f32> for Degrees { } } +impl From<f32> for Degrees { + fn from(degrees: f32) -> Self { + Self(degrees) + } +} + +impl From<u8> for Degrees { + fn from(degrees: u8) -> Self { + Self(f32::from(degrees)) + } +} + +impl From<Degrees> for f32 { + fn from(degrees: Degrees) -> Self { + degrees.0 + } +} + +impl From<Degrees> for f64 { + fn from(degrees: Degrees) -> Self { + Self::from(degrees.0) + } +} + +impl Mul<f32> for Degrees { + type Output = Degrees; + + fn mul(self, rhs: f32) -> Self::Output { + Self(self.0 * rhs) + } +} + +impl num_traits::FromPrimitive for Degrees { + fn from_i64(n: i64) -> Option<Self> { + Some(Self(n as f32)) + } + + fn from_u64(n: u64) -> Option<Self> { + Some(Self(n as f32)) + } + + fn from_f64(n: f64) -> Option<Self> { + Some(Self(n as f32)) + } +} + /// Radians #[derive(Debug, Copy, Clone, PartialEq, PartialOrd)] pub struct Radians(pub f32); @@ -65,6 +116,12 @@ impl From<u8> for Radians { } } +impl From<Radians> for f32 { + fn from(radians: Radians) -> Self { + radians.0 + } +} + impl From<Radians> for f64 { fn from(radians: Radians) -> Self { Self::from(radians.0) @@ -107,6 +164,14 @@ impl Add for Radians { } } +impl Add<Degrees> for Radians { + type Output = Self; + + fn add(self, rhs: Degrees) -> Self::Output { + Self(self.0 + rhs.0.to_radians()) + } +} + impl AddAssign for Radians { fn add_assign(&mut self, rhs: Radians) { self.0 = self.0 + rhs.0; @@ -153,6 +218,14 @@ impl Div for Radians { } } +impl Rem for Radians { + type Output = Self; + + fn rem(self, rhs: Self) -> Self::Output { + Self(self.0 % rhs.0) + } +} + impl PartialEq<f32> for Radians { fn eq(&self, other: &f32) -> bool { self.0.eq(other) diff --git a/core/src/content_fit.rs b/core/src/content_fit.rs index 6bbedc7a..19642716 100644 --- a/core/src/content_fit.rs +++ b/core/src/content_fit.rs @@ -1,6 +1,8 @@ //! Control the fit of some content (like an image) within a space. use crate::Size; +use std::fmt; + /// The strategy used to fit the contents of a widget to its bounding box. /// /// Each variant of this enum is a strategy that can be applied for resolving @@ -11,7 +13,7 @@ use crate::Size; /// in CSS, see [Mozilla's docs][1], or run the `tour` example /// /// [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit -#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq, Default)] pub enum ContentFit { /// Scale as big as it can be without needing to crop or hide parts. /// @@ -23,6 +25,7 @@ pub enum ContentFit { /// This is a great fit for when you need to display an image without losing /// any part of it, particularly when the image itself is the focus of the /// screen. + #[default] Contain, /// Scale the image to cover all of the bounding box, cropping if needed. @@ -117,3 +120,15 @@ impl ContentFit { } } } + +impl fmt::Display for ContentFit { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + ContentFit::Contain => "Contain", + ContentFit::Cover => "Cover", + ContentFit::Fill => "Fill", + ContentFit::None => "None", + ContentFit::ScaleDown => "Scale Down", + }) + } +} diff --git a/core/src/image.rs b/core/src/image.rs index c38239bc..91a7fd36 100644 --- a/core/src/image.rs +++ b/core/src/image.rs @@ -1,7 +1,7 @@ //! Load and draw raster graphics. pub use bytes::Bytes; -use crate::{Rectangle, Size}; +use crate::{Radians, Rectangle, Size}; use rustc_hash::FxHasher; use std::hash::{Hash, Hasher}; @@ -173,5 +173,6 @@ pub trait Renderer: crate::Renderer { handle: Self::Handle, filter_method: FilterMethod, bounds: Rectangle, + rotation: Radians, ); } diff --git a/core/src/lib.rs b/core/src/lib.rs index feda4fb4..32156441 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -39,6 +39,7 @@ mod padding; mod pixels; mod point; mod rectangle; +mod rotation; mod shadow; mod shell; mod size; @@ -64,6 +65,7 @@ pub use pixels::Pixels; pub use point::Point; pub use rectangle::Rectangle; pub use renderer::Renderer; +pub use rotation::Rotation; pub use shadow::Shadow; pub use shell::Shell; pub use size::Size; diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 2ab50137..1556e072 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -1,6 +1,6 @@ -use crate::{Point, Size, Vector}; +use crate::{Point, Radians, Size, Vector}; -/// A rectangle. +/// An axis-aligned rectangle. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub struct Rectangle<T = f32> { /// X coordinate of the top-left corner. @@ -172,6 +172,18 @@ impl Rectangle<f32> { height: self.height + amount * 2.0, } } + + /// Rotates the [`Rectangle`] and returns the smallest [`Rectangle`] + /// containing it. + pub fn rotate(self, rotation: Radians) -> Self { + let size = self.size().rotate(rotation); + let position = Point::new( + self.center_x() - size.width / 2.0, + self.center_y() - size.height / 2.0, + ); + + Self::new(position, size) + } } impl std::ops::Mul<f32> for Rectangle<f32> { @@ -227,3 +239,19 @@ where } } } + +impl<T> std::ops::Mul<Vector<T>> for Rectangle<T> +where + T: std::ops::Mul<Output = T> + Copy, +{ + type Output = Rectangle<T>; + + fn mul(self, scale: Vector<T>) -> Self { + Rectangle { + x: self.x * scale.x, + y: self.y * scale.y, + width: self.width * scale.x, + height: self.height * scale.y, + } + } +} diff --git a/core/src/renderer/null.rs b/core/src/renderer/null.rs index fe38ec87..91519b40 100644 --- a/core/src/renderer/null.rs +++ b/core/src/renderer/null.rs @@ -4,7 +4,8 @@ use crate::renderer::{self, Renderer}; use crate::svg; use crate::text::{self, Text}; use crate::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, + Transformation, }; impl Renderer for () { @@ -171,6 +172,7 @@ impl image::Renderer for () { _handle: Self::Handle, _filter_method: image::FilterMethod, _bounds: Rectangle, + _rotation: Radians, ) { } } @@ -185,6 +187,7 @@ impl svg::Renderer for () { _handle: svg::Handle, _color: Option<Color>, _bounds: Rectangle, + _rotation: Radians, ) { } } diff --git a/core/src/rotation.rs b/core/src/rotation.rs new file mode 100644 index 00000000..afa8d79e --- /dev/null +++ b/core/src/rotation.rs @@ -0,0 +1,72 @@ +//! Control the rotation of some content (like an image) within a space. +use crate::{Degrees, Radians, Size}; + +/// The strategy used to rotate the content. +/// +/// This is used to control the behavior of the layout when the content is rotated +/// by a certain angle. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum Rotation { + /// The element will float while rotating. The layout will be kept exactly as it was + /// before the rotation. + /// + /// This is especially useful when used for animations, as it will avoid the + /// layout being shifted or resized when smoothly i.e. an icon. + /// + /// This is the default. + Floating(Radians), + /// The element will be solid while rotating. The layout will be adjusted to fit + /// the rotated content. + /// + /// This allows you to rotate an image and have the layout adjust to fit the new + /// size of the image. + Solid(Radians), +} + +impl Rotation { + /// Returns the angle of the [`Rotation`] in [`Radians`]. + pub fn radians(self) -> Radians { + match self { + Rotation::Floating(radians) | Rotation::Solid(radians) => radians, + } + } + + /// Returns a mutable reference to the angle of the [`Rotation`] in [`Radians`]. + pub fn radians_mut(&mut self) -> &mut Radians { + match self { + Rotation::Floating(radians) | Rotation::Solid(radians) => radians, + } + } + + /// Returns the angle of the [`Rotation`] in [`Degrees`]. + pub fn degrees(self) -> Degrees { + Degrees(self.radians().0.to_degrees()) + } + + /// Applies the [`Rotation`] to the given [`Size`], returning + /// the minimum [`Size`] containing the rotated one. + pub fn apply(self, size: Size) -> Size { + match self { + Self::Floating(_) => size, + Self::Solid(rotation) => size.rotate(rotation), + } + } +} + +impl Default for Rotation { + fn default() -> Self { + Self::Floating(Radians(0.0)) + } +} + +impl From<Radians> for Rotation { + fn from(radians: Radians) -> Self { + Self::Floating(radians) + } +} + +impl From<f32> for Rotation { + fn from(radians: f32) -> Self { + Self::Floating(Radians(radians)) + } +} diff --git a/core/src/size.rs b/core/src/size.rs index c2b5671a..d7459355 100644 --- a/core/src/size.rs +++ b/core/src/size.rs @@ -1,4 +1,4 @@ -use crate::Vector; +use crate::{Radians, Vector}; /// An amount of space in 2 dimensions. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)] @@ -51,6 +51,19 @@ impl Size { height: self.height + other.height, } } + + /// Rotates the given [`Size`] and returns the minimum [`Size`] + /// containing it. + pub fn rotate(self, rotation: Radians) -> Size { + let radians = f32::from(rotation); + + Size { + width: (self.width * radians.cos()).abs() + + (self.height * radians.sin()).abs(), + height: (self.width * radians.sin()).abs() + + (self.height * radians.cos()).abs(), + } + } } impl<T> From<[T; 2]> for Size<T> { @@ -113,3 +126,17 @@ where } } } + +impl<T> std::ops::Mul<Vector<T>> for Size<T> +where + T: std::ops::Mul<Output = T> + Copy, +{ + type Output = Size<T>; + + fn mul(self, scale: Vector<T>) -> Self::Output { + Size { + width: self.width * scale.x, + height: self.height * scale.y, + } + } +} diff --git a/core/src/svg.rs b/core/src/svg.rs index 0106e0c2..01f102e3 100644 --- a/core/src/svg.rs +++ b/core/src/svg.rs @@ -1,5 +1,5 @@ //! Load and draw vector graphics. -use crate::{Color, Rectangle, Size}; +use crate::{Color, Radians, Rectangle, Size}; use rustc_hash::FxHasher; use std::borrow::Cow; @@ -100,5 +100,6 @@ pub trait Renderer: crate::Renderer { handle: Handle, color: Option<Color>, bounds: Rectangle, + rotation: Radians, ); } diff --git a/core/src/vector.rs b/core/src/vector.rs index 1380c3b3..049e648f 100644 --- a/core/src/vector.rs +++ b/core/src/vector.rs @@ -18,6 +18,9 @@ impl<T> Vector<T> { impl Vector { /// The zero [`Vector`]. pub const ZERO: Self = Self::new(0.0, 0.0); + + /// The unit [`Vector`]. + pub const UNIT: Self = Self::new(0.0, 0.0); } impl<T> std::ops::Add for Vector<T> diff --git a/examples/ferris/Cargo.toml b/examples/ferris/Cargo.toml new file mode 100644 index 00000000..e98341d2 --- /dev/null +++ b/examples/ferris/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "ferris" +version = "0.1.0" +authors = ["Héctor Ramón Jiménez <hector0193@gmail.com>"] +edition = "2021" +publish = false + +[dependencies] +iced.workspace = true +iced.features = ["image", "tokio", "debug"] diff --git a/examples/ferris/src/main.rs b/examples/ferris/src/main.rs new file mode 100644 index 00000000..b7536740 --- /dev/null +++ b/examples/ferris/src/main.rs @@ -0,0 +1,201 @@ +use iced::time::Instant; +use iced::widget::{ + checkbox, column, container, image, pick_list, row, slider, text, +}; +use iced::window; +use iced::{ + Alignment, Color, ContentFit, Degrees, Element, Length, Radians, Rotation, + Subscription, Theme, +}; + +pub fn main() -> iced::Result { + iced::program("Ferris - Iced", Image::update, Image::view) + .subscription(Image::subscription) + .theme(|_| Theme::TokyoNight) + .run() +} + +struct Image { + width: f32, + rotation: Rotation, + content_fit: ContentFit, + spin: bool, + last_tick: Instant, +} + +#[derive(Debug, Clone, Copy)] +enum Message { + WidthChanged(f32), + RotationStrategyChanged(RotationStrategy), + RotationChanged(Degrees), + ContentFitChanged(ContentFit), + SpinToggled(bool), + RedrawRequested(Instant), +} + +impl Image { + fn update(&mut self, message: Message) { + match message { + Message::WidthChanged(width) => { + self.width = width; + } + Message::RotationStrategyChanged(strategy) => { + self.rotation = match strategy { + RotationStrategy::Floating => { + Rotation::Floating(self.rotation.radians()) + } + RotationStrategy::Solid => { + Rotation::Solid(self.rotation.radians()) + } + }; + } + Message::RotationChanged(rotation) => { + self.rotation = match self.rotation { + Rotation::Floating(_) => { + Rotation::Floating(rotation.into()) + } + Rotation::Solid(_) => Rotation::Solid(rotation.into()), + }; + } + Message::ContentFitChanged(content_fit) => { + self.content_fit = content_fit; + } + Message::SpinToggled(spin) => { + self.spin = spin; + self.last_tick = Instant::now(); + } + Message::RedrawRequested(now) => { + const ROTATION_SPEED: Degrees = Degrees(360.0); + + let delta = (now - self.last_tick).as_millis() as f32 / 1_000.0; + + *self.rotation.radians_mut() = (self.rotation.radians() + + ROTATION_SPEED * delta) + % (2.0 * Radians::PI); + + self.last_tick = now; + } + } + } + + fn subscription(&self) -> Subscription<Message> { + if self.spin { + window::frames().map(Message::RedrawRequested) + } else { + Subscription::none() + } + } + + fn view(&self) -> Element<Message> { + let i_am_ferris = container( + column![ + "Hello!", + Element::from( + image(format!( + "{}/../tour/images/ferris.png", + env!("CARGO_MANIFEST_DIR") + )) + .width(self.width) + .content_fit(self.content_fit) + .rotation(self.rotation) + ) + .explain(Color::WHITE), + "I am Ferris!" + ] + .spacing(20) + .align_items(Alignment::Center), + ) + .width(Length::Fill) + .height(Length::Fill) + .center_x() + .center_y(); + + let sizing = row![ + pick_list( + [ + ContentFit::Contain, + ContentFit::Cover, + ContentFit::Fill, + ContentFit::None, + ContentFit::ScaleDown + ], + Some(self.content_fit), + Message::ContentFitChanged + ) + .width(Length::Fill), + column![ + slider(100.0..=500.0, self.width, Message::WidthChanged), + text(format!("Width: {}px", self.width)) + .size(14) + .line_height(1.0) + ] + .spacing(5) + .align_items(Alignment::Center) + ] + .spacing(10); + + let rotation = row![ + pick_list( + [RotationStrategy::Floating, RotationStrategy::Solid], + Some(match self.rotation { + Rotation::Floating(_) => RotationStrategy::Floating, + Rotation::Solid(_) => RotationStrategy::Solid, + }), + Message::RotationStrategyChanged, + ) + .width(Length::Fill), + row![ + column![ + slider( + Degrees::RANGE, + self.rotation.degrees(), + Message::RotationChanged + ), + text(format!( + "Rotation: {:.0}°", + f32::from(self.rotation.degrees()) + )) + .size(14) + .line_height(1.0) + ] + .spacing(5) + .align_items(Alignment::Center), + checkbox("Spin!", self.spin).on_toggle(Message::SpinToggled) + ] + .spacing(5) + .align_items(Alignment::Center) + ] + .spacing(10); + + container(column![i_am_ferris, sizing, rotation].spacing(10)) + .padding(10) + .into() + } +} + +impl Default for Image { + fn default() -> Self { + Self { + width: 300.0, + rotation: Rotation::default(), + content_fit: ContentFit::default(), + spin: false, + last_tick: Instant::now(), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum RotationStrategy { + Floating, + Solid, +} + +impl std::fmt::Display for RotationStrategy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(match self { + Self::Floating => "Floating", + Self::Solid => "Solid", + }) + } +} diff --git a/graphics/src/image.rs b/graphics/src/image.rs index 04c45057..9d09bf4c 100644 --- a/graphics/src/image.rs +++ b/graphics/src/image.rs @@ -2,9 +2,7 @@ #[cfg(feature = "image")] pub use ::image as image_rs; -use crate::core::image; -use crate::core::svg; -use crate::core::{Color, Rectangle}; +use crate::core::{image, svg, Color, Radians, Rectangle}; /// A raster or vector image. #[derive(Debug, Clone, PartialEq)] @@ -19,6 +17,9 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, + + /// The rotation of the image in radians + rotation: Radians, }, /// A vector image. Vector { @@ -30,6 +31,9 @@ pub enum Image { /// The bounds of the image. bounds: Rectangle, + + /// The rotation of the image in radians + rotation: Radians, }, } @@ -37,9 +41,12 @@ impl Image { /// Returns the bounds of the [`Image`]. pub fn bounds(&self) -> Rectangle { match self { - Image::Raster { bounds, .. } | Image::Vector { bounds, .. } => { - *bounds + Image::Raster { + bounds, rotation, .. } + | Image::Vector { + bounds, rotation, .. + } => bounds.rotate(*rotation), } } } diff --git a/renderer/src/fallback.rs b/renderer/src/fallback.rs index 5f69b420..a077031b 100644 --- a/renderer/src/fallback.rs +++ b/renderer/src/fallback.rs @@ -3,7 +3,7 @@ use crate::core::image; use crate::core::renderer; use crate::core::svg; use crate::core::{ - self, Background, Color, Point, Rectangle, Size, Transformation, + self, Background, Color, Point, Radians, Rectangle, Size, Transformation, }; use crate::graphics; use crate::graphics::compositor; @@ -154,11 +154,12 @@ where handle: Self::Handle, filter_method: image::FilterMethod, bounds: Rectangle, + rotation: Radians, ) { delegate!( self, renderer, - renderer.draw_image(handle, filter_method, bounds) + renderer.draw_image(handle, filter_method, bounds, rotation) ); } } @@ -177,8 +178,13 @@ where handle: svg::Handle, color: Option<Color>, bounds: Rectangle, + rotation: Radians, ) { - delegate!(self, renderer, renderer.draw_svg(handle, color, bounds)); + delegate!( + self, + renderer, + renderer.draw_svg(handle, color, bounds, rotation) + ); } } @@ -200,8 +200,8 @@ pub use crate::core::gradient; pub use crate::core::theme; pub use crate::core::{ Alignment, Background, Border, Color, ContentFit, Degrees, Gradient, - Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Theme, - Transformation, Vector, + Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size, + Theme, Transformation, Vector, }; pub mod clipboard { diff --git a/tiny_skia/src/engine.rs b/tiny_skia/src/engine.rs index fbca1274..e9935bdb 100644 --- a/tiny_skia/src/engine.rs +++ b/tiny_skia/src/engine.rs @@ -539,10 +539,10 @@ impl Engine { pub fn draw_image( &mut self, image: &Image, - _transformation: Transformation, - _pixels: &mut tiny_skia::PixmapMut<'_>, - _clip_mask: &mut tiny_skia::Mask, - _clip_bounds: Rectangle, + transformation: Transformation, + pixels: &mut tiny_skia::PixmapMut<'_>, + clip_mask: &mut tiny_skia::Mask, + clip_bounds: Rectangle, ) { match image { #[cfg(feature = "image")] @@ -550,22 +550,32 @@ impl Engine { handle, filter_method, bounds, + rotation, } => { - let physical_bounds = *bounds * _transformation; + let physical_bounds = *bounds * transformation; - if !_clip_bounds.intersects(&physical_bounds) { + if !clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) - .then_some(_clip_mask as &_); + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + let center = physical_bounds.center(); + let radians = f32::from(*rotation); + + let transform = into_transform(transformation).post_rotate_at( + radians.to_degrees(), + center.x, + center.y, + ); self.raster_pipeline.draw( handle, *filter_method, *bounds, - _pixels, - into_transform(_transformation), + pixels, + transform, clip_mask, ); } @@ -574,21 +584,32 @@ impl Engine { handle, color, bounds, + rotation, } => { - let physical_bounds = *bounds * _transformation; + let physical_bounds = *bounds * transformation; - if !_clip_bounds.intersects(&physical_bounds) { + if !clip_bounds.intersects(&physical_bounds) { return; } - let clip_mask = (!physical_bounds.is_within(&_clip_bounds)) - .then_some(_clip_mask as &_); + let clip_mask = (!physical_bounds.is_within(&clip_bounds)) + .then_some(clip_mask as &_); + + let center = physical_bounds.center(); + let radians = f32::from(*rotation); + + let transform = into_transform(transformation).post_rotate_at( + radians.to_degrees(), + center.x, + center.y, + ); self.vector_pipeline.draw( handle, *color, physical_bounds, - _pixels, + pixels, + transform, clip_mask, ); } diff --git a/tiny_skia/src/layer.rs b/tiny_skia/src/layer.rs index 3e42e4aa..c907c93c 100644 --- a/tiny_skia/src/layer.rs +++ b/tiny_skia/src/layer.rs @@ -1,7 +1,7 @@ -use crate::core::image; -use crate::core::renderer::Quad; -use crate::core::svg; -use crate::core::{Background, Color, Point, Rectangle, Transformation}; +use crate::core::{ + image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle, + Transformation, +}; use crate::graphics::damage; use crate::graphics::layer; use crate::graphics::text::{Editor, Paragraph, Text}; @@ -121,11 +121,13 @@ impl Layer { filter_method: image::FilterMethod, bounds: Rectangle, transformation: Transformation, + rotation: Radians, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, + rotation, }; self.images.push(image); @@ -137,11 +139,13 @@ impl Layer { color: Option<Color>, bounds: Rectangle, transformation: Transformation, + rotation: Radians, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, + rotation, }; self.images.push(svg); diff --git a/tiny_skia/src/lib.rs b/tiny_skia/src/lib.rs index 4c2c9430..75aaaf92 100644 --- a/tiny_skia/src/lib.rs +++ b/tiny_skia/src/lib.rs @@ -29,7 +29,7 @@ pub use geometry::Geometry; use crate::core::renderer; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Transformation, }; use crate::engine::Engine; use crate::graphics::compositor; @@ -377,9 +377,16 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image(handle, filter_method, bounds, transformation); + layer.draw_image( + handle, + filter_method, + bounds, + transformation, + rotation, + ); } } @@ -397,9 +404,10 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color: Option<Color>, bounds: Rectangle, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color, bounds, transformation); + layer.draw_svg(handle, color, bounds, transformation, rotation); } } diff --git a/tiny_skia/src/vector.rs b/tiny_skia/src/vector.rs index 5150cffe..8e3463f2 100644 --- a/tiny_skia/src/vector.rs +++ b/tiny_skia/src/vector.rs @@ -4,6 +4,7 @@ use crate::graphics::text; use resvg::usvg::{self, TreeTextToPath}; use rustc_hash::{FxHashMap, FxHashSet}; +use tiny_skia::Transform; use std::cell::RefCell; use std::collections::hash_map; @@ -34,6 +35,7 @@ impl Pipeline { color: Option<Color>, bounds: Rectangle, pixels: &mut tiny_skia::PixmapMut<'_>, + transform: Transform, clip_mask: Option<&tiny_skia::Mask>, ) { if let Some(image) = self.cache.borrow_mut().draw( @@ -46,7 +48,7 @@ impl Pipeline { bounds.y as i32, image, &tiny_skia::PixmapPaint::default(), - tiny_skia::Transform::identity(), + transform, clip_mask, ); } diff --git a/wgpu/src/image/mod.rs b/wgpu/src/image/mod.rs index 8b831a3c..285eb2f6 100644 --- a/wgpu/src/image/mod.rs +++ b/wgpu/src/image/mod.rs @@ -135,14 +135,18 @@ impl Pipeline { attributes: &wgpu::vertex_attr_array!( // Position 0 => Float32x2, - // Scale + // Center 1 => Float32x2, - // Atlas position + // Image size 2 => Float32x2, + // Rotation + 3 => Float32, + // Atlas position + 4 => Float32x2, // Atlas scale - 3 => Float32x2, + 5 => Float32x2, // Layer - 4 => Sint32, + 6 => Sint32, ), }], }, @@ -224,6 +228,7 @@ impl Pipeline { handle, filter_method, bounds, + rotation, } => { if let Some(atlas_entry) = cache.upload_raster(device, encoder, handle) @@ -231,6 +236,7 @@ impl Pipeline { add_instances( [bounds.x, bounds.y], [bounds.width, bounds.height], + f32::from(*rotation), atlas_entry, match filter_method { crate::core::image::FilterMethod::Nearest => { @@ -251,6 +257,7 @@ impl Pipeline { handle, color, bounds, + rotation, } => { let size = [bounds.width, bounds.height]; @@ -260,6 +267,7 @@ impl Pipeline { add_instances( [bounds.x, bounds.y], size, + f32::from(*rotation), atlas_entry, nearest_instances, ); @@ -487,7 +495,9 @@ impl Data { #[derive(Debug, Clone, Copy, Zeroable, Pod)] struct Instance { _position: [f32; 2], + _center: [f32; 2], _size: [f32; 2], + _rotation: f32, _position_in_atlas: [f32; 2], _size_in_atlas: [f32; 2], _layer: u32, @@ -506,12 +516,25 @@ struct Uniforms { fn add_instances( image_position: [f32; 2], image_size: [f32; 2], + rotation: f32, entry: &atlas::Entry, instances: &mut Vec<Instance>, ) { + let center = [ + image_position[0] + image_size[0] / 2.0, + image_position[1] + image_size[1] / 2.0, + ]; + match entry { atlas::Entry::Contiguous(allocation) => { - add_instance(image_position, image_size, allocation, instances); + add_instance( + image_position, + center, + image_size, + rotation, + allocation, + instances, + ); } atlas::Entry::Fragmented { fragments, size } => { let scaling_x = image_size[0] / size.width as f32; @@ -537,7 +560,9 @@ fn add_instances( fragment_height as f32 * scaling_y, ]; - add_instance(position, size, allocation, instances); + add_instance( + position, center, size, rotation, allocation, instances, + ); } } } @@ -546,7 +571,9 @@ fn add_instances( #[inline] fn add_instance( position: [f32; 2], + center: [f32; 2], size: [f32; 2], + rotation: f32, allocation: &atlas::Allocation, instances: &mut Vec<Instance>, ) { @@ -556,7 +583,9 @@ fn add_instance( let instance = Instance { _position: position, + _center: center, _size: size, + _rotation: rotation, _position_in_atlas: [ (x as f32 + 0.5) / atlas::SIZE as f32, (y as f32 + 0.5) / atlas::SIZE as f32, diff --git a/wgpu/src/layer.rs b/wgpu/src/layer.rs index 9526c5a8..e0242c59 100644 --- a/wgpu/src/layer.rs +++ b/wgpu/src/layer.rs @@ -1,5 +1,6 @@ -use crate::core::renderer; -use crate::core::{Background, Color, Point, Rectangle, Transformation}; +use crate::core::{ + renderer, Background, Color, Point, Radians, Rectangle, Transformation, +}; use crate::graphics; use crate::graphics::color; use crate::graphics::layer; @@ -117,11 +118,13 @@ impl Layer { filter_method: crate::core::image::FilterMethod, bounds: Rectangle, transformation: Transformation, + rotation: Radians, ) { let image = Image::Raster { handle, filter_method, bounds: bounds * transformation, + rotation, }; self.images.push(image); @@ -133,11 +136,13 @@ impl Layer { color: Option<Color>, bounds: Rectangle, transformation: Transformation, + rotation: Radians, ) { let svg = Image::Vector { handle, color, bounds: bounds * transformation, + rotation, }; self.images.push(svg); diff --git a/wgpu/src/lib.rs b/wgpu/src/lib.rs index 178522de..6920067b 100644 --- a/wgpu/src/lib.rs +++ b/wgpu/src/lib.rs @@ -61,7 +61,8 @@ pub use settings::Settings; pub use geometry::Geometry; use crate::core::{ - Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation, + Background, Color, Font, Pixels, Point, Radians, Rectangle, Size, + Transformation, Vector, }; use crate::graphics::text::{Editor, Paragraph}; use crate::graphics::Viewport; @@ -378,7 +379,6 @@ impl Renderer { use crate::core::alignment; use crate::core::text::Renderer as _; use crate::core::Renderer as _; - use crate::core::Vector; self.with_layer( Rectangle::with_size(viewport.logical_size()), @@ -517,9 +517,16 @@ impl core::image::Renderer for Renderer { handle: Self::Handle, filter_method: core::image::FilterMethod, bounds: Rectangle, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_image(handle, filter_method, bounds, transformation); + layer.draw_image( + handle, + filter_method, + bounds, + transformation, + rotation, + ); } } @@ -534,9 +541,10 @@ impl core::svg::Renderer for Renderer { handle: core::svg::Handle, color_filter: Option<Color>, bounds: Rectangle, + rotation: Radians, ) { let (layer, transformation) = self.layers.current_mut(); - layer.draw_svg(handle, color_filter, bounds, transformation); + layer.draw_svg(handle, color_filter, bounds, transformation, rotation); } } diff --git a/wgpu/src/shader/image.wgsl b/wgpu/src/shader/image.wgsl index 7b2e5238..71bf939c 100644 --- a/wgpu/src/shader/image.wgsl +++ b/wgpu/src/shader/image.wgsl @@ -9,10 +9,12 @@ struct Globals { struct VertexInput { @builtin(vertex_index) vertex_index: u32, @location(0) pos: vec2<f32>, - @location(1) scale: vec2<f32>, - @location(2) atlas_pos: vec2<f32>, - @location(3) atlas_scale: vec2<f32>, - @location(4) layer: i32, + @location(1) center: vec2<f32>, + @location(2) scale: vec2<f32>, + @location(3) rotation: f32, + @location(4) atlas_pos: vec2<f32>, + @location(5) atlas_scale: vec2<f32>, + @location(6) layer: i32, } struct VertexOutput { @@ -25,24 +27,34 @@ struct VertexOutput { fn vs_main(input: VertexInput) -> VertexOutput { var out: VertexOutput; - let v_pos = vertex_position(input.vertex_index); + // Generate a vertex position in the range [0, 1] from the vertex index. + var v_pos = vertex_position(input.vertex_index); + // Map the vertex position to the atlas texture. out.uv = vec2<f32>(v_pos * input.atlas_scale + input.atlas_pos); out.layer = f32(input.layer); - var transform: mat4x4<f32> = mat4x4<f32>( - vec4<f32>(input.scale.x, 0.0, 0.0, 0.0), - vec4<f32>(0.0, input.scale.y, 0.0, 0.0), + // Calculate the vertex position and move the center to the origin + v_pos = input.pos + v_pos * input.scale - input.center; + + // Apply the rotation around the center of the image + let cos_rot = cos(input.rotation); + let sin_rot = sin(input.rotation); + let rotate = mat4x4<f32>( + vec4<f32>(cos_rot, sin_rot, 0.0, 0.0), + vec4<f32>(-sin_rot, cos_rot, 0.0, 0.0), vec4<f32>(0.0, 0.0, 1.0, 0.0), - vec4<f32>(input.pos, 0.0, 1.0) + vec4<f32>(0.0, 0.0, 0.0, 1.0) ); - out.position = globals.transform * transform * vec4<f32>(v_pos, 0.0, 1.0); + // Calculate the final position of the vertex + out.position = globals.transform * (vec4<f32>(input.center, 0.0, 0.0) + rotate * vec4<f32>(v_pos, 0.0, 1.0)); return out; } @fragment fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> { + // Sample the texture at the given UV coordinate and layer. return textureSample(u_texture, u_sampler, input.uv, i32(input.layer)); } diff --git a/widget/src/image.rs b/widget/src/image.rs index 21d371b7..45209a91 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -8,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, Vector, Widget, + ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size, + Vector, Widget, }; pub use image::{FilterMethod, Handle}; @@ -36,6 +37,7 @@ pub struct Image<Handle> { height: Length, content_fit: ContentFit, filter_method: FilterMethod, + rotation: Rotation, } impl<Handle> Image<Handle> { @@ -45,8 +47,9 @@ impl<Handle> Image<Handle> { handle: handle.into(), width: Length::Shrink, height: Length::Shrink, - content_fit: ContentFit::Contain, + content_fit: ContentFit::default(), filter_method: FilterMethod::default(), + rotation: Rotation::default(), } } @@ -75,6 +78,12 @@ impl<Handle> Image<Handle> { self.filter_method = filter_method; self } + + /// Applies the given [`Rotation`] to the [`Image`]. + pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self { + self.rotation = rotation.into(); + self + } } /// Computes the layout of an [`Image`]. @@ -85,22 +94,24 @@ pub fn layout<Renderer, Handle>( width: Length, height: Length, content_fit: ContentFit, + rotation: Rotation, ) -> layout::Node where Renderer: image::Renderer<Handle = Handle>, { // 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.apply(image_size); // 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 +135,44 @@ pub fn draw<Renderer, Handle>( handle: &Handle, content_fit: ContentFit, filter_method: FilterMethod, + rotation: Rotation, ) where Renderer: image::Renderer<Handle = Handle>, 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.apply(image_size); let bounds = layout.bounds(); - let adjusted_fit = content_fit.fit(image_size, bounds.size()); - - 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 adjusted_fit = content_fit.fit(rotated_size, bounds.size()); + + 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 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 { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; + let drawing_bounds = Rectangle::new(position, final_size); + let render = |renderer: &mut Renderer| { renderer.draw_image( handle.clone(), filter_method, - drawing_bounds + offset, + drawing_bounds, + rotation.radians(), ); }; @@ -187,6 +210,7 @@ where self.width, self.height, self.content_fit, + self.rotation, ) } @@ -206,6 +230,7 @@ where &self.handle, self.content_fit, self.filter_method, + self.rotation, ); } } diff --git a/widget/src/image/viewer.rs b/widget/src/image/viewer.rs index 2e6a7528..5eb76452 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,6 +341,7 @@ where y: bounds.y, ..Rectangle::with_size(image_size) }, + Radians(0.0), ); }); }); diff --git a/widget/src/svg.rs b/widget/src/svg.rs index eb142189..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, Rectangle, Size, Theme, Vector, - Widget, + Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, + Size, Theme, Vector, Widget, }; use std::path::PathBuf; @@ -29,6 +29,7 @@ where height: Length, content_fit: ContentFit, class: Theme::Class<'a>, + rotation: Rotation, } impl<'a, Theme> Svg<'a, Theme> @@ -43,6 +44,7 @@ where height: Length::Shrink, content_fit: ContentFit::Contain, class: Theme::default(), + rotation: Rotation::default(), } } @@ -95,6 +97,12 @@ where self.class = class.into(); self } + + /// Applies the given [`Rotation`] to the [`Svg`]. + pub fn rotation(mut self, rotation: impl Into<Rotation>) -> Self { + self.rotation = rotation.into(); + self + } } impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer> @@ -120,11 +128,14 @@ 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.apply(image_size); + // 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,35 +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.apply(image_size); let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); - let is_mouse_over = cursor.is_over(bounds); + let adjusted_fit = self.content_fit.fit(rotated_size, bounds.size()); + 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 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::new(position, final_size); - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; + let is_mouse_over = cursor.is_over(bounds); - let status = if is_mouse_over { - Status::Hovered - } else { - Status::Idle - }; + let status = if is_mouse_over { + Status::Hovered + } else { + Status::Idle + }; - let style = theme.style(&self.class, status); + let style = theme.style(&self.class, status); + let render = |renderer: &mut Renderer| { renderer.draw_svg( self.handle.clone(), style.color, - drawing_bounds + offset, + drawing_bounds, + self.rotation.radians(), ); }; |