Merge pull request #2334 from DKolter/image-rotation

Adding feature: Image rotation
This commit is contained in:
Héctor Ramón 2024-05-03 07:31:34 +02:00 committed by GitHub
commit 1cefe6be21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 697 additions and 111 deletions

View file

@ -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)

View file

@ -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",
})
}
}

View file

@ -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,
);
}

View file

@ -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;

View file

@ -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,
}
}
}

View file

@ -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,
) {
}
}

72
core/src/rotation.rs Normal file
View 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))
}
}

View file

@ -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,
}
}
}

View file

@ -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,
);
}

View file

@ -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>

View 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
View 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",
})
}
}

View file

@ -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),
}
}
}

View file

@ -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)
);
}
}

View file

@ -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 {

View file

@ -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,
);
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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,
);
}

View file

@ -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,

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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));
}

View file

@ -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 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 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(
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,
);
}
}

View file

@ -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),
);
});
});

View file

@ -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 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 status = if is_mouse_over {
Status::Hovered
} else {
Status::Idle
};
let style = theme.style(&self.class, status);
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(
self.handle.clone(),
style.color,
drawing_bounds + offset,
drawing_bounds,
self.rotation.radians(),
);
};