diff --git a/masonry/src/layers/selector_menu.rs b/masonry/src/layers/selector_menu.rs index 951bbe3895..9e1aef0dd2 100644 --- a/masonry/src/layers/selector_menu.rs +++ b/masonry/src/layers/selector_menu.rs @@ -363,7 +363,7 @@ impl Layer for SelectorMenu { PointerEvent::Down(PointerButtonEvent { state, .. }) => { let local_pos = ctx.local_position(state.position); - !ctx.border_box_size().to_rect().contains(local_pos) + !ctx.border_box().contains(local_pos) } PointerEvent::Cancel(..) => true, _ => false, diff --git a/masonry/src/tests/layout.rs b/masonry/src/tests/layout.rs index db41091724..631474a244 100644 --- a/masonry/src/tests/layout.rs +++ b/masonry/src/tests/layout.rs @@ -7,10 +7,11 @@ use std::rc::Rc; use assert_matches::assert_matches; use crate::core::{NewWidget, Widget, WidgetTag}; -use crate::kurbo::{Insets, Point, Rect, Size}; +use crate::kurbo::{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::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); @@ -327,9 +328,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 +358,158 @@ 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.run_layout(child, child_size); + ctx.place_child(child, Point::new(2., 3.)); + }) + .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.run_layout(child, child_size); + ctx.place_child(child, Point::new(5.3, 7.6)); + }) + .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 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.run_layout(child, child_size); + ctx.place_child(child, Point::new(0.6, 0.6)); + }) + .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 5c56e31625..560f4e615c 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; @@ -27,6 +27,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/widgets/disclosure_button.rs b/masonry/src/widgets/disclosure_button.rs index 9632285ac3..580cab2436 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/portal.rs b/masonry/src/widgets/portal.rs index 04bdf32a9d..8bc3832753 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 @@ -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); @@ -857,7 +857,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); diff --git a/masonry/src/widgets/resize_observer.rs b/masonry/src/widgets/resize_observer.rs index b56d0fa73b..d1416e2000 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; @@ -201,7 +201,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .border_box() + .size(), Size { width: 100., height: 100., @@ -218,7 +219,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .border_box() + .size(), Size { width: 100., height: 200., @@ -250,7 +252,8 @@ mod tests { harness .get_widget_with_id(observer_id) .ctx() - .border_box_size(), + .border_box() + .size(), Size { width: 200., height: 200., @@ -267,7 +270,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 c132f09c5a..377fcad939 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/slider.rs b/masonry/src/widgets/slider.rs index 12d9b30720..550dbe84ce 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 f56afeb48a..49799765f6 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 f41ac0a176..a04dd2188b 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 { @@ -716,7 +716,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 +740,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 +857,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 +871,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 +893,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 +916,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 +938,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 +965,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 +974,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 33f01299ca..6feeef5aa9 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 { @@ -1531,7 +1531,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 +1604,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 1ea04c0b23..1390e8ab49 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/virtual_scroll.rs b/masonry/src/widgets/virtual_scroll.rs index 882fda875a..69536c9934 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; @@ -1002,7 +1002,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_core/src/core/contexts.rs b/masonry_core/src/core/contexts.rs index efdcf6b431..f8d5f864d2 100644 --- a/masonry_core/src/core/contexts.rs +++ b/masonry_core/src/core/contexts.rs @@ -513,7 +513,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)); @@ -1189,9 +1189,9 @@ impl ComposeCtx<'_> { } } -// --- 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,46 +1202,18 @@ 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 /// 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 /// 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 diff --git a/masonry_core/src/core/widget_state.rs b/masonry_core/src/core/widget_state.rs index 63c189e0bd..92366c5116 100644 --- a/masonry_core/src/core/widget_state.rs +++ b/masonry_core/src/core/widget_state.rs @@ -400,14 +400,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 aligned 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 aligned border-box in the widget's border-box coordinate space. + pub(crate) fn border_box(&self) -> Rect { + (self.end_point - self.origin).to_size().to_rect() + } + + /// Returns the widget's aligned 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 @@ -461,10 +471,8 @@ impl WidgetState { 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/passes/accessibility.rs b/masonry_core/src/passes/accessibility.rs index 2a0d86eb2c..bb406e9bbf 100644 --- a/masonry_core/src/passes/accessibility.rs +++ b/masonry_core/src/passes/accessibility.rs @@ -89,9 +89,7 @@ fn build_access_node( 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); diff --git a/masonry_core/src/passes/event.rs b/masonry_core/src/passes/event.rs index 72f1ff75fc..f39a681611 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 3c3967224f..65d13917e5 100644 --- a/masonry_core/src/passes/layout.rs +++ b/masonry_core/src/passes/layout.rs @@ -564,7 +564,7 @@ pub(crate) fn run_layout_pass(root: &mut RenderRoot) { 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(); + 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 a61f8837f8..aa30050c5c 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/xilem_masonry/src/view/resize_observer.rs b/xilem_masonry/src/view/resize_observer.rs index 469605e837..44f4220656 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?