Improve bounding rect code (#874)

Document concepts of "layout rect" and "bounding rect".
Deprecate `local_layout_rect()` method.
Add `global_layout_rect()` method.
Remove `transform_changed()` method.
Improve bounding rect merging code.
Tweak `TestHarness::mouse_move_to`.
Tweak WidgetState doc.
This commit is contained in:
Olivier FAURE 2025-03-06 12:58:51 +00:00 committed by GitHub
parent 60c037dc1f
commit 93e000d9c1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 76 additions and 43 deletions

View File

@ -632,12 +632,8 @@ impl LayoutCtx<'_> {
self.get_child_state(child).baseline_offset
}
/// Get the given child's layout rect.
///
/// ## Panics
///
/// This method will panic if [`LayoutCtx::run_layout`] and [`LayoutCtx::place_child`]
/// have not been called yet for the child.
// TODO - Remove (used in Flex)
#[doc(hidden)]
#[track_caller]
pub fn child_layout_rect(&self, child: &WidgetPod<impl Widget + ?Sized>) -> Rect {
self.assert_layout_done(child, "child_layout_rect");
@ -667,7 +663,7 @@ impl LayoutCtx<'_> {
#[track_caller]
pub fn child_size(&self, child: &WidgetPod<impl Widget + ?Sized>) -> Size {
self.assert_layout_done(child, "child_size");
self.get_child_state(child).layout_rect().size()
self.get_child_state(child).size
}
/// Skips running the layout pass and calling [`LayoutCtx::place_child`] on the child.
@ -768,12 +764,9 @@ impl_context_method!(
self.widget_state.size
}
// TODO - Remove? A widget doesn't really have a concept of its own "origin",
// it's more useful for the parent widget.
/// The layout rect of the widget.
///
/// This is the layout [size](Self::size) and origin (in the parent's coordinate space) combined.
pub fn layout_rect(&self) -> Rect {
// TODO - Remove
#[allow(dead_code, reason = "Only used in tests")]
pub(crate) fn local_layout_rect(&self) -> Rect {
self.widget_state.layout_rect()
}
@ -788,7 +781,10 @@ impl_context_method!(
self.widget_state.window_origin()
}
/// The axis aligned bounding rect of this widget in window coordinates.
/// The bounding rect of the widget in window coordinates.
///
/// See [bounding rect documentation](crate::doc::doc_06_masonry_concepts#bounding-rect)
/// for details.
pub fn bounding_rect(&self) -> Rect {
self.widget_state.bounding_rect()
}
@ -1030,13 +1026,6 @@ impl_context_method!(MutateCtx<'_>, EventCtx<'_>, UpdateCtx<'_>, {
self.request_layout();
}
/// Indicate that the transform of this widget has changed.
pub fn transform_changed(&mut self) {
trace!("transform_changed");
self.widget_state.transform_changed = true;
self.request_compose();
}
/// Indicate that a child is about to be removed from the tree.
///
/// Container widgets should avoid dropping `WidgetPod`s. Instead, they should
@ -1073,7 +1062,8 @@ impl_context_method!(MutateCtx<'_>, EventCtx<'_>, UpdateCtx<'_>, {
/// It behaves similarly as CSS transforms
pub fn set_transform(&mut self, transform: Affine) {
self.widget_state.transform = transform;
self.transform_changed();
self.widget_state.transform_changed = true;
self.request_compose();
}
});

View File

@ -96,11 +96,13 @@ pub(crate) struct WidgetState {
// efficiently hold an arbitrary shape.
pub(crate) clip_path: Option<Rect>,
/// This is being computed out of all ancestor transforms and `translation`
pub(crate) window_transform: Affine,
/// Local transform of this widget in the parent coordinate space.
pub(crate) transform: Affine,
/// translation applied by scrolling, this is applied after applying `transform` to this widget.
/// Global transform of this widget in the window coordinate space.
///
/// Computed from all `transform` and `scroll_translation` values from this to the root widget.
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.
pub(crate) transform_changed: bool,
@ -292,6 +294,8 @@ impl WidgetState {
///
/// By default, returns the same as [`Self::bounding_rect`].
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.size.to_rect()))
}
@ -300,6 +304,24 @@ impl WidgetState {
self.window_transform.translation().to_point()
}
/// Return the result of intersecting the widget's clip path (if any) with the given rect.
///
/// Both the argument and the result are in window coordinates.
///
/// Returns `None` if the given rect is clipped out.
pub(crate) fn clip_child(&self, child_rect: Rect) -> Option<Rect> {
if let Some(clip_path) = self.clip_path {
let clip_path_global = self.window_transform.transform_rect_bbox(clip_path);
if clip_path_global.overlaps(child_rect) {
Some(clip_path_global.intersect(child_rect))
} else {
None
}
} else {
Some(child_rect)
}
}
pub(crate) fn needs_rewrite_passes(&self) -> bool {
self.needs_layout
|| self.needs_compose

View File

@ -107,6 +107,27 @@ These properties are mostly used for styling and event handling.
See [Reading Widget Properties](crate::doc::doc_04b_widget_properties) for more info.
## Bounding rect
A widget's bounding rect is a window-space axis-aligned rectangle inside of which pointer events might affect either the widget or its descendants.
In general, the bounding rect is a union or a widget's layout rect and the bounding rects of all its descendants.
The bounding rects 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 rect.
<!-- TODO - Include illustration. -->
<!-- TODO - Add section about clip paths and pointer detection. -->
## Layout rect
Previous versions of Masonry had a concept of a widget's "layout rect", composed of its self-declared size and the position attributed by its parent.
However, given that widgets can have arbitrary transforms, the concept of an axis-aligned layout rect doesn't really make sense anymore.
## Safety rails
When debug assertions are on, Masonry runs a bunch of checks every frame to make sure widget code doesn't have logical errors.

View File

@ -80,18 +80,10 @@ fn compose_widget(
);
let parent_bounding_rect = parent_state.bounding_rect;
// This could be further optimized by more tightly clipping the child bounding rect according to the clip path.
let clipped_child_bounding_rect = if let Some(clip_path) = parent_state.clip_path {
let clip_path_bounding_rect =
parent_state.window_transform.transform_rect_bbox(clip_path);
state.item.bounding_rect.intersect(clip_path_bounding_rect)
} else {
state.item.bounding_rect
};
if !clipped_child_bounding_rect.is_zero_area() {
parent_state.bounding_rect =
parent_bounding_rect.union(clipped_child_bounding_rect);
if let Some(child_bounding_rect) = parent_state.clip_child(state.item.bounding_rect) {
parent_state.bounding_rect = parent_bounding_rect.union(child_bounding_rect);
}
parent_state.merge_up(state.item);
},
);

