Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 4 additions & 5 deletions masonry/examples/custom_widget.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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);
}

Expand Down
43 changes: 24 additions & 19 deletions masonry/src/properties/object_fit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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`.
Comment on lines +73 to +74
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"Correctly translated" doesn't give me a strong intuition for how this function will behave given rects with non-zero origin.

My guess would have been "it just adds a applies of container.origin - content.origin to the affine", but the code seems to do something more complex than that?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well it does do that, but also accounts for scale changes. In every case the content will end up being inside of container, but depending on exact fit strategy, some of content might overflow.

I'll give a brief example and its journey through this method.

10x10 Container, origin (10,10), end (20,20)
20x10 Content, origin (30,30), end(50,40)

ObjectFit::Contain

// Container vs Content factor
raw_scalex = 0.5
raw_scaley = 1.0

// For Contain we end up with this scale target
(scalex, scaley) = (0.5, 0.5)

// The following is the target origin for content
origin_x = 10 + (10 - (20 * 0.5)) * 0.5 = 10
origin_y = 10 + (10 - (10 * 0.5)) * 0.5 = 12.5

// To achieve the target size and origin, we calculate the affine
Affine::scale_x = 0.5
Affine::scale_y = 0.5
Affine::translate_x = 10 - 30 * 0.5 = -5
Affine::translate_y = 12.5 - 30 * 0.5 = -2.5

// Applying the affine later ends up giving us the correct origin and end for the content
(30,30) * affine = (30 * 0.5 + -5, 30 * 0.5 + -2.5) = (10,12.5)
(50,40) * affine = (50 * 0.5 + -5, 40 * 0.5 + -2.5) = (20,17.5)

///
/// 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 => {
Expand All @@ -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;
Comment on lines +116 to +117
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this new function adds a translation component to account for differences in Rect position? This need to be documented.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it now translates arbitrary origins instead of just assuming (0,0). I added a comment to make it explicit.


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`.
Expand Down
18 changes: 18 additions & 0 deletions masonry/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
32 changes: 31 additions & 1 deletion masonry/src/tests/properties.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -30,3 +32,31 @@ fn widget_new_properties() {
assert_eq!(props.get::<Gap>(), Some(&Gap::ZERO));
assert_eq!(props.get::<ContentColor>(), 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.),
);
}
2 changes: 1 addition & 1 deletion masonry/src/widgets/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion masonry/src/widgets/svg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading