From a2a7c18c9d0a0b7cc469bd4000a7239618167477 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A9ctor=20Ram=C3=B3n=20Jim=C3=A9nez?= Date: Fri, 25 Apr 2025 13:31:49 +0200 Subject: [PATCH] Implement `Image::translate` for positioning floating images --- core/src/rectangle.rs | 35 ++++++++++++++++++++ examples/gallery/src/main.rs | 6 ++++ widget/src/image.rs | 62 +++++++++++++++++++++++------------- 3 files changed, 81 insertions(+), 22 deletions(-) diff --git a/core/src/rectangle.rs b/core/src/rectangle.rs index 14d2a2e8..5ffdafa1 100644 --- a/core/src/rectangle.rs +++ b/core/src/rectangle.rs @@ -157,6 +157,30 @@ impl Rectangle { distance_x.hypot(distance_y) } + /// Computes the offset that must be applied to the [`Rectangle`] to be placed + /// inside the given `container`. + pub fn offset(&self, container: &Rectangle) -> Vector { + if let Some(intersection) = self.intersection(container) { + let left = intersection.x - self.x; + let top = intersection.y - self.y; + + Vector::new( + if left > 0.0 { + left + } else { + intersection.x + intersection.width - self.x - self.width + }, + if top > 0.0 { + top + } else { + intersection.y + intersection.height - self.y - self.height + }, + ) + } else { + Vector::ZERO + } + } + /// Returns true if the current [`Rectangle`] is completely within the given /// `container`. pub fn is_within(&self, container: &Rectangle) -> bool { @@ -268,6 +292,17 @@ impl Rectangle { Self::new(position, size) } + + /// Scales the [`Rectangle`] without changing its position, effectively + /// "zooming" it. + pub fn zoom(self, zoom: f32) -> Self { + Self { + x: self.x - (self.width * (zoom - 1.0)) / 2.0, + y: self.y - (self.height * (zoom - 1.0)) / 2.0, + width: self.width * zoom, + height: self.height * zoom, + } + } } impl std::ops::Mul for Rectangle { diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index e8cc95a3..47653686 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -210,6 +210,12 @@ fn card<'a>( .content_fit(ContentFit::Cover) .opacity(thumbnail.fade_in.interpolate(0.0, 1.0, now)) .scale(thumbnail.zoom.interpolate(1.0, 1.1, now)) + .translate(move |bounds, viewport| { + let final_bounds = bounds.zoom(1.1); + + final_bounds.offset(&viewport.shrink(10)) + * thumbnail.zoom.interpolate(0.0, 1.0, now) + }) .float(true) .style(move |_theme| image::Style { shadow: Shadow { diff --git a/widget/src/image.rs b/widget/src/image.rs index d06fe759..5aaf98c7 100644 --- a/widget/src/image.rs +++ b/widget/src/image.rs @@ -57,7 +57,7 @@ pub fn viewer(handle: Handle) -> Viewer { /// } /// ``` /// -#[derive(Debug)] +#[allow(missing_debug_implementations)] pub struct Image<'a, Handle = image::Handle, Theme = crate::Theme> where Theme: Catalog, @@ -70,6 +70,7 @@ where rotation: Rotation, opacity: f32, scale: f32, + translate: Option Vector + 'a>>, float: bool, class: Theme::Class<'a>, } @@ -89,6 +90,7 @@ where rotation: Rotation::default(), opacity: 1.0, scale: 1.0, + translate: None, float: false, class: Theme::default(), } @@ -144,10 +146,27 @@ where self } - /// Sets whether an [`Image`] should float above other content when scaled up. + /// Sets the translation to apply to the [`Image`] when floating. + /// + /// This method takes a closure that will receive the non-scaled bounds of the [`Image`] + /// and the bounds of the viewport. The closure must produce a [`Vector`] representing + /// the translation to be applied. + /// + /// Translating can be useful to ensure floating images stay visible inside the + /// viewport. + pub fn translate( + mut self, + translate: impl Fn(Rectangle, Rectangle) -> Vector + 'a, + ) -> Self { + self.translate = Some(Box::new(translate)); + self + } + + /// Sets whether an [`Image`] should float above other content when + /// scaled up. /// /// By default, an [`Image`] has this flag set to `false`; meaning it - /// will be clipped or "framed" inside its bounds when scaled. + /// will be clipped or "framed" inside its bounds. /// /// Enabling this flag is useful to create cool hover effects! pub fn float(mut self, float: bool) -> Self { @@ -459,7 +478,7 @@ pub trait Catalog { fn style(&self, class: &Self::Class<'_>) -> Style; } -/// A styling function for a [`Button`]. +/// A styling function for an [`Image`]. pub type StyleFn<'a, Theme> = Box Style + 'a>; impl Catalog for crate::Theme { @@ -499,9 +518,18 @@ where Handle: Clone, Theme: Catalog, { - fn layout(&mut self, _renderer: &Renderer, _bounds: Size) -> layout::Node { - layout::Node::new(self.clip_bounds.size()) - .move_to(self.clip_bounds.position()) + fn layout(&mut self, _renderer: &Renderer, bounds: Size) -> layout::Node { + let bounds = if let Some(translate) = &self.image.translate { + self.clip_bounds + + translate( + self.clip_bounds, + Rectangle::new(Point::ORIGIN, bounds), + ) + } else { + self.clip_bounds + }; + + layout::Node::new(bounds.size()).move_to(bounds.position()) } fn is_over( @@ -518,22 +546,12 @@ where renderer: &mut Renderer, theme: &Theme, _style: &renderer::Style, - _layout: Layout<'_>, + layout: Layout<'_>, _cursor: mouse::Cursor, ) { - let clip_bounds = Rectangle { - x: self.clip_bounds.x - - (self.clip_bounds.width * self.image.scale - - self.clip_bounds.width) - / 2.0, - y: self.clip_bounds.y - - (self.clip_bounds.height * self.image.scale - - self.clip_bounds.height) - / 2.0, - width: self.clip_bounds.width * self.image.scale, - height: self.clip_bounds.height * self.image.scale, - }; - + let bounds = layout.bounds(); + let translation = bounds.position() - self.clip_bounds.position(); + let clip_bounds = bounds.zoom(self.image.scale); let style = theme.style(&self.image.class); if style.shadow.color.a > 0.0 { @@ -559,7 +577,7 @@ where self.image.filter_method, self.image.rotation, self.image.opacity, - self.drawing_bounds, + self.drawing_bounds + translation, ); }); }