diff --git a/masonry/examples/custom_widget.rs b/masonry/examples/custom_widget.rs index 8c85743b7..b9cab1efe 100644 --- a/masonry/examples/custom_widget.rs +++ b/masonry/examples/custom_widget.rs @@ -113,10 +113,9 @@ impl Widget for CustomWidget { painter: &mut Painter<'_>, ) { // Clear the whole widget with the color of your choice - // (ctx.content_box_size() returns the size of the content rect we're painting in) - let size = ctx.content_box_size(); - let rect = ctx.content_box(); - painter.fill(rect, palette::css::WHITE).draw(); + let content_box = ctx.content_box(); + let size = content_box.size(); + painter.fill(content_box, palette::css::WHITE).draw(); // Create an arbitrary bezier path let mut path = BezPath::new(); @@ -166,7 +165,7 @@ impl Widget for CustomWidget { width: 256, height: 256, }); - let transform = ObjectFit::Stretch.affine(size, Size::new(256., 256.)); + let transform = ObjectFit::Stretch.affine(content_box, Rect::new(0., 0., 256., 256.)); painter.draw_image(&image_data, transform); } diff --git a/masonry/src/properties/object_fit.rs b/masonry/src/properties/object_fit.rs index 673fada38..183978146 100644 --- a/masonry/src/properties/object_fit.rs +++ b/masonry/src/properties/object_fit.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::core::Property; -use crate::kurbo::{Affine, Axis, Size}; +use crate::kurbo::{Affine, Axis, Rect, Size}; use crate::layout::{LenReq, Length}; use crate::util::Sanitize; @@ -67,34 +67,32 @@ impl Default for ObjectFit { } } -// TODO - Need to write tests for this, in a way that's relatively easy to visualize. - impl ObjectFit { /// Calculates an [`Affine`] transform to fit `content` inside `container`. /// + /// Both `content` and `container` can have arbitrary origins, which will be + /// correctly translated in the returned `Affine`. + /// /// See [`ObjectFit`] variant documentation for fitting details. /// /// # Panics /// - /// Panics if either `content` or `container` is non-finite or negative + /// Panics if either `content` or `container` has non-finite or negative size /// and debug assertions are enabled. - pub fn affine(self, container: Size, content: Size) -> Affine { + pub fn affine(self, container: Rect, content: Rect) -> Affine { // Guard against invalid input - let container = Size::new( - container.width.sanitize("container width"), - container.height.sanitize("container height"), - ); - let content = Size::new( - content.width.sanitize("content width"), - content.height.sanitize("content height"), - ); + let container_width = container.width().sanitize("container width"); + let container_height = container.height().sanitize("container height"); + let content_width = content.width().sanitize("content width"); + let content_height = content.height().sanitize("content height"); + // Guard against division by zero - if content.width == 0. || content.height == 0. { + if content_width == 0. || content_height == 0. { return Affine::IDENTITY; } - let raw_scalex = container.width / content.width; - let raw_scaley = container.height / content.height; + let raw_scalex = container_width / content_width; + let raw_scaley = container_height / content_height; let (scalex, scaley) = match self { Self::Contain => { @@ -115,10 +113,17 @@ impl ObjectFit { Self::Stretch => (raw_scalex, raw_scaley), }; - let origin_x = (container.width - (content.width * scalex)) * 0.5; - let origin_y = (container.height - (content.height * scaley)) * 0.5; + let origin_x = container.x0 + (container_width - (content_width * scalex)) * 0.5; + let origin_y = container.y0 + (container_height - (content_height * scaley)) * 0.5; - Affine::new([scalex, 0., 0., scaley, origin_x, origin_y]) + Affine::new([ + scalex, + 0., + 0., + scaley, + origin_x - content.x0 * scalex, + origin_y - content.y0 * scaley, + ]) } /// Calculates the [`Length`] of `axis`. diff --git a/masonry/src/tests/mod.rs b/masonry/src/tests/mod.rs index 64da0e86a..5c56e3162 100644 --- a/masonry/src/tests/mod.rs +++ b/masonry/src/tests/mod.rs @@ -5,6 +5,8 @@ //! both to centralize tests in a single crate and to have access to the `masonry` //! widget/property set in our tests if needed. +use crate::kurbo::Rect; + mod accessibility; mod action; mod anim; @@ -16,3 +18,19 @@ mod paint; mod properties; mod update; mod widget_tag; + +#[track_caller] +pub(crate) fn assert_approx_eq(name: &str, actual: f64, expected: f64) { + assert!( + (actual - expected).abs() <= 1e-9, + "{name}: expected {expected}, got {actual}" + ); +} + +#[track_caller] +pub(crate) fn assert_rect_approx_eq(name: &str, actual: Rect, expected: Rect) { + assert_approx_eq(&format!("{name}.x0"), actual.x0, expected.x0); + assert_approx_eq(&format!("{name}.y0"), actual.y0, expected.y0); + assert_approx_eq(&format!("{name}.x1"), actual.x1, expected.x1); + assert_approx_eq(&format!("{name}.y1"), actual.y1, expected.y1); +} diff --git a/masonry/src/tests/properties.rs b/masonry/src/tests/properties.rs index 8f2b44b9a..c99f0365a 100644 --- a/masonry/src/tests/properties.rs +++ b/masonry/src/tests/properties.rs @@ -2,9 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use crate::core::Widget as _; +use crate::kurbo::Rect; use crate::layout::AsUnit; use crate::palette::css::BLUE; -use crate::properties::{ContentColor, Dimensions, Gap}; +use crate::properties::{ContentColor, Dimensions, Gap, ObjectFit}; +use crate::tests::assert_rect_approx_eq; use crate::widgets::Button; #[test] @@ -30,3 +32,31 @@ fn widget_new_properties() { assert_eq!(props.get::(), Some(&Gap::ZERO)); assert_eq!(props.get::(), Some(&ContentColor::new(BLUE))); } + +#[test] +fn object_fit_affine_stretch_maps_rect_to_rect() { + let container = Rect::new(10., -20., 110., 30.); + let content = Rect::new(-5., 10., 15., 20.); + + let transform = ObjectFit::Stretch.affine(container, content); + + assert_rect_approx_eq( + "transformed", + transform.transform_rect_bbox(content), + container, + ); +} + +#[test] +fn object_fit_affine_contain_handles_negative_origins() { + let container = Rect::new(-30., -20., 70., 30.); + let content = Rect::new(-10., -5., 10., 15.); + + let transform = ObjectFit::Contain.affine(container, content); + + assert_rect_approx_eq( + "transformed", + transform.transform_rect_bbox(content), + Rect::new(-5., -20., 45., 30.), + ); +} diff --git a/masonry/src/widgets/image.rs b/masonry/src/widgets/image.rs index af8a1d615..263f04cf4 100644 --- a/masonry/src/widgets/image.rs +++ b/masonry/src/widgets/image.rs @@ -179,7 +179,7 @@ impl Widget for Image { self.image_data.image.width as f64, self.image_data.image.height as f64, ); - let transform = object_fit.affine(content_box.size(), image_size); + let transform = object_fit.affine(content_box, image_size.to_rect()); painter.with_fill_clip(content_box, |painter| { painter.draw_image(&self.image_data, transform); diff --git a/masonry/src/widgets/svg.rs b/masonry/src/widgets/svg.rs index 5b6449eac..6f4fa6aaa 100644 --- a/masonry/src/widgets/svg.rs +++ b/masonry/src/widgets/svg.rs @@ -189,7 +189,9 @@ impl Widget for Svg { let svg_size = self.tree.size(); let svg_size = Size::new(svg_size.width() as f64, svg_size.height() as f64); - let coeffs = object_fit.affine(content_box.size(), svg_size).as_coeffs(); + let coeffs = object_fit + .affine(content_box.size().to_rect(), svg_size.to_rect()) + .as_coeffs(); let transform = tiny_skia::Transform::from_row( coeffs[0] as f32, coeffs[1] as f32,