View File

@ -212,12 +212,12 @@ impl<W: Widget + FromDynWidget + ?Sized> Portal<W> {
}
pub fn set_viewport_pos(this: &mut WidgetMut<'_, Self>, position: Point) -> bool {
let portal_size = this.ctx.layout_rect().size();
let portal_size = this.ctx.local_layout_rect().size();
let content_size = this
.ctx
.get_mut(&mut this.widget.child)
.ctx
.layout_rect()
.local_layout_rect()
.size();
let pos_changed = this
@ -269,7 +269,11 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for Portal<W> {
const SCROLLING_SPEED: f64 = 10.0;
let portal_size = ctx.size();
let content_size = ctx.get_raw_ref(&mut self.child).ctx().layout_rect().size();
let content_size = ctx
.get_raw_ref(&mut self.child)
.ctx()
.local_layout_rect()
.size();
match event {
PointerEvent::MouseWheel(delta, _) => {
@ -353,7 +357,11 @@ impl<W: Widget + FromDynWidget + ?Sized> Widget for Portal<W> {
match event {
Update::RequestPanToChild(target) => {
let portal_size = ctx.size();
let content_size = ctx.get_raw_ref(&mut self.child).ctx().layout_rect().size();
let content_size = ctx
.get_raw_ref(&mut self.child)
.ctx()
.local_layout_rect()
.size();
self.pan_viewport_to_raw(portal_size, content_size, *target);
ctx.request_compose();
@ -561,7 +569,7 @@ mod tests {
assert_render_snapshot!(harness, "button_list_scrolled");
let item_3_rect = harness.get_widget(item_3_id).ctx().layout_rect();
let item_3_rect = harness.get_widget(item_3_id).ctx().local_layout_rect();
harness.edit_root_widget(|mut portal| {
let mut portal = portal.downcast::<Portal<Flex>>();
Portal::pan_viewport_to(&mut portal, item_3_rect);
@ -569,7 +577,7 @@ mod tests {
assert_render_snapshot!(harness, "button_list_scroll_to_item_3");
let item_13_rect = harness.get_widget(item_13_id).ctx().layout_rect();
let item_13_rect = harness.get_widget(item_13_id).ctx().local_layout_rect();
harness.edit_root_widget(|mut portal| {
let mut portal = portal.downcast::<Portal<Flex>>();
Portal::pan_viewport_to(&mut portal, item_13_rect);

View File

@ -25,7 +25,7 @@ fn layout_simple() {
let harness = TestHarness::create(widget);
let first_box_rect = harness.get_widget(id_1).ctx().layout_rect();
let first_box_rect = harness.get_widget(id_1).ctx().local_layout_rect();
let first_box_paint_rect = harness.get_widget(id_1).ctx().paint_rect();
assert_eq!(first_box_rect.x0, 0.0);