Skip to content
Closed
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
Binary file modified masonry/screenshots/badged_button.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/badged_button_no_badge.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/example_calc_masonry_initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/example_grid_masonry_initial.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/flex_row_baselines_four_center_and_first.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/flex_row_baselines_four_center_and_last.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/flex_row_baselines_four_first_and_first.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/flex_row_baselines_four_first_and_last.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/flex_row_baselines_four_last_and_first.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/flex_row_baselines_four_last_and_last.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/grid_baselines_first.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified masonry/screenshots/grid_baselines_last.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 3 additions & 3 deletions masonry/src/layers/selector_menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,12 +295,12 @@ impl Widget for SelectorMenu {

if !self.children.is_empty() {
let first_child = self.children.first().unwrap();
let (first_baseline, _) = ctx.child_aligned_baselines(first_child);
let (first_baseline, _) = ctx.child_baselines(first_child);
let first_child_origin = ctx.child_origin(first_child);
let first_baseline = first_child_origin.y + first_baseline;

let last_child = self.children.last().unwrap();
let (_, last_baseline) = ctx.child_aligned_baselines(last_child);
let (_, last_baseline) = ctx.child_baselines(last_child);
let last_child_origin = ctx.child_origin(last_child);
let last_baseline = last_child_origin.y + last_baseline;

Expand Down Expand Up @@ -363,7 +363,7 @@ impl Layer for SelectorMenu {
PointerEvent::Down(PointerButtonEvent { state, .. }) => {
let local_pos = ctx.local_position(state.position);

!ctx.border_box_size().to_rect().contains(local_pos)
!ctx.border_box().contains(local_pos)
}
PointerEvent::Cancel(..) => true,
_ => false,
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`.
///
/// 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;

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
181 changes: 177 additions & 4 deletions masonry/src/tests/compose.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
use assert_matches::assert_matches;

use crate::core::{ChildrenIds, NewWidget, Widget, WidgetPod, WidgetTag};
use crate::kurbo::{Affine, Point, Vec2};
use crate::layout::{Length, SizeDef};
use crate::kurbo::{Affine, Point, Rect, Size, Vec2};
use crate::layout::{AsUnit, Length, SizeDef};
use crate::testing::{ModularWidget, Record, TestHarness, TestWidgetExt};
use crate::tests::assert_rect_approx_eq;
use crate::theme::test_property_set;
use crate::widgets::SizedBox;

Expand Down Expand Up @@ -71,13 +72,180 @@ fn request_compose() {
});

// Origin should be "parent_origin + pos + scroll_offset"
let origin = harness.get_widget(child_tag).ctx().window_origin();
let border_box = harness.get_widget(child_tag).ctx().border_box();
let origin = harness
.get_widget(child_tag)
.ctx()
.to_window(border_box.origin());
assert_eq!(
origin.to_vec2(),
Vec2::new(7., 7.) + Point::new(30., 30.).to_vec2() + Vec2::new(8., 8.)
);
}

#[test]
fn pixel_snapping() {
let child_tag = WidgetTag::named("child");
let child = NewWidget::new(SizedBox::empty().size(10.3.px(), 10.3.px())).with_tag(child_tag);
let pos = Point::new(5.1, 5.3);
let parent = ModularWidget::new_parent(child).layout_fn(move |child, ctx, _, size| {
let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into());
ctx.run_layout(child, child_size);
ctx.place_child(child, pos);
ctx.set_baselines(2.4, 2.6);
});
let parent_tag = WidgetTag::named("parent");
let parent = NewWidget::new(parent).with_tag(parent_tag);

let harness = TestHarness::create(test_property_set(), parent);

let child = harness.get_widget(child_tag);
let ctx = child.ctx();
let border_box = ctx.border_box();
let content_box = ctx.content_box();
let layout_content_box = ctx.layout_content_box();
let child_pos = ctx.to_window(border_box.origin());
let first_baseline = harness.get_widget(parent_tag).ctx().first_baseline();
let last_baseline = harness.get_widget(parent_tag).ctx().last_baseline();

assert_eq!(child_pos, Point::new(5.0, 5.0));
assert_eq!(content_box.origin(), Point::ORIGIN);
assert_rect_approx_eq(
"layout_content_box",
layout_content_box,
Rect::from_origin_size(Point::new(0.1, 0.3), Size::new(10.3, 10.3)),
);
assert_eq!(border_box.size(), Size::new(10., 11.));
assert_eq!(first_baseline, 2.4);
assert_eq!(last_baseline, 2.6);
}

#[test]
fn pixel_snapping_after_window_transforms() {
#[track_caller]
fn assert_has_fractional_edge(name: &str, rect: Rect) {
let edges = [rect.x0, rect.y0, rect.x1, rect.y1];
assert!(
edges.iter().any(|edge| (edge - edge.round()).abs() > 1e-9),
"{name}: expected at least one fractional layout edge, got {rect:?}"
);
}

let translated_tag = WidgetTag::unique();
let scaled_tag = WidgetTag::unique();
let flipped_tag = WidgetTag::unique();
let nested_tag = WidgetTag::unique();

let translated = NewWidget::new(SizedBox::empty().size(12.2.px(), 8.4.px()))
.with_tag(translated_tag)
.with_transform(Affine::translate(Vec2::new(0.37, 0.61)))
.erased();
let scaled = NewWidget::new(SizedBox::empty().size(9.3.px(), 11.7.px()))
.with_tag(scaled_tag)
.with_transform(Affine::scale_non_uniform(1.25, 0.8).then_translate(Vec2::new(0.41, 0.29)))
.erased();
let flipped = NewWidget::new(SizedBox::empty().size(10.6.px(), 7.5.px()))
.with_tag(flipped_tag)
.with_transform(
Affine::scale_non_uniform(-0.75, 1.4).then_translate(Vec2::new(0.48, -0.33)),
)
.erased();
let nested = NewWidget::new(SizedBox::empty().size(8.2.px(), 6.6.px()))
.with_tag(nested_tag)
.with_transform(Affine::scale_non_uniform(0.6, 1.35).then_translate(Vec2::new(0.27, 0.43)))
.erased();

let inner = ModularWidget::new_parent(nested)
.layout_fn(|child, ctx, _, size| {
let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into());
ctx.run_layout(child, child_size);
ctx.place_child(child, Point::new(1.7, 2.2));
})
.compose_fn(|child, ctx| {
ctx.set_child_scroll_translation(child, Vec2::new(0.33, -0.47));
});
let inner = NewWidget::new(inner)
.with_transform(Affine::scale_non_uniform(1.5, -0.9).then_translate(Vec2::new(0.19, 0.71)))
.erased();

let outer = ModularWidget::new_parent(inner)
.layout_fn(|child, ctx, _, size| {
let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into());
ctx.run_layout(child, child_size);
ctx.place_child(child, Point::new(4.6, 3.9));
})
.compose_fn(|child, ctx| {
ctx.set_child_scroll_translation(child, Vec2::new(-0.22, 0.35));
});
let outer = NewWidget::new(outer)
.with_transform(Affine::scale_non_uniform(-1.2, 0.7).then_translate(Vec2::new(0.52, -0.24)))
.erased();

let positions = [
Point::new(2.3, 4.7),
Point::new(19.4, 3.6),
Point::new(37.8, 8.2),
Point::new(57.1, 5.4),
];
let scroll_offsets = [
Vec2::new(0.21, 0.36),
Vec2::new(-0.44, 0.52),
Vec2::new(0.68, -0.17),
Vec2::new(-0.31, 0.49),
];

let root = ModularWidget::new(vec![
translated.to_pod(),
scaled.to_pod(),
flipped.to_pod(),
outer.to_pod(),
])
.layout_fn(move |children, ctx, _, size| {
for (idx, child) in children.iter_mut().enumerate() {
let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into());
ctx.run_layout(child, child_size);
ctx.place_child(child, positions[idx]);
}
})
.compose_fn(move |children, ctx| {
for (idx, child) in children.iter_mut().enumerate() {
ctx.set_child_scroll_translation(child, scroll_offsets[idx]);
}
})
.register_children_fn(|children, ctx| {
for child in children {
ctx.register_child(child);
}
})
.children_fn(|children| children.iter().map(|child| child.id()).collect())
.prepare();

let harness = TestHarness::create_with_size(test_property_set(), root, (200, 120));

let assert_snapped = |name: &str, tag: WidgetTag<SizedBox>| {
let widget = harness.get_widget(tag);
let ctx = widget.ctx();
let layout_window = ctx
.window_transform()
.transform_rect_bbox(ctx.layout_border_box());
let visual_window = ctx.window_transform().transform_rect_bbox(ctx.border_box());
let expected_visual_window = Rect::new(
layout_window.x0.round(),
layout_window.y0.round(),
layout_window.x1.round(),
layout_window.y1.round(),
);

assert_has_fractional_edge(name, layout_window);
assert_rect_approx_eq(name, visual_window, expected_visual_window);
};

assert_snapped("translated", translated_tag);
assert_snapped("scaled", scaled_tag);
assert_snapped("flipped", flipped_tag);
assert_snapped("nested", nested_tag);
}

#[test]
fn scroll_pixel_snap() {
let child_tag = WidgetTag::named("child");
Expand All @@ -93,7 +261,12 @@ fn scroll_pixel_snap() {

let harness = TestHarness::create(test_property_set(), parent);

let border_box = harness.get_widget(child_tag).ctx().border_box();
let origin = harness
.get_widget(child_tag)
.ctx()
.to_window(border_box.origin());

// Origin should be rounded to (0., 1.) by pixel-snapping.
let origin = harness.get_widget(child_tag).ctx().window_origin();
assert_eq!(origin, Point::new(0., 1.));
}
Loading
Loading