diff --git a/masonry/ARCHITECTURE.md b/masonry/ARCHITECTURE.md index dd2b166a8..bd514c843 100644 --- a/masonry/ARCHITECTURE.md +++ b/masonry/ARCHITECTURE.md @@ -166,7 +166,7 @@ The current passes are: - **on_xxx_event:** Handles UX-related events, e.g. clicks, text entered, IME updates and accessibility input. Widgets can declare these events as "handled" which has a bunch of semantic implications. - **anim:** Do updates related to an animation frame. - **update:** Handles internal changes to some widgets, e.g. when the widget is marked as "disabled" or Masonry detects that a widget is hovered by a pointer. -- **layout:** Container widgets measure their children with `LayoutCtx::compute_size` and then lay them out with `LayoutCtx::run_layout`, finally giving them a position with `LayoutCtx::place_child`. +- **layout:** Container widgets measure their children with `LayoutCtx::compute_size` and then lay them out with `LayoutCtx::layout_child`, choosing an origin and size for each child. - **compose:** Computes the global transform/origin for every widget. - **paint** Paint every widget. - **accessibility:** Compute every widget's node in the accessibility tree. diff --git a/masonry/examples/layers.rs b/masonry/examples/layers.rs index 3e577ba9f..de2be2fa1 100644 --- a/masonry/examples/layers.rs +++ b/masonry/examples/layers.rs @@ -133,8 +133,7 @@ impl Widget for OverlayBox { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let child_size = ctx.compute_size(&mut self.child, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.child, child_size); - ctx.place_child(&mut self.child, Point::ORIGIN); + ctx.layout_child(&mut self.child, Point::ORIGIN, child_size); } fn paint( diff --git a/masonry/screenshots/badged_button.png b/masonry/screenshots/badged_button.png index 217ad96e5..31173b3e2 100644 Binary files a/masonry/screenshots/badged_button.png and b/masonry/screenshots/badged_button.png differ diff --git a/masonry/screenshots/divider_label.png b/masonry/screenshots/divider_label.png index ddee7f7cd..7e8a93a48 100644 Binary files a/masonry/screenshots/divider_label.png and b/masonry/screenshots/divider_label.png differ diff --git a/masonry/screenshots/example_calc_masonry_initial.png b/masonry/screenshots/example_calc_masonry_initial.png index 9d4bf69e2..543916956 100644 Binary files a/masonry/screenshots/example_calc_masonry_initial.png and b/masonry/screenshots/example_calc_masonry_initial.png differ diff --git a/masonry/screenshots/example_grid_masonry_initial.png b/masonry/screenshots/example_grid_masonry_initial.png index efd83a0b2..c5ac60dd5 100644 Binary files a/masonry/screenshots/example_grid_masonry_initial.png and b/masonry/screenshots/example_grid_masonry_initial.png differ diff --git a/masonry/screenshots/flex_row_baseline_pixel_snapping.png b/masonry/screenshots/flex_row_baseline_pixel_snapping.png index b9a931503..a69e1fcec 100644 Binary files a/masonry/screenshots/flex_row_baseline_pixel_snapping.png and b/masonry/screenshots/flex_row_baseline_pixel_snapping.png differ diff --git a/masonry/screenshots/grid_with_changed_spacing.png b/masonry/screenshots/grid_with_changed_spacing.png index e2acd2027..b6c2a8519 100644 Binary files a/masonry/screenshots/grid_with_changed_spacing.png and b/masonry/screenshots/grid_with_changed_spacing.png differ diff --git a/masonry/screenshots/transforms_pointer_events.png b/masonry/screenshots/transforms_pointer_events.png index 8c3bf2585..b68088e4a 100644 Binary files a/masonry/screenshots/transforms_pointer_events.png and b/masonry/screenshots/transforms_pointer_events.png differ diff --git a/masonry/screenshots/transforms_translation_rotation.png b/masonry/screenshots/transforms_translation_rotation.png index 51690d11d..d222b18cf 100644 Binary files a/masonry/screenshots/transforms_translation_rotation.png and b/masonry/screenshots/transforms_translation_rotation.png differ diff --git a/masonry/src/doc/implementing_container_widget.md b/masonry/src/doc/implementing_container_widget.md index b9aee953e..216283d64 100644 --- a/masonry/src/doc/implementing_container_widget.md +++ b/masonry/src/doc/implementing_container_widget.md @@ -72,11 +72,11 @@ Like with a leaf widget, the `measure` method must compute and return the length Before that, it must call [`MeasureCtx::compute_length`] for each of its own children. For a vertical stack, we want to sum these on the vertical axis and take the largest on the horizontal axis. -Then later in `layout`, it must call [`LayoutCtx::run_layout`] then [`LayoutCtx::place_child`] for each of its own children: +Then later in `layout`, it must call [`LayoutCtx::layout_child`] for each of its own children: -- `LayoutCtx::run_layout` recursively calls `Widget::layout` on the child. - It takes a [`Size`] argument, which is the chosen size of the child. -- `LayoutCtx::place_child` sets the child's position relative to the container. +- `LayoutCtx::layout_child` recursively calls `Widget::layout` on the child. + It takes both a [`Point`] and [`Size`] argument, which are the chosen origin and size of the child. + The child's origin is in relation to the container. The `layout` method *must* iterate over all its children. Not doing so is a logical bug. @@ -144,8 +144,7 @@ impl Widget for VerticalStack { let mut y_offset = 0.0; for child in &mut self.children { let child_size = ctx.compute_size(child, auto_size, context_size); - ctx.run_layout(child, child_size); - ctx.place_child(child, Point::new(0.0, y_offset)); + ctx.layout_child(child, Point::new(0.0, y_offset), child_size); y_offset += child_size.height + self.gap; } @@ -322,11 +321,11 @@ So for instance, if `VerticalStack::children_ids()` returns a list of three chil Pass methods in container widgets should only implement the logic that is specific to the container itself. For instance, a container widget with a background color should implement `paint` to draw the background. +[`Point`]: crate::kurbo::Point [`Size`]: crate::kurbo::Size [`Widget`]: crate::core::Widget [`WidgetPod`]: crate::core::WidgetPod [`WidgetMut`]: crate::core::WidgetMut [`MeasureCtx::compute_length`]: crate::core::MeasureCtx::compute_length -[`LayoutCtx::place_child`]: crate::core::LayoutCtx::place_child -[`LayoutCtx::run_layout`]: crate::core::LayoutCtx::run_layout +[`LayoutCtx::layout_child`]: crate::core::LayoutCtx::layout_child [`RegisterCtx::register_child`]: crate::core::RegisterCtx::register_child diff --git a/masonry/src/doc/vertical_stack.rs b/masonry/src/doc/vertical_stack.rs index cb19b8641..6e88421dc 100644 --- a/masonry/src/doc/vertical_stack.rs +++ b/masonry/src/doc/vertical_stack.rs @@ -140,8 +140,7 @@ impl Widget for VerticalStack { let mut y_offset = 0.0; for child in &mut self.children { let child_size = ctx.compute_size(child, auto_size, context_size); - ctx.run_layout(child, child_size); - ctx.place_child(child, Point::new(0.0, y_offset)); + ctx.layout_child(child, Point::new(0.0, y_offset), child_size); y_offset += child_size.height + self.gap; } diff --git a/masonry/src/layers/selector_menu.rs b/masonry/src/layers/selector_menu.rs index 951bbe389..d608e1962 100644 --- a/masonry/src/layers/selector_menu.rs +++ b/masonry/src/layers/selector_menu.rs @@ -287,20 +287,19 @@ impl Widget for SelectorMenu { let mut y_offset = 0.0; for child in &mut self.children { let child_size = ctx.compute_size(child, auto_size, context_size); - ctx.run_layout(child, child_size); - ctx.place_child(child, Point::new(0.0, y_offset)); + ctx.layout_child(child, Point::new(0.0, y_offset), child_size); y_offset += child_size.height + gap_length; } 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; @@ -363,7 +362,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, diff --git a/masonry/src/layers/tooltip.rs b/masonry/src/layers/tooltip.rs index 78953fff6..f6b8d755d 100644 --- a/masonry/src/layers/tooltip.rs +++ b/masonry/src/layers/tooltip.rs @@ -110,10 +110,9 @@ impl Widget for Tooltip { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let child_size = ctx.compute_size(&mut self.child, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.child, child_size); let child_origin = ((size - child_size).to_vec2() * 0.5).to_point(); - ctx.place_child(&mut self.child, child_origin); + ctx.layout_child(&mut self.child, child_origin, child_size); ctx.derive_baselines(&self.child); } diff --git a/masonry/src/tests/action.rs b/masonry/src/tests/action.rs index 212307e9b..d79ccd652 100644 --- a/masonry/src/tests/action.rs +++ b/masonry/src/tests/action.rs @@ -66,8 +66,7 @@ fn action_source_removed() { }) .layout_fn(move |child, ctx, _props, size| { if let Some(child) = child { - ctx.run_layout(child, size); - ctx.place_child(child, Point::ZERO); + ctx.layout_child(child, Point::ZERO, size); } }) .children_fn(|child| { diff --git a/masonry/src/tests/compose.rs b/masonry/src/tests/compose.rs index 2db3e5891..fb15fb916 100644 --- a/masonry/src/tests/compose.rs +++ b/masonry/src/tests/compose.rs @@ -3,10 +3,11 @@ 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::core::{ChildrenIds, NewWidget, Update, Widget, WidgetPod, WidgetTag}; +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_point_approx_eq, assert_rect_approx_eq}; use crate::theme::test_property_set; use crate::widgets::SizedBox; @@ -32,8 +33,7 @@ fn request_compose() { .measure_fn(|_state, _ctx, _props, _axis, _len_req, _cross_length| Length::ZERO) .layout_fn(|state, ctx, _props, size| { let child_size = ctx.compute_size(&mut state.child, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut state.child, child_size); - ctx.place_child(&mut state.child, state.pos); + ctx.layout_child(&mut state.child, state.pos, child_size); }) .compose_fn(|state, ctx| { ctx.set_child_scroll_translation(&mut state.child, state.offset); @@ -80,9 +80,108 @@ fn request_compose() { ); } +#[test] +fn scroll_translation_updates_composed_geometry_without_layout() { + struct ChildAndOffset { + child: WidgetPod, + offset: Vec2, + } + + let child_tag = WidgetTag::unique(); + let parent_tag = WidgetTag::unique(); + let child = NewWidget::new( + ModularWidget::new(()) + .measure_fn(|_, _, _, _, _, _| 10.3.px()) + .record(), + ) + .with_tag(child_tag); + + let parent = ModularWidget::new(ChildAndOffset { + child: child.erased().to_pod(), + offset: Vec2::ZERO, + }) + .layout_fn(|state, ctx, _, size| { + let child_size = ctx.compute_size(&mut state.child, SizeDef::fit(size), size.into()); + ctx.layout_child(&mut state.child, Point::new(5.1, 5.3), child_size); + }) + .compose_fn(|state, ctx| { + ctx.set_child_scroll_translation(&mut state.child, state.offset); + }) + .register_children_fn(|state, ctx| { + ctx.register_child(&mut state.child); + }) + .children_fn(|state| ChildrenIds::from_slice(&[state.child.id()])) + .prepare() + .with_tag(parent_tag); + + let mut harness = TestHarness::create(test_property_set(), parent); + harness.flush_records_of(child_tag); + + let hit_after_scroll = Point::new(16., 16.); + harness.mouse_move(hit_after_scroll); + let records = harness.take_records_of(child_tag); + assert!( + !records.iter().any(|record| matches!( + record, + Record::PointerEvent(_) | Record::Update(Update::HoveredChanged(true)) + )), + "pointer should not reach the child before scroll translation" + ); + assert!(!harness.get_widget(child_tag).ctx().is_hovered()); + + harness.edit_widget(parent_tag, |mut parent| { + parent.widget.state.offset = Vec2::new(2.2, 0.8); + parent.ctx.request_compose(); + }); + + let records = harness.take_records_of(child_tag); + assert!( + !records + .iter() + .any(|record| matches!(record, Record::Layout(_))), + "scroll translation should not rerun child layout" + ); + assert!( + records.iter().any(|record| matches!( + record, + Record::PointerEvent(_) | Record::Update(Update::HoveredChanged(true)) + )), + "stationary pointer should reach the child after scroll translation" + ); + assert!(harness.get_widget(child_tag).ctx().is_hovered()); + + let child = harness.get_widget(child_tag); + let child_id = child.id(); + let ctx = child.ctx(); + assert_eq!(ctx.border_box().size(), Size::new(10., 11.)); + assert_rect_approx_eq( + "window border box", + ctx.window_transform().transform_rect_bbox(ctx.border_box()), + Rect::new(7., 6., 17., 17.), + ); + + let _ = harness.redraw(); + let access_bounds = harness + .access_node(child_id) + .unwrap() + .bounding_box() + .unwrap(); + assert_rect_approx_eq( + "access bounds", + Rect::new( + access_bounds.x0, + access_bounds.y0, + access_bounds.x1, + access_bounds.y1, + ), + Rect::new(7., 6., 17., 17.), + ); +} + #[test] fn scroll_pixel_snap() { - let child_tag = WidgetTag::named("child"); + let parent_tag = WidgetTag::unique(); + let child_tag = WidgetTag::unique(); let child = NewWidget::new(SizedBox::empty()).with_tag(child_tag); let parent = ModularWidget::new_parent(child) @@ -91,13 +190,23 @@ fn scroll_pixel_snap() { ctx.set_child_scroll_translation(state, offset); }) - .prepare(); + .prepare() + .with_tag(parent_tag); - let harness = TestHarness::create(test_property_set(), parent); + let mut harness = TestHarness::create(test_property_set(), parent); // Origin should be rounded to (0., 1.) by pixel-snapping. let child = harness.get_widget(child_tag); let ctx = child.ctx(); let origin = ctx.to_window(ctx.border_box().origin()); assert_eq!(origin, Point::new(0., 1.)); + + harness.edit_widget(parent_tag, |mut parent| { + parent.set_snap_disabled(true); + }); + + let child = harness.get_widget(child_tag); + let ctx = child.ctx(); + let origin = ctx.to_window(ctx.border_box().origin()); + assert_point_approx_eq("origin", origin, Point::new(0.1, 0.9)); } diff --git a/masonry/src/tests/layout.rs b/masonry/src/tests/layout.rs index db4109172..f35eaefec 100644 --- a/masonry/src/tests/layout.rs +++ b/masonry/src/tests/layout.rs @@ -6,11 +6,12 @@ use std::rc::Rc; use assert_matches::assert_matches; -use crate::core::{NewWidget, Widget, WidgetTag}; -use crate::kurbo::{Insets, Point, Rect, Size}; +use crate::core::{ChildrenIds, NewWidget, Widget, WidgetPod, WidgetTag, WindowEvent}; +use crate::kurbo::{Affine, Insets, Point, Rect, Size, Vec2}; use crate::layout::{AsUnit, Length, SizeDef}; use crate::properties::{BorderWidth, Dimensions, Padding}; -use crate::testing::{ModularWidget, TestHarness, TestWidgetExt, assert_debug_panics}; +use crate::testing::{ModularWidget, Record, TestHarness, TestWidgetExt, assert_debug_panics}; +use crate::tests::{assert_point_approx_eq, assert_rect_approx_eq, assert_vec2_approx_eq}; use crate::theme::test_property_set; use crate::widgets::{Button, ChildAlignment, Flex, Portal, SizedBox, ZStack}; @@ -41,7 +42,7 @@ fn layout_simple() { let harness = TestHarness::create(test_property_set(), widget); - let first_box_size = harness.get_widget(tag_1).ctx().border_box_size(); + let first_box_size = harness.get_widget(tag_1).ctx().border_box().size(); let first_box_paint_rect = harness.get_widget(tag_1).ctx().paint_box(); assert_eq!(first_box_size.width, BOX_WIDTH); @@ -58,44 +59,13 @@ fn forget_to_recurse_layout() { let widget = ModularWidget::new_parent(Flex::row().prepare()) .measure_fn(|_, _, _, _, _, _| Length::ZERO) .layout_fn(|_child, _ctx, _, _| { - // We forget to call ctx.run_layout(); + // We forget to call ctx.layout_child(); }) .prepare(); assert_debug_panics!( TestHarness::create(test_property_set(), widget), - "LayoutCtx::run_layout() was not called" - ); -} - -#[test] -fn forget_to_call_place_child() { - let widget = ModularWidget::new_parent(Flex::row().prepare()) - .layout_fn(|child, ctx, _, size| { - // We call ctx.run_layout(), but forget place_child - ctx.run_layout(child, size); - }) - .prepare(); - - assert_debug_panics!( - TestHarness::create(test_property_set(), widget), - "LayoutCtx::place_child() was not called" - ); -} - -#[test] -fn call_place_child_before_layout() { - let widget = ModularWidget::new_parent(Flex::row().prepare()) - .measure_fn(|_, _, _, _, _, _| Length::ZERO) - .layout_fn(|child, ctx, _, _| { - // We call ctx.place_child(), but forget run_layout - ctx.place_child(child, Point::ORIGIN); - }) - .prepare(); - - assert_debug_panics!( - TestHarness::create(test_property_set(), widget), - "trying to call 'place_child'" + "LayoutCtx::layout_child() was not called" ); } @@ -114,8 +84,7 @@ fn call_child_size_before_layout() { layout_count_for_fn.set(layout_count_for_fn.get() + 1); let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); - ctx.run_layout(child, child_size); - ctx.place_child(child, Point::ORIGIN); + ctx.layout_child(child, Point::ORIGIN, child_size); }); let widget = NewWidget::new(parent).with_tag(parent_tag); @@ -130,12 +99,11 @@ fn call_child_size_before_layout() { } #[test] -fn run_layout_on_stashed() { +fn layout_child_on_stashed() { let parent_tag = WidgetTag::named("parent"); let widget = ModularWidget::new_parent(Flex::row().prepare()).layout_fn(|child, ctx, _, size| { - ctx.run_layout(child, size); - ctx.place_child(child, Point::ZERO); + ctx.layout_child(child, Point::ZERO, size); }); let widget = NewWidget::new(widget).with_tag(parent_tag); @@ -151,15 +119,14 @@ fn run_layout_on_stashed() { } #[test] -fn stash_then_run_layout() { +fn stash_then_layout_child() { let parent_tag = WidgetTag::named("parent"); let widget = ModularWidget::new_parent(Flex::row().prepare()).layout_fn(|child, ctx, _, size| { // We check that stashing a widget is effective "immediately" // and triggers an error. ctx.set_stashed(child, true); - ctx.run_layout(child, size); - ctx.place_child(child, Point::ZERO); + ctx.layout_child(child, Point::ZERO, size); }); let widget = NewWidget::new(widget).with_tag(parent_tag); @@ -170,15 +137,14 @@ fn stash_then_run_layout() { } #[test] -fn unstash_then_run_layout() { +fn unstash_then_layout_child() { let parent_tag = WidgetTag::named("parent"); let widget = ModularWidget::new_parent(Flex::row().prepare()).layout_fn(|child, ctx, _, size| { // We check that unstashing a widget is effective "immediately" // and avoids an error. ctx.set_stashed(child, false); - ctx.run_layout(child, size); - ctx.place_child(child, Point::ZERO); + ctx.layout_child(child, Point::ZERO, size); }); let widget = NewWidget::new(widget).with_tag(parent_tag); @@ -220,8 +186,7 @@ fn skip_layout_when_cached() { // Measurements will still happen with debug assertions enabled because we verify the cache. #[cfg(debug_assertions)] - let button_records_iter = - button_records_iter.filter(|r| !matches!(r, masonry_testing::Record::Measure(_))); + let button_records_iter = button_records_iter.filter(|r| !matches!(r, Record::Measure(_))); let button_records: Vec<_> = button_records_iter.collect(); assert_matches!(button_records[..], []); @@ -229,16 +194,23 @@ fn skip_layout_when_cached() { #[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 child_tag = WidgetTag::unique(); + let child = ModularWidget::new(()) + .layout_fn(|_, _ctx, _, size| { + assert_eq!(size, Size::new(10., 11.)); + }) + .prepare() + .with_tag(child_tag) + .with_props(Dimensions::fixed(10.3.px(), 10.3.px())); 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.layout_child(child, pos, child_size); + assert_eq!(ctx.child_size(child), Size::new(10., 11.)); + assert_point_approx_eq("child_origin", ctx.child_origin(child), Point::new(5., 5.)); ctx.set_baselines(2.4, 2.6); }); - let parent_tag = WidgetTag::named("parent"); + let parent_tag = WidgetTag::unique(); let parent = NewWidget::new(parent).with_tag(parent_tag); let harness = TestHarness::create(test_property_set(), parent); @@ -259,6 +231,170 @@ fn pixel_snapping() { assert_eq!(last_baseline, 2.6); } +#[test] +fn equal_nested_sizes_snap_consistently() { + let parent_tag = WidgetTag::unique(); + let child_tag = WidgetTag::unique(); + + let child = NewWidget::new(SizedBox::empty()).with_tag(child_tag); + let parent = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + ctx.layout_child(child, Point::ORIGIN, size); + }) + .prepare(); + let parent = parent + .with_tag(parent_tag) + .with_props(Dimensions::fixed(70.6.px(), 10.2.px())); + + let root = ModularWidget::new_parent(parent) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::new(84.7, 0.0), child_size); + }) + .prepare(); + + let harness = TestHarness::create(test_property_set(), root); + let parent = harness.get_widget(parent_tag); + let child = harness.get_widget(child_tag); + let parent_ctx = parent.ctx(); + let child_ctx = child.ctx(); + let parent_rect = parent_ctx + .window_transform() + .transform_rect_bbox(parent_ctx.border_box()); + let child_rect = child_ctx + .window_transform() + .transform_rect_bbox(child_ctx.border_box()); + + assert_eq!( + parent_ctx.border_box().size(), + child_ctx.border_box().size() + ); + assert_rect_approx_eq("parent_rect", parent_rect, Rect::new(85., 0., 155., 10.)); + assert_rect_approx_eq("child_rect", child_rect, Rect::new(85., 0., 155., 10.)); +} + +#[test] +fn shared_edges_snap_without_gaps_or_overlaps() { + let tag_1 = WidgetTag::unique(); + let tag_2 = WidgetTag::unique(); + let tag_3 = WidgetTag::unique(); + + let child_1 = NewWidget::new(SizedBox::empty()).with_tag(tag_1).erased(); + let child_2 = NewWidget::new(SizedBox::empty()).with_tag(tag_2).erased(); + let child_3 = NewWidget::new(SizedBox::empty()).with_tag(tag_3).erased(); + + let root = ModularWidget::new(vec![child_1.to_pod(), child_2.to_pod(), child_3.to_pod()]) + .layout_fn(|children, ctx, _, _| { + let placements = [ + (Point::new(0.0, 0.0), Size::new(33.3, 10.0)), + (Point::new(33.3, 0.0), Size::new(33.3, 10.0)), + (Point::new(66.6, 0.0), Size::new(33.4, 10.0)), + ]; + for (child, (origin, size)) in children.iter_mut().zip(placements) { + ctx.layout_child(child, origin, size); + } + }) + .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(test_property_set(), root); + let rect = |tag: WidgetTag| { + let widget = harness.get_widget(tag); + let ctx = widget.ctx(); + ctx.window_transform().transform_rect_bbox(ctx.border_box()) + }; + let rect_1 = rect(tag_1); + let rect_2 = rect(tag_2); + let rect_3 = rect(tag_3); + + assert_rect_approx_eq("rect_1", rect_1, Rect::new(0., 0., 33., 10.)); + assert_rect_approx_eq("rect_2", rect_2, Rect::new(33., 0., 67., 10.)); + assert_rect_approx_eq("rect_3", rect_3, Rect::new(67., 0., 100., 10.)); + assert_eq!(rect_1.x1, rect_2.x0); + assert_eq!(rect_2.x1, rect_3.x0); +} + +#[test] +fn rescale_requests_layout_for_pixel_snapping() { + let child_tag = WidgetTag::unique(); + let child = + NewWidget::new(SizedBox::empty().size(10.3.px(), 10.3.px()).record()).with_tag(child_tag); + let parent = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::new(5.1, 5.3), child_size); + }) + .prepare(); + + let mut harness = TestHarness::create(test_property_set(), parent); + + assert_eq!( + harness.get_widget(child_tag).ctx().border_box().size(), + Size::new(10., 11.) + ); + + harness.flush_records_of(child_tag); + harness.process_window_event(WindowEvent::Rescale(2.0)); + + assert!( + harness + .take_records_of(child_tag) + .into_iter() + .any(|record| matches!(record, Record::Layout(_))), + "scale factor changes must rerun layout because layout sizes can change under pixel snapping" + ); + assert_eq!( + harness.get_widget(child_tag).ctx().border_box().size(), + Size::new(10.5, 10.) + ); +} + +#[test] +fn pixel_snapping_can_be_disabled() { + let parent_tag = WidgetTag::unique(); + let child_tag = WidgetTag::unique(); + 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.layout_child(child, pos, child_size); + }); + let parent = NewWidget::new(parent) + .with_snap_disabled(true) + .with_tag(parent_tag); + + let mut 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 child_pos = ctx.to_window(border_box.origin()); + + assert_rect_approx_eq( + "border_box", + border_box, + Rect::from_origin_size(Point::ORIGIN, Size::new(10.3, 10.3)), + ); + assert_eq!(child_pos, pos); + + harness.edit_widget(parent_tag, |mut parent| { + parent.set_snap_disabled(false); + }); + + let child = harness.get_widget(child_tag); + let ctx = child.ctx(); + let border_box = ctx.border_box(); + let child_pos = ctx.to_window(border_box.origin()); + + assert_eq!(child_pos, Point::new(5., 5.)); + assert_eq!(border_box.size(), Size::new(10., 11.)); +} + #[test] fn layout_insets() { const BOX_WIDTH: f64 = 50.; @@ -327,9 +463,9 @@ fn content_box() { let harness = TestHarness::create(test_property_set(), hero); let border_box = harness.get_widget(tag).ctx().border_box(); - let border_box_size = harness.get_widget(tag).ctx().border_box_size(); + let border_box_size = border_box.size(); let content_box = harness.get_widget(tag).ctx().content_box(); - let content_box_size = harness.get_widget(tag).ctx().content_box_size(); + let content_box_size = content_box.size(); let border_box_translation = harness.get_widget(tag).ctx().border_box_translation(); let expected_border_box_size = Size::new(100., 100.); @@ -357,3 +493,363 @@ fn content_box() { assert_eq!(border_box, expected_border_box); assert_eq!(content_box, expected_content_box); } + +#[test] +fn boxes_match_without_insets_or_snapping() { + let tag = WidgetTag::unique(); + let child = NewWidget::new(SizedBox::empty().size(10.px(), 8.px())).with_tag(tag); + let root = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::new(2., 3.), child_size); + }) + .prepare(); + + let harness = TestHarness::create(test_property_set(), root); + let child = harness.get_widget(tag); + let ctx = child.ctx(); + + // Everything besides the bounding box will be exactly the same + let local_box = Rect::new(0., 0., 10., 8.); + assert_rect_approx_eq("content_box", ctx.content_box(), local_box); + assert_rect_approx_eq("border_box", ctx.border_box(), local_box); + assert_rect_approx_eq("paint_box", ctx.paint_box(), local_box); + assert_rect_approx_eq( + "bounding_box", + ctx.bounding_box(), + Rect::new(2., 3., 12., 11.), + ); +} + +#[test] +fn boxes_use_content_box_coordinates() { + let tag = WidgetTag::unique(); + + let child = ModularWidget::new(()) + .layout_fn(|_, ctx, _, _| { + ctx.set_paint_insets(Insets::new(5.9, 6.1, 7.4, 8.2)); + }) + .prepare() + .with_tag(tag) + .with_props(( + Dimensions::fixed(23.4.px(), 17.6.px()), + BorderWidth::all(0.7.px()), + Padding { + left: 1.2.px(), + right: 2.3.px(), + top: 3.4.px(), + bottom: 4.5.px(), + }, + )); + + let root = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::new(5.3, 7.6), child_size); + }) + .prepare() + .with_props(Dimensions::fixed(80.px(), 80.px())); + + let harness = TestHarness::create(test_property_set(), root); + let child = harness.get_widget(tag); + let ctx = child.ctx(); + + // Border 0.7 + padding (1.2,3.4) gives top-left content inset (1.9,4.1). + // The chosen child origin is (5.3,7.6), and snapping rounds its border-box to (5,8)-(29,25). + assert_vec2_approx_eq( + "border_box_translation", + ctx.border_box_translation(), + Vec2::new(1.9, 4.1), + ); + + assert_rect_approx_eq( + "content_box", + ctx.content_box(), + Rect::new(0., 0., 19.1, 7.7), + ); + assert_rect_approx_eq( + "border_box", + ctx.border_box(), + Rect::new(-1.9, -4.1, 22.1, 12.9), + ); + assert_rect_approx_eq( + "paint_box", + ctx.paint_box(), + Rect::new(-5.9, -6.1, 26.5, 15.9), + ); + assert_rect_approx_eq( + "bounding_box", + ctx.bounding_box(), + Rect::new(1., 6., 33.4, 28.), + ); + + assert_point_approx_eq( + "to_window content box origin", + ctx.to_window(Point::ORIGIN), + Point::new(6.9, 12.1), + ); + assert_point_approx_eq( + "to_window border box origin", + ctx.to_window(ctx.border_box().origin()), + Point::new(5., 8.), + ); + assert_point_approx_eq( + "to_local content box origin", + ctx.to_local(Point::new(6.9, 12.1)), + Point::ORIGIN, + ); + assert_point_approx_eq( + "window_transform", + ctx.window_transform() * Point::new(2., 1.), + Point::new(8.9, 13.1), + ); +} + +#[test] +fn resnap_layout_when_intermediate_size_is_unchanged() { + let root_tag = WidgetTag::unique(); + let leaf_tag = WidgetTag::unique(); + + let leaf = + NewWidget::new(SizedBox::empty().size(10.3.px(), 10.3.px()).record()).with_tag(leaf_tag); + + let middle = ModularWidget::new_parent(leaf) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::ORIGIN, child_size); + }) + .prepare() + .with_props(Dimensions::fixed(20.px(), 20.px())); + + let root = ModularWidget::new_parent(middle) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::ORIGIN, child_size); + }) + .prepare() + .with_tag(root_tag); + + let mut harness = TestHarness::create(test_property_set(), root); + + assert_eq!( + harness.get_widget(leaf_tag).ctx().border_box().size(), + Size::new(10., 10.) + ); + + harness.flush_records_of(leaf_tag); + harness.edit_widget(root_tag, |mut root| { + root.set_transform(Affine::scale(2.)); + }); + + assert!( + harness + .take_records_of(leaf_tag) + .into_iter() + .any(|record| matches!(record, Record::Layout(_))), + "leaf layout should rerun because snapping depends on inherited scale" + ); + assert_eq!( + harness.get_widget(leaf_tag).ctx().border_box().size(), + Size::new(10.5, 10.5) + ); +} + +#[test] +fn cached_layout_origin_changes_update_window_transforms() { + struct MovingRoot { + child: WidgetPod, + layout_origin: Point, + move_origin: Option, + } + + fn assert_no_layout(records: Vec, name: &str) { + assert!( + !records + .into_iter() + .any(|record| matches!(record, Record::Layout(_))), + "{name} should have reused its cached layout" + ); + } + + let root_tag = WidgetTag::unique(); + let middle_tag = WidgetTag::unique(); + let leaf_tag = WidgetTag::unique(); + + let leaf = NewWidget::new(SizedBox::empty().size(5.px(), 6.px()).record()).with_tag(leaf_tag); + + let middle = ModularWidget::new_parent(leaf).layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::new(3., 4.), child_size); + }); + let middle = NewWidget::new(middle.record()) + .with_tag(middle_tag) + .with_props(Dimensions::fixed(20.px(), 20.px())); + + let root_state = MovingRoot { + child: middle.erased().to_pod(), + layout_origin: Point::ORIGIN, + move_origin: None, + }; + let root = ModularWidget::new(root_state) + .register_children_fn(|state, ctx| { + ctx.register_child(&mut state.child); + }) + .layout_fn(|state, ctx, _, size| { + let child_size = ctx.compute_size(&mut state.child, SizeDef::fit(size), size.into()); + ctx.layout_child(&mut state.child, state.layout_origin, child_size); + if let Some(move_origin) = state.move_origin { + ctx.move_child(&mut state.child, move_origin); + } + }) + .children_fn(|state| ChildrenIds::from_slice(&[state.child.id()])) + .prepare() + .with_tag(root_tag); + + let mut harness = TestHarness::create(test_property_set(), root); + + assert_point_approx_eq( + "initial middle window origin", + harness.get_widget(middle_tag).ctx().window_transform() * Point::ORIGIN, + Point::ORIGIN, + ); + assert_point_approx_eq( + "initial leaf window origin", + harness.get_widget(leaf_tag).ctx().window_transform() * Point::ORIGIN, + Point::new(3., 4.), + ); + + harness.flush_records_of(middle_tag); + harness.flush_records_of(leaf_tag); + harness.edit_widget(root_tag, |mut root| { + root.widget.state.layout_origin = Point::ORIGIN; + root.widget.state.move_origin = None; + root.ctx.request_layout(); + }); + assert_no_layout(harness.take_records_of(middle_tag), "middle"); + assert_no_layout(harness.take_records_of(leaf_tag), "leaf"); + + harness.edit_widget(root_tag, |mut root| { + root.widget.state.layout_origin = Point::new(10., 20.); + root.widget.state.move_origin = None; + root.ctx.request_layout(); + }); + assert_no_layout(harness.take_records_of(middle_tag), "middle"); + assert_no_layout(harness.take_records_of(leaf_tag), "leaf"); + assert_point_approx_eq( + "moved middle window origin", + harness.get_widget(middle_tag).ctx().window_transform() * Point::ORIGIN, + Point::new(10., 20.), + ); + assert_point_approx_eq( + "moved leaf window origin", + harness.get_widget(leaf_tag).ctx().window_transform() * Point::ORIGIN, + Point::new(13., 24.), + ); + + harness.edit_widget(root_tag, |mut root| { + root.widget.state.layout_origin = Point::new(10., 20.); + root.widget.state.move_origin = Some(Point::new(30., 40.)); + root.ctx.request_layout(); + }); + assert_no_layout(harness.take_records_of(middle_tag), "middle"); + assert_no_layout(harness.take_records_of(leaf_tag), "leaf"); + assert_point_approx_eq( + "move_child middle window origin", + harness.get_widget(middle_tag).ctx().window_transform() * Point::ORIGIN, + Point::new(30., 40.), + ); + assert_point_approx_eq( + "move_child leaf window origin", + harness.get_widget(leaf_tag).ctx().window_transform() * Point::ORIGIN, + Point::new(33., 44.), + ); +} + +#[test] +fn move_child_respects_snap_state() { + fn check(snap_disabled: bool, expected_size: Size, expected_origin: Point) { + let child = ModularWidget::new(()) + .prepare() + .with_snap_disabled(snap_disabled) + .with_props(Dimensions::fixed(10.3.px(), 10.3.px())); + + let root = ModularWidget::new_parent(child) + .layout_fn(move |child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::new(5.1, 5.3), child_size); + ctx.move_child(child, Point::new(6.4, 7.6)); + + assert_eq!(ctx.child_size(child), expected_size); + assert_point_approx_eq("child_origin", ctx.child_origin(child), expected_origin); + }) + .prepare() + .with_props(Dimensions::fixed(80.px(), 80.px())); + + let _harness = TestHarness::create(test_property_set(), root); + } + + check(false, Size::new(10., 11.), Point::new(6., 8.)); + check(true, Size::new(10.3, 10.3), Point::new(6.4, 7.6)); +} + +#[test] +fn move_child_quantizes_delta_in_window_space() { + let child = ModularWidget::new(()) + .prepare() + .with_props(Dimensions::fixed(10.px(), 10.px())); + + let root = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::ORIGIN, child_size); + ctx.move_child(child, Point::new(0.3, 1.2)); + + assert_point_approx_eq("child_origin", ctx.child_origin(child), Point::new(0.5, 2.)); + }) + .prepare() + .with_transform(Affine::scale_non_uniform(-2., 0.5)) + .with_props(Dimensions::fixed(80.px(), 80.px())); + + let _harness = TestHarness::create(test_property_set(), root); +} + +#[test] +fn content_box_clamps_when_insets_exceed_size() { + let tag = WidgetTag::unique(); + + let child = ModularWidget::new(()).prepare().with_tag(tag).with_props(( + Dimensions::fixed(2.5.px(), 2.5.px()), + BorderWidth::all(0.5.px()), + Padding { + left: 0.7.px(), + right: 0.8.px(), + top: 0.6.px(), + bottom: 0.9.px(), + }, + )); + + let root = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::new(0.6, 0.6), child_size); + }) + .prepare() + .with_props(Dimensions::fixed(20.px(), 20.px())); + + let harness = TestHarness::create(test_property_set(), root); + let child = harness.get_widget(tag); + let ctx = child.ctx(); + + // Border 0.5 + padding (0.7,0.6) gives top-left content inset (1.2,1.1). + assert_vec2_approx_eq( + "border_box_translation", + ctx.border_box_translation(), + Vec2::new(1.2, 1.1), + ); + assert_rect_approx_eq( + "border_box", + ctx.border_box(), + Rect::new(-1.2, -1.1, 0.8, 0.9), + ); + assert_rect_approx_eq("content_box", ctx.content_box(), Rect::ZERO); +} diff --git a/masonry/src/tests/mod.rs b/masonry/src/tests/mod.rs index 5c56e3162..809cb41d9 100644 --- a/masonry/src/tests/mod.rs +++ b/masonry/src/tests/mod.rs @@ -5,7 +5,7 @@ //! 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; +use crate::kurbo::{Point, Rect, Vec2}; mod accessibility; mod action; @@ -16,6 +16,7 @@ mod layout; mod mutate; mod paint; mod properties; +mod transforms; mod update; mod widget_tag; @@ -27,6 +28,18 @@ pub(crate) fn assert_approx_eq(name: &str, actual: f64, expected: f64) { ); } +#[track_caller] +pub(crate) fn assert_point_approx_eq(name: &str, actual: Point, expected: Point) { + assert_approx_eq(&format!("{name}.x"), actual.x, expected.x); + assert_approx_eq(&format!("{name}.y"), actual.y, expected.y); +} + +#[track_caller] +pub(crate) fn assert_vec2_approx_eq(name: &str, actual: Vec2, expected: Vec2) { + assert_approx_eq(&format!("{name}.x"), actual.x, expected.x); + assert_approx_eq(&format!("{name}.y"), actual.y, expected.y); +} + #[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); diff --git a/masonry/src/tests/paint.rs b/masonry/src/tests/paint.rs index 5852d5895..ead4166df 100644 --- a/masonry/src/tests/paint.rs +++ b/masonry/src/tests/paint.rs @@ -81,8 +81,7 @@ fn paint_order() { let mut pos = Point::ZERO; for child in children { let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); - ctx.run_layout(child, child_size); - ctx.place_child(child, pos); + ctx.layout_child(child, pos, child_size); pos += Vec2::new(SQUARE_SIZE / 2., SQUARE_SIZE / 2.); } }) diff --git a/masonry/src/tests/transforms.rs b/masonry/src/tests/transforms.rs index bf81b52bd..02d7d6125 100644 --- a/masonry/src/tests/transforms.rs +++ b/masonry/src/tests/transforms.rs @@ -3,18 +3,16 @@ //! Tests related to transforms. -use std::f64::consts::PI; +use core::f64::consts::PI; -use masonry_testing::WrapperWidget; - -use crate::core::{NewWidget, PropertySet, Widget, WidgetOptions}; -use crate::kurbo::{Affine, Vec2}; -use crate::layout::AsUnit; +use crate::core::{NewWidget, PropertySet, Widget, WidgetTag}; +use crate::kurbo::{Affine, Point, Rect, Size, Vec2}; +use crate::layout::{AsUnit, SizeDef, UnitPoint}; use crate::peniko::color::palette; -use crate::properties::types::UnitPoint; -use crate::properties::{Background, BorderColor, BorderWidth}; -use crate::testing::{TestHarness, assert_render_snapshot}; -use crate::theme::default_property_set; +use crate::properties::{Background, BorderColor, BorderWidth, Dimensions, Padding}; +use crate::testing::{ModularWidget, TestHarness, WrapperWidget, assert_render_snapshot}; +use crate::tests::{assert_point_approx_eq, assert_vec2_approx_eq}; +use crate::theme::test_property_set; use crate::widgets::{Button, ChildAlignment, Label, SizedBox, ZStack}; fn blue_box(inner: impl Widget) -> impl Widget { @@ -24,50 +22,246 @@ fn blue_box(inner: impl Widget) -> impl Widget { box_props.insert(BorderWidth::all(2.px())); WrapperWidget::new( - SizedBox::new(inner.prepare()) - .width(200.px()) - .height(100.px()) - .with_props(box_props), + NewWidget::new( + SizedBox::new(inner.prepare()) + .width(100.px()) + .height(50.px()), + ) + .with_props(box_props), ) } #[test] fn transforms_translation_rotation() { let translation = Vec2::new(100.0, 50.0); - let transformed_widget = NewWidget::new(blue_box(Label::new("Background"))).with_options( - // Currently there's no support for changing the transform-origin, which is currently at the top left. + let transformed_widget = NewWidget::new(blue_box(Label::new("Background"))).with_transform( + // Currently there's no support for changing the transform-origin, + // which is currently at the top left. // This rotates around the center of the widget - WidgetOptions { - transform: Affine::translate(-translation) - .then_rotate(PI * 0.25) - .then_translate(translation), - ..Default::default() - }, + Affine::translate(-translation) + .then_rotate(PI * 0.25) + .then_translate(translation), ); let widget = ZStack::new() - .with_fixed(transformed_widget, ChildAlignment::ParentAligned) + .with(transformed_widget, ChildAlignment::ParentAligned) .prepare(); - let mut harness = TestHarness::create(default_property_set(), widget); + let mut harness = TestHarness::create(test_property_set(), widget); + assert_render_snapshot!(harness, "transforms_translation_rotation"); } #[test] fn transforms_pointer_events() { - let transformed_widget = NewWidget::new(blue_box(ZStack::new().with_fixed( - Button::with_text("Should be pressed").prepare(), - UnitPoint::BOTTOM_RIGHT, - ))) - .with_options(WidgetOptions { - transform: Affine::rotate(PI * 0.125).then_translate(Vec2::new(100.0, 50.0)), - ..Default::default() - }); + let transformed_widget = NewWidget::new(blue_box( + ZStack::new().with(Button::with_text("OK").prepare(), UnitPoint::BOTTOM_RIGHT), + )) + .with_transform(Affine::rotate(PI * 0.125).then_translate(Vec2::new(100.0, 50.0))); let widget = ZStack::new() - .with_fixed(transformed_widget, ChildAlignment::ParentAligned) + .with(transformed_widget, ChildAlignment::ParentAligned) .prepare(); - let mut harness = TestHarness::create(default_property_set(), widget); - harness.mouse_move((335.0, 350.0)); // Should hit the last "d" of the button text + let mut harness = TestHarness::create(test_property_set(), widget); + + harness.mouse_move((300.0, 280.0)); // Should hit the "O" of the button text harness.mouse_button_press(None); + assert_render_snapshot!(harness, "transforms_pointer_events"); } + +#[test] +fn transforms_handle_content_box_space_translation() { + let tag = WidgetTag::unique(); + let child = NewWidget::new(SizedBox::empty().size(10.px(), 8.px())) + .with_tag(tag) + .with_transform(Affine::scale_non_uniform(2., 3.)) + .with_props(( + BorderWidth::all(0.5.px()), + Padding { + left: 1.px(), + right: 0.5.px(), + top: 2.px(), + bottom: 1.5.px(), + }, + )); + + let root = ModularWidget::new_parent(child) + .layout_fn(|child, ctx, _, size| { + let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); + ctx.layout_child(child, Point::new(5., 7.), child_size); + }) + .prepare() + .with_props(Dimensions::fixed(40.px(), 40.px())); + + let harness = TestHarness::create(test_property_set(), root); + let child = harness.get_widget(tag); + let ctx = child.ctx(); + + // Border 0.5 + padding (1.0,2.0) gives top-left content inset (1.5,2.5). + assert_vec2_approx_eq( + "border_box_translation", + ctx.border_box_translation(), + Vec2::new(1.5, 2.5), + ); + // (0,0) + content inset (1.5,2.5), then scale (2,3) and add layout origin (5,7). + assert_point_approx_eq( + "to_window content origin", + ctx.to_window(Point::ORIGIN), + Point::new(8., 14.5), + ); + // (2,1) + content inset (1.5,2.5) = (3.5,3.5), then scale (2,3) and add (5,7). + assert_point_approx_eq( + "to_window local point", + ctx.to_window(Point::new(2., 1.)), + Point::new(12., 17.5), + ); + // Border box origin (-1.5,-2.5) cancels content inset, leaving the layout origin (5,7). + assert_point_approx_eq( + "to_window border origin", + ctx.to_window(ctx.border_box().origin()), + Point::new(5., 7.), + ); + // Inverse: ((12,17.5) - (5,7)) / (2,3) = (3.5,3.5), then subtract inset (1.5,2.5). + assert_point_approx_eq( + "to_local", + ctx.to_local(Point::new(12., 17.5)), + Point::new(2., 1.), + ); + // window_transform bakes in the required calculations and achieves the same result. + assert_point_approx_eq( + "window_transform", + ctx.window_transform() * Point::new(2., 1.), + Point::new(12., 17.5), + ); +} + +#[test] +fn unsupported_transform_disables_pixel_snap() { + let child_tag = WidgetTag::unique(); + let child = NewWidget::new(SizedBox::empty().size(10.3.px(), 10.3.px())).with_tag(child_tag); + 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.layout_child(child, Point::new(5.1, 5.3), child_size); + }); + let parent = NewWidget::new(parent).with_transform(Affine::rotate(0.25)); + + let harness = TestHarness::create(test_property_set(), parent); + + let child = harness.get_widget(child_tag); + let ctx = child.ctx(); + + assert_eq!(ctx.border_box().size(), Size::new(10.3, 10.3)); +} + +#[test] +fn pixel_snapping_after_window_transforms() { + #[track_caller] + fn assert_integer_edges(name: &str, rect: Rect) { + let edges = [rect.x0, rect.y0, rect.x1, rect.y1]; + assert!( + edges.iter().all(|edge| (edge - edge.round()).abs() < 1e-9), + "{name}: expected integer edges, 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.layout_child(child, Point::new(1.7, 2.2), child_size); + }) + .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.layout_child(child, Point::new(4.6, 3.9), child_size); + }) + .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.layout_child(child, positions[idx], child_size); + } + }) + .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_pixel_aligned = |name: &str, tag: WidgetTag| { + let widget = harness.get_widget(tag); + let ctx = widget.ctx(); + let window_border_box = ctx.window_transform().transform_rect_bbox(ctx.border_box()); + + assert_integer_edges(name, window_border_box); + }; + + assert_pixel_aligned("translated", translated_tag); + assert_pixel_aligned("scaled", scaled_tag); + assert_pixel_aligned("flipped", flipped_tag); + assert_pixel_aligned("nested", nested_tag); +} diff --git a/masonry/src/widgets/align.rs b/masonry/src/widgets/align.rs index bf6445f85..071c813f9 100644 --- a/masonry/src/widgets/align.rs +++ b/masonry/src/widgets/align.rs @@ -165,14 +165,13 @@ impl Widget for Align { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let child_size = ctx.compute_size(&mut self.child, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.child, child_size); let extra_width = size.width - child_size.width; let extra_height = size.height - child_size.height; let child_origin = self .align .resolve(Rect::new(0., 0., extra_width, extra_height)); - ctx.place_child(&mut self.child, child_origin); + ctx.layout_child(&mut self.child, child_origin, child_size); ctx.derive_baselines(&self.child); } diff --git a/masonry/src/widgets/badge.rs b/masonry/src/widgets/badge.rs index 39bd8e30b..5b23f6fe8 100644 --- a/masonry/src/widgets/badge.rs +++ b/masonry/src/widgets/badge.rs @@ -181,10 +181,9 @@ impl Widget for Badge { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let child_size = ctx.compute_size(&mut self.child, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.child, child_size); let child_origin = ((size - child_size).to_vec2() * 0.5).to_point(); - ctx.place_child(&mut self.child, child_origin); + ctx.layout_child(&mut self.child, child_origin, child_size); ctx.derive_baselines(&self.child); } diff --git a/masonry/src/widgets/badged.rs b/masonry/src/widgets/badged.rs index c41912f53..8980c7a2a 100644 --- a/masonry/src/widgets/badged.rs +++ b/masonry/src/widgets/badged.rs @@ -200,8 +200,8 @@ impl Widget for Badged { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let content_size = ctx.compute_size(&mut self.content, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.content, content_size); - ctx.place_child(&mut self.content, Point::ORIGIN); + ctx.layout_child(&mut self.content, Point::ORIGIN, content_size); + let content_size = ctx.child_size(&self.content); ctx.derive_baselines(&self.content); let Some(badge) = &mut self.badge else { @@ -209,13 +209,11 @@ impl Widget for Badged { }; let badge_size = ctx.compute_size(badge, SizeDef::MAX, size.into()); - ctx.run_layout(badge, badge_size); - let content_anchor = self.placement.resolve(content_size); let badge_origin = content_anchor + self.offset - Vec2::new(badge_size.width * 0.5, badge_size.height * 0.5); - ctx.place_child(badge, badge_origin); + ctx.layout_child(badge, badge_origin, badge_size); } fn paint( diff --git a/masonry/src/widgets/button.rs b/masonry/src/widgets/button.rs index 84c6d43fa..6a626a2ff 100644 --- a/masonry/src/widgets/button.rs +++ b/masonry/src/widgets/button.rs @@ -204,10 +204,9 @@ impl Widget for Button { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let child_size = ctx.compute_size(&mut self.child, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.child, child_size); let child_origin = ((size - child_size).to_vec2() * 0.5).to_point(); - ctx.place_child(&mut self.child, child_origin); + ctx.layout_child(&mut self.child, child_origin, child_size); ctx.derive_baselines(&self.child); } diff --git a/masonry/src/widgets/checkbox.rs b/masonry/src/widgets/checkbox.rs index 234d5136a..8d1375bc3 100644 --- a/masonry/src/widgets/checkbox.rs +++ b/masonry/src/widgets/checkbox.rs @@ -231,10 +231,9 @@ impl Widget for Checkbox { ); let label_size = ctx.compute_size(&mut self.label, SizeDef::fit(space), space.into()); - ctx.run_layout(&mut self.label, label_size); let label_origin = Point::new(check_side + check_padding, 0.); - ctx.place_child(&mut self.label, label_origin); + ctx.layout_child(&mut self.label, label_origin, label_size); ctx.derive_baselines(&self.label); } diff --git a/masonry/src/widgets/collapse_panel.rs b/masonry/src/widgets/collapse_panel.rs index d90f4e2df..52b700d9d 100644 --- a/masonry/src/widgets/collapse_panel.rs +++ b/masonry/src/widgets/collapse_panel.rs @@ -237,7 +237,6 @@ impl Widget for CollapsePanel { // Square button let button_size = Size::new(button_width, button_width); - ctx.run_layout(&mut self.disclosure_button, button_size); let label_auto_size = SizeDef::new( LenDef::FitContent( @@ -249,8 +248,6 @@ impl Widget for CollapsePanel { ); let label_size = ctx.compute_size(&mut self.header_label, label_auto_size, size.into()); - ctx.run_layout(&mut self.header_label, label_size); - let header_height = button_size.height.max(label_size.height); // Place it at the center of the label height. @@ -258,10 +255,10 @@ impl Widget for CollapsePanel { header_padding_width, (label_size.height - button_size.height) * 0.5, ); - ctx.place_child(&mut self.disclosure_button, btn_origin); + ctx.layout_child(&mut self.disclosure_button, btn_origin, button_size); let label_origin = Point::new(button_size.width + header_padding_width * 2.0, 0.0); - ctx.place_child(&mut self.header_label, label_origin); + ctx.layout_child(&mut self.header_label, label_origin, label_size); // Collapsed = !Disclosed let is_collapsed = !ctx.get_raw(&mut self.disclosure_button).0.is_disclosed(); @@ -280,10 +277,8 @@ impl Widget for CollapsePanel { let child_size = ctx.compute_size(&mut self.child, child_auto_size, child_context_size); - ctx.run_layout(&mut self.child, child_size); - let child_origin = Point::new(0.0, header_height + separator_height); - ctx.place_child(&mut self.child, child_origin); + ctx.layout_child(&mut self.child, child_origin, child_size); self.separator_line_y = Some(header_height + SEPARATOR_PAD.top.get() + border_width * 0.5); diff --git a/masonry/src/widgets/disclosure_button.rs b/masonry/src/widgets/disclosure_button.rs index 9632285ac..580cab243 100644 --- a/masonry/src/widgets/disclosure_button.rs +++ b/masonry/src/widgets/disclosure_button.rs @@ -165,7 +165,8 @@ impl Widget for DisclosureButton { let cache = ctx.property_cache(); let button_color = props.get::(cache); - let size = ctx.content_box_size(); + let content_box = ctx.content_box(); + let size = content_box.size(); let half_size = size * 0.5; let mut arrow = BezPath::new(); @@ -173,7 +174,7 @@ impl Widget for DisclosureButton { arrow.line_to((half_size.width, 0.0)); arrow.line_to((0.0, half_size.height)); - let mut affine = Affine::translate(half_size.to_vec2()); + let mut affine = Affine::translate(content_box.center().to_vec2()); // Rotate if it's disclosed if self.is_disclosed() { diff --git a/masonry/src/widgets/divider.rs b/masonry/src/widgets/divider.rs index c054cce65..b535828b9 100644 --- a/masonry/src/widgets/divider.rs +++ b/masonry/src/widgets/divider.rs @@ -661,7 +661,6 @@ impl Widget for Divider { if let Some(content) = &mut self.content { let content_size = ctx.compute_size(content, SizeDef::fit(size), size.into()); - ctx.run_layout(content, content_size); let placement = match self.placement { Placement::Start => match self.axis { @@ -675,7 +674,7 @@ impl Widget for Divider { }, }; let content_origin = placement.resolve((size - content_size).to_rect()); - ctx.place_child(content, content_origin); + ctx.layout_child(content, content_origin, content_size); ctx.derive_baselines(content); diff --git a/masonry/src/widgets/flex.rs b/masonry/src/widgets/flex.rs index 91d55d7bb..48e271cab 100644 --- a/masonry/src/widgets/flex.rs +++ b/masonry/src/widgets/flex.rs @@ -958,16 +958,15 @@ impl Widget for Flex { main.pack_size(child_main_length.get(), child_cross_length.get()) }; - // Sum flex factors, resolve bases, subtract bases from main space, - // and lay out inflexible widgets. + // Sum flex factors, resolve bases, and subtract bases from main space. for child in &mut self.children { match child { Child::Widget { widget, - alignment, flex, basis, basis_resolved, + .. } => { match effective_basis(*basis, *flex) { FlexBasis::Auto => { @@ -989,15 +988,7 @@ impl Widget for Flex { *basis_resolved = Length::ZERO; } } - if *flex == 0. { - let child_main_length = *basis_resolved; - let child_size = - compute_child_size(ctx, widget, child_main_length, alignment); - - ctx.run_layout(widget, child_size); - } else { - flex_sum += *flex; - } + flex_sum += *flex; } Child::Spacer { flex, @@ -1028,8 +1019,6 @@ impl Widget for Flex { for child in &mut self.children { match child { Child::Widget { - widget, - alignment, flex, basis_resolved, .. @@ -1040,10 +1029,6 @@ impl Widget for Flex { // this distribution will need to evolve into a looped solver. let child_main_length = basis_resolved.saturating_add((*flex * flex_fraction).px()); - let child_size = compute_child_size(ctx, widget, child_main_length, alignment); - - ctx.run_layout(widget, child_size); - main_space = main_space .saturating_sub(child_main_length.saturating_sub(*basis_resolved)); } @@ -1072,6 +1057,57 @@ impl Widget for Flex { let (space_before, space_between) = get_spacing(self.main_alignment, main_space.get(), widget_count); + // Lay out widgets at their ideal main-axis positions. + let mut main_offset = space_before; + let mut previous_was_widget = false; + for child in &mut self.children { + match child { + Child::Widget { + widget, + alignment, + flex, + basis_resolved, + .. + } => { + if previous_was_widget { + main_offset += space_between; + } + + let child_main_length = if *flex > 0. { + basis_resolved.saturating_add((*flex * flex_fraction).px()) + } else { + *basis_resolved + }; + let child_size = compute_child_size(ctx, widget, child_main_length, alignment); + let alignment = alignment.unwrap_or(self.cross_alignment); + let child_origin_cross = if main == Axis::Horizontal + && matches!( + alignment, + CrossAxisAlignment::FirstBaseline | CrossAxisAlignment::LastBaseline + ) { + 0. + } else { + let cross_unused = cross_space.get() - child_size.get_coord(cross); + alignment.offset(cross_unused) + }; + let child_origin = main.pack_point(main_offset, child_origin_cross); + + ctx.layout_child(widget, child_origin, child_size); + + main_offset += child_main_length.get(); + main_offset += gap_length; + previous_was_widget = true; + } + Child::Spacer { + length_resolved, .. + } => { + main_offset += length_resolved.get(); + main_offset += gap_length; + previous_was_widget = false; + } + } + } + // Determine the shared cross alignment baselines. // As we currently only support the horizontal-tb writing mode, we do it only for rows. let mut alignment_ascent: Option = None; @@ -1085,7 +1121,7 @@ impl Widget for Flex { let alignment = alignment.unwrap_or(self.cross_alignment); match alignment { CrossAxisAlignment::FirstBaseline => { - let (first_baseline, _) = ctx.child_layout_baselines(widget); + let (first_baseline, _) = ctx.child_baselines(widget); alignment_ascent = Some( alignment_ascent .unwrap_or(first_baseline) @@ -1093,7 +1129,7 @@ impl Widget for Flex { ); } CrossAxisAlignment::LastBaseline => { - let (_, last_baseline) = ctx.child_layout_baselines(widget); + let (_, last_baseline) = ctx.child_baselines(widget); let child_size = ctx.child_size(widget); let descent = child_size.get_coord(cross) - last_baseline; alignment_descent = @@ -1107,52 +1143,34 @@ impl Widget for Flex { } } - // Distribute free space and place children - let mut main_offset = space_before; - let mut previous_was_widget = false; + // Apply baseline alignment. Other alignments were already positioned before layout. for child in &mut self.children { match child { Child::Widget { widget, alignment, .. } => { - if previous_was_widget { - main_offset += space_between; - } - let child_size = ctx.child_size(widget); let alignment = alignment.unwrap_or(self.cross_alignment); let child_origin_cross = match alignment { CrossAxisAlignment::FirstBaseline if main == Axis::Horizontal => { - let (first_baseline, _) = ctx.child_layout_baselines(widget); + let (first_baseline, _) = ctx.child_baselines(widget); alignment_ascent.unwrap() - first_baseline } CrossAxisAlignment::LastBaseline if main == Axis::Horizontal => { - let (_, last_baseline) = ctx.child_layout_baselines(widget); + let (_, last_baseline) = ctx.child_baselines(widget); let descent = child_size.get_coord(cross) - last_baseline; let end_gap = alignment_descent.unwrap() - descent; let cross_unused = cross_space.get() - child_size.get_coord(cross); cross_unused - end_gap } - _ => { - let cross_unused = cross_space.get() - child_size.get_coord(cross); - alignment.offset(cross_unused) - } + _ => continue, }; - let child_origin = main.pack_point(main_offset, child_origin_cross); - ctx.place_child(widget, child_origin); - - main_offset += child_size.get_coord(main); - main_offset += gap_length; - previous_was_widget = true; - } - Child::Spacer { - length_resolved, .. - } => { - main_offset += length_resolved.get(); - main_offset += gap_length; - previous_was_widget = false; + let mut child_origin = ctx.child_origin(widget); + child_origin.set_coord(cross, child_origin_cross); + ctx.move_child(widget, child_origin); } + Child::Spacer { .. } => (), } } @@ -1190,7 +1208,7 @@ impl Widget for Flex { .unwrap() .widget() .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; @@ -1201,7 +1219,7 @@ impl Widget for Flex { .unwrap() .widget() .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; diff --git a/masonry/src/widgets/grid.rs b/masonry/src/widgets/grid.rs index 0675f8ba2..15242d44d 100644 --- a/masonry/src/widgets/grid.rs +++ b/masonry/src/widgets/grid.rs @@ -377,11 +377,9 @@ impl Widget for Grid { let auto_size = SizeDef::fixed(area); let child_size = ctx.compute_size(&mut child.widget, auto_size, area.into()); - ctx.run_layout(&mut child.widget, child_size); - let child_origin = Point::new(child.x as f64 * cell_width, child.y as f64 * cell_height); - ctx.place_child(&mut child.widget, child_origin); + ctx.layout_child(&mut child.widget, child_origin, child_size); } if !self.children.is_empty() { @@ -398,7 +396,7 @@ impl Widget for Grid { min_row_children.sort_by_key(|c| c.x); let min_row_child = min_row_children.first().unwrap(); let min_row_child_origin = ctx.child_origin(&min_row_child.widget); - let (first_baseline, _) = ctx.child_aligned_baselines(&min_row_child.widget); + let (first_baseline, _) = ctx.child_baselines(&min_row_child.widget); let first_baseline = min_row_child_origin.y + first_baseline; // Find the last occupied cell @@ -410,7 +408,7 @@ impl Widget for Grid { max_row_children.sort_by_key(|c| c.x + c.width); let max_row_child = max_row_children.last().unwrap(); let max_row_child_origin = ctx.child_origin(&max_row_child.widget); - let (_, last_baseline) = ctx.child_aligned_baselines(&max_row_child.widget); + let (_, last_baseline) = ctx.child_baselines(&max_row_child.widget); let last_baseline = max_row_child_origin.y + last_baseline; // Set the container baselines diff --git a/masonry/src/widgets/indexed_stack.rs b/masonry/src/widgets/indexed_stack.rs index ee27d358f..4ca46bd90 100644 --- a/masonry/src/widgets/indexed_stack.rs +++ b/masonry/src/widgets/indexed_stack.rs @@ -265,10 +265,13 @@ impl Widget for IndexedStack { SizeDef::fit(size), size.into(), ); - ctx.run_layout(&mut self.children[self.active_child], child_size); let child_origin = Point::ORIGIN; - ctx.place_child(&mut self.children[self.active_child], child_origin); + ctx.layout_child( + &mut self.children[self.active_child], + child_origin, + child_size, + ); ctx.derive_baselines(&self.children[self.active_child]); } diff --git a/masonry/src/widgets/pagination.rs b/masonry/src/widgets/pagination.rs index ad5bc2001..c0aee9f08 100644 --- a/masonry/src/widgets/pagination.rs +++ b/masonry/src/widgets/pagination.rs @@ -346,10 +346,8 @@ impl Widget for Pagination { let button_size = ctx.compute_size(&mut button.widget, SizeDef::fit(space), size.into()); - ctx.run_layout(&mut button.widget, button_size); - let button_origin = Point::new(used_width, 0.); - ctx.place_child(&mut button.widget, button_origin); + ctx.layout_child(&mut button.widget, button_origin, button_size); used_width += button_size.width; } diff --git a/masonry/src/widgets/passthrough.rs b/masonry/src/widgets/passthrough.rs index 09a9b4f30..dc89b0c26 100644 --- a/masonry/src/widgets/passthrough.rs +++ b/masonry/src/widgets/passthrough.rs @@ -99,8 +99,7 @@ impl Widget for Passthrough { } fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { - ctx.run_layout(&mut self.inner, size); - ctx.place_child(&mut self.inner, Point::ORIGIN); + ctx.layout_child(&mut self.inner, Point::ORIGIN, size); ctx.derive_baselines(&self.inner); } diff --git a/masonry/src/widgets/portal.rs b/masonry/src/widgets/portal.rs index 04bdf32a9..35bf7237d 100644 --- a/masonry/src/widgets/portal.rs +++ b/masonry/src/widgets/portal.rs @@ -359,7 +359,7 @@ impl Portal { /// /// A position of zero means no scrolling at all. pub fn set_viewport_pos(this: &mut WidgetMut<'_, Self>, position: Point) -> bool { - let portal_size = this.ctx.content_box_size(); + let portal_size = this.ctx.content_box().size(); let content_size = this.widget.content_size; let pos_changed = this @@ -372,7 +372,7 @@ impl Portal { let progress_y = this.widget.viewport_pos.y / (content_size - portal_size).height; Self::vertical_scrollbar_mut(this).widget.cursor_progress = progress_y; Self::vertical_scrollbar_mut(this).ctx.request_render(); - this.ctx.request_layout(); + this.ctx.request_compose(); } pos_changed } @@ -387,7 +387,7 @@ impl Portal { /// `target` is in the child's border-box coordinate space, meaning a target /// of `(0, 0, 10, 10)` will scroll an item at the top-left of the child into view. pub fn pan_viewport_to(this: &mut WidgetMut<'_, Self>, target: Rect) -> bool { - let portal_size = this.ctx.content_box_size(); + let portal_size = this.ctx.content_box().size(); let viewport = Rect::from_origin_size(this.widget.viewport_pos, portal_size); let new_pos_x = compute_pan_range( @@ -420,7 +420,7 @@ impl Widget for Portal { let cache = ctx.property_cache(); let auto_hide_scroll_bar = props.get::(cache).0; - let portal_size = ctx.content_box_size(); + let portal_size = ctx.content_box().size(); let content_size = self.content_size; match *event { @@ -479,7 +479,7 @@ impl Widget for Portal { _props: &mut PropertiesMut<'_>, event: &TextEvent, ) { - let portal_size = ctx.content_box_size(); + let portal_size = ctx.content_box().size(); let content_size = self.content_size; let target = ctx.target(); let scrollbar_target = @@ -583,7 +583,7 @@ impl Widget for Portal { _props: &mut PropertiesMut<'_>, event: &AccessEvent, ) { - let portal_size = ctx.content_box_size(); + let portal_size = ctx.content_box().size(); let content_size = self.content_size; let target = ctx.target(); let scrollbar_target = @@ -682,7 +682,7 @@ impl Widget for Portal { fn update(&mut self, ctx: &mut UpdateCtx<'_>, _props: &mut PropertiesMut<'_>, event: &Update) { match event { Update::RequestPanToChild(target) => { - let portal_size = ctx.content_box_size(); + let portal_size = ctx.content_box().size(); let content_size = self.content_size; self.pan_viewport_to_raw(portal_size, content_size, *target); @@ -766,7 +766,8 @@ impl Widget for Portal { child_size } }; - ctx.run_layout(&mut self.child, content_size); + ctx.layout_child(&mut self.child, Point::ZERO, content_size); + let content_size = ctx.child_size(&self.child); self.content_size = content_size; // TODO - document better @@ -776,8 +777,6 @@ impl Widget for Portal { ctx.set_clip_path(size.to_rect()); - ctx.place_child(&mut self.child, Point::ZERO); - self.scrollbar_horizontal_visible = !self.constrain_horizontal && size.width < content_size.width; self.scrollbar_vertical_visible = @@ -799,10 +798,10 @@ impl Widget for Portal { SizeDef::fit(size), size.into(), ); - ctx.run_layout(&mut self.scrollbar_horizontal, scrollbar_size); - ctx.place_child( + ctx.layout_child( &mut self.scrollbar_horizontal, Point::new(0.0, size.height - scrollbar_size.height), + scrollbar_size, ); } @@ -822,10 +821,10 @@ impl Widget for Portal { SizeDef::fit(size), size.into(), ); - ctx.run_layout(&mut self.scrollbar_vertical, scrollbar_size); - ctx.place_child( + ctx.layout_child( &mut self.scrollbar_vertical, Point::new(size.width - scrollbar_size.width, 0.0), + scrollbar_size, ); } } @@ -857,7 +856,7 @@ impl Widget for Portal { ) { node.set_clips_children(); - let portal_size = ctx.content_box_size(); + let portal_size = ctx.content_box().size(); let content_size = self.content_size; let scroll_range = (content_size - portal_size).max(Size::ZERO); @@ -950,10 +949,9 @@ mod tests { }) .layout_fn(move |child, ctx, _props, size| { let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); - ctx.run_layout(child, child_size); // We don't place it at (0,0) to test that stacked-origin translation works. // Because if we were at (0,0) it would be effectively the same as no parent. - ctx.place_child(child, Point::new(0., top_pad)); + ctx.layout_child(child, Point::new(0., top_pad), child_size); }) .prepare() } diff --git a/masonry/src/widgets/progress_bar.rs b/masonry/src/widgets/progress_bar.rs index 70a479377..6df3afb99 100644 --- a/masonry/src/widgets/progress_bar.rs +++ b/masonry/src/widgets/progress_bar.rs @@ -169,10 +169,9 @@ impl Widget for ProgressBar { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let label_size = ctx.compute_size(&mut self.label, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.label, label_size); let child_origin = ((size - label_size).to_vec2() * 0.5).to_point(); - ctx.place_child(&mut self.label, child_origin); + ctx.layout_child(&mut self.label, child_origin, label_size); ctx.derive_baselines(&self.label); } diff --git a/masonry/src/widgets/prose.rs b/masonry/src/widgets/prose.rs index d1aeec79a..e7059915d 100644 --- a/masonry/src/widgets/prose.rs +++ b/masonry/src/widgets/prose.rs @@ -153,8 +153,7 @@ impl Widget for Prose { } fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { - ctx.run_layout(&mut self.text, size); - ctx.place_child(&mut self.text, Point::ORIGIN); + ctx.layout_child(&mut self.text, Point::ORIGIN, size); ctx.derive_baselines(&self.text); if self.clip { diff --git a/masonry/src/widgets/radio_button.rs b/masonry/src/widgets/radio_button.rs index e033d4528..fa788b444 100644 --- a/masonry/src/widgets/radio_button.rs +++ b/masonry/src/widgets/radio_button.rs @@ -280,10 +280,9 @@ impl Widget for RadioButton { ); let label_size = ctx.compute_size(&mut self.label, SizeDef::fit(space), space.into()); - ctx.run_layout(&mut self.label, label_size); let label_origin = Point::new(check_side + check_padding, 0.); - ctx.place_child(&mut self.label, label_origin); + ctx.layout_child(&mut self.label, label_origin, label_size); ctx.derive_baselines(&self.label); } diff --git a/masonry/src/widgets/radio_group.rs b/masonry/src/widgets/radio_group.rs index 774508730..e42e6711a 100644 --- a/masonry/src/widgets/radio_group.rs +++ b/masonry/src/widgets/radio_group.rs @@ -59,8 +59,7 @@ impl Widget for RadioGroup { } fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { - ctx.run_layout(&mut self.child, size); - ctx.place_child(&mut self.child, Point::ORIGIN); + ctx.layout_child(&mut self.child, Point::ORIGIN, size); ctx.derive_baselines(&self.child); } diff --git a/masonry/src/widgets/resize_observer.rs b/masonry/src/widgets/resize_observer.rs index b56d0fa73..92ef96ea1 100644 --- a/masonry/src/widgets/resize_observer.rs +++ b/masonry/src/widgets/resize_observer.rs @@ -16,7 +16,7 @@ use crate::layout::{LenReq, Length}; /// It reports the child's length as its own in [`measure`], syncing its size with the child's. /// /// The size of this widget can be accessed through [`MutateCtx`] methods like -/// [`border_box_size`] and [`content_box_size`]. +/// [`border_box`] and [`content_box`]. /// /// Ensure that `ResizeObserver` has [`Dimensions`] set via props to [`Dimensions::MAX`]. /// Max preferred size of `ResizeObserver` means that the question of size @@ -44,8 +44,8 @@ use crate::layout::{LenReq, Length}; /// [`Dimensions`]: crate::properties::Dimensions /// [`Dimensions::MAX`]: crate::properties::Dimensions::MAX /// [`MutateCtx`]: crate::core::MutateCtx -/// [`border_box_size`]: crate::core::MutateCtx::border_box_size -/// [`content_box_size`]: crate::core::MutateCtx::content_box_size +/// [`border_box`]: crate::core::MutateCtx::border_box +/// [`content_box`]: crate::core::MutateCtx::content_box // TODO: It would be nice to at least catch these loops. // We could see how many times layout is executed without us being painted, and setting a threshold. // The response if that gets too high (100?) could be debug_panicking, then stopping @@ -100,11 +100,11 @@ impl ResizeObserver { /// Currently only used by [`ResizeObserver`]. /// Note that this event does not itself include the final size. /// That should instead be accessed through [`MutateCtx`] methods like -/// [`border_box_size`] and [`content_box_size`]. +/// [`border_box`] and [`content_box`]. /// /// [`MutateCtx`]: crate::core::MutateCtx -/// [`border_box_size`]: crate::core::MutateCtx::border_box_size -/// [`content_box_size`]: crate::core::MutateCtx::content_box_size +/// [`border_box`]: crate::core::MutateCtx::border_box +/// [`content_box`]: crate::core::MutateCtx::content_box #[derive(Debug)] pub struct LayoutChanged; @@ -132,8 +132,7 @@ impl Widget for ResizeObserver { } fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { - ctx.run_layout(&mut self.child, size); - ctx.place_child(&mut self.child, Point::ORIGIN); + ctx.layout_child(&mut self.child, Point::ORIGIN, size); ctx.derive_baselines(&self.child); if self.last_size.is_none_or(|it| it != size) { @@ -201,7 +200,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .border_box() + .size(), Size { width: 100., height: 100., @@ -218,7 +218,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .border_box() + .size(), Size { width: 100., height: 200., @@ -250,7 +251,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .border_box() + .size(), Size { width: 200., height: 200., @@ -267,7 +269,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .border_box() + .size(), Size { width: 100., height: 150., diff --git a/masonry/src/widgets/scroll_bar.rs b/masonry/src/widgets/scroll_bar.rs index c132f09c5..377fcad93 100644 --- a/masonry/src/widgets/scroll_bar.rs +++ b/masonry/src/widgets/scroll_bar.rs @@ -188,7 +188,7 @@ impl Widget for ScrollBar { PointerEvent::Down(PointerButtonEvent { state, .. }) => { ctx.capture_pointer(); - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let cursor_min_length = theme::SCROLLBAR_MIN_SIZE; let cursor_rect = self.cursor_rect(size, cursor_min_length); let mouse_pos = ctx.local_position(state.position); @@ -211,7 +211,7 @@ impl Widget for ScrollBar { if ctx.is_active() && let Some(grab_anchor) = self.grab_anchor { - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let cursor_min_length = theme::SCROLLBAR_MIN_SIZE; let progress = self.progress_from_mouse_pos( size, @@ -414,7 +414,7 @@ impl Widget for ScrollBar { let cursor_min_length = theme::SCROLLBAR_MIN_SIZE; let scrollbar_width = theme::SCROLLBAR_WIDTH; - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let inset_start = if !collapsible || ctx.is_hovered() || self.grab_anchor.is_some() { cursor_padding } else { diff --git a/masonry/src/widgets/selector.rs b/masonry/src/widgets/selector.rs index 8d3b9264e..d08c9fc29 100644 --- a/masonry/src/widgets/selector.rs +++ b/masonry/src/widgets/selector.rs @@ -255,10 +255,9 @@ impl Widget for Selector { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let child_size = ctx.compute_size(&mut self.child, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.child, child_size); let child_origin = ((size - child_size).to_vec2() * 0.5).to_point(); - ctx.place_child(&mut self.child, child_origin); + ctx.layout_child(&mut self.child, child_origin, child_size); ctx.derive_baselines(&self.child); } diff --git a/masonry/src/widgets/selector_item.rs b/masonry/src/widgets/selector_item.rs index 5c4795258..a0649a4f3 100644 --- a/masonry/src/widgets/selector_item.rs +++ b/masonry/src/widgets/selector_item.rs @@ -107,10 +107,9 @@ impl Widget for SelectorItem { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let child_size = ctx.compute_size(&mut self.child, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.child, child_size); let child_origin = ((size - child_size).to_vec2() * 0.5).to_point(); - ctx.place_child(&mut self.child, child_origin); + ctx.layout_child(&mut self.child, child_origin, child_size); ctx.derive_baselines(&self.child); } diff --git a/masonry/src/widgets/sized_box.rs b/masonry/src/widgets/sized_box.rs index 8ffecaa76..b8cfb9cb2 100644 --- a/masonry/src/widgets/sized_box.rs +++ b/masonry/src/widgets/sized_box.rs @@ -274,8 +274,7 @@ impl Widget for SizedBox { return; }; - ctx.run_layout(child, size); - ctx.place_child(child, Point::ORIGIN); + ctx.layout_child(child, Point::ORIGIN, size); ctx.derive_baselines(child); } diff --git a/masonry/src/widgets/slider.rs b/masonry/src/widgets/slider.rs index 12d9b3072..550dbe84c 100644 --- a/masonry/src/widgets/slider.rs +++ b/masonry/src/widgets/slider.rs @@ -159,14 +159,14 @@ impl Widget for Slider { ctx.request_focus(); ctx.capture_pointer(); let local_pos = ctx.local_position(state.position); - let width = ctx.content_box_size().width; + let width = ctx.content_box().size().width; if self.update_value_from_position(local_pos.x, width) { ctx.submit_action::(SliderMoved { value: self.value }); } } PointerEvent::Move(PointerUpdate { current, .. }) if ctx.is_active() => { let local_pos = ctx.local_position(current.position); - let width = ctx.content_box_size().width; + let width = ctx.content_box().size().width; if self.update_value_from_position(local_pos.x, width) { ctx.submit_action::(SliderMoved { value: self.value }); } @@ -387,7 +387,7 @@ impl Widget for Slider { }; // Calculate geometry based on state - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let track_y = (size.height - track_thickness) / 2.0; let border_box = ctx.border_box(); diff --git a/masonry/src/widgets/spinner.rs b/masonry/src/widgets/spinner.rs index f56afeb48..49799765f 100644 --- a/masonry/src/widgets/spinner.rs +++ b/masonry/src/widgets/spinner.rs @@ -13,7 +13,7 @@ use crate::core::{ PropertiesRef, RegisterCtx, Update, UpdateCtx, UsesProperty, Widget, WidgetId, }; use crate::imaging::Painter; -use crate::kurbo::{Axis, Cap, Line, Point, Size, Stroke, Vec2}; +use crate::kurbo::{Axis, Cap, Line, Size, Stroke, Vec2}; use crate::layout::{LenReq, Length}; use crate::properties::ContentColor; use crate::theme; @@ -111,8 +111,9 @@ impl Widget for Spinner { let color = props.get::(cache); let t = self.t; - let size = ctx.content_box_size(); - let center = Point::new(size.width / 2.0, size.height / 2.0); + let content_box = ctx.content_box(); + let size = content_box.size(); + let center = content_box.center(); let scale_factor = size.width.min(size.height) / 40.0; for step in 1..=12 { diff --git a/masonry/src/widgets/split.rs b/masonry/src/widgets/split.rs index f41ac0a17..508d4c91a 100644 --- a/masonry/src/widgets/split.rs +++ b/masonry/src/widgets/split.rs @@ -293,7 +293,7 @@ impl Split { } fn paint_focus_bar(&mut self, ctx: &mut PaintCtx<'_>, scene: &mut Painter<'_>) { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let (edge1, edge2) = self.bar_edges(length); let mut rect = ctx.border_box(); @@ -307,7 +307,7 @@ impl Split { } fn paint_solid_bar(&mut self, ctx: &mut PaintCtx<'_>, scene: &mut Painter<'_>, color: Color) { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let (edge1, edge2) = self.bar_edges(length); let mut rect = ctx.border_box(); @@ -317,7 +317,7 @@ impl Split { } fn paint_stroked_bar(&mut self, ctx: &mut PaintCtx<'_>, scene: &mut Painter<'_>, color: Color) { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); // Set the line width to a third of the splitter bar thickness, // because we'll paint two equal lines at the edges. let line_width = self.bar_thickness.get() / 3.0; @@ -462,7 +462,7 @@ where let pos = ctx .local_position(state.position) .get_coord(self.split_axis); - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); if self.bar_area_hit_test(length, pos) { ctx.set_handled(); ctx.capture_pointer(); @@ -475,7 +475,7 @@ where let pos = ctx .local_position(current.position) .get_coord(self.split_axis); - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); // If widget has pointer capture, assume always it's hovered let effective_center = pos - self.click_offset; self.update_split_point_from_bar_center(length, effective_center); @@ -506,7 +506,7 @@ where return; } - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let bar_thickness = self.bar_thickness.get(); let split_space = (length - bar_thickness).max(0.0); if split_space <= f64::EPSILON { @@ -558,7 +558,7 @@ where return; } - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let bar_thickness = self.bar_thickness.get(); let split_space = (length - bar_thickness).max(0.0); if split_space <= f64::EPSILON { @@ -681,16 +681,13 @@ where let child1_size = self.split_axis.pack_size(child1_split_space, cross_space); let child2_size = self.split_axis.pack_size(child2_split_space, cross_space); - ctx.run_layout(&mut self.child1, child1_size); - ctx.run_layout(&mut self.child2, child2_size); - // Top-left align both children. let child1_origin = Point::ORIGIN; let child2_origin = self .split_axis .pack_point(child1_split_space + bar_thickness, 0.); - ctx.place_child(&mut self.child1, child1_origin); - ctx.place_child(&mut self.child2, child2_origin); + ctx.layout_child(&mut self.child1, child1_origin, child1_size); + ctx.layout_child(&mut self.child2, child2_origin, child2_size); } fn paint( @@ -716,7 +713,7 @@ where } fn get_cursor(&self, ctx: &QueryCtx<'_>, pos: Point) -> CursorIcon { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let local_pos = ctx.to_local(pos).get_coord(self.split_axis); let is_bar_area_hovered = self.bar_area_hit_test(length, local_pos); @@ -740,7 +737,7 @@ where _props: &PropertiesRef<'_>, node: &mut Node, ) { - let length = ctx.content_box_size().get_coord(self.split_axis); + let length = ctx.content_box().size().get_coord(self.split_axis); let bar_thickness = self.bar_thickness.get(); let split_space = (length - bar_thickness).max(0.0); let (min_limit, max_limit) = self.split_side_limits(split_space); @@ -857,7 +854,7 @@ mod tests { let child1_initial_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().border_box().size().width }; // Initial bar center with default settings: @@ -871,8 +868,8 @@ mod tests { let root = harness.root_widget(); let children = root.children(); ( - children[0].ctx().border_box_size().width, - children[1].ctx().border_box_size().width, + children[0].ctx().border_box().size().width, + children[1].ctx().border_box().size().width, ) }; @@ -893,14 +890,14 @@ mod tests { let child1_initial_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().border_box().size().width }; harness.process_text_event(TextEvent::key_down(Key::Named(NamedKey::ArrowRight))); let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().border_box().size().width }; assert!(child1_width > child1_initial_width); @@ -916,14 +913,14 @@ mod tests { let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().border_box().size().width }; assert!((child1_width - 50.0).abs() < 0.01); harness.process_window_event(WindowEvent::Resize(PhysicalSize::new(300, 100))); let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().border_box().size().width }; assert!((child1_width - 50.0).abs() < 0.01); } @@ -938,14 +935,14 @@ mod tests { let child2_width = { let root = harness.root_widget(); - root.children()[1].ctx().border_box_size().width + root.children()[1].ctx().border_box().size().width }; assert!((child2_width - 50.0).abs() < 0.01); harness.process_window_event(WindowEvent::Resize(PhysicalSize::new(300, 100))); let child2_width = { let root = harness.root_widget(); - root.children()[1].ctx().border_box_size().width + root.children()[1].ctx().border_box().size().width }; assert!((child2_width - 50.0).abs() < 0.01); } @@ -965,7 +962,7 @@ mod tests { }); let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().border_box().size().width }; assert!((child1_width - 144.0).abs() < 0.01); @@ -974,7 +971,7 @@ mod tests { }); let child1_width = { let root = harness.root_widget(); - root.children()[0].ctx().border_box_size().width + root.children()[0].ctx().border_box().size().width }; assert!((child1_width - 0.0).abs() < 0.01); } diff --git a/masonry/src/widgets/step_input.rs b/masonry/src/widgets/step_input.rs index 33f01299c..c31610930 100644 --- a/masonry/src/widgets/step_input.rs +++ b/masonry/src/widgets/step_input.rs @@ -1106,9 +1106,9 @@ impl Widget for StepInput { PointerEvent::Move(pu) => { // If we're hovered, highlight the correct side's button. if ctx.is_hovered() { - let size = ctx.content_box_size(); + let content_box_center_x = ctx.content_box().center().x; let local_x = ctx.local_position(pu.current.position).x; - let hover_backward = local_x <= size.width * 0.5; + let hover_backward = local_x <= content_box_center_x; if hover_backward != self.hover_backward { self.hover_backward = hover_backward; ctx.request_paint_only(); @@ -1214,7 +1214,7 @@ impl Widget for StepInput { // * The button was previously pressed down on us (active) // * The pointer is still on us (hovered) if self.slide_last.is_none() && ctx.is_active() && ctx.is_hovered() { - let size = ctx.content_box_size(); + let content_box_center_x = ctx.content_box().center().x; let local_x = ctx.local_position(pbe.state.position).x; // Snap based on modifier @@ -1225,7 +1225,7 @@ impl Widget for StepInput { }; // Update the active value based on which side was clicked - let value_changed = if local_x <= size.width * 0.5 { + let value_changed = if local_x <= content_box_center_x { if snap { self.prev_snap() } else { @@ -1410,13 +1410,12 @@ impl Widget for StepInput { let label_space = Size::new((size.width - buttons_width).max(0.), size.height); let auto_size = SizeDef::fit(label_space); let label_size = ctx.compute_size(label, auto_size, size.into()); - ctx.run_layout(label, label_size); let label_origin = Point::new( (size.width - label_size.width) * 0.5, (size.height - label_size.height) * 0.5, ); - ctx.place_child(label, label_origin); + ctx.layout_child(label, label_origin, label_size); self.label_x_start = label_origin.x; self.label_x_end = label_origin.x + label_size.width; @@ -1531,7 +1530,7 @@ impl StepInput { let color_backward = *props.get::(cache); let color_forward = *props.get::(cache); - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let (_, forward, backward) = self.visual_speed(); let (btn_length, btn_edge_pad) = Self::basic_button_length(size.height, Some(size.width)); @@ -1604,7 +1603,7 @@ impl StepInput { let color_forward = *props.get::(cache); let color_heat = *props.get::(cache); - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let (speed, forward, backward) = self.visual_speed(); let sliding = forward || backward; diff --git a/masonry/src/widgets/switch.rs b/masonry/src/widgets/switch.rs index 1ea04c0b2..1390e8ab4 100644 --- a/masonry/src/widgets/switch.rs +++ b/masonry/src/widgets/switch.rs @@ -222,7 +222,7 @@ impl Widget for Switch { ) { let is_disabled = ctx.is_disabled(); - let size = ctx.border_box_size(); + let size = ctx.border_box().size(); let border_box_translation = ctx.border_box_translation(); let cache = ctx.property_cache(); @@ -416,7 +416,8 @@ mod tests { let size = harness .get_widget_with_id(switch_id) .ctx() - .border_box_size(); + .border_box() + .size(); // Switch should maintain its intrinsic size, not fill available space assert_eq!( diff --git a/masonry/src/widgets/text_input.rs b/masonry/src/widgets/text_input.rs index 09287af15..7e7f01ffb 100644 --- a/masonry/src/widgets/text_input.rs +++ b/masonry/src/widgets/text_input.rs @@ -290,18 +290,15 @@ impl Widget for TextInput { } fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { - ctx.run_layout(&mut self.text, size); - let child_origin = Point::ORIGIN; - ctx.place_child(&mut self.text, child_origin); + ctx.layout_child(&mut self.text, child_origin, size); ctx.derive_baselines(&self.text); let text_is_empty = ctx.get_raw(&mut self.text).0.is_empty(); ctx.set_stashed(&mut self.placeholder, !text_is_empty); if text_is_empty { - ctx.run_layout(&mut self.placeholder, size); - ctx.place_child(&mut self.placeholder, child_origin); + ctx.layout_child(&mut self.placeholder, child_origin, size); } if self.clip { diff --git a/masonry/src/widgets/variable_label.rs b/masonry/src/widgets/variable_label.rs index b5305703e..6a85af9b8 100644 --- a/masonry/src/widgets/variable_label.rs +++ b/masonry/src/widgets/variable_label.rs @@ -247,8 +247,7 @@ impl Widget for VariableLabel { } fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { - ctx.run_layout(&mut self.label, size); - ctx.place_child(&mut self.label, Point::ORIGIN); + ctx.layout_child(&mut self.label, Point::ORIGIN, size); ctx.derive_baselines(&self.label); } diff --git a/masonry/src/widgets/virtual_scroll.rs b/masonry/src/widgets/virtual_scroll.rs index 882fda875..d276c99f4 100644 --- a/masonry/src/widgets/virtual_scroll.rs +++ b/masonry/src/widgets/virtual_scroll.rs @@ -480,7 +480,7 @@ impl VirtualScroll { /// A wrapper to use [`post_scroll`](Self::post_scroll) in event methods. fn event_post_scroll(&mut self, ctx: &mut EventCtx<'_>) { - match self.post_scroll(ctx.content_box_size()) { + match self.post_scroll(ctx.content_box().size()) { PostScrollResult::Layout => { ctx.request_layout(); } @@ -491,7 +491,7 @@ impl VirtualScroll { /// A wrapper to use [`post_scroll`](Self::post_scroll) in update methods. fn update_post_scroll(&mut self, ctx: &mut UpdateCtx<'_>) { - match self.post_scroll(ctx.content_box_size()) { + match self.post_scroll(ctx.content_box().size()) { PostScrollResult::Layout => { ctx.request_layout(); } @@ -536,7 +536,7 @@ impl Widget for VirtualScroll { ) { match event { PointerEvent::Scroll(PointerScrollEvent { delta, .. }) => { - let size = ctx.content_box_size(); + let size = ctx.content_box().size(); let scale_factor = ctx.scale_factor(); let line_px = PhysicalPosition { x: 120.0 * scale_factor, @@ -613,7 +613,7 @@ impl Widget for VirtualScroll { }; let amount = match unit { accesskit::ScrollUnit::Item => self.anchor_height, - accesskit::ScrollUnit::Page => ctx.content_box_size().height, + accesskit::ScrollUnit::Page => ctx.content_box().size().height, }; if event.action == accesskit::Action::ScrollUp { self.scroll_offset_from_anchor -= amount; @@ -636,7 +636,7 @@ impl Widget for VirtualScroll { match event { Update::RequestPanToChild(target) => { let new_pos_y = super::portal::compute_pan_range( - 0.0..ctx.content_box_size().height, + 0.0..ctx.content_box().size().height, target.min_y()..target.max_y(), ) .start; @@ -703,7 +703,8 @@ impl Widget for VirtualScroll { last_item = last_item.map(|it| it.max(*idx)).or(Some(*idx)); let auto_size = SizeDef::fit(size).with_height(LenDef::MaxContent); let child_size = ctx.compute_size(child, auto_size, size.into()); - ctx.run_layout(child, child_size); + ctx.layout_child(child, Point::ORIGIN, child_size); + let child_size = ctx.child_size(child); if *idx < self.anchor_index { height_before_anchor += child_size.height; } @@ -833,7 +834,7 @@ impl Widget for VirtualScroll { let item = self.items.get_mut(&idx); if let Some(item) = item { let size = ctx.child_size(item); - ctx.place_child(item, Point::new(0., y)); + ctx.move_child(item, Point::new(0., y)); // TODO: Padding/gap? y += size.height; } else { @@ -1002,7 +1003,7 @@ impl Widget for VirtualScroll { node.add_action(accesskit::Action::ScrollUp); } let at_end = self.anchor_index + 1 == self.valid_range.end && { - let max_scroll = (self.anchor_height - ctx.content_box_size().height / 2.).max(0.0); + let max_scroll = (self.anchor_height - ctx.content_box().size().height / 2.).max(0.0); self.scroll_offset_from_anchor >= max_scroll }; if !at_end { diff --git a/masonry/src/widgets/zstack.rs b/masonry/src/widgets/zstack.rs index d9e20f9d7..da6fbd2f6 100644 --- a/masonry/src/widgets/zstack.rs +++ b/masonry/src/widgets/zstack.rs @@ -245,7 +245,6 @@ impl Widget for ZStack { let mut max_baseline = f64::NEG_INFINITY; for child in &mut self.children { let child_size = ctx.compute_size(&mut child.widget, auto_size, context_size); - ctx.run_layout(&mut child.widget, child_size); let child_alignment = match child.alignment { ChildAlignment::SelfAligned(alignment) => alignment, @@ -256,11 +255,11 @@ impl Widget for ZStack { let extra_height = size.height - child_size.height; let child_origin = child_alignment.resolve(Rect::new(0., 0., extra_width, extra_height)); - ctx.place_child(&mut child.widget, child_origin); + ctx.layout_child(&mut child.widget, child_origin, child_size); let child_origin = ctx.child_origin(&child.widget); - let (first_baseline, last_baseline) = ctx.child_aligned_baselines(&child.widget); + let (first_baseline, last_baseline) = ctx.child_baselines(&child.widget); min_baseline = min_baseline.min(child_origin.y + first_baseline); max_baseline = max_baseline.max(child_origin.y + last_baseline); } diff --git a/masonry_core/src/app/layer_stack.rs b/masonry_core/src/app/layer_stack.rs index 526496809..715ed64cf 100644 --- a/masonry_core/src/app/layer_stack.rs +++ b/masonry_core/src/app/layer_stack.rs @@ -200,9 +200,8 @@ impl Widget for LayerStack { // Our measurement is just the passed on result from the base layer, // so the size we received is effectively meant for the base layer. - ctx.run_layout(&mut base_layer.root, size); // The base layer is always located at our origin. - ctx.place_child(&mut base_layer.root, Point::ORIGIN); + ctx.layout_child(&mut base_layer.root, Point::ORIGIN, size); ctx.derive_baselines(&base_layer.root); @@ -213,8 +212,7 @@ impl Widget for LayerStack { // so we won't give any FitContent fallback and instead just use MaxContent. // These other layers still have access to the window size via context size. let layer_size = ctx.compute_size(&mut layer.root, SizeDef::MAX, size.into()); - ctx.run_layout(&mut layer.root, layer_size); - ctx.place_child(&mut layer.root, layer.position); + ctx.layout_child(&mut layer.root, layer.position, layer_size); } } diff --git a/masonry_core/src/app/render_root.rs b/masonry_core/src/app/render_root.rs index 729bb4ef5..9dc944250 100644 --- a/masonry_core/src/app/render_root.rs +++ b/masonry_core/src/app/render_root.rs @@ -475,6 +475,11 @@ impl RenderRoot { match event { WindowEvent::Rescale(scale_factor) => { self.global_state.scale_factor = scale_factor; + // Request layout, which may end up being skipped. + self.root_state_mut().request_layout = true; + self.root_state_mut().set_needs_layout(true); + self.run_rewrite_passes(); + // Request render on everything, always. self.request_render_all(); Handled::Yes } diff --git a/masonry_core/src/core/contexts.rs b/masonry_core/src/core/contexts.rs index b64e7fc63..df3e5286a 100644 --- a/masonry_core/src/core/contexts.rs +++ b/masonry_core/src/core/contexts.rs @@ -27,7 +27,7 @@ use crate::core::{ }; use crate::kurbo::{Affine, Axis, Insets, Point, Rect, Size, Vec2}; use crate::layout::{LayoutSize, LenDef, Length, SizeDef}; -use crate::passes::layout::{place_widget, resolve_length, resolve_size, run_layout_on}; +use crate::passes::layout::{move_widget, resolve_length, resolve_size, run_layout_on}; use crate::peniko::Color; use crate::util::{ParentLinkedList, get_debug_color}; @@ -139,6 +139,11 @@ pub struct LayoutCtx<'a> { pub(crate) widget_state: &'a mut WidgetState, pub(crate) children: ArenaMutList<'a, WidgetArenaNode>, pub(crate) property_arena: &'a PropertyArena, + /// The transform used for pixel snapping. + /// + /// It enables pixel snapping and is similar to the border-box to window transform. + /// It does not account for snap-retaining translations like widget moving or scrolling. + pub(crate) snap_transform: Affine, } /// A context provided to the [`Widget::compose`] method. @@ -224,8 +229,7 @@ impl_context_method!( /// This transform is used during the mapping of this widget's border-box coordinate space /// to the parent's border-box coordinate space. /// - /// When calculating the effective border-box of this widget, first this transform - /// will be applied and then `scroll_translation` and `origin` applied on top. + /// This transform is applied before `scroll_translation` and `origin`. pub fn transform(&self) -> Affine { self.widget_state.transform } @@ -513,7 +517,7 @@ impl EventCtx<'_> { impl_context_method!(ActionCtx<'_>, EventCtx<'_>, { /// Sends a signal to parent widgets to scroll this widget's border-box into view. pub fn request_scroll_to_this(&mut self) { - let rect = self.widget_state.border_box_size().to_rect(); + let rect = self.widget_state.border_box(); self.global_state .scroll_request_targets .push((self.widget_state.id, rect)); @@ -786,25 +790,12 @@ impl LayoutCtx<'_> { } } - #[track_caller] - fn assert_placed(&self, child: &WidgetPod, method_name: &str) { - if self.get_child_state(child).is_expecting_place_child_call { - debug_panic!( - "Error in {}: trying to call '{}' with child '{}' {} before placing it", - self.widget_id(), - method_name, - self.get_child_dyn(child).short_type_name(), - child.id(), - ); - } - } - /// Computes the `child`'s preferred border-box size. /// /// The returned size will be finite, non-negative, and in logical pixels. /// /// Container widgets usually call this method as part of their [`layout`] logic, but - /// ultimately they can disregard the result and pass a different size to [`run_layout`]. + /// ultimately they can disregard the result and pass a different size to [`layout_child`]. /// Read [`layout`] docs for more details about that process. /// /// `auto_size` specifies the fallback behavior if the child has any dimension as [`Dim::Auto`]. @@ -822,7 +813,7 @@ impl LayoutCtx<'_> { /// [`layout`]: Widget::layout /// [`Ratio(0.5)`]: crate::layout::Dim::Ratio /// [`Dim::Auto`]: crate::layout::Dim::Auto - /// [`run_layout`]: Self::run_layout + /// [`layout_child`]: Self::layout_child pub fn compute_size( &mut self, child: &mut WidgetPod, @@ -840,7 +831,7 @@ impl LayoutCtx<'_> { ) } - /// Lays out the `child` widget with a chosen border-box `size`. + /// Lays out the `child` widget with the chosen border-box `origin` and `size`. /// /// Container widgets must call this on every child as part of their [`layout`] method. /// @@ -850,68 +841,108 @@ impl LayoutCtx<'_> { /// /// If the chosen border-box `size` is smaller than what is required to fit the child's /// borders and padding, then the `size` will be expanded to meet those constraints. + /// Also, if possible, the child's border-box will be auto-snapped to device pixels, + /// which might slightly change the `origin` and `size` as well. The `size` that the child + /// receives in its [`layout`] will be the final layout size. + /// + /// The layout origin and size can be accessed via [`child_origin`] and [`child_size`]. + /// If for some reason you want to change the origin, you can do so via [`move_child`]. + /// + /// The provided `origin` must be finite, in logical pixels, + /// and in this widget's content-box coordinate space. + /// Non-finite origin will fall back to zero with a logged warning. /// /// The provided `size` must be finite, non-negative, and in logical pixels. /// Non-finite or negative size will fall back to zero with a logged warning. /// /// # Panics /// + /// Panics if the provided `origin` is non-finite and debug assertions are enabled. + /// /// Panics if the provided `size` is non-finite or negative and debug assertions are enabled. /// /// [`layout`]: Widget::layout /// [`compute_size`]: Self::compute_size - pub fn run_layout(&mut self, child: &mut WidgetPod, chosen_size: Size) { + /// [`child_origin`]: Self::child_origin + /// [`child_size`]: Self::child_size + /// [`move_child`]: Self::move_child + #[track_caller] + pub fn layout_child( + &mut self, + child: &mut WidgetPod, + origin: Point, + size: Size, + ) { + // Convert the child's origin from this widget's content-box space + // to this widget's border-box space. + let translation = self.widget_state.border_box_translation(); + let child_origin = origin + translation; let id = child.id(); let node = self.children.item_mut(id).unwrap(); - run_layout_on(self.global_state, self.property_arena, node, chosen_size); + run_layout_on( + self.global_state, + self.property_arena, + node, + child_origin, + size, + self.snap_transform, + self.widget_state.is_snap_disabled, + self.widget_state.is_snap_unsupported, + ); let state_mut = &mut self.children.item_mut(id).unwrap().item.state; self.widget_state.merge_up(state_mut); } - /// Sets the position of the `child` widget, in this widget's content-box coordinate space. + /// Moves an already laid-out `child` widget. /// - /// Container widgets must call this method with each non-stashed child in their - /// [`layout`] method, after calling `ctx.run_layout(child, size)`. + /// This is meant for special cases where the desired position of the child + /// can't be known before it has been laid out, e.g. for baseline alignment. + /// + /// The provided `origin` must be finite, in logical pixels, + /// and in this widget's content-box coordinate space. + /// Non-finite origin will fall back to zero with a logged warning. + /// + /// If the child's origin was pixel-snapped during layout, then this new `origin` will be + /// auto-adjusted so that the delta from the old origin is an integer of device pixels. + /// This ensures that the previous result of the child's layout continues to be valid. + /// + /// To get the latest child origin use [`child_origin`]. /// /// # Panics /// - /// This method will panic if [`LayoutCtx::run_layout`] has not been called yet for the child. + /// This method will panic if [`LayoutCtx::layout_child`] has not been called yet for the child. /// - /// [`layout`]: Widget::layout + /// Panics if the provided `origin` is non-finite and debug assertions are enabled. + /// + /// [`child_origin`]: Self::child_origin #[track_caller] - pub fn place_child(&mut self, child: &mut WidgetPod, origin: Point) { - self.assert_layout_done(child, "place_child"); - if origin.x.is_nan() - || origin.x.is_infinite() - || origin.y.is_nan() - || origin.y.is_infinite() - { - debug_panic!( - "Error in {}: trying to call 'place_child' with child '{}' {} with invalid origin {:?}", - self.widget_id(), - self.get_child_dyn(child).short_type_name(), - child.id(), - origin, - ); - } + pub fn move_child(&mut self, child: &mut WidgetPod, origin: Point) { + self.assert_layout_done(child, "move_child"); - // Convert child's origin from this widget's content-box space to border-box space. + // Convert the child's origin from this widget's content-box space + // to this widget's border-box space. let translation = self.widget_state.border_box_translation(); let child_origin = origin + translation; - let child_state = self.get_child_state_mut(child); + let snap_transform = self.snap_transform; + let scale_factor = self.global_state.scale_factor; + let id = child.id(); + let child_state = &mut self.children.item_mut(id).unwrap().item.state; + + move_widget(child_state, child_origin, snap_transform, scale_factor); - place_widget(child_state, child_origin); + let child_state = &mut self.children.item_mut(id).unwrap().item.state; + self.widget_state.merge_up(child_state); } /// Sets explicit paint [`Insets`] for this widget. /// /// The argument is an [`Insets`] struct that indicates where your widget will overpaint, - /// relative to its layout content-box, as defined by the `size` given to the widget's + /// relative to its content-box, as defined by the `size` given to the widget's /// [`layout`] method. /// - /// You are only required to notify of painting that actually overflows the layout border-box. + /// You are only required to notify of painting that actually overflows the border-box. /// The insets will still be relative to the content-box, it's just that Masonry doesn't /// really need to be notified if you're just painting over your padding or borders. /// @@ -942,34 +973,35 @@ impl LayoutCtx<'_> { /// baselines then set the same baseline as both `first` and `last`. /// /// Most container widgets can use [`derive_baselines`] instead. - /// Multi-child containers should derive their baselines using [`child_aligned_baselines`]. + /// Multi-child containers should derive their baselines using [`child_baselines`]. /// /// [`derive_baselines`]: Self::derive_baselines - /// [`child_aligned_baselines`]: Self::child_aligned_baselines + /// [`child_baselines`]: Self::child_baselines + /// [`child_origin`]: Self::child_origin pub fn set_baselines(&mut self, first_baseline: f64, last_baseline: f64) { self.widget_state.first_baseline = first_baseline + self.widget_state.border_box_insets.y0; self.widget_state.last_baseline = last_baseline + self.widget_state.border_box_insets.y0; } - /// Sets explicit baselines for this widget such that they match the child's aligned baselines. + /// Sets explicit baselines for this widget such that they match the child's baselines. /// /// Most container widgets should use this method to derive their baselines from their child. - /// More complex containers can use [`child_aligned_baselines`] in multi-child scenarios. + /// More complex containers can use [`child_baselines`] in multi-child scenarios. /// /// # Panics /// - /// This method will panic if [`LayoutCtx::run_layout`] or [`LayoutCtx::place_child`] - /// have not been called yet for the child. + /// This method will panic if [`LayoutCtx::layout_child`] has not been called + /// yet for the child. /// - /// [`child_aligned_baselines`]: Self::child_aligned_baselines + /// [`child_baselines`]: Self::child_baselines + /// [`child_origin`]: Self::child_origin #[track_caller] pub fn derive_baselines(&mut self, child: &WidgetPod) { self.assert_layout_done(child, "derive_baselines"); - self.assert_placed(child, "derive_baselines"); let child_state = self.get_child_state(child); - let first_baseline = child_state.origin.y + child_state.aligned_first_baseline(); - let last_baseline = child_state.origin.y + child_state.aligned_last_baseline(); + let first_baseline = child_state.origin.y + child_state.first_baseline(); + let last_baseline = child_state.origin.y + child_state.last_baseline(); self.widget_state.first_baseline = first_baseline; self.widget_state.last_baseline = last_baseline; } @@ -990,94 +1022,59 @@ impl LayoutCtx<'_> { self.widget_state.border_box_insets } - /// Returns whether this widget needs to call [`LayoutCtx::run_layout`]. + /// Returns whether this widget needs to call [`LayoutCtx::layout_child`]. pub fn needs_layout(&self) -> bool { self.widget_state.needs_layout() } - /// Returns whether a child of this widget needs to call [`LayoutCtx::run_layout`]. + /// Returns whether a child of this widget needs to call [`LayoutCtx::layout_child`]. pub fn child_needs_layout(&self, child: &WidgetPod) -> bool { self.get_child_state(child).needs_layout() } - /// Returns the `child` widget's `(first, last)` layout baselines. + /// Returns the `child` widget's `(first, last)` baselines. /// - /// The distances are from the top of the child widget's layout border-box to its baseline. + /// The distances are from the top of the child widget's border-box to its baseline. /// - /// Call this if the child's baseline plays a role in choosing its placement. - /// For deriving this widget's baselines call [`child_aligned_baselines`] instead, - /// or better yet use [`derive_baselines`] if possible. + /// This should be used for deriving this widget's own baselines based on the child's baselines. + /// That is if [`derive_baselines`] can't be used, which makes common cases much easier. /// /// # Panics /// - /// This method will panic if [`LayoutCtx::run_layout`] has not been called yet for - /// the child. + /// This method will panic if [`LayoutCtx::layout_child`] has not been called yet for the child. /// - /// [`child_aligned_baselines`]: Self::child_aligned_baselines /// [`derive_baselines`]: Self::derive_baselines #[track_caller] - pub fn child_layout_baselines(&self, child: &WidgetPod) -> (f64, f64) { - self.assert_layout_done(child, "child_layout_baselines"); + pub fn child_baselines(&self, child: &WidgetPod) -> (f64, f64) { + self.assert_layout_done(child, "child_baselines"); let child_state = self.get_child_state(child); - ( - child_state.layout_first_baseline(), - child_state.layout_last_baseline(), - ) + (child_state.first_baseline(), child_state.last_baseline()) } - /// Returns the `child` widget's `(first, last)` aligned baselines. - /// - /// The distances are from the top of the child widget's aligned border-box to its baseline. - /// - /// This aligned version should be used for deriving this widget's own baselines based - /// on the child's baselines. That is if [`derive_baselines`] can't be used. - /// For deciding where to place the child based on its baselines, - /// you need to use [`child_layout_baselines`] instead. - /// - /// # Panics - /// - /// This method will panic if [`LayoutCtx::run_layout`] or [`LayoutCtx::place_child`] - /// have not been called yet for the child. - /// - /// [`derive_baselines`]: Self::derive_baselines - /// [`child_layout_baselines`]: Self::child_layout_baselines - #[track_caller] - pub fn child_aligned_baselines(&self, child: &WidgetPod) -> (f64, f64) { - self.assert_layout_done(child, "child_aligned_baselines"); - self.assert_placed(child, "child_aligned_baselines"); - - let child_state = self.get_child_state(child); - ( - child_state.aligned_first_baseline(), - child_state.aligned_last_baseline(), - ) - } - - /// Returns the given child's aligned border-box origin + /// Returns the given child's border-box origin /// in this widget's content-box coordinate space. /// /// # Panics /// - /// This method will panic if [`LayoutCtx::run_layout`] or [`LayoutCtx::place_child`] - /// have not been called yet for the child. + /// This method will panic if [`LayoutCtx::layout_child`] has not been called + /// yet for the child. #[track_caller] pub fn child_origin(&self, child: &WidgetPod) -> Point { self.assert_layout_done(child, "child_origin"); - self.assert_placed(child, "child_origin"); self.get_child_state(child).origin - self.widget_state.border_box_translation() } - /// Returns the given child's layout border-box size. + /// Returns the given child's border-box size. /// /// # Panics /// - /// This method will panic if [`LayoutCtx::run_layout`] has not been called yet for - /// the child. + /// This method will panic if [`LayoutCtx::layout_child`] has not been called + /// yet for the child. #[track_caller] pub fn child_size(&self, child: &WidgetPod) -> Size { self.assert_layout_done(child, "child_size"); - self.get_child_state(child).layout_border_box_size + self.get_child_state(child).border_box_size } /// Sets the widget's clip path in the widget's content-box coordinate space. @@ -1124,10 +1121,9 @@ impl LayoutCtx<'_> { impl ComposeCtx<'_> { /// Sets the scroll translation for the child widget. /// - /// The translation is applied on top of the position from [`LayoutCtx::place_child`]. + /// The translation is applied on top of the position from [`LayoutCtx::layout_child`]. /// - /// The given translation may be quantized so the child's final position - /// stays pixel-perfect. + /// The total translation may get quantized so the child's position stays pixel-perfect. pub fn set_child_scroll_translation( &mut self, child: &mut WidgetPod, @@ -1147,51 +1143,17 @@ impl ComposeCtx<'_> { ); } - let translation = translation.round(); - let child = self.get_child_state_mut(child); if translation != child.scroll_translation { child.scroll_translation = translation; - child.transform_changed = true; - } - } - - /// Sets the scroll translation for the child widget. - /// - /// The translation is applied on top of the position from [`LayoutCtx::place_child`]. - /// - /// Unlike [`Self::set_child_scroll_translation`], doesn't perform pixel-snapping. - /// This method should be used for intermediary scroll values during scroll animations. - pub fn set_animated_child_scroll_translation( - &mut self, - child: &mut WidgetPod, - translation: Vec2, - ) { - if translation.x.is_nan() - || translation.x.is_infinite() - || translation.y.is_nan() - || translation.y.is_infinite() - { - debug_panic!( - "Error in {}: trying to call 'set_animated_child_scroll_translation' with child '{}' {} with invalid translation {:?}", - self.widget_id(), - self.get_child_dyn(child).short_type_name(), - child.id(), - translation, - ); - } - - let child = self.get_child_state_mut(child); - if translation != child.scroll_translation { - child.scroll_translation = translation; - child.transform_changed = true; + child.mark_compose_transform_changed(); } } } -// --- MARK: GET LAYOUT +// --- MARK: GET GEOMETRY // Methods on all context types except MeasureCtx and LayoutCtx -// These methods access layout info calculated during the layout pass. +// These methods access geometry resolved during layout and compose. impl_context_method!( MutateCtx<'_>, ActionCtx<'_>, @@ -1202,49 +1164,21 @@ impl_context_method!( PaintCtx<'_>, AccessCtx<'_>, { - /// Returns the aligned content-box size of this widget. - pub fn content_box_size(&self) -> Size { - let border_box_size = self.widget_state.border_box_size(); - Size::new( - (border_box_size.width - self.widget_state.border_box_insets.x_value()).max(0.), - (border_box_size.height - self.widget_state.border_box_insets.y_value()).max(0.), - ) - } - - /// Returns the aligned border-box size of this widget. - pub fn border_box_size(&self) -> Size { - self.widget_state.border_box_size() - } - - /// Returns the aligned paint-box size of this widget. - pub fn paint_box_size(&self) -> Size { - self.widget_state.paint_box().size() - } - - /// Returns the aligned content-box rect of this widget + /// Returns the content-box rect of this widget /// in this widget's content-box coordinate space. pub fn content_box(&self) -> Rect { - let border_box_size = self.widget_state.border_box_size(); - Rect::new( - 0., - 0., - (border_box_size.width - self.widget_state.border_box_insets.x_value()).max(0.), - (border_box_size.height - self.widget_state.border_box_insets.y_value()).max(0.), - ) + let translation = self.widget_state.border_box_translation(); + self.widget_state.content_box() - translation } - /// Returns the aligned border-box rect of this widget + /// Returns the border-box rect of this widget /// in this widget's content-box coordinate space. pub fn border_box(&self) -> Rect { - let border_box_size = self.widget_state.border_box_size(); - let origin = Point::new( - -self.widget_state.border_box_insets.x0, - -self.widget_state.border_box_insets.y0, - ); - Rect::from_origin_size(origin, border_box_size) + let translation = self.widget_state.border_box_translation(); + self.widget_state.border_box() - translation } - /// Returns the aligned paint-box rect of this widget + /// Returns the paint-box rect of this widget /// in this widget's content-box coordinate space. /// /// Covers the area we expect to be invalidated when the widget is painted. @@ -1257,8 +1191,7 @@ impl_context_method!( /// /// It contains this widget and all of its descendents. /// - /// This is the union of clipped effective paint-box rects, i.e. the union of - /// globally transformed aligned border-box rects with paint insets applied. + /// This is the union of clipped paint-box rects in the window's coordinate space. /// /// See [bounding box documentation] for more details. /// @@ -1267,15 +1200,15 @@ impl_context_method!( self.widget_state.bounding_box } - /// Returns the first baseline relative to the top of the widget's aligned content-box. + /// Returns the first baseline relative to the top of the widget's content-box. pub fn first_baseline(&self) -> f64 { - let border_box_baseline = self.widget_state.aligned_first_baseline(); + let border_box_baseline = self.widget_state.first_baseline(); border_box_baseline - self.widget_state.border_box_insets.y0 } - /// Returns the last baseline relative to the top of the widget's aligned content-box. + /// Returns the last baseline relative to the top of the widget's content-box. pub fn last_baseline(&self) -> f64 { - let border_box_baseline = self.widget_state.aligned_last_baseline(); + let border_box_baseline = self.widget_state.last_baseline(); border_box_baseline - self.widget_state.border_box_insets.y0 } @@ -1701,8 +1634,20 @@ impl_context_method!( /// It behaves similarly as CSS transforms. pub fn set_transform(&mut self, transform: Affine) { self.widget_state.transform = transform; - self.widget_state.transform_changed = true; - self.request_compose(); + self.widget_state.mark_transform_changed(); + if self.widget_state.is_snap_disabled { + self.request_compose(); + } else { + self.request_layout(); + } + } + + /// Sets whether pixel snapping is disabled for this widget and its descendants. + /// + /// Changing this requests layout because snapping affects layout geometry. + pub fn set_snap_disabled(&mut self, disabled: bool) { + self.widget_state.is_explicitly_snap_disabled = disabled; + self.request_layout(); } /// Adds a string to this widget's [class set]. @@ -1910,7 +1855,7 @@ impl_context_method!( /// to the platform. The area can be used by the platform to, for example, place a /// candidate box near that area, while ensuring the area is not obscured. /// - /// If no IME area is set, then Masonry will use the widget's aligned border-box rect. + /// If no IME area is set, then Masonry will use the widget's border-box rect. /// /// [focused]: EventCtx::request_focus /// [accepts text input]: Widget::accepts_text_input diff --git a/masonry_core/src/core/widget.rs b/masonry_core/src/core/widget.rs index 767f3e55e..ee5d6921e 100644 --- a/masonry_core/src/core/widget.rs +++ b/masonry_core/src/core/widget.rs @@ -336,15 +336,15 @@ pub trait Widget: AsDynWidget + Any { /// Note, however, that the child will still be in control of its own [`paint`] method. /// If a child is given a size smaller than its [`MinContent`], its painting is likely /// to overflow its bounds, depending on both the child's and the parent's clip settings. - /// 3. Call [`run_layout`] on the child with the chosen border-box size. + /// 3. Decide on the border-box origin of the child, relative to the parent's content-box. + /// 4. Call [`layout_child`] on the child with the chosen origin and border-box size. /// This will recursively trigger the layout pass on both the child and all its descendants. - /// 4. Call [`place_child`] to give the child a location, relative to the parent's content-box. - /// With that, the laying out of the child is finished. + /// The final layout origin and size may slightly change due to pixel snapping. /// /// The order of laying out children doesn't matter. It is also valid to interleave the calls. /// For example you might `compute_size` for a few, lay out one, re-compute the others. /// - /// Failing to lay out and place some child is a logic error and may lead to panics. + /// Failing to lay out some child is a logic error and may lead to panics. /// /// Container widgets must not add or remove children during layout. /// Doing so is a logic error and may lead to panics. @@ -354,8 +354,7 @@ pub trait Widget: AsDynWidget + Any { /// /// [`compute_size`]: LayoutCtx::compute_size /// [`compute_length`]: LayoutCtx::compute_length - /// [`run_layout`]: LayoutCtx::run_layout - /// [`place_child`]: LayoutCtx::place_child + /// [`layout_child`]: LayoutCtx::layout_child /// [`children_ids`]: Self::children_ids /// [`paint`]: Self::paint /// [`MinContent`]: crate::layout::Dim::MinContent diff --git a/masonry_core/src/core/widget_mut.rs b/masonry_core/src/core/widget_mut.rs index 538dff9d6..158b3057e 100644 --- a/masonry_core/src/core/widget_mut.rs +++ b/masonry_core/src/core/widget_mut.rs @@ -122,6 +122,16 @@ impl WidgetMut<'_, W> { self.ctx.set_transform(transform); } + /// Sets whether pixel snapping is disabled for this widget and its descendants. + /// + /// This is primarily useful for continuous motion such as drag, scroll, or transform + /// animation, where stable fractional presentation is preferred over pixel-aligned layout. + /// + /// Changing this requests layout because snapping affects layout geometry. + pub fn set_snap_disabled(&mut self, disabled: bool) { + self.ctx.set_snap_disabled(disabled); + } + /// Attempts to downcast to `WidgetMut` of concrete widget type. pub fn try_downcast( &mut self, diff --git a/masonry_core/src/core/widget_pod.rs b/masonry_core/src/core/widget_pod.rs index 1c1ec12d0..6ebd2eca3 100644 --- a/masonry_core/src/core/widget_pod.rs +++ b/masonry_core/src/core/widget_pod.rs @@ -83,9 +83,10 @@ pub struct WidgetOptions { /// Local transform used during the mapping of this widget's border-box coordinate space /// to the parent's border-box coordinate space. /// - /// When calculating the effective border-box of this widget, first this transform - /// will be applied and then `scroll_translation` and `origin` applied on top. + /// This transform is applied before `scroll_translation` and `origin`. pub transform: Affine, + /// Whether pixel snapping should be disabled for this widget and its descendants. + pub snap_disabled: bool, /// The disabled state the widget will be created with. pub disabled: bool, } @@ -179,6 +180,15 @@ impl NewWidget { self } + /// Sets whether pixel snapping is disabled for this widget and its descendants. + /// + /// This is primarily useful for continuous motion such as drag, scroll, or transform + /// animation, where stable fractional presentation is preferred over pixel-aligned layout. + pub fn with_snap_disabled(mut self, disabled: bool) -> Self { + self.options.snap_disabled = disabled; + self + } + /// Assigns a [`PropertyStack`](crate::core::PropertyStack) to this widget. pub fn with_property_stack(mut self, id: PropertyStackId) -> Self { self.property_stack_id = Some(id); diff --git a/masonry_core/src/core/widget_ref.rs b/masonry_core/src/core/widget_ref.rs index a626595d7..58c575a9b 100644 --- a/masonry_core/src/core/widget_ref.rs +++ b/masonry_core/src/core/widget_ref.rs @@ -177,7 +177,7 @@ impl WidgetRef<'_, dyn Widget> { /// Recursively finds the innermost widget at the given position, using /// [`Widget::find_widget_under_pointer`] to descend the widget tree. If `self` does not contain the - /// given position in its aligned border-box or clip path, this returns `None`. + /// given position in its border-box or clip path, this returns `None`. /// /// **pos** - the position is in the window's coordinate space, /// e.g. `(0,0)` is the top-left corner of the window. diff --git a/masonry_core/src/core/widget_state.rs b/masonry_core/src/core/widget_state.rs index 0227a6324..8bd7e784c 100644 --- a/masonry_core/src/core/widget_state.rs +++ b/masonry_core/src/core/widget_state.rs @@ -9,7 +9,7 @@ use tracing::Span; use crate::core::{ ClassSetDiff, PaintLayerMode, PropertyCache, PropertyStackId, WidgetId, WidgetOptions, }; -use crate::layout::MeasurementCache; +use crate::layout::{MeasurementCache, SnapKey, snap_translation_delta}; // TODO - Reduce WidgetState size. // See https://github.com/linebender/xilem/issues/706 @@ -72,25 +72,13 @@ pub(crate) struct WidgetState { pub(crate) id: WidgetId, // --- LAYOUT --- - /// The origin (top-left) of the widget's aligned border-box + /// The origin (top-left) of the widget's border-box /// in the parent's border-box coordinate space. - /// - /// Together with `end_point`, these constitute the widget's aligned border-box. pub(crate) origin: Point, - /// The bottom right of the widget's aligned border-box - /// in the parent's border-box coordinate space. - /// - /// Computed from the widget's `origin` and `layout_border_box_size`, with some pixel snapping. - pub(crate) end_point: Point, - /// The widget's layout border-box size. + /// The size of the widget's border-box. /// - /// This is the chosen border-box size with min/max constraints applied. - /// - /// It is used to: - /// * Determine layout cache validity. - /// * Derive the widget's layout content-box size that will be given to `Widget::layout`. - /// * Compute `end_point` when the widget is placed to an `origin` by its parent. - pub(crate) layout_border_box_size: Size, + /// This is also used to determine layout cache validity. + pub(crate) border_box_size: Size, /// The insets for converting between content-box and border-box rects. /// /// Add these insets to the content-box to get the border-box, @@ -109,17 +97,16 @@ pub(crate) struct WidgetState { /// An axis aligned bounding rect (AABB in 2D), /// containing itself and all its descendents in the window's coordinate space. /// - /// This is the union of clipped effective paint-box rects, i.e. the union of - /// globally transformed aligned border-box rects with paint insets applied. + /// This is the union of clipped paint-box rects in the window's coordinate space. pub(crate) bounding_box: Rect, - /// The offset of the first baseline relative to the top of the widget's layout border-box. + /// The offset of the first baseline relative to the top of the widget's border-box. /// /// In general, this will be `f64::NAN`; the bottom of the widget will be considered /// the baseline. Widgets that contain text or controls that expect to be /// laid out alongside text can set this as appropriate. pub(crate) first_baseline: f64, - /// The offset of the last baseline relative to the top of the widget's layout border-box. + /// The offset of the last baseline relative to the top of the widget's border-box. /// /// In general, this will be `f64::NAN`; the bottom of the widget will be considered /// the baseline. Widgets that contain text or controls that expect to be @@ -138,8 +125,7 @@ pub(crate) struct WidgetState { /// Local transform used during the mapping of this widget's border-box coordinate space /// to the parent's border-box coordinate space. /// - /// When calculating the effective border-box of this widget, first this transform - /// will be applied and then `scroll_translation` and `origin` applied on top. + /// This transform is applied before `scroll_translation` and `origin`. pub(crate) transform: Affine, /// Global transform mapping this widget's border-box coordinate space /// to the window's coordinate space. @@ -152,8 +138,23 @@ pub(crate) struct WidgetState { pub(crate) window_transform: Affine, /// Translation applied by scrolling, applied after applying `transform` to this widget. pub(crate) scroll_translation: Vec2, - /// The `transform` or `scroll_translation` has changed. + /// The local `transform` has changed. + /// + /// This is tracked separately for Xilem's benefit. pub(crate) transform_changed: bool, + /// The effective compose transform has changed because `transform`, `scroll_translation`, or + /// `origin` has changed. + pub(crate) compose_transform_changed: bool, + + /// This widget explicitly disables pixel snapping for itself and descendants. + pub(crate) is_explicitly_snap_disabled: bool, + /// Pixel snapping is disabled for this widget because this widget or an ancestor disabled it. + pub(crate) is_snap_disabled: bool, + /// Pixel snapping is unsupported for this widget because its transform branch does not + /// preserve axis-aligned boxes in window space. + pub(crate) is_snap_unsupported: bool, + /// Cache key for the active snap transform. + pub(crate) snap_key: Option, // --- INTERACTIONS --- /// The `TypeId` of the widget's `Widget::Action` type. @@ -180,9 +181,6 @@ pub(crate) struct WidgetState { /// `WidgetAdded` hasn't been sent to this widget yet. pub(crate) is_new: bool, - /// A flag used to track and debug missing calls to `place_child`. - pub(crate) is_expecting_place_child_call: bool, - /// This widget explicitly requested layout pub(crate) request_layout: bool, /// This widget or a descendant explicitly requested layout @@ -294,8 +292,7 @@ impl WidgetState { id, origin: Point::ORIGIN, - end_point: Point::ORIGIN, - layout_border_box_size: Size::ZERO, + border_box_size: Size::ZERO, border_box_insets: Insets::ZERO, paint_box_insets: Insets::ZERO, bounding_box: Rect::ZERO, @@ -306,6 +303,11 @@ impl WidgetState { window_transform: Affine::IDENTITY, scroll_translation: Vec2::ZERO, transform_changed: false, + compose_transform_changed: false, + is_explicitly_snap_disabled: options.snap_disabled, + is_snap_disabled: false, + is_snap_unsupported: false, + snap_key: None, action_type, accepts_pointer_interaction: true, @@ -315,7 +317,6 @@ impl WidgetState { ime_area: None, is_new: true, - is_expecting_place_child_call: false, request_layout: true, needs_layout: true, measurement_cache: MeasurementCache::new(), @@ -383,6 +384,18 @@ impl WidgetState { self.needs_update_props |= child_state.needs_update_props; } + /// Marks this widget's local transform as changed. + pub(crate) fn mark_transform_changed(&mut self) { + self.transform_changed = true; + self.mark_compose_transform_changed(); + } + + /// Marks this widget's effective compose transform as changed. + pub(crate) fn mark_compose_transform_changed(&mut self) { + self.compose_transform_changed = true; + self.needs_compose = true; + } + // TODO: Add WidgetState::add_diff method that merges a ClassSetDiff into the WidgetState's class_diff. /// Returns `true` if this widget or a descendant explicitly requested layout. @@ -400,14 +413,24 @@ impl WidgetState { self.needs_layout = needs_layout; } - /// The aligned border-box size of this widget. - pub(crate) fn border_box_size(&self) -> Size { - (self.end_point - self.origin).to_size() + /// Returns the widget's content-box in the widget's border-box coordinate space. + pub(crate) fn content_box(&self) -> Rect { + let border_box = self.border_box(); + let x0 = border_box.x0 + self.border_box_insets.x0; + let y0 = border_box.y0 + self.border_box_insets.y0; + let x1 = (border_box.x1 - self.border_box_insets.x1).max(x0); + let y1 = (border_box.y1 - self.border_box_insets.y1).max(y0); + Rect::new(x0, y0, x1, y1) } - /// Returns the widget's aligned paint-box rect in the widget's border-box coordinate space. + /// Returns the widget's border-box in the widget's border-box coordinate space. + pub(crate) fn border_box(&self) -> Rect { + self.border_box_size.to_rect() + } + + /// Returns the widget's paint-box in the widget's border-box coordinate space. pub(crate) fn paint_box(&self) -> Rect { - self.border_box_size().to_rect() + self.paint_box_insets + self.border_box() + self.paint_box_insets } /// Returns the [`Vec2`] for translating between this widget's @@ -419,37 +442,50 @@ impl WidgetState { Vec2::new(self.border_box_insets.x0, self.border_box_insets.y0) } - /// Returns the first baseline relative to the top of the widget's layout border-box. - pub(crate) fn layout_first_baseline(&self) -> f64 { - if self.first_baseline.is_nan() { - self.layout_border_box_size.height + /// Returns this widget's total local transform needed for composing. + /// + /// This maps from this widget's border-box coordinate space into its + /// parent's border-box coordinate space. It includes the widget's local + /// transform, origin, and the pixel-snapped scroll translation. + pub(crate) fn compose_local_transform( + &self, + parent_window_transform: Affine, + scale_factor: f64, + ) -> Affine { + let scroll_translation = if self.is_snap_active() { + snap_translation_delta( + self.scroll_translation, + parent_window_transform, + scale_factor, + ) } else { - self.first_baseline - } + self.scroll_translation + }; + + // The translation needs to be applied after the local transform so scrolling + // and layout origin are in the transformed coordinate space, similar to CSS. + let local_translation = scroll_translation + self.origin.to_vec2(); + self.transform.then_translate(local_translation) } - /// Returns the last baseline relative to the top of the widget's layout border-box. - pub(crate) fn layout_last_baseline(&self) -> f64 { - if self.last_baseline.is_nan() { - self.layout_border_box_size.height - } else { - self.last_baseline - } + /// Returns whether pixel snapping is active for this widget. + pub(crate) fn is_snap_active(&self) -> bool { + !self.is_snap_disabled && !self.is_snap_unsupported } - /// Returns the first baseline relative to the top of the widget's aligned border-box. - pub(crate) fn aligned_first_baseline(&self) -> f64 { + /// Returns the first baseline relative to the top of the widget's border-box. + pub(crate) fn first_baseline(&self) -> f64 { if self.first_baseline.is_nan() { - self.end_point.y - self.origin.y + self.border_box_size.height } else { self.first_baseline } } - /// Returns the last baseline relative to the top of the widget's aligned border-box. - pub(crate) fn aligned_last_baseline(&self) -> f64 { + /// Returns the last baseline relative to the top of the widget's border-box. + pub(crate) fn last_baseline(&self) -> f64 { if self.last_baseline.is_nan() { - self.end_point.y - self.origin.y + self.border_box_size.height } else { self.last_baseline } @@ -457,14 +493,13 @@ impl WidgetState { /// Returns the area being edited by an IME, in the window's coordinate space. /// - /// If no explicit `ime_area` has been defined this will return the effective border-box area. + /// If no explicit `ime_area` has been defined this will return the border-box + /// area in the window's coordinate space. pub(crate) fn get_ime_area(&self) -> Rect { // Note: this returns sensible values for a widget that is translated and/or rescaled. // Other transformations like rotation may produce weird IME areas. - self.window_transform.transform_rect_bbox( - self.ime_area - .unwrap_or_else(|| self.border_box_size().to_rect()), - ) + self.window_transform + .transform_rect_bbox(self.ime_area.unwrap_or(self.border_box())) } /// Returns the result of intersecting the widget's clip path (if any) with the given rect. diff --git a/masonry_core/src/doc/masonry_concepts.md b/masonry_core/src/doc/masonry_concepts.md index 86fb2f676..87901a98a 100644 --- a/masonry_core/src/doc/masonry_concepts.md +++ b/masonry_core/src/doc/masonry_concepts.md @@ -210,13 +210,12 @@ The box model refers to the following box hierarchy: The box lifecycle terms describe what stage a box is in during its journey from idea to painting. 1. **Preferred** - The size that a widget wishes to be and is the result of `LayoutCtx::compute_size`. -2. **Chosen** - The size that the parent of a widget ends up choosing for it and is given to `LayoutCtx::run_layout`. -3. **Layout** - The result of the chosen size being potentially adjusted to meet min/max constraints. - For example, if the parent gave a size too small to even contain the child's borders and padding. -4. **Aligned** - Once a parent places its child to a specific position, that position will be aligned to the pixel grid. - This alignment is done in the parent's border-box coordinate space using the child's layout border-box size. -5. **Effective** - The actual visual box that gets painted on the screen. - This is the result when all of the transforms of the widget's tree branch are applied to its aligned box. +2. **Chosen** - The origin and size that the parent of a widget ends up choosing for it and is given to `LayoutCtx::layout_child`. +3. **Layout** - The origin and size used for layout, painting, hit testing, accessibility bounds, etc. + These may have been adjusted to meet min/max constraints; for example, if the parent gave a size too small to even contain the child's borders and padding. + When pixel snapping is active, the layout geometry also accounts for the device pixel grid in order to have a sharp presentation. + +Unless documentation explicitly says a box is preferred or chosen, unqualified box terms such as content-box and border-box refer to the layout stage. ### Presence of descendants @@ -226,11 +225,11 @@ As the descendants may be overflowing these bounds or a transformation may move ### Bounding-box -We only calculate the effective variant of the bounding-box, i.e. where all transforms have been applied. -The effective bounding-box is a union of the widget's effective paint-box and the bounding-boxes of all of its descendants. +We only calculate the bounding-box in the window's coordinate space, i.e. where all transforms have been applied. +The bounding-box is a union of the widget's paint-box, transformed into the window's coordinate space, and the bounding-boxes of all of its descendants. Additionally, these are clipped according to the per-widget clip rules. -This effective bounding-box in the window's coordinate space is used to determine which pointer events might affect either the widget or its descendants. +This window-space bounding-box is used to determine which pointer events might affect either the widget or its descendants. The bounding-boxes of the widget tree form a kind of "bounding volume hierarchy": when looking to find which widget a pointer is on, Masonry will automatically exclude any widget if the pointer is outside its bounding-box. @@ -302,17 +301,16 @@ We will probably need to implement other features before we can handle it proper ## Pixel snapping -Masonry currently handles pixel snapping for its widgets. - -The basic idea is that when widgets are laid out, Masonry takes their reported sizes and positions, and rounds them to integer values, so that the drawn shapes line up with pixels. - -This is done "at the end" of the layout pass, so to speak, so that widgets can lay themselves out assuming a floating point coordinate space, and without worrying about rounding errors. +Masonry handles pixel snapping for its widgets. -The snapping is done in a way that preserves relations between widgets: if one widget ends precisely where another stops, Masonry will pick values so that their pixel-snapped layout rects have no gap or overlap. +Widgets manage their layout sizes and positions in logical pixels which can be fractional. +When a parent lays out a child, the child's chosen border-box is mapped to device pixels using the layout-time transform. +Then the border-box edges are snapped to the pixel grid if the transform supports it. +The result is then mapped back to widget-local logical coordinates and becomes the child's layout geometry. - -**Note:** This may produce incorrect results with DPI scaling. -DPI-aware pixel snapping is a future feature. +The snapping is done in a way that preserves relations between widgets. +If one widget ends precisely where another starts, Masonry will snap coordinates so that their boxes have no gap or overlap. +Snapping also accounts for the active DPI scale factor and is disabled for transforms such as rotation or shear. [`Cancel`]: ui_events::pointer::PointerEvent::Cancel diff --git a/masonry_core/src/doc/pass_system.md b/masonry_core/src/doc/pass_system.md index 5f1521fde..0673f0f00 100644 --- a/masonry_core/src/doc/pass_system.md +++ b/masonry_core/src/doc/pass_system.md @@ -60,7 +60,7 @@ To address these invalidations, Masonry runs a set of **rewrite passes** over th - **update_stashed:** Updates the stashed status of widgets. - **update_focusable:** Updates whether widgets have focusable children. (Internal-only, doesn't call widget methods.) - **update_focus:** Updates the focused status of widgets. -- **layout:** Computes the layout of the widget tree. +- **layout:** Computes the layout of the widget tree, including layout-time pixel snapping. - **update_scrolls:** Updates the scroll positions of widgets. - **compose:** Assigns transforms to widgets. - **update_pointer:** Updates the hovered status of widgets and the current cursor icon. @@ -183,14 +183,17 @@ The layout pass will determine the size of widgets, which might involve calling Then as sizes get resolved, they get passed to the `layout` methods on widgets. -Unlike with other passes, container widgets' `Widget::layout()` method must "manually" recurse by calling [`LayoutCtx::run_layout`] then [`LayoutCtx::place_child`] for each of their children. - +Unlike with other passes, container widgets' `Widget::layout()` method must "manually" recurse by calling [`LayoutCtx::layout_child`] for each of their children. Not doing so is a logical bug, and may trigger debug assertions. +During layout, the parent chooses the origin and size for all its children. +When pixel snapping is active and the child's transform supports it, Masonry pixel-snaps the chosen border-box before the child's `Widget::layout()` method runs. +The resulting layout geometry is the local geometry used by paint, events, accessibility, and query contexts. ### Compose pass The **compose** pass runs top-down and assigns transforms to children. Transform-only presentation changes (e.g. scrolling) should request compose instead of requesting layout. +When snapping is active, compose quantizes scroll translation to integer device-pixel deltas so layout geometry remains snapped. [`Widget::compose`] is called when the widget explicitly requests compose or after that widget's [`Widget::layout`] method runs. It may also be called in other scenarios, but widgets should not rely on [`Widget::compose`] as a notification for every change to their window transform. @@ -244,8 +247,7 @@ They can access the layout of children if they have already been laid out. [`Widget::measure`]: crate::core::Widget::measure [`Widget::layout`]: crate::core::Widget::layout [`Widget::compose`]: crate::core::Widget::compose -[`LayoutCtx::place_child`]: crate::core::LayoutCtx::place_child -[`LayoutCtx::run_layout`]: crate::core::LayoutCtx::run_layout +[`LayoutCtx::layout_child`]: crate::core::LayoutCtx::layout_child [`WidgetMut`]: crate::core::WidgetMut [`RenderRoot`]: crate::app::RenderRoot [`PaintCtx`]: crate::core::PaintCtx diff --git a/masonry_core/src/layout/mod.rs b/masonry_core/src/layout/mod.rs index 3a284151c..f3f0d73d9 100644 --- a/masonry_core/src/layout/mod.rs +++ b/masonry_core/src/layout/mod.rs @@ -11,6 +11,7 @@ mod len_req; mod length; mod measurement_cache; mod size_def; +mod snap; mod unit_point; pub use as_unit::*; @@ -21,4 +22,5 @@ pub use len_req::*; pub use length::*; pub(crate) use measurement_cache::*; pub use size_def::*; +pub(crate) use snap::*; pub use unit_point::*; diff --git a/masonry_core/src/layout/snap.rs b/masonry_core/src/layout/snap.rs new file mode 100644 index 000000000..f895c8e21 --- /dev/null +++ b/masonry_core/src/layout/snap.rs @@ -0,0 +1,129 @@ +// Copyright 2026 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +use kurbo::{Affine, Point, Rect, Vec2}; + +/// Cache key for axis-aligned layout-time snapping. +#[derive(Clone, Copy, Debug, PartialEq)] +pub(crate) struct SnapKey { + /// X scale from border-box space to window space. + pub(crate) scale_x: f64, + /// Y scale from border-box space to window space. + pub(crate) scale_y: f64, + /// Window space pixel x translation's fractional part. + pub(crate) translation_x: f64, + /// Window space pixel y translation's fractional part. + pub(crate) translation_y: f64, +} + +impl SnapKey { + /// Creates a new snap key based on the provided `snap_transform`. + /// + /// The provided `snap_transform` must support snapping, + /// which can be checked via [`supports_box_snapping`]. + /// + /// # Panics + /// + /// Panics if `snap_transform` does not support snapping and debug assertions are enabled. + pub(crate) fn new(snap_transform: Affine, scale_factor: f64) -> Self { + let local_to_device = snap_transform.then_scale(scale_factor); + debug_assert!( + supports_box_snapping(local_to_device), + "snap key creation attempted with an incompatible transform" + ); + + let [scale_x, _, _, scale_y, translation_x, translation_y] = local_to_device.as_coeffs(); + + Self { + scale_x, + scale_y, + translation_x: translation_x.rem_euclid(1.0), + translation_y: translation_y.rem_euclid(1.0), + } + } +} + +/// Returns whether the transform supports box snapping. +/// +/// Box snapping is supported when the transform maps local widget axes to device +/// axes without rotation or shear. Scaling, translation, and axis flips are fine. +pub(crate) fn supports_box_snapping(transform: Affine) -> bool { + let [a, b, c, d, _, _] = transform.as_coeffs(); + + // Kurbo affine coefficients represent: + // + // x' = a*x + c*y + e + // y' = b*x + d*y + f + // + // The off-diagonal coefficients b and c must be zero. If either is non-zero, x contributes + // to output y or y contributes to output x. That means the transform mixes axes, as in + // rotation, shear, or axis swapping, which this snapping path intentionally does not support. + // + // The scale coefficients a and d must be non-zero so the transform can be inverted + // when mapping snapped device edges back to local coordinates. + // + // The translation coefficients e and f do not affect whether edges stay axis-aligned, + // so they are intentionally ignored. + b == 0. && c == 0. && a != 0. && d != 0. +} + +/// Snaps the given `border_box` to device pixel edges. +/// +/// The provided `snap_transform` must support snapping, +/// which can be checked via [`supports_box_snapping`]. +/// +/// # Panics +/// +/// Panics if `snap_transform` does not support snapping and debug assertions are enabled. +pub(crate) fn snap_border_box(border_box: Rect, snap_transform: Affine, scale_factor: f64) -> Rect { + let local_to_device = snap_transform.then_scale(scale_factor); + debug_assert!( + supports_box_snapping(local_to_device), + "box snapping attempted with an incompatible transform" + ); + + let device_border_box = local_to_device.transform_rect_bbox(border_box); + let snapped_device_border_box = Rect::new( + device_border_box.x0.round(), + device_border_box.y0.round(), + device_border_box.x1.round(), + device_border_box.y1.round(), + ); + + let device_to_local = local_to_device.inverse(); + Rect::from_points( + device_to_local * Point::new(snapped_device_border_box.x0, snapped_device_border_box.y0), + device_to_local * Point::new(snapped_device_border_box.x1, snapped_device_border_box.y1), + ) +} + +/// Snaps a local translation delta to an integer device-pixel delta. +/// +/// The provided `snap_transform` must support snapping, +/// which can be checked via [`supports_box_snapping`]. +/// +/// # Panics +/// +/// Panics if `snap_transform` does not support snapping and debug assertions are enabled. +pub(crate) fn snap_translation_delta( + delta: Vec2, + snap_transform: Affine, + scale_factor: f64, +) -> Vec2 { + let local_to_device = snap_transform.then_scale(scale_factor); + debug_assert!( + supports_box_snapping(local_to_device), + "translation delta snapping attempted with an incompatible transform" + ); + + let device_origin = local_to_device * Point::ORIGIN; + let device_delta_point = local_to_device * Point::new(delta.x, delta.y); + let device_delta = device_delta_point - device_origin; + let snapped_device_delta = Vec2::new(device_delta.x.round(), device_delta.y.round()); + + let device_to_local = local_to_device.inverse(); + let local_origin = device_to_local * Point::ORIGIN; + let local_delta_point = + device_to_local * Point::new(snapped_device_delta.x, snapped_device_delta.y); + local_delta_point - local_origin +} diff --git a/masonry_core/src/passes/accessibility.rs b/masonry_core/src/passes/accessibility.rs index 2a0d86eb2..67f5a1c1d 100644 --- a/masonry_core/src/passes/accessibility.rs +++ b/masonry_core/src/passes/accessibility.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use accesskit::{Node, NodeId, Role, Tree, TreeId, TreeUpdate}; -use kurbo::Rect; +use kurbo::{Affine, Rect}; use tracing::{info_span, trace}; use tree_arena::ArenaMut; @@ -19,6 +19,7 @@ fn build_accessibility_tree( property_arena: &PropertyArena, tree_update: &mut TreeUpdate, node: ArenaMut<'_, WidgetArenaNode>, + parent_window_transform: Affine, scale_factor: Option, ) { let mut children = node.children; @@ -49,7 +50,7 @@ fn build_accessibility_tree( children: children.reborrow_mut(), tree_update, }; - let mut node = build_access_node(widget, &mut ctx, scale_factor); + let mut node = build_access_node(widget, &mut ctx, parent_window_transform, scale_factor); let props = PropertiesRef { local: properties, default_map: default_properties.for_widget(widget.type_id()), @@ -76,6 +77,7 @@ fn build_accessibility_tree( property_arena, tree_update, node.reborrow_mut(), + parent_state.window_transform, None, ); parent_state.merge_up(&mut node.item.state); @@ -86,15 +88,15 @@ fn build_accessibility_tree( fn build_access_node( widget: &mut dyn Widget, ctx: &mut AccessCtx<'_>, + parent_window_transform: Affine, scale_factor: Option, ) -> Node { let mut node = Node::new(widget.accessibility_role()); - node.set_bounds(to_accesskit_rect( - ctx.widget_state.border_box_size().to_rect(), - )); + node.set_bounds(to_accesskit_rect(ctx.widget_state.border_box())); - let local_translation = ctx.widget_state.scroll_translation + ctx.widget_state.origin.to_vec2(); - let mut local_transform = ctx.widget_state.transform.then_translate(local_translation); + let mut local_transform = ctx + .widget_state + .compose_local_transform(parent_window_transform, ctx.global_state.scale_factor); // TODO - Remove once Masonry uses physical coordinates. // See https://github.com/linebender/xilem/issues/1264 @@ -176,6 +178,7 @@ pub(crate) fn run_accessibility_pass(root: &mut RenderRoot, scale_factor: f64) - &root.property_arena, &mut tree_update, root_node, + Affine::IDENTITY, Some(scale_factor), ); diff --git a/masonry_core/src/passes/compose.rs b/masonry_core/src/passes/compose.rs index 061af0266..122cd78fa 100644 --- a/masonry_core/src/passes/compose.rs +++ b/masonry_core/src/passes/compose.rs @@ -23,19 +23,15 @@ fn compose_widget( let id = state.id; let _span = enter_span_if(global_state.trace.compose, state); - let transformed = parent_transformed || state.transform_changed; + let transformed = parent_transformed || state.compose_transform_changed; if !transformed && !state.needs_compose { return; } - // The translation needs to be applied *after* applying the transform, - // as translation by scrolling should be within the transformed coordinate space. - // Same is true for the aligned border-box origin, to behave similar as in CSS. - let local_translation = state.scroll_translation + state.origin.to_vec2(); - - state.window_transform = - parent_window_transform * state.transform.then_translate(local_translation); + let local_transform = + state.compose_local_transform(parent_window_transform, global_state.scale_factor); + state.window_transform = parent_window_transform * local_transform; let paint_box = state.paint_box(); state.bounding_box = state.window_transform.transform_rect_bbox(paint_box); @@ -57,6 +53,7 @@ fn compose_widget( state.needs_compose = false; state.request_compose = false; + state.compose_transform_changed = false; state.transform_changed = false; let parent_transform = state.window_transform; diff --git a/masonry_core/src/passes/event.rs b/masonry_core/src/passes/event.rs index 72f1ff75f..f39a68161 100644 --- a/masonry_core/src/passes/event.rs +++ b/masonry_core/src/passes/event.rs @@ -394,7 +394,7 @@ pub(crate) fn run_on_access_event_pass( } accesskit::Action::ScrollIntoView if !handled.is_handled() => { let widget_state = root.widget_arena.get_state(target); - let rect = widget_state.border_box_size().to_rect(); + let rect = widget_state.border_box(); root.global_state .scroll_request_targets .push((target, rect)); diff --git a/masonry_core/src/passes/layout.rs b/masonry_core/src/passes/layout.rs index 125ab0d5b..5b2b14434 100644 --- a/masonry_core/src/passes/layout.rs +++ b/masonry_core/src/passes/layout.rs @@ -16,8 +16,11 @@ use crate::core::{ ChildrenIds, LayoutCtx, MeasureCtx, PropertiesRef, PropertyArena, Widget, WidgetArenaNode, WidgetState, }; -use crate::kurbo::{Axis, Insets, Point, Size}; -use crate::layout::{LayoutSize, LenDef, LenReq, Length, MeasurementInputs, SizeDef}; +use crate::kurbo::{Affine, Axis, Insets, Point, Size}; +use crate::layout::{ + LayoutSize, LenDef, LenReq, Length, MeasurementInputs, SizeDef, SnapKey, snap_border_box, + snap_translation_delta, supports_box_snapping, +}; use crate::passes::{enter_span_if, recurse_on_children}; use crate::properties::{BorderWidth, BoxShadow, Dimensions, Padding}; use crate::util::Sanitize; @@ -280,28 +283,45 @@ pub(crate) fn resolve_size( } // --- MARK: RUN LAYOUT -/// Run [`Widget::layout`] method on the given widget. -/// This will be called by [`LayoutCtx::run_layout`], which is itself called in the parent widget's `layout`. +/// Places the widget based on `chosen_origin` and runs its [`Widget::layout`] method. /// -/// The provided `size` will be the given widget's chosen border-box size. +/// This will be called by [`LayoutCtx::layout_child`], which is itself called +/// in the parent widget's `layout`. /// -/// If the chosen border-box `size` is smaller than what is required to fit the widget's -/// borders and padding, then the `size` will be expanded to meet those constraints. +/// The provided `chosen_size` will be the given widget's chosen border-box size, +/// before minimum border/padding constraints and pixel snapping are applied. /// -/// The provided `size` must be finite, non-negative, and in logical pixels. +/// The provided `chosen_size` must be finite, non-negative, and in logical pixels. /// Non-finite or negative length will fall back to zero with a logged warning. /// +/// The provided `chosen_origin` must be finite and in logical pixels. +/// Non-finite origin will fall back to zero with a logged warning. +/// /// # Panics /// -/// Panics if `size` is non-finite or negative and debug assertions are enabled. +/// Panics if `chosen_size` is non-finite or negative and debug assertions are enabled. +/// +/// Panics if `chosen_origin` is non-finite and debug assertions are enabled. /// /// [`Widget::layout`]: crate::core::Widget::layout pub(crate) fn run_layout_on( global_state: &mut RenderRootState, property_arena: &PropertyArena, node: ArenaMut<'_, WidgetArenaNode>, + chosen_origin: Point, chosen_size: Size, + parent_snap_transform: Affine, + parent_snap_disabled: bool, + parent_snap_unsupported: bool, ) { + // Ensure the chosen origin is sanitized. + let chosen_origin = if chosen_origin.is_finite() { + chosen_origin + } else { + debug_panic!("chosen origin must be finite, got {}", chosen_origin); + Point::ZERO + }; + // Ensure the chosen size is sanitized. let chosen_size = Size::new( chosen_size.width.sanitize("chosen border-box size width"), @@ -319,8 +339,8 @@ pub(crate) fn run_layout_on( // This checks reads `is_explicitly_stashed` instead of `is_stashed` because the latter may be outdated. // A widget's `is_explicitly_stashed` flag is controlled by its direct parent. - // The parent may set this flag during layout, in which case it should avoid calling `run_layout`. - // Note that, because this check exits before recursing, `run_layout` can only ever be + // The parent may set this flag during layout, in which case it should avoid calling `layout_child`. + // Note that, because this check exits before recursing, layout can only ever be // reached for a widget whose parent is not stashed, which means `is_explicitly_stashed` // being false is sufficient to know the widget is non-stashed. if state.is_explicitly_stashed { @@ -330,8 +350,7 @@ pub(crate) fn run_layout_on( id, ); state.origin = Point::ZERO; - state.end_point = Point::ZERO; - state.layout_border_box_size = Size::ZERO; + state.border_box_size = Size::ZERO; return; } @@ -348,18 +367,76 @@ pub(crate) fn run_layout_on( let border_width = props.get::(&mut state.property_cache); let padding = props.get::(&mut state.property_cache); - // Force the border-box size to be large enough to actually contain the border and padding. + // Force the chosen border-box to be large enough to actually contain the border and padding. let minimum_size = Size::ZERO; let minimum_size = border_width.size_up(minimum_size); let minimum_size = padding.size_up(minimum_size); - let border_box_size = minimum_size.max(chosen_size); + let chosen_border_box = minimum_size.max(chosen_size).to_rect(); + + // Calculate the chosen snap transform based on the chosen origin. + // Snap transform excludes scroll translation, which will be dealt with during compose. + let chosen_snap_transform = + parent_snap_transform * state.transform.then_translate(chosen_origin.to_vec2()); + + // Update the flags that determine whether snapping is active. + let is_snap_disabled = parent_snap_disabled || state.is_explicitly_snap_disabled; + let snap_disabled_changed = state.is_snap_disabled != is_snap_disabled; + state.is_snap_disabled = is_snap_disabled; + state.is_snap_unsupported = parent_snap_unsupported + || !supports_box_snapping(chosen_snap_transform.then_scale(global_state.scale_factor)); + + // Snap the chosen border-box to the pixel grid. + let snapped_border_box = if state.is_snap_active() { + snap_border_box( + chosen_border_box, + chosen_snap_transform, + global_state.scale_factor, + ) + } else { + chosen_border_box + }; - if !state.needs_layout() && state.layout_border_box_size == border_box_size { + // The layout origin will be the chosen origin adjusted so that it will be pixel-snapped. + let origin_delta = + state.transform * snapped_border_box.origin() - state.transform * Point::ORIGIN; + let origin = chosen_origin + origin_delta; + if state.origin != origin { + state.origin = origin; + state.mark_compose_transform_changed(); + } + + // Now that we have the layout origin, we can calculate the corresponding snap transform. + let snap_transform = + parent_snap_transform * state.transform.then_translate(state.origin.to_vec2()); + + // The border-box size will be exactly the pixel-snapped chosen border-box size. + let border_box_size = snapped_border_box.size(); + + // We can skip redoing layout for this branch, if all the following conditions are true: + // - No widget in this branch explicitly requested layout. + // - The border-box size matches the cached layout. + // The box size can change due to snapping, constraints, or just a new chosen input. + // - The snap key matches the cached layout. + // Even though the cached layout for this widget may be valid, if the snap key changed, + // then deeper descendant widgets may end up with different border-boxes due to snapping. + // - The snap disabled state remains the same as it was during the cached layout. + // Even though the cached layout for this whole branch may be valid, + // we still need to propagate the is_snap_disabled flag updates, as those are used + // in context methods like set_transform to decide whether layout or only compose is needed. + let snap_key = state + .is_snap_active() + .then(|| SnapKey::new(snap_transform, global_state.scale_factor)); + if !state.needs_layout() + && state.border_box_size == border_box_size + && state.snap_key == snap_key + && !snap_disabled_changed + { // We reset this to false to mark that the current widget has been visited. state.request_layout = false; return; } - state.layout_border_box_size = border_box_size; + state.border_box_size = border_box_size; + state.snap_key = snap_key; // TODO - Not everything that has been re-laid out needs to be repainted. state.needs_paint = true; @@ -419,6 +496,7 @@ pub(crate) fn run_layout_on( widget_state: state, children: children.reborrow_mut(), property_arena, + snap_transform, }; // Run the widget's layout @@ -445,7 +523,6 @@ pub(crate) fn run_layout_on( state.request_layout = false; state.set_needs_layout(false); - state.is_expecting_place_child_call = true; #[cfg(debug_assertions)] { @@ -459,17 +536,7 @@ pub(crate) fn run_layout_on( if child_state.request_layout { debug_panic!( - "Error in '{}' {}: LayoutCtx::run_layout() was not called with child widget '{}' {}.", - name, - id, - child_state.widget_name, - child_state.id, - ); - } - - if child_state.is_expecting_place_child_call { - debug_panic!( - "Error in '{}' {}: LayoutCtx::place_child() was not called with child widget '{}' {}.", + "Error in '{}' {}: LayoutCtx::layout_child() was not called with child widget '{}' {}.", name, id, child_state.widget_name, @@ -506,23 +573,46 @@ fn clear_layout_flags(node: ArenaMut<'_, WidgetArenaNode>) { }); } -// --- MARK: PLACE WIDGET -/// Places the child at `origin` in its parent's border-box coordinate space. -pub(crate) fn place_widget(child_state: &mut WidgetState, origin: Point) { - let end_point = origin + child_state.layout_border_box_size.to_vec2(); - // TODO - Account for display scale in pixel snapping - // See https://github.com/linebender/xilem/issues/1264 - let origin = origin.round(); - let end_point = end_point.round(); - - // TODO - We may want to invalidate in other cases as well - if origin != child_state.origin { - child_state.transform_changed = true; - } - child_state.origin = origin; - child_state.end_point = end_point; +// --- MARK: MOVE WIDGET +/// Moves a placed child to `origin`, quantizing the movement to an +/// integer device-pixel delta when snapping is active. +/// +/// The provided `chosen_origin` must be finite and in logical pixels. +/// Non-finite origin will fall back to zero with a logged warning. +/// +/// # Panics +/// +/// Panics if `chosen_origin` is non-finite and debug assertions are enabled. +pub(crate) fn move_widget( + child_state: &mut WidgetState, + chosen_origin: Point, + parent_snap_transform: Affine, + scale_factor: f64, +) { + // Ensure the chosen origin is sanitized. + let chosen_origin = if chosen_origin.is_finite() { + chosen_origin + } else { + debug_panic!("chosen origin must be finite, got {}", chosen_origin); + Point::ZERO + }; - child_state.is_expecting_place_child_call = false; + // We snap the delta instead of the chosen origin, because then we can skip dealing with + // the local transform of the child. Which is to say, the current child origin is snapped + // only after the child's local transform is applied. Adding a snapped delta means that + // the new origin will also end up snapped after the child's local transform is applied. + let requested_delta = chosen_origin - child_state.origin; + let snapped_delta = if child_state.is_snap_active() { + snap_translation_delta(requested_delta, parent_snap_transform, scale_factor) + } else { + requested_delta + }; + let origin = child_state.origin + snapped_delta; + + if child_state.origin != origin { + child_state.origin = origin; + child_state.mark_compose_transform_changed(); + } } // --- MARK: ROOT @@ -558,13 +648,16 @@ pub(crate) fn run_layout_pass(root: &mut RenderRoot) { &mut root.global_state, &root.property_arena, root_node.reborrow_mut(), + Point::ORIGIN, root_node_size, + Affine::IDENTITY, + false, + false, ); - place_widget(&mut root_node.item.state, Point::ORIGIN); if let WindowSizePolicy::Content = root.global_state.size_policy { - // We use the aligned border-box size, which means that transforms won't affect window size. - let size = root_node.item.state.border_box_size(); + // We use the border-box size, which means that transforms won't affect window size. + let size = root_node.item.state.border_box_size; let new_size = LogicalSize::new(size.width, size.height).to_physical(root.global_state.scale_factor); if root.global_state.size != new_size { diff --git a/masonry_core/src/passes/paint.rs b/masonry_core/src/passes/paint.rs index a61f8837f..aa30050c5 100644 --- a/masonry_core/src/passes/paint.rs +++ b/masonry_core/src/passes/paint.rs @@ -197,7 +197,7 @@ fn paint_widget( recurse_on_children(id, widget, children, |mut node| { // TODO: We could skip painting children outside the parent clip path. // There's a few things to consider if we do: - // - Some widgets can paint outside of their layout box. + // - Some widgets can paint outside of their border-box. // - Once we implement compositor layers, we may want to paint outside of the clip path anyway in anticipation of user scrolling. // - We still want to reset needs_paint and request_paint flags. paint_widget( @@ -224,7 +224,8 @@ fn paint_widget( // Draw the widget's explicit baselines let mut draw_baseline = |baseline| { - let line = Line::new((0., baseline), (state.end_point.x, baseline)); + let border_box = state.border_box(); + let line = Line::new((border_box.x0, baseline), (border_box.x1, baseline)); let baseline_style = Stroke::new(1.0).with_dashes(0., [4.0, 4.0]); painter .stroke(line, &baseline_style, color) @@ -256,7 +257,7 @@ fn paint_widget( if global_state.inspector_state.hovered_widget == Some(id) { const HOVER_FILL_COLOR: Color = Color::from_rgba8(60, 60, 250, 100); - let rect = state.border_box_size().to_rect(); + let rect = state.border_box(); Painter::new(layer_collector.scene_mut()) .fill(rect, HOVER_FILL_COLOR) .transform(border_box_to_layer_transform) @@ -265,7 +266,7 @@ fn paint_widget( } if paint_as_external { - layer_collector.push_external_layer(id, state.border_box_size().to_rect()); + layer_collector.push_external_layer(id, state.border_box()); } if matches!( diff --git a/masonry_testing/src/harness.rs b/masonry_testing/src/harness.rs index 59ea10d91..fc2954a85 100644 --- a/masonry_testing/src/harness.rs +++ b/masonry_testing/src/harness.rs @@ -1045,7 +1045,8 @@ impl TestHarness { /// Returns the rectangle of the IME session. /// - /// This is usually the effective border-box rectangle of the focused widget. + /// This is usually the border-box rectangle of the focused widget, + /// transformed into the window's coordinate space. pub fn ime_rect(&self) -> (LogicalPosition, LogicalSize) { self.ime_rect } diff --git a/masonry_testing/src/modular_widget.rs b/masonry_testing/src/modular_widget.rs index dd62c7a26..26521dcfe 100644 --- a/masonry_testing/src/modular_widget.rs +++ b/masonry_testing/src/modular_widget.rs @@ -129,8 +129,7 @@ impl ModularWidget> { }) .layout_fn(move |child, ctx, _props, size| { let child_size = ctx.compute_size(child, SizeDef::fit(size), size.into()); - ctx.run_layout(child, child_size); - ctx.place_child(child, Point::ZERO); + ctx.layout_child(child, Point::ZERO, child_size); ctx.derive_baselines(child); }) .children_fn(|child| ChildrenIds::from_slice(&[child.id()])) @@ -174,13 +173,12 @@ impl ModularWidget>> { continue; } let child_size = ctx.compute_size(child, auto_size, context_size); - ctx.run_layout(child, child_size); - ctx.place_child(child, Point::ZERO); + ctx.layout_child(child, Point::ZERO, child_size); } if let Some(child) = children.first() { - let (first_baseline, _) = ctx.child_aligned_baselines(child); - let (_, last_baseline) = ctx.child_aligned_baselines(children.last().unwrap()); + let (first_baseline, _) = ctx.child_baselines(child); + let (_, last_baseline) = ctx.child_baselines(children.last().unwrap()); ctx.set_baselines(first_baseline, last_baseline); } else { ctx.clear_baselines(); diff --git a/masonry_testing/src/wrapper_widget.rs b/masonry_testing/src/wrapper_widget.rs index 08913d8f9..f81d12360 100644 --- a/masonry_testing/src/wrapper_widget.rs +++ b/masonry_testing/src/wrapper_widget.rs @@ -116,10 +116,8 @@ impl Widget for WrapperWidget { fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { let child_size = ctx.compute_size(&mut self.child, SizeDef::fit(size), size.into()); - ctx.run_layout(&mut self.child, child_size); - let child_origin = Point::ORIGIN; - ctx.place_child(&mut self.child, child_origin); + ctx.layout_child(&mut self.child, child_origin, child_size); ctx.derive_baselines(&self.child); } diff --git a/xilem_masonry/src/one_of.rs b/xilem_masonry/src/one_of.rs index f26bec076..dd9d421a2 100644 --- a/xilem_masonry/src/one_of.rs +++ b/xilem_masonry/src/one_of.rs @@ -273,42 +273,15 @@ impl< fn layout(&mut self, ctx: &mut LayoutCtx<'_>, _props: &PropertiesRef<'_>, size: Size) { match self { - Self::A(w) => { - ctx.run_layout(w, size); - ctx.place_child(w, Point::ORIGIN); - } - Self::B(w) => { - ctx.run_layout(w, size); - ctx.place_child(w, Point::ORIGIN); - } - Self::C(w) => { - ctx.run_layout(w, size); - ctx.place_child(w, Point::ORIGIN); - } - Self::D(w) => { - ctx.run_layout(w, size); - ctx.place_child(w, Point::ORIGIN); - } - Self::E(w) => { - ctx.run_layout(w, size); - ctx.place_child(w, Point::ORIGIN); - } - Self::F(w) => { - ctx.run_layout(w, size); - ctx.place_child(w, Point::ORIGIN); - } - Self::G(w) => { - ctx.run_layout(w, size); - ctx.place_child(w, Point::ORIGIN); - } - Self::H(w) => { - ctx.run_layout(w, size); - ctx.place_child(w, Point::ORIGIN); - } - Self::I(w) => { - ctx.run_layout(w, size); - ctx.place_child(w, Point::ORIGIN); - } + Self::A(w) => ctx.layout_child(w, Point::ORIGIN, size), + Self::B(w) => ctx.layout_child(w, Point::ORIGIN, size), + Self::C(w) => ctx.layout_child(w, Point::ORIGIN, size), + Self::D(w) => ctx.layout_child(w, Point::ORIGIN, size), + Self::E(w) => ctx.layout_child(w, Point::ORIGIN, size), + Self::F(w) => ctx.layout_child(w, Point::ORIGIN, size), + Self::G(w) => ctx.layout_child(w, Point::ORIGIN, size), + Self::H(w) => ctx.layout_child(w, Point::ORIGIN, size), + Self::I(w) => ctx.layout_child(w, Point::ORIGIN, size), } } diff --git a/xilem_masonry/src/view/resize_observer.rs b/xilem_masonry/src/view/resize_observer.rs index 469605e83..44f422065 100644 --- a/xilem_masonry/src/view/resize_observer.rs +++ b/xilem_masonry/src/view/resize_observer.rs @@ -162,7 +162,7 @@ where None => match message.take_message::() { Some(_) => MessageResult::Action((self.on_resize)( app_state, - element.ctx.content_box_size(), + element.ctx.content_box().size(), )), None => { // TODO: Panic?