iced-yoda/widget/src/image.rs
Héctor Ramón Jiménez 31bc6d48cd
Make Widget::layout and operate mutable
... and bless `responsive`!
2025-08-20 22:42:15 +02:00

441 lines
12 KiB
Rust

//! Images display raster graphics in different formats (PNG, JPG, etc.).
//!
//! # Example
//! ```no_run
//! # mod iced { pub mod widget { pub use iced_widget::*; } }
//! # pub type State = ();
//! # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
//! use iced::widget::image;
//!
//! enum Message {
//! // ...
//! }
//!
//! fn view(state: &State) -> Element<'_, Message> {
//! image("ferris.png").into()
//! }
//! ```
//! <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
pub mod viewer;
pub use viewer::Viewer;
use crate::core::image;
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::Tree;
use crate::core::{
ContentFit, Element, Layout, Length, Point, Rectangle, Rotation, Size,
Vector, Widget,
};
pub use image::{FilterMethod, Handle};
/// Creates a new [`Viewer`] with the given image `Handle`.
pub fn viewer<Handle>(handle: Handle) -> Viewer<Handle> {
Viewer::new(handle)
}
/// A frame that displays an image while keeping aspect ratio.
///
/// # Example
/// ```no_run
/// # mod iced { pub mod widget { pub use iced_widget::*; } }
/// # pub type State = ();
/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>;
/// use iced::widget::image;
///
/// enum Message {
/// // ...
/// }
///
/// fn view(state: &State) -> Element<'_, Message> {
/// image("ferris.png").into()
/// }
/// ```
/// <img src="https://github.com/iced-rs/iced/blob/9712b319bb7a32848001b96bd84977430f14b623/examples/resources/ferris.png?raw=true" width="300">
#[allow(missing_debug_implementations)]
pub struct Image<Handle = image::Handle> {
handle: Handle,
width: Length,
height: Length,
crop: Option<Rectangle<u32>>,
content_fit: ContentFit,
filter_method: FilterMethod,
rotation: Rotation,
opacity: f32,
scale: f32,
expand: bool,
}
impl<Handle> Image<Handle> {
/// Creates a new [`Image`] with the given path.
pub fn new(handle: impl Into<Handle>) -> Self {
Image {
handle: handle.into(),
width: Length::Shrink,
height: Length::Shrink,
crop: None,
content_fit: ContentFit::default(),
filter_method: FilterMethod::default(),
rotation: Rotation::default(),
opacity: 1.0,
scale: 1.0,
expand: false,
}
}
/// Sets the width of the [`Image`] boundaries.
pub fn width(mut self, width: impl Into<Length>) -> Self {
self.width = width.into();
self
}
/// Sets the height of the [`Image`] boundaries.
pub fn height(mut self, height: impl Into<Length>) -> Self {
self.height = height.into();
self
}
/// Sets whether the [`Image`] should try to fill as much space
/// available as possible while keeping aspect ratio and without
/// allocating extra space in any axis with a [`Length::Shrink`]
/// sizing strategy.
///
/// This is similar to using [`Length::Fill`] for both the
/// [`width`](Self::width) and the [`height`](Self::height),
/// but without the downside of blank space.
pub fn expand(mut self, expand: bool) -> Self {
self.expand = expand;
self
}
/// Sets the [`ContentFit`] of the [`Image`].
///
/// Defaults to [`ContentFit::Contain`]
pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
self.content_fit = content_fit;
self
}
/// Sets the [`FilterMethod`] of the [`Image`].
pub fn filter_method(mut self, filter_method: FilterMethod) -> Self {
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
}
/// Sets the opacity of the [`Image`].
///
/// It should be in the [0.0, 1.0] range—`0.0` meaning completely transparent,
/// and `1.0` meaning completely opaque.
pub fn opacity(mut self, opacity: impl Into<f32>) -> Self {
self.opacity = opacity.into();
self
}
/// Sets the scale of the [`Image`].
///
/// The region of the [`Image`] drawn will be scaled from the center by the given scale factor.
/// This can be useful to create certain effects and animations, like smooth zoom in / out.
pub fn scale(mut self, scale: impl Into<f32>) -> Self {
self.scale = scale.into();
self
}
/// Crops the [`Image`] to the given region described by the [`Rectangle`] in absolute
/// coordinates.
///
/// Cropping is done before applying any transformation or [`ContentFit`]. In practice,
/// this means that cropping an [`Image`] with this method should produce the same result
/// as cropping it externally (e.g. with an image editor) and creating a new [`Handle`]
/// for the cropped version.
///
/// However, this method is much more efficient; since it just leverages scissoring during
/// rendering and no image cropping actually takes place. Instead, it reuses the existing
/// image allocations and should be as efficient as not cropping at all!
///
/// The `region` coordinates will be clamped to the image dimensions, if necessary.
pub fn crop(mut self, region: Rectangle<u32>) -> Self {
self.crop = Some(region);
self
}
}
/// Computes the layout of an [`Image`].
pub fn layout<Renderer, Handle>(
renderer: &Renderer,
limits: &layout::Limits,
handle: &Handle,
width: Length,
height: Length,
region: Option<Rectangle<u32>>,
content_fit: ContentFit,
rotation: Rotation,
expand: bool,
) -> layout::Node
where
Renderer: image::Renderer<Handle = Handle>,
{
// The raw w/h of the underlying image
let image_size = crop(renderer.measure_image(handle), region);
// 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 bounds = if expand {
limits.width(width).height(height).max()
} else {
limits.resolve(width, height, rotated_size)
};
// The uncropped size of the image when fit to the bounds above
let full_size = content_fit.fit(rotated_size, bounds);
// Shrink the widget to fit the resized image, if requested
let final_size = Size {
width: match width {
Length::Shrink => f32::min(bounds.width, full_size.width),
_ => bounds.width,
},
height: match height {
Length::Shrink => f32::min(bounds.height, full_size.height),
_ => bounds.height,
},
};
layout::Node::new(final_size)
}
fn drawing_bounds<Renderer, Handle>(
renderer: &Renderer,
bounds: Rectangle,
handle: &Handle,
region: Option<Rectangle<u32>>,
content_fit: ContentFit,
rotation: Rotation,
scale: f32,
) -> Rectangle
where
Renderer: image::Renderer<Handle = Handle>,
{
let original_size = renderer.measure_image(handle);
let image_size = crop(original_size, region);
let rotated_size = rotation.apply(image_size);
let adjusted_fit = content_fit.fit(rotated_size, bounds.size());
let fit_scale = Vector::new(
adjusted_fit.width / rotated_size.width,
adjusted_fit.height / rotated_size.height,
);
let final_size = image_size * fit_scale * scale;
let (crop_offset, final_size) = if let Some(region) = region {
let x = region.x.min(original_size.width) as f32;
let y = region.y.min(original_size.height) as f32;
let width = image_size.width;
let height = image_size.height;
let ratio = Vector::new(
original_size.width as f32 / width,
original_size.height as f32 / height,
);
let final_size = final_size * ratio;
let scale = Vector::new(
final_size.width / original_size.width as f32,
final_size.height / original_size.height as f32,
);
let offset = match content_fit {
ContentFit::None => Vector::new(x * scale.x, y * scale.y),
_ => Vector::new(
((original_size.width as f32 - width) / 2.0 - x) * scale.x,
((original_size.height as f32 - height) / 2.0 - y) * scale.y,
),
};
(offset, final_size)
} else {
(Vector::ZERO, final_size)
};
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,
),
};
Rectangle::new(position + crop_offset, final_size)
}
fn must_clip(bounds: Rectangle, drawing_bounds: Rectangle) -> bool {
drawing_bounds.width > bounds.width || drawing_bounds.height > bounds.height
}
fn crop(size: Size<u32>, region: Option<Rectangle<u32>>) -> Size<f32> {
if let Some(region) = region {
Size::new(
region.width.min(size.width) as f32,
region.height.min(size.height) as f32,
)
} else {
Size::new(size.width as f32, size.height as f32)
}
}
/// Draws an [`Image`]
pub fn draw<Renderer, Handle>(
renderer: &mut Renderer,
layout: Layout<'_>,
viewport: &Rectangle,
handle: &Handle,
crop: Option<Rectangle<u32>>,
content_fit: ContentFit,
filter_method: FilterMethod,
rotation: Rotation,
opacity: f32,
scale: f32,
) where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone,
{
let bounds = layout.bounds();
let drawing_bounds = drawing_bounds(
renderer,
bounds,
handle,
crop,
content_fit,
rotation,
scale,
);
if must_clip(bounds, drawing_bounds) {
if let Some(bounds) = bounds.intersection(viewport) {
renderer.with_layer(bounds, |renderer| {
render(
renderer,
handle,
filter_method,
rotation,
opacity,
drawing_bounds,
);
});
}
} else {
render(
renderer,
handle,
filter_method,
rotation,
opacity,
drawing_bounds,
);
}
}
fn render<Renderer, Handle>(
renderer: &mut Renderer,
handle: &Handle,
filter_method: FilterMethod,
rotation: Rotation,
opacity: f32,
drawing_bounds: Rectangle,
) where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone,
{
renderer.draw_image(
image::Image {
handle: handle.clone(),
filter_method,
rotation: rotation.radians(),
opacity,
snap: true,
},
drawing_bounds,
);
}
impl<Message, Theme, Renderer, Handle> Widget<Message, Theme, Renderer>
for Image<Handle>
where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone,
{
fn size(&self) -> Size<Length> {
Size {
width: self.width,
height: self.height,
}
}
fn layout(
&mut self,
_tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
layout(
renderer,
limits,
&self.handle,
self.width,
self.height,
self.crop,
self.content_fit,
self.rotation,
self.expand,
)
}
fn draw(
&self,
_state: &Tree,
renderer: &mut Renderer,
_theme: &Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor: mouse::Cursor,
viewport: &Rectangle,
) {
draw(
renderer,
layout,
viewport,
&self.handle,
self.crop,
self.content_fit,
self.filter_method,
self.rotation,
self.opacity,
self.scale,
);
}
}
impl<'a, Message, Theme, Renderer, Handle> From<Image<Handle>>
for Element<'a, Message, Theme, Renderer>
where
Renderer: image::Renderer<Handle = Handle>,
Handle: Clone + 'a,
{
fn from(image: Image<Handle>) -> Element<'a, Message, Theme, Renderer> {
Element::new(image)
}
}