Merge pull request #2334 from DKolter/image-rotation
Adding feature: Image rotation
This commit is contained in:
commit
1cefe6be21
26 changed files with 697 additions and 111 deletions
|
|
@ -1,12 +1,17 @@
|
||||||
use crate::{Point, Rectangle, Vector};
|
use crate::{Point, Rectangle, Vector};
|
||||||
|
|
||||||
use std::f32::consts::{FRAC_PI_2, PI};
|
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
|
/// Degrees
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
|
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
|
||||||
pub struct Degrees(pub f32);
|
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 {
|
impl PartialEq<f32> for Degrees {
|
||||||
fn eq(&self, other: &f32) -> bool {
|
fn eq(&self, other: &f32) -> bool {
|
||||||
self.0.eq(other)
|
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
|
/// Radians
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
|
#[derive(Debug, Copy, Clone, PartialEq, PartialOrd)]
|
||||||
pub struct Radians(pub f32);
|
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 {
|
impl From<Radians> for f64 {
|
||||||
fn from(radians: Radians) -> Self {
|
fn from(radians: Radians) -> Self {
|
||||||
Self::from(radians.0)
|
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 {
|
impl AddAssign for Radians {
|
||||||
fn add_assign(&mut self, rhs: Radians) {
|
fn add_assign(&mut self, rhs: Radians) {
|
||||||
self.0 = self.0 + rhs.0;
|
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 {
|
impl PartialEq<f32> for Radians {
|
||||||
fn eq(&self, other: &f32) -> bool {
|
fn eq(&self, other: &f32) -> bool {
|
||||||
self.0.eq(other)
|
self.0.eq(other)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
//! Control the fit of some content (like an image) within a space.
|
//! Control the fit of some content (like an image) within a space.
|
||||||
use crate::Size;
|
use crate::Size;
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
/// The strategy used to fit the contents of a widget to its bounding box.
|
/// 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
|
/// 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
|
/// in CSS, see [Mozilla's docs][1], or run the `tour` example
|
||||||
///
|
///
|
||||||
/// [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/object-fit
|
/// [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 {
|
pub enum ContentFit {
|
||||||
/// Scale as big as it can be without needing to crop or hide parts.
|
/// 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
|
/// 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
|
/// any part of it, particularly when the image itself is the focus of the
|
||||||
/// screen.
|
/// screen.
|
||||||
|
#[default]
|
||||||
Contain,
|
Contain,
|
||||||
|
|
||||||
/// Scale the image to cover all of the bounding box, cropping if needed.
|
/// 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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Load and draw raster graphics.
|
//! Load and draw raster graphics.
|
||||||
pub use bytes::Bytes;
|
pub use bytes::Bytes;
|
||||||
|
|
||||||
use crate::{Rectangle, Size};
|
use crate::{Radians, Rectangle, Size};
|
||||||
|
|
||||||
use rustc_hash::FxHasher;
|
use rustc_hash::FxHasher;
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
|
|
@ -173,5 +173,6 @@ pub trait Renderer: crate::Renderer {
|
||||||
handle: Self::Handle,
|
handle: Self::Handle,
|
||||||
filter_method: FilterMethod,
|
filter_method: FilterMethod,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
rotation: Radians,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ mod padding;
|
||||||
mod pixels;
|
mod pixels;
|
||||||
mod point;
|
mod point;
|
||||||
mod rectangle;
|
mod rectangle;
|
||||||
|
mod rotation;
|
||||||
mod shadow;
|
mod shadow;
|
||||||
mod shell;
|
mod shell;
|
||||||
mod size;
|
mod size;
|
||||||
|
|
@ -64,6 +65,7 @@ pub use pixels::Pixels;
|
||||||
pub use point::Point;
|
pub use point::Point;
|
||||||
pub use rectangle::Rectangle;
|
pub use rectangle::Rectangle;
|
||||||
pub use renderer::Renderer;
|
pub use renderer::Renderer;
|
||||||
|
pub use rotation::Rotation;
|
||||||
pub use shadow::Shadow;
|
pub use shadow::Shadow;
|
||||||
pub use shell::Shell;
|
pub use shell::Shell;
|
||||||
pub use size::Size;
|
pub use size::Size;
|
||||||
|
|
|
||||||
|
|
@ -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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
||||||
pub struct Rectangle<T = f32> {
|
pub struct Rectangle<T = f32> {
|
||||||
/// X coordinate of the top-left corner.
|
/// X coordinate of the top-left corner.
|
||||||
|
|
@ -172,6 +172,18 @@ impl Rectangle<f32> {
|
||||||
height: self.height + amount * 2.0,
|
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> {
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ use crate::renderer::{self, Renderer};
|
||||||
use crate::svg;
|
use crate::svg;
|
||||||
use crate::text::{self, Text};
|
use crate::text::{self, Text};
|
||||||
use crate::{
|
use crate::{
|
||||||
Background, Color, Font, Pixels, Point, Rectangle, Size, Transformation,
|
Background, Color, Font, Pixels, Point, Radians, Rectangle, Size,
|
||||||
|
Transformation,
|
||||||
};
|
};
|
||||||
|
|
||||||
impl Renderer for () {
|
impl Renderer for () {
|
||||||
|
|
@ -171,6 +172,7 @@ impl image::Renderer for () {
|
||||||
_handle: Self::Handle,
|
_handle: Self::Handle,
|
||||||
_filter_method: image::FilterMethod,
|
_filter_method: image::FilterMethod,
|
||||||
_bounds: Rectangle,
|
_bounds: Rectangle,
|
||||||
|
_rotation: Radians,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -185,6 +187,7 @@ impl svg::Renderer for () {
|
||||||
_handle: svg::Handle,
|
_handle: svg::Handle,
|
||||||
_color: Option<Color>,
|
_color: Option<Color>,
|
||||||
_bounds: Rectangle,
|
_bounds: Rectangle,
|
||||||
|
_rotation: Radians,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
72
core/src/rotation.rs
Normal file
72
core/src/rotation.rs
Normal file
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::Vector;
|
use crate::{Radians, Vector};
|
||||||
|
|
||||||
/// An amount of space in 2 dimensions.
|
/// An amount of space in 2 dimensions.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
|
||||||
|
|
@ -51,6 +51,19 @@ impl Size {
|
||||||
height: self.height + other.height,
|
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> {
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
//! Load and draw vector graphics.
|
//! Load and draw vector graphics.
|
||||||
use crate::{Color, Rectangle, Size};
|
use crate::{Color, Radians, Rectangle, Size};
|
||||||
|
|
||||||
use rustc_hash::FxHasher;
|
use rustc_hash::FxHasher;
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
@ -100,5 +100,6 @@ pub trait Renderer: crate::Renderer {
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
rotation: Radians,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,9 @@ impl<T> Vector<T> {
|
||||||
impl Vector {
|
impl Vector {
|
||||||
/// The zero [`Vector`].
|
/// The zero [`Vector`].
|
||||||
pub const ZERO: Self = Self::new(0.0, 0.0);
|
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>
|
impl<T> std::ops::Add for Vector<T>
|
||||||
|
|
|
||||||
10
examples/ferris/Cargo.toml
Normal file
10
examples/ferris/Cargo.toml
Normal file
|
|
@ -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"]
|
||||||
201
examples/ferris/src/main.rs
Normal file
201
examples/ferris/src/main.rs
Normal file
|
|
@ -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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,9 +2,7 @@
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
pub use ::image as image_rs;
|
pub use ::image as image_rs;
|
||||||
|
|
||||||
use crate::core::image;
|
use crate::core::{image, svg, Color, Radians, Rectangle};
|
||||||
use crate::core::svg;
|
|
||||||
use crate::core::{Color, Rectangle};
|
|
||||||
|
|
||||||
/// A raster or vector image.
|
/// A raster or vector image.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
|
@ -19,6 +17,9 @@ pub enum Image {
|
||||||
|
|
||||||
/// The bounds of the image.
|
/// The bounds of the image.
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
|
||||||
|
/// The rotation of the image in radians
|
||||||
|
rotation: Radians,
|
||||||
},
|
},
|
||||||
/// A vector image.
|
/// A vector image.
|
||||||
Vector {
|
Vector {
|
||||||
|
|
@ -30,6 +31,9 @@ pub enum Image {
|
||||||
|
|
||||||
/// The bounds of the image.
|
/// The bounds of the image.
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
|
||||||
|
/// The rotation of the image in radians
|
||||||
|
rotation: Radians,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,9 +41,12 @@ impl Image {
|
||||||
/// Returns the bounds of the [`Image`].
|
/// Returns the bounds of the [`Image`].
|
||||||
pub fn bounds(&self) -> Rectangle {
|
pub fn bounds(&self) -> Rectangle {
|
||||||
match self {
|
match self {
|
||||||
Image::Raster { bounds, .. } | Image::Vector { bounds, .. } => {
|
Image::Raster {
|
||||||
*bounds
|
bounds, rotation, ..
|
||||||
}
|
}
|
||||||
|
| Image::Vector {
|
||||||
|
bounds, rotation, ..
|
||||||
|
} => bounds.rotate(*rotation),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use crate::core::image;
|
||||||
use crate::core::renderer;
|
use crate::core::renderer;
|
||||||
use crate::core::svg;
|
use crate::core::svg;
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
self, Background, Color, Point, Rectangle, Size, Transformation,
|
self, Background, Color, Point, Radians, Rectangle, Size, Transformation,
|
||||||
};
|
};
|
||||||
use crate::graphics;
|
use crate::graphics;
|
||||||
use crate::graphics::compositor;
|
use crate::graphics::compositor;
|
||||||
|
|
@ -154,11 +154,12 @@ where
|
||||||
handle: Self::Handle,
|
handle: Self::Handle,
|
||||||
filter_method: image::FilterMethod,
|
filter_method: image::FilterMethod,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
rotation: Radians,
|
||||||
) {
|
) {
|
||||||
delegate!(
|
delegate!(
|
||||||
self,
|
self,
|
||||||
renderer,
|
renderer,
|
||||||
renderer.draw_image(handle, filter_method, bounds)
|
renderer.draw_image(handle, filter_method, bounds, rotation)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -177,8 +178,13 @@ where
|
||||||
handle: svg::Handle,
|
handle: svg::Handle,
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
bounds: Rectangle,
|
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::theme;
|
||||||
pub use crate::core::{
|
pub use crate::core::{
|
||||||
Alignment, Background, Border, Color, ContentFit, Degrees, Gradient,
|
Alignment, Background, Border, Color, ContentFit, Degrees, Gradient,
|
||||||
Length, Padding, Pixels, Point, Radians, Rectangle, Shadow, Size, Theme,
|
Length, Padding, Pixels, Point, Radians, Rectangle, Rotation, Shadow, Size,
|
||||||
Transformation, Vector,
|
Theme, Transformation, Vector,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub mod clipboard {
|
pub mod clipboard {
|
||||||
|
|
|
||||||
|
|
@ -539,10 +539,10 @@ impl Engine {
|
||||||
pub fn draw_image(
|
pub fn draw_image(
|
||||||
&mut self,
|
&mut self,
|
||||||
image: &Image,
|
image: &Image,
|
||||||
_transformation: Transformation,
|
transformation: Transformation,
|
||||||
_pixels: &mut tiny_skia::PixmapMut<'_>,
|
pixels: &mut tiny_skia::PixmapMut<'_>,
|
||||||
_clip_mask: &mut tiny_skia::Mask,
|
clip_mask: &mut tiny_skia::Mask,
|
||||||
_clip_bounds: Rectangle,
|
clip_bounds: Rectangle,
|
||||||
) {
|
) {
|
||||||
match image {
|
match image {
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
|
|
@ -550,22 +550,32 @@ impl Engine {
|
||||||
handle,
|
handle,
|
||||||
filter_method,
|
filter_method,
|
||||||
bounds,
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
|
let clip_mask = (!physical_bounds.is_within(&clip_bounds))
|
||||||
.then_some(_clip_mask as &_);
|
.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(
|
self.raster_pipeline.draw(
|
||||||
handle,
|
handle,
|
||||||
*filter_method,
|
*filter_method,
|
||||||
*bounds,
|
*bounds,
|
||||||
_pixels,
|
pixels,
|
||||||
into_transform(_transformation),
|
transform,
|
||||||
clip_mask,
|
clip_mask,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -574,21 +584,32 @@ impl Engine {
|
||||||
handle,
|
handle,
|
||||||
color,
|
color,
|
||||||
bounds,
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let clip_mask = (!physical_bounds.is_within(&_clip_bounds))
|
let clip_mask = (!physical_bounds.is_within(&clip_bounds))
|
||||||
.then_some(_clip_mask as &_);
|
.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(
|
self.vector_pipeline.draw(
|
||||||
handle,
|
handle,
|
||||||
*color,
|
*color,
|
||||||
physical_bounds,
|
physical_bounds,
|
||||||
_pixels,
|
pixels,
|
||||||
|
transform,
|
||||||
clip_mask,
|
clip_mask,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::core::image;
|
use crate::core::{
|
||||||
use crate::core::renderer::Quad;
|
image, renderer::Quad, svg, Background, Color, Point, Radians, Rectangle,
|
||||||
use crate::core::svg;
|
Transformation,
|
||||||
use crate::core::{Background, Color, Point, Rectangle, Transformation};
|
};
|
||||||
use crate::graphics::damage;
|
use crate::graphics::damage;
|
||||||
use crate::graphics::layer;
|
use crate::graphics::layer;
|
||||||
use crate::graphics::text::{Editor, Paragraph, Text};
|
use crate::graphics::text::{Editor, Paragraph, Text};
|
||||||
|
|
@ -121,11 +121,13 @@ impl Layer {
|
||||||
filter_method: image::FilterMethod,
|
filter_method: image::FilterMethod,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
transformation: Transformation,
|
transformation: Transformation,
|
||||||
|
rotation: Radians,
|
||||||
) {
|
) {
|
||||||
let image = Image::Raster {
|
let image = Image::Raster {
|
||||||
handle,
|
handle,
|
||||||
filter_method,
|
filter_method,
|
||||||
bounds: bounds * transformation,
|
bounds: bounds * transformation,
|
||||||
|
rotation,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.images.push(image);
|
self.images.push(image);
|
||||||
|
|
@ -137,11 +139,13 @@ impl Layer {
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
transformation: Transformation,
|
transformation: Transformation,
|
||||||
|
rotation: Radians,
|
||||||
) {
|
) {
|
||||||
let svg = Image::Vector {
|
let svg = Image::Vector {
|
||||||
handle,
|
handle,
|
||||||
color,
|
color,
|
||||||
bounds: bounds * transformation,
|
bounds: bounds * transformation,
|
||||||
|
rotation,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.images.push(svg);
|
self.images.push(svg);
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ pub use geometry::Geometry;
|
||||||
|
|
||||||
use crate::core::renderer;
|
use crate::core::renderer;
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
Background, Color, Font, Pixels, Point, Rectangle, Transformation,
|
Background, Color, Font, Pixels, Point, Radians, Rectangle, Transformation,
|
||||||
};
|
};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
use crate::graphics::compositor;
|
use crate::graphics::compositor;
|
||||||
|
|
@ -377,9 +377,16 @@ impl core::image::Renderer for Renderer {
|
||||||
handle: Self::Handle,
|
handle: Self::Handle,
|
||||||
filter_method: core::image::FilterMethod,
|
filter_method: core::image::FilterMethod,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
rotation: Radians,
|
||||||
) {
|
) {
|
||||||
let (layer, transformation) = self.layers.current_mut();
|
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,
|
handle: core::svg::Handle,
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
rotation: Radians,
|
||||||
) {
|
) {
|
||||||
let (layer, transformation) = self.layers.current_mut();
|
let (layer, transformation) = self.layers.current_mut();
|
||||||
layer.draw_svg(handle, color, bounds, transformation);
|
layer.draw_svg(handle, color, bounds, transformation, rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use crate::graphics::text;
|
||||||
|
|
||||||
use resvg::usvg::{self, TreeTextToPath};
|
use resvg::usvg::{self, TreeTextToPath};
|
||||||
use rustc_hash::{FxHashMap, FxHashSet};
|
use rustc_hash::{FxHashMap, FxHashSet};
|
||||||
|
use tiny_skia::Transform;
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::hash_map;
|
use std::collections::hash_map;
|
||||||
|
|
@ -34,6 +35,7 @@ impl Pipeline {
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
pixels: &mut tiny_skia::PixmapMut<'_>,
|
pixels: &mut tiny_skia::PixmapMut<'_>,
|
||||||
|
transform: Transform,
|
||||||
clip_mask: Option<&tiny_skia::Mask>,
|
clip_mask: Option<&tiny_skia::Mask>,
|
||||||
) {
|
) {
|
||||||
if let Some(image) = self.cache.borrow_mut().draw(
|
if let Some(image) = self.cache.borrow_mut().draw(
|
||||||
|
|
@ -46,7 +48,7 @@ impl Pipeline {
|
||||||
bounds.y as i32,
|
bounds.y as i32,
|
||||||
image,
|
image,
|
||||||
&tiny_skia::PixmapPaint::default(),
|
&tiny_skia::PixmapPaint::default(),
|
||||||
tiny_skia::Transform::identity(),
|
transform,
|
||||||
clip_mask,
|
clip_mask,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -135,14 +135,18 @@ impl Pipeline {
|
||||||
attributes: &wgpu::vertex_attr_array!(
|
attributes: &wgpu::vertex_attr_array!(
|
||||||
// Position
|
// Position
|
||||||
0 => Float32x2,
|
0 => Float32x2,
|
||||||
// Scale
|
// Center
|
||||||
1 => Float32x2,
|
1 => Float32x2,
|
||||||
// Atlas position
|
// Image size
|
||||||
2 => Float32x2,
|
2 => Float32x2,
|
||||||
|
// Rotation
|
||||||
|
3 => Float32,
|
||||||
|
// Atlas position
|
||||||
|
4 => Float32x2,
|
||||||
// Atlas scale
|
// Atlas scale
|
||||||
3 => Float32x2,
|
5 => Float32x2,
|
||||||
// Layer
|
// Layer
|
||||||
4 => Sint32,
|
6 => Sint32,
|
||||||
),
|
),
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
|
|
@ -224,6 +228,7 @@ impl Pipeline {
|
||||||
handle,
|
handle,
|
||||||
filter_method,
|
filter_method,
|
||||||
bounds,
|
bounds,
|
||||||
|
rotation,
|
||||||
} => {
|
} => {
|
||||||
if let Some(atlas_entry) =
|
if let Some(atlas_entry) =
|
||||||
cache.upload_raster(device, encoder, handle)
|
cache.upload_raster(device, encoder, handle)
|
||||||
|
|
@ -231,6 +236,7 @@ impl Pipeline {
|
||||||
add_instances(
|
add_instances(
|
||||||
[bounds.x, bounds.y],
|
[bounds.x, bounds.y],
|
||||||
[bounds.width, bounds.height],
|
[bounds.width, bounds.height],
|
||||||
|
f32::from(*rotation),
|
||||||
atlas_entry,
|
atlas_entry,
|
||||||
match filter_method {
|
match filter_method {
|
||||||
crate::core::image::FilterMethod::Nearest => {
|
crate::core::image::FilterMethod::Nearest => {
|
||||||
|
|
@ -251,6 +257,7 @@ impl Pipeline {
|
||||||
handle,
|
handle,
|
||||||
color,
|
color,
|
||||||
bounds,
|
bounds,
|
||||||
|
rotation,
|
||||||
} => {
|
} => {
|
||||||
let size = [bounds.width, bounds.height];
|
let size = [bounds.width, bounds.height];
|
||||||
|
|
||||||
|
|
@ -260,6 +267,7 @@ impl Pipeline {
|
||||||
add_instances(
|
add_instances(
|
||||||
[bounds.x, bounds.y],
|
[bounds.x, bounds.y],
|
||||||
size,
|
size,
|
||||||
|
f32::from(*rotation),
|
||||||
atlas_entry,
|
atlas_entry,
|
||||||
nearest_instances,
|
nearest_instances,
|
||||||
);
|
);
|
||||||
|
|
@ -487,7 +495,9 @@ impl Data {
|
||||||
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
|
#[derive(Debug, Clone, Copy, Zeroable, Pod)]
|
||||||
struct Instance {
|
struct Instance {
|
||||||
_position: [f32; 2],
|
_position: [f32; 2],
|
||||||
|
_center: [f32; 2],
|
||||||
_size: [f32; 2],
|
_size: [f32; 2],
|
||||||
|
_rotation: f32,
|
||||||
_position_in_atlas: [f32; 2],
|
_position_in_atlas: [f32; 2],
|
||||||
_size_in_atlas: [f32; 2],
|
_size_in_atlas: [f32; 2],
|
||||||
_layer: u32,
|
_layer: u32,
|
||||||
|
|
@ -506,12 +516,25 @@ struct Uniforms {
|
||||||
fn add_instances(
|
fn add_instances(
|
||||||
image_position: [f32; 2],
|
image_position: [f32; 2],
|
||||||
image_size: [f32; 2],
|
image_size: [f32; 2],
|
||||||
|
rotation: f32,
|
||||||
entry: &atlas::Entry,
|
entry: &atlas::Entry,
|
||||||
instances: &mut Vec<Instance>,
|
instances: &mut Vec<Instance>,
|
||||||
) {
|
) {
|
||||||
|
let center = [
|
||||||
|
image_position[0] + image_size[0] / 2.0,
|
||||||
|
image_position[1] + image_size[1] / 2.0,
|
||||||
|
];
|
||||||
|
|
||||||
match entry {
|
match entry {
|
||||||
atlas::Entry::Contiguous(allocation) => {
|
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 } => {
|
atlas::Entry::Fragmented { fragments, size } => {
|
||||||
let scaling_x = image_size[0] / size.width as f32;
|
let scaling_x = image_size[0] / size.width as f32;
|
||||||
|
|
@ -537,7 +560,9 @@ fn add_instances(
|
||||||
fragment_height as f32 * scaling_y,
|
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]
|
#[inline]
|
||||||
fn add_instance(
|
fn add_instance(
|
||||||
position: [f32; 2],
|
position: [f32; 2],
|
||||||
|
center: [f32; 2],
|
||||||
size: [f32; 2],
|
size: [f32; 2],
|
||||||
|
rotation: f32,
|
||||||
allocation: &atlas::Allocation,
|
allocation: &atlas::Allocation,
|
||||||
instances: &mut Vec<Instance>,
|
instances: &mut Vec<Instance>,
|
||||||
) {
|
) {
|
||||||
|
|
@ -556,7 +583,9 @@ fn add_instance(
|
||||||
|
|
||||||
let instance = Instance {
|
let instance = Instance {
|
||||||
_position: position,
|
_position: position,
|
||||||
|
_center: center,
|
||||||
_size: size,
|
_size: size,
|
||||||
|
_rotation: rotation,
|
||||||
_position_in_atlas: [
|
_position_in_atlas: [
|
||||||
(x as f32 + 0.5) / atlas::SIZE as f32,
|
(x as f32 + 0.5) / atlas::SIZE as f32,
|
||||||
(y as f32 + 0.5) / atlas::SIZE as f32,
|
(y as f32 + 0.5) / atlas::SIZE as f32,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
use crate::core::renderer;
|
use crate::core::{
|
||||||
use crate::core::{Background, Color, Point, Rectangle, Transformation};
|
renderer, Background, Color, Point, Radians, Rectangle, Transformation,
|
||||||
|
};
|
||||||
use crate::graphics;
|
use crate::graphics;
|
||||||
use crate::graphics::color;
|
use crate::graphics::color;
|
||||||
use crate::graphics::layer;
|
use crate::graphics::layer;
|
||||||
|
|
@ -117,11 +118,13 @@ impl Layer {
|
||||||
filter_method: crate::core::image::FilterMethod,
|
filter_method: crate::core::image::FilterMethod,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
transformation: Transformation,
|
transformation: Transformation,
|
||||||
|
rotation: Radians,
|
||||||
) {
|
) {
|
||||||
let image = Image::Raster {
|
let image = Image::Raster {
|
||||||
handle,
|
handle,
|
||||||
filter_method,
|
filter_method,
|
||||||
bounds: bounds * transformation,
|
bounds: bounds * transformation,
|
||||||
|
rotation,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.images.push(image);
|
self.images.push(image);
|
||||||
|
|
@ -133,11 +136,13 @@ impl Layer {
|
||||||
color: Option<Color>,
|
color: Option<Color>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
transformation: Transformation,
|
transformation: Transformation,
|
||||||
|
rotation: Radians,
|
||||||
) {
|
) {
|
||||||
let svg = Image::Vector {
|
let svg = Image::Vector {
|
||||||
handle,
|
handle,
|
||||||
color,
|
color,
|
||||||
bounds: bounds * transformation,
|
bounds: bounds * transformation,
|
||||||
|
rotation,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.images.push(svg);
|
self.images.push(svg);
|
||||||
|
|
|
||||||
|
|
@ -61,7 +61,8 @@ pub use settings::Settings;
|
||||||
pub use geometry::Geometry;
|
pub use geometry::Geometry;
|
||||||
|
|
||||||
use crate::core::{
|
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::text::{Editor, Paragraph};
|
||||||
use crate::graphics::Viewport;
|
use crate::graphics::Viewport;
|
||||||
|
|
@ -378,7 +379,6 @@ impl Renderer {
|
||||||
use crate::core::alignment;
|
use crate::core::alignment;
|
||||||
use crate::core::text::Renderer as _;
|
use crate::core::text::Renderer as _;
|
||||||
use crate::core::Renderer as _;
|
use crate::core::Renderer as _;
|
||||||
use crate::core::Vector;
|
|
||||||
|
|
||||||
self.with_layer(
|
self.with_layer(
|
||||||
Rectangle::with_size(viewport.logical_size()),
|
Rectangle::with_size(viewport.logical_size()),
|
||||||
|
|
@ -517,9 +517,16 @@ impl core::image::Renderer for Renderer {
|
||||||
handle: Self::Handle,
|
handle: Self::Handle,
|
||||||
filter_method: core::image::FilterMethod,
|
filter_method: core::image::FilterMethod,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
rotation: Radians,
|
||||||
) {
|
) {
|
||||||
let (layer, transformation) = self.layers.current_mut();
|
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,
|
handle: core::svg::Handle,
|
||||||
color_filter: Option<Color>,
|
color_filter: Option<Color>,
|
||||||
bounds: Rectangle,
|
bounds: Rectangle,
|
||||||
|
rotation: Radians,
|
||||||
) {
|
) {
|
||||||
let (layer, transformation) = self.layers.current_mut();
|
let (layer, transformation) = self.layers.current_mut();
|
||||||
layer.draw_svg(handle, color_filter, bounds, transformation);
|
layer.draw_svg(handle, color_filter, bounds, transformation, rotation);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,12 @@ struct Globals {
|
||||||
struct VertexInput {
|
struct VertexInput {
|
||||||
@builtin(vertex_index) vertex_index: u32,
|
@builtin(vertex_index) vertex_index: u32,
|
||||||
@location(0) pos: vec2<f32>,
|
@location(0) pos: vec2<f32>,
|
||||||
@location(1) scale: vec2<f32>,
|
@location(1) center: vec2<f32>,
|
||||||
@location(2) atlas_pos: vec2<f32>,
|
@location(2) scale: vec2<f32>,
|
||||||
@location(3) atlas_scale: vec2<f32>,
|
@location(3) rotation: f32,
|
||||||
@location(4) layer: i32,
|
@location(4) atlas_pos: vec2<f32>,
|
||||||
|
@location(5) atlas_scale: vec2<f32>,
|
||||||
|
@location(6) layer: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
|
|
@ -25,24 +27,34 @@ struct VertexOutput {
|
||||||
fn vs_main(input: VertexInput) -> VertexOutput {
|
fn vs_main(input: VertexInput) -> VertexOutput {
|
||||||
var out: 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.uv = vec2<f32>(v_pos * input.atlas_scale + input.atlas_pos);
|
||||||
out.layer = f32(input.layer);
|
out.layer = f32(input.layer);
|
||||||
|
|
||||||
var transform: mat4x4<f32> = mat4x4<f32>(
|
// Calculate the vertex position and move the center to the origin
|
||||||
vec4<f32>(input.scale.x, 0.0, 0.0, 0.0),
|
v_pos = input.pos + v_pos * input.scale - input.center;
|
||||||
vec4<f32>(0.0, input.scale.y, 0.0, 0.0),
|
|
||||||
|
// 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>(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;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@fragment
|
@fragment
|
||||||
fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
|
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));
|
return textureSample(u_texture, u_sampler, input.uv, i32(input.layer));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,8 @@ use crate::core::mouse;
|
||||||
use crate::core::renderer;
|
use crate::core::renderer;
|
||||||
use crate::core::widget::Tree;
|
use crate::core::widget::Tree;
|
||||||
use crate::core::{
|
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};
|
pub use image::{FilterMethod, Handle};
|
||||||
|
|
@ -36,6 +37,7 @@ pub struct Image<Handle> {
|
||||||
height: Length,
|
height: Length,
|
||||||
content_fit: ContentFit,
|
content_fit: ContentFit,
|
||||||
filter_method: FilterMethod,
|
filter_method: FilterMethod,
|
||||||
|
rotation: Rotation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<Handle> Image<Handle> {
|
impl<Handle> Image<Handle> {
|
||||||
|
|
@ -45,8 +47,9 @@ impl<Handle> Image<Handle> {
|
||||||
handle: handle.into(),
|
handle: handle.into(),
|
||||||
width: Length::Shrink,
|
width: Length::Shrink,
|
||||||
height: Length::Shrink,
|
height: Length::Shrink,
|
||||||
content_fit: ContentFit::Contain,
|
content_fit: ContentFit::default(),
|
||||||
filter_method: FilterMethod::default(),
|
filter_method: FilterMethod::default(),
|
||||||
|
rotation: Rotation::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,6 +78,12 @@ impl<Handle> Image<Handle> {
|
||||||
self.filter_method = filter_method;
|
self.filter_method = filter_method;
|
||||||
self
|
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`].
|
/// Computes the layout of an [`Image`].
|
||||||
|
|
@ -85,22 +94,24 @@ pub fn layout<Renderer, Handle>(
|
||||||
width: Length,
|
width: Length,
|
||||||
height: Length,
|
height: Length,
|
||||||
content_fit: ContentFit,
|
content_fit: ContentFit,
|
||||||
|
rotation: Rotation,
|
||||||
) -> layout::Node
|
) -> layout::Node
|
||||||
where
|
where
|
||||||
Renderer: image::Renderer<Handle = Handle>,
|
Renderer: image::Renderer<Handle = Handle>,
|
||||||
{
|
{
|
||||||
// The raw w/h of the underlying image
|
// The raw w/h of the underlying image
|
||||||
let image_size = {
|
let image_size = renderer.measure_image(handle);
|
||||||
let Size { width, height } = 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
|
// 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
|
// 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
|
// Shrink the widget to fit the resized image, if requested
|
||||||
let final_size = Size {
|
let final_size = Size {
|
||||||
|
|
@ -124,32 +135,44 @@ pub fn draw<Renderer, Handle>(
|
||||||
handle: &Handle,
|
handle: &Handle,
|
||||||
content_fit: ContentFit,
|
content_fit: ContentFit,
|
||||||
filter_method: FilterMethod,
|
filter_method: FilterMethod,
|
||||||
|
rotation: Rotation,
|
||||||
) where
|
) where
|
||||||
Renderer: image::Renderer<Handle = Handle>,
|
Renderer: image::Renderer<Handle = Handle>,
|
||||||
Handle: Clone,
|
Handle: Clone,
|
||||||
{
|
{
|
||||||
let Size { width, height } = renderer.measure_image(handle);
|
let Size { width, height } = renderer.measure_image(handle);
|
||||||
let image_size = Size::new(width as f32, height as f32);
|
let image_size = Size::new(width as f32, height as f32);
|
||||||
|
let rotated_size = rotation.apply(image_size);
|
||||||
|
|
||||||
let bounds = layout.bounds();
|
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 = 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::new(position, final_size);
|
||||||
|
|
||||||
let render = |renderer: &mut Renderer| {
|
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
|
|
||||||
};
|
|
||||||
|
|
||||||
renderer.draw_image(
|
renderer.draw_image(
|
||||||
handle.clone(),
|
handle.clone(),
|
||||||
filter_method,
|
filter_method,
|
||||||
drawing_bounds + offset,
|
drawing_bounds,
|
||||||
|
rotation.radians(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -187,6 +210,7 @@ where
|
||||||
self.width,
|
self.width,
|
||||||
self.height,
|
self.height,
|
||||||
self.content_fit,
|
self.content_fit,
|
||||||
|
self.rotation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -206,6 +230,7 @@ where
|
||||||
&self.handle,
|
&self.handle,
|
||||||
self.content_fit,
|
self.content_fit,
|
||||||
self.filter_method,
|
self.filter_method,
|
||||||
|
self.rotation,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ use crate::core::mouse;
|
||||||
use crate::core::renderer;
|
use crate::core::renderer;
|
||||||
use crate::core::widget::tree::{self, Tree};
|
use crate::core::widget::tree::{self, Tree};
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
Clipboard, Element, Layout, Length, Pixels, Point, Rectangle, Shell, Size,
|
Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle,
|
||||||
Vector, Widget,
|
Shell, Size, Vector, Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A frame that displays 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.
|
||||||
|
|
@ -341,6 +341,7 @@ where
|
||||||
y: bounds.y,
|
y: bounds.y,
|
||||||
..Rectangle::with_size(image_size)
|
..Rectangle::with_size(image_size)
|
||||||
},
|
},
|
||||||
|
Radians(0.0),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@ use crate::core::renderer;
|
||||||
use crate::core::svg;
|
use crate::core::svg;
|
||||||
use crate::core::widget::Tree;
|
use crate::core::widget::Tree;
|
||||||
use crate::core::{
|
use crate::core::{
|
||||||
Color, ContentFit, Element, Layout, Length, Rectangle, Size, Theme, Vector,
|
Color, ContentFit, Element, Layout, Length, Point, Rectangle, Rotation,
|
||||||
Widget,
|
Size, Theme, Vector, Widget,
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -29,6 +29,7 @@ where
|
||||||
height: Length,
|
height: Length,
|
||||||
content_fit: ContentFit,
|
content_fit: ContentFit,
|
||||||
class: Theme::Class<'a>,
|
class: Theme::Class<'a>,
|
||||||
|
rotation: Rotation,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, Theme> Svg<'a, Theme>
|
impl<'a, Theme> Svg<'a, Theme>
|
||||||
|
|
@ -43,6 +44,7 @@ where
|
||||||
height: Length::Shrink,
|
height: Length::Shrink,
|
||||||
content_fit: ContentFit::Contain,
|
content_fit: ContentFit::Contain,
|
||||||
class: Theme::default(),
|
class: Theme::default(),
|
||||||
|
rotation: Rotation::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -95,6 +97,12 @@ where
|
||||||
self.class = class.into();
|
self.class = class.into();
|
||||||
self
|
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>
|
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
|
||||||
|
|
@ -120,11 +128,14 @@ where
|
||||||
let Size { width, height } = renderer.measure_svg(&self.handle);
|
let Size { width, height } = renderer.measure_svg(&self.handle);
|
||||||
let image_size = Size::new(width as f32, height as f32);
|
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
|
// 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
|
// 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
|
// Shrink the widget to fit the resized image, if requested
|
||||||
let final_size = Size {
|
let final_size = Size {
|
||||||
|
|
@ -153,35 +164,46 @@ where
|
||||||
) {
|
) {
|
||||||
let Size { width, height } = renderer.measure_svg(&self.handle);
|
let Size { width, height } = renderer.measure_svg(&self.handle);
|
||||||
let image_size = Size::new(width as f32, height as f32);
|
let image_size = Size::new(width as f32, height as f32);
|
||||||
|
let rotated_size = self.rotation.apply(image_size);
|
||||||
|
|
||||||
let bounds = layout.bounds();
|
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 = 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 is_mouse_over = cursor.is_over(bounds);
|
||||||
|
|
||||||
|
let status = if is_mouse_over {
|
||||||
|
Status::Hovered
|
||||||
|
} else {
|
||||||
|
Status::Idle
|
||||||
|
};
|
||||||
|
|
||||||
|
let style = theme.style(&self.class, status);
|
||||||
|
|
||||||
let render = |renderer: &mut Renderer| {
|
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 status = if is_mouse_over {
|
|
||||||
Status::Hovered
|
|
||||||
} else {
|
|
||||||
Status::Idle
|
|
||||||
};
|
|
||||||
|
|
||||||
let style = theme.style(&self.class, status);
|
|
||||||
|
|
||||||
renderer.draw_svg(
|
renderer.draw_svg(
|
||||||
self.handle.clone(),
|
self.handle.clone(),
|
||||||
style.color,
|
style.color,
|
||||||
drawing_bounds + offset,
|
drawing_bounds,
|
||||||
|
self.rotation.radians(),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue