Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement content_fit for viewer widget #2330

Merged
merged 12 commits into from
Jun 17, 2024
156 changes: 80 additions & 76 deletions widget/src/image/viewer.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
//! Zoom and pan on an image.
use crate::core::event::{self, Event};
use crate::core::image;
use crate::core::image::{self, FilterMethod};
use crate::core::layout;
use crate::core::mouse;
use crate::core::renderer;
use crate::core::widget::tree::{self, Tree};
use crate::core::{
Clipboard, Element, Layout, Length, Pixels, Point, Radians, Rectangle,
Shell, Size, Vector, Widget,
Clipboard, ContentFit, 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.
Expand All @@ -20,30 +20,38 @@ pub struct Viewer<Handle> {
max_scale: f32,
scale_step: f32,
handle: Handle,
filter_method: image::FilterMethod,
filter_method: FilterMethod,
content_fit: ContentFit,
}

impl<Handle> Viewer<Handle> {
/// Creates a new [`Viewer`] with the given [`State`].
pub fn new(handle: Handle) -> Self {
pub fn new<T: Into<Handle>>(handle: T) -> Self {
Viewer {
handle,
handle: handle.into(),
padding: 0.0,
width: Length::Shrink,
height: Length::Shrink,
min_scale: 0.25,
max_scale: 10.0,
scale_step: 0.10,
filter_method: image::FilterMethod::default(),
filter_method: FilterMethod::default(),
content_fit: ContentFit::default(),
}
}

/// Sets the [`image::FilterMethod`] of the [`Viewer`].
/// Sets the [`FilterMethod`] of the [`Viewer`].
pub fn filter_method(mut self, filter_method: image::FilterMethod) -> Self {
self.filter_method = filter_method;
self
}

/// Sets the [`ContentFit`] of the [`Viewer`].
pub fn content_fit(mut self, content_fit: ContentFit) -> Self {
self.content_fit = content_fit;
self
}

/// Sets the padding of the [`Viewer`].
pub fn padding(mut self, padding: impl Into<Pixels>) -> Self {
self.padding = padding.into().0;
Expand Down Expand Up @@ -115,36 +123,30 @@ where
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let Size { width, height } = renderer.measure_image(&self.handle);

let mut size = limits.resolve(
self.width,
self.height,
Size::new(width as f32, height as f32),
);

let expansion_size = if height > width {
self.width
} else {
self.height
// The raw w/h of the underlying image
let image_size = renderer.measure_image(&self.handle);
let image_size =
Size::new(image_size.width as f32, image_size.height as f32);

// The size to be available to the widget prior to `Shrink`ing
let raw_size = limits.resolve(self.width, self.height, image_size);

// The uncropped size of the image when fit to the bounds above
let full_size = self.content_fit.fit(image_size, raw_size);

// Shrink the widget to fit the resized image, if requested
let final_size = Size {
width: match self.width {
Length::Shrink => f32::min(raw_size.width, full_size.width),
_ => raw_size.width,
},
height: match self.height {
Length::Shrink => f32::min(raw_size.height, full_size.height),
_ => raw_size.height,
},
};

// Only calculate viewport sizes if the images are constrained to a limited space.
// If they are Fill|Portion let them expand within their allotted space.
match expansion_size {
Length::Shrink | Length::Fixed(_) => {
let aspect_ratio = width as f32 / height as f32;
let viewport_aspect_ratio = size.width / size.height;
if viewport_aspect_ratio > aspect_ratio {
size.width = width as f32 * size.height / height as f32;
} else {
size.height = height as f32 * size.width / width as f32;
}
}
Length::Fill | Length::FillPortion(_) => {}
}

layout::Node::new(size)
layout::Node::new(final_size)
}

fn on_event(
Expand Down Expand Up @@ -182,11 +184,12 @@ where
})
.clamp(self.min_scale, self.max_scale);

let image_size = image_size(
let scaled_size = scaled_image_size(
renderer,
&self.handle,
state,
bounds.size(),
self.content_fit,
);

let factor = state.scale / previous_scale - 1.0;
Expand All @@ -198,12 +201,12 @@ where
+ state.current_offset * factor;

state.current_offset = Vector::new(
if image_size.width > bounds.width {
if scaled_size.width > bounds.width {
state.current_offset.x + adjustment.x
} else {
0.0
},
if image_size.height > bounds.height {
if scaled_size.height > bounds.height {
state.current_offset.y + adjustment.y
} else {
0.0
Expand Down Expand Up @@ -242,32 +245,32 @@ where
let state = tree.state.downcast_mut::<State>();

if let Some(origin) = state.cursor_grabbed_at {
let image_size = image_size(
let scaled_size = scaled_image_size(
renderer,
&self.handle,
state,
bounds.size(),
self.content_fit,
);

let hidden_width = (image_size.width - bounds.width / 2.0)
let hidden_width = (scaled_size.width - bounds.width / 2.0)
.max(0.0)
.round();

let hidden_height = (image_size.height
let hidden_height = (scaled_size.height
- bounds.height / 2.0)
.max(0.0)
.round();

let delta = position - origin;

let x = if bounds.width < image_size.width {
let x = if bounds.width < scaled_size.width {
(state.starting_offset.x - delta.x)
.clamp(-hidden_width, hidden_width)
} else {
0.0
};

let y = if bounds.height < image_size.height {
let y = if bounds.height < scaled_size.height {
(state.starting_offset.y - delta.y)
.clamp(-hidden_height, hidden_height)
} else {
Expand Down Expand Up @@ -319,33 +322,43 @@ where
let state = tree.state.downcast_ref::<State>();
let bounds = layout.bounds();

let image_size =
image_size(renderer, &self.handle, state, bounds.size());
let final_size = scaled_image_size(
renderer,
&self.handle,
state,
bounds.size(),
self.content_fit,
);

let translation = {
let image_top_left = Vector::new(
bounds.width / 2.0 - image_size.width / 2.0,
bounds.height / 2.0 - image_size.height / 2.0,
);
let diff_w = bounds.width - final_size.width;
let diff_h = bounds.height - final_size.height;

image_top_left - state.offset(bounds, image_size)
let image_top_left = match self.content_fit {
ContentFit::None => {
Vector::new(diff_w.max(0.0) / 2.0, diff_h.max(0.0) / 2.0)
}
_ => Vector::new(diff_w / 2.0, diff_h / 2.0),
};

image_top_left - state.offset(bounds, final_size)
};

renderer.with_layer(bounds, |renderer| {
let drawing_bounds = Rectangle::new(bounds.position(), final_size);

let render = |renderer: &mut Renderer| {
renderer.with_translation(translation, |renderer| {
renderer.draw_image(
self.handle.clone(),
self.filter_method,
Rectangle {
x: bounds.x,
y: bounds.y,
..Rectangle::with_size(image_size)
},
drawing_bounds,
Radians(0.0),
1.0,
);
});
});
};

renderer.with_layer(bounds, render);
}
}

Expand Down Expand Up @@ -411,32 +424,23 @@ where
/// Returns the bounds of the underlying image, given the bounds of
/// the [`Viewer`]. Scaling will be applied and original aspect ratio
/// will be respected.
pub fn image_size<Renderer>(
pub fn scaled_image_size<Renderer>(
renderer: &Renderer,
handle: &<Renderer as image::Renderer>::Handle,
state: &State,
bounds: Size,
content_fit: ContentFit,
) -> Size
where
Renderer: image::Renderer,
{
let Size { width, height } = renderer.measure_image(handle);
let image_size = Size::new(width as f32, height as f32);

let (width, height) = {
let dimensions = (width as f32, height as f32);

let width_ratio = bounds.width / dimensions.0;
let height_ratio = bounds.height / dimensions.1;

let ratio = width_ratio.min(height_ratio);
let scale = state.scale;

if ratio < 1.0 {
(dimensions.0 * ratio * scale, dimensions.1 * ratio * scale)
} else {
(dimensions.0 * scale, dimensions.1 * scale)
}
};
let adjusted_fit = content_fit.fit(image_size, bounds);

Size::new(width, height)
Size::new(
adjusted_fit.width * state.scale,
adjusted_fit.height * state.scale,
)
}
Loading