diff --git a/masonry/examples/calc_masonry.rs b/masonry/examples/calc_masonry.rs index ca7abceb8..022707be1 100644 --- a/masonry/examples/calc_masonry.rs +++ b/masonry/examples/calc_masonry.rs @@ -217,7 +217,7 @@ impl Widget for CalcButton { } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { - let size = self.inner.layout(ctx, bc); + let size = ctx.run_layout(&mut self.inner, bc); ctx.place_child(&mut self.inner, Point::ORIGIN); size diff --git a/masonry/src/contexts.rs b/masonry/src/contexts.rs index 791ee37d6..69a8bfa37 100644 --- a/masonry/src/contexts.rs +++ b/masonry/src/contexts.rs @@ -11,12 +11,15 @@ use tracing::{trace, warn}; use vello::kurbo::Vec2; use crate::action::Action; +use crate::passes::layout::run_layout_on; use crate::render_root::{MutateCallback, RenderRootSignal, RenderRootState}; use crate::text::TextBrush; use crate::text_helpers::{ImeChangeSignal, TextFieldRegistration}; use crate::tree_arena::ArenaMutChildren; use crate::widget::{WidgetMut, WidgetState}; -use crate::{AllowRawMut, CursorIcon, Insets, Point, Rect, Size, Widget, WidgetId, WidgetPod}; +use crate::{ + AllowRawMut, BoxConstraints, CursorIcon, Insets, Point, Rect, Size, Widget, WidgetId, WidgetPod, +}; /// A macro for implementing methods on multiple contexts. /// @@ -738,6 +741,19 @@ impl LayoutCtx<'_> { } } + // TODO - Reorder methods so that methods necessary for layout + // appear higher in documentation. + + /// Compute layout of a child widget. + /// + /// Container widgets must call this on every child as part of + /// their [`layout`] method. + /// + /// [`layout`]: Widget::layout + pub fn run_layout(&mut self, child: &mut WidgetPod, bc: &BoxConstraints) -> Size { + run_layout_on(self, child, bc) + } + /// Set explicit paint [`Insets`] for this widget. /// /// You are not required to set explicit paint bounds unless you need diff --git a/masonry/src/passes/layout.rs b/masonry/src/passes/layout.rs new file mode 100644 index 000000000..7fa389788 --- /dev/null +++ b/masonry/src/passes/layout.rs @@ -0,0 +1,217 @@ +// Copyright 2024 the Xilem Authors +// SPDX-License-Identifier: Apache-2.0 + +//! The layout pass, which provides the size and position of each widget +//! before any translations applied in [`compose`](crate::passes::compose). +//! Most of the logic for this pass happens in [`Widget::layout`] implementations. + +use tracing::{info_span, trace}; +use vello::kurbo::{Point, Rect, Size}; + +use crate::render_root::RenderRoot; +use crate::widget::WidgetState; +use crate::{BoxConstraints, LayoutCtx, Widget, WidgetPod}; + +// TODO - Replace with contains_rect once new Kurbo version is released. +// See https://github.com/linebender/kurbo/pull/347 +/// Return `true` if all of `smaller` is within `larger`. +fn rect_contains(larger: &Rect, smaller: &Rect) -> bool { + smaller.x0 >= larger.x0 + && smaller.x1 <= larger.x1 + && smaller.y0 >= larger.y0 + && smaller.y1 <= larger.y1 +} + +// Returns "true" if the Widget's layout method was called, in which case debug checks +// need to be run. (See 'called_widget' in WidgetPod::call_widget_method_with_checks) +pub(crate) fn run_layout_inner( + parent_ctx: &mut LayoutCtx<'_>, + pod: &mut WidgetPod, + bc: &BoxConstraints, +) -> bool { + let id = pod.id().to_raw(); + let widget_mut = parent_ctx + .widget_children + .get_child_mut(id) + .expect("WidgetPod: inner widget not found in widget tree"); + let mut state_mut = parent_ctx + .widget_state_children + .get_child_mut(id) + .expect("WidgetPod: inner widget not found in widget tree"); + let widget = widget_mut.item; + let state = state_mut.item; + + if state.is_stashed { + debug_panic!( + "Error in '{}' #{}: trying to compute layout of stashed widget.", + widget.short_type_name(), + id, + ); + state.size = Size::ZERO; + return false; + } + + // TODO - Not everything that has been re-laid out needs to be repainted. + state.needs_paint = true; + state.needs_compose = true; + state.needs_accessibility = true; + state.request_paint = true; + state.request_compose = true; + state.request_accessibility = true; + + bc.debug_check(widget.short_type_name()); + trace!("Computing layout with constraints {:?}", bc); + + state.local_paint_rect = Rect::ZERO; + + let new_size = { + let mut inner_ctx = LayoutCtx { + widget_state: state, + widget_state_children: state_mut.children.reborrow_mut(), + widget_children: widget_mut.children, + global_state: parent_ctx.global_state, + mouse_pos: parent_ctx.mouse_pos, + }; + + // TODO - If constraints are the same and request_layout isn't set, + // skip calling layout + inner_ctx.widget_state.request_layout = false; + widget.layout(&mut inner_ctx, bc) + }; + if state.request_layout { + debug_panic!( + "Error in '{}' #{}: layout request flag was set during layout pass", + widget.short_type_name(), + id, + ); + } + + state.needs_layout = false; + state.is_expecting_place_child_call = true; + + state.local_paint_rect = state + .local_paint_rect + .union(new_size.to_rect() + state.paint_insets); + + #[cfg(debug_assertions)] + { + for child_id in widget.children_ids() { + let child_id = child_id.to_raw(); + let child_state_mut = state_mut + .children + .get_child_mut(child_id) + .unwrap_or_else(|| panic!("widget #{child_id} not found")); + let child_state = child_state_mut.item; + if child_state.is_expecting_place_child_call { + debug_panic!( + "Error in '{}' #{}: missing call to place_child method for child widget '{}' #{}. During layout pass, if a widget calls WidgetPod::layout() on its child, it then needs to call LayoutCtx::place_child() on the same child.", + widget.short_type_name(), + id, + child_state.widget_name, + child_state.id.to_raw(), + ); + } + + // TODO - This check might be redundant with the code updating local_paint_rect + let child_rect = child_state.paint_rect(); + if !rect_contains(&state.local_paint_rect, &child_rect) && !state.is_portal { + debug_panic!( + "Error in '{}' #{}: paint_rect {:?} doesn't contain paint_rect {:?} of child widget '{}' #{}", + widget.short_type_name(), + id, + state.local_paint_rect, + child_rect, + child_state.widget_name, + child_state.id.to_raw(), + ); + } + } + } + + // TODO - Figure out how to deal with the overflow problem, eg: + // What happens if a widget returns a size larger than the allowed constraints? + // Some possibilities are: + // - Always clip: might be expensive + // - Display it anyway: might lead to graphical bugs + // - Panic: too harsh? + // Also, we need to avoid spurious crashes when we initialize the app and the + // size is (0,0) + // See https://github.com/linebender/xilem/issues/377 + + let state_mut = parent_ctx + .widget_state_children + .get_child_mut(id) + .expect("WidgetPod: inner widget not found in widget tree"); + parent_ctx.widget_state.merge_up(state_mut.item); + state_mut.item.size = new_size; + + { + if new_size.width.is_infinite() { + debug_panic!( + "Error in '{}' #{}: width is infinite", + widget.short_type_name(), + id, + ); + } + if new_size.height.is_infinite() { + debug_panic!( + "Error in '{}' #{}: height is infinite", + widget.short_type_name(), + id, + ); + } + } + + true +} + +/// Run [`Widget::layout`] method on the widget contained in `pod`. +/// This will be called by [`LayoutCtx::run_layout`], which is itself called in the parent widget's `layout`. +pub(crate) fn run_layout_on( + parent_ctx: &mut LayoutCtx<'_>, + pod: &mut WidgetPod, + bc: &BoxConstraints, +) -> Size { + pod.call_widget_method_with_checks( + "layout", + parent_ctx, + |ctx| { + ( + ctx.widget_state_children.reborrow(), + ctx.widget_children.reborrow(), + ) + }, + |child, ctx| run_layout_inner(ctx, child, bc), + ); + + let id = pod.id().to_raw(); + let state_mut = parent_ctx + .widget_state_children + .get_child_mut(id) + .expect("run_layout_on: inner widget not found in widget tree"); + state_mut.item.size +} + +pub(crate) fn root_layout( + root: &mut RenderRoot, + synthetic_root_state: &mut WidgetState, + bc: &BoxConstraints, +) -> Size { + let _span = info_span!("layout").entered(); + + let mouse_pos = root.last_mouse_pos.map(|pos| (pos.x, pos.y).into()); + let root_state_token = root.widget_arena.widget_states.root_token_mut(); + let root_widget_token = root.widget_arena.widgets.root_token_mut(); + let mut ctx = LayoutCtx { + global_state: &mut root.state, + widget_state: synthetic_root_state, + widget_state_children: root_state_token, + widget_children: root_widget_token, + mouse_pos, + }; + + let size = run_layout_on(&mut ctx, &mut root.root, bc); + ctx.place_child(&mut root.root, Point::ORIGIN); + + size +} diff --git a/masonry/src/passes/mod.rs b/masonry/src/passes/mod.rs index 4bcad33ba..aa82a913d 100644 --- a/masonry/src/passes/mod.rs +++ b/masonry/src/passes/mod.rs @@ -8,6 +8,7 @@ use crate::{Widget, WidgetId, WidgetState}; pub mod accessibility; pub mod compose; pub mod event; +pub mod layout; pub mod mutate; pub mod paint; pub mod update; diff --git a/masonry/src/render_root.rs b/masonry/src/render_root.rs index a7ea6e8ee..800d7bb16 100644 --- a/masonry/src/render_root.rs +++ b/masonry/src/render_root.rs @@ -7,7 +7,7 @@ use accesskit::{ActionRequest, Tree, TreeUpdate}; use parley::fontique::{self, Collection, CollectionOptions}; use parley::{FontContext, LayoutContext}; use tracing::{info_span, warn}; -use vello::kurbo::{self, Point, Rect}; +use vello::kurbo::{self, Rect}; use vello::Scene; #[cfg(not(target_arch = "wasm32"))] @@ -15,13 +15,14 @@ use std::time::Instant; #[cfg(target_arch = "wasm32")] use web_time::Instant; -use crate::contexts::{LayoutCtx, LifeCycleCtx}; +use crate::contexts::LifeCycleCtx; use crate::debug_logger::DebugLogger; use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize}; use crate::event::{PointerEvent, TextEvent, WindowEvent}; use crate::passes::accessibility::root_accessibility; use crate::passes::compose::root_compose; use crate::passes::event::{root_on_access_event, root_on_pointer_event, root_on_text_event}; +use crate::passes::layout::root_layout; use crate::passes::mutate::{mutate_widget, run_mutate_pass}; use crate::passes::paint::root_paint; use crate::passes::update::{ @@ -466,41 +467,20 @@ impl RenderRoot { // --- MARK: LAYOUT --- pub(crate) fn root_layout(&mut self) { - let mut dummy_state = WidgetState::synthetic(self.root.id(), self.get_kurbo_size()); - let size = self.get_kurbo_size(); - let mouse_pos = self.last_mouse_pos.map(|pos| (pos.x, pos.y).into()); - let root_state_token = self.widget_arena.widget_states.root_token_mut(); - let root_widget_token = self.widget_arena.widgets.root_token_mut(); - let mut layout_ctx = LayoutCtx { - global_state: &mut self.state, - widget_state: &mut dummy_state, - widget_state_children: root_state_token, - widget_children: root_widget_token, - mouse_pos, - }; - + let window_size = self.get_kurbo_size(); let bc = match self.size_policy { - WindowSizePolicy::User => BoxConstraints::tight(size), + WindowSizePolicy::User => BoxConstraints::tight(window_size), WindowSizePolicy::Content => BoxConstraints::UNBOUNDED, }; - let size = { - layout_ctx - .global_state - .debug_logger - .push_important_span("LAYOUT"); - let _span = info_span!("layout").entered(); - self.root.layout(&mut layout_ctx, &bc) - }; - layout_ctx.place_child(&mut self.root, Point::ORIGIN); - layout_ctx.global_state.debug_logger.pop_span(); + let mut dummy_state = WidgetState::synthetic(self.root.id(), self.get_kurbo_size()); + let size = root_layout(self, &mut dummy_state, &bc); if let WindowSizePolicy::Content = self.size_policy { let new_size = LogicalSize::new(size.width, size.height).to_physical(self.scale_factor); if self.size != new_size { self.size = new_size; - layout_ctx - .global_state + self.state .signal_queue .push_back(RenderRootSignal::SetSize(new_size)); } diff --git a/masonry/src/testing/helper_widgets.rs b/masonry/src/testing/helper_widgets.rs index a0fa43ac8..c3b677e09 100644 --- a/masonry/src/testing/helper_widgets.rs +++ b/masonry/src/testing/helper_widgets.rs @@ -331,7 +331,7 @@ impl Widget for ReplaceChild { } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { - self.child.layout(ctx, bc) + ctx.run_layout(&mut self.child, bc) } fn compose(&mut self, _ctx: &mut ComposeCtx) {} diff --git a/masonry/src/widget/align.rs b/masonry/src/widget/align.rs index 98cf7bdcd..3b9ebd154 100644 --- a/masonry/src/widget/align.rs +++ b/masonry/src/widget/align.rs @@ -98,7 +98,7 @@ impl Widget for Align { fn on_status_change(&mut self, _ctx: &mut LifeCycleCtx, _event: &StatusChange) {} fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { - let size = self.child.layout(ctx, &bc.loosen()); + let size = ctx.run_layout(&mut self.child, &bc.loosen()); log_size_warnings(size); diff --git a/masonry/src/widget/button.rs b/masonry/src/widget/button.rs index fad62135e..542655a8c 100644 --- a/masonry/src/widget/button.rs +++ b/masonry/src/widget/button.rs @@ -129,7 +129,7 @@ impl Widget for Button { let padding = Size::new(LABEL_INSETS.x_value(), LABEL_INSETS.y_value()); let label_bc = bc.shrink(padding).loosen(); - let label_size = self.label.layout(ctx, &label_bc); + let label_size = ctx.run_layout(&mut self.label, &label_bc); let baseline = ctx.child_baseline_offset(&self.label); ctx.set_baseline_offset(baseline + LABEL_INSETS.y1); diff --git a/masonry/src/widget/checkbox.rs b/masonry/src/widget/checkbox.rs index 84833eb6a..e32566764 100644 --- a/masonry/src/widget/checkbox.rs +++ b/masonry/src/widget/checkbox.rs @@ -115,7 +115,7 @@ impl Widget for Checkbox { let x_padding = theme::WIDGET_CONTROL_COMPONENT_PADDING; let check_size = theme::BASIC_WIDGET_HEIGHT; - let label_size = self.label.layout(ctx, bc); + let label_size = ctx.run_layout(&mut self.label, bc); ctx.place_child(&mut self.label, (check_size + x_padding, 0.0).into()); let desired_size = Size::new( diff --git a/masonry/src/widget/flex.rs b/masonry/src/widget/flex.rs index b19be0d8f..cf0edd8e9 100644 --- a/masonry/src/widget/flex.rs +++ b/masonry/src/widget/flex.rs @@ -671,7 +671,7 @@ impl Widget for Flex { any_use_baseline |= alignment == CrossAxisAlignment::Baseline; let old_size = ctx.widget_state.layout_rect().size(); - let child_size = widget.layout(ctx, &loosened_bc); + let child_size = ctx.run_layout(widget, &loosened_bc); if child_size.width.is_infinite() { tracing::warn!("A non-Flex child has an infinite width."); @@ -737,7 +737,7 @@ impl Widget for Flex { let old_size = ctx.widget_state.layout_rect().size(); let child_bc = self.direction.constraints(&loosened_bc, 0.0, actual_major); - let child_size = widget.layout(ctx, &child_bc); + let child_size = ctx.run_layout(widget, &child_bc); if old_size != child_size { any_changed = true; @@ -818,7 +818,7 @@ impl Widget for Flex { //TODO: this is the second call of layout on the same child, which // is bad, because it can lead to exponential increase in layout calls // when used multiple times in the widget hierarchy. - widget.layout(ctx, &child_bc); + ctx.run_layout(widget, &child_bc); } 0.0 } diff --git a/masonry/src/widget/portal.rs b/masonry/src/widget/portal.rs index d3cdd9ea2..907de45a4 100644 --- a/masonry/src/widget/portal.rs +++ b/masonry/src/widget/portal.rs @@ -373,7 +373,7 @@ impl Widget for Portal { let child_bc = BoxConstraints::new(min_child_size, max_child_size); - let content_size = self.child.layout(ctx, &child_bc); + let content_size = ctx.run_layout(&mut self.child, &child_bc); let portal_size = bc.constrain(content_size); // TODO - document better @@ -406,7 +406,7 @@ impl Widget for Portal { // TODO - request paint for scrollbar? std::mem::drop(scrollbar); - let scrollbar_size = self.scrollbar_horizontal.layout(ctx, bc); + let scrollbar_size = ctx.run_layout(&mut self.scrollbar_horizontal, bc); ctx.place_child( &mut self.scrollbar_horizontal, Point::new(0.0, portal_size.height - scrollbar_size.height), @@ -421,7 +421,7 @@ impl Widget for Portal { // TODO - request paint for scrollbar? std::mem::drop(scrollbar); - let scrollbar_size = self.scrollbar_vertical.layout(ctx, bc); + let scrollbar_size = ctx.run_layout(&mut self.scrollbar_vertical, bc); ctx.place_child( &mut self.scrollbar_vertical, Point::new(portal_size.width - scrollbar_size.width, 0.0), diff --git a/masonry/src/widget/root_widget.rs b/masonry/src/widget/root_widget.rs index a22f792a6..5edd7d51a 100644 --- a/masonry/src/widget/root_widget.rs +++ b/masonry/src/widget/root_widget.rs @@ -51,7 +51,7 @@ impl Widget for RootWidget { } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { - let size = self.pod.layout(ctx, bc); + let size = ctx.run_layout(&mut self.pod, bc); ctx.place_child(&mut self.pod, Point::ORIGIN); size } diff --git a/masonry/src/widget/sized_box.rs b/masonry/src/widget/sized_box.rs index f143643c0..41ccb96e2 100644 --- a/masonry/src/widget/sized_box.rs +++ b/masonry/src/widget/sized_box.rs @@ -332,7 +332,7 @@ impl Widget for SizedBox { let mut size; match self.child.as_mut() { Some(child) => { - size = child.layout(ctx, &child_bc); + size = ctx.run_layout(child, &child_bc); ctx.place_child(child, origin); size = Size::new( size.width + 2.0 * border_width, diff --git a/masonry/src/widget/split.rs b/masonry/src/widget/split.rs index 1a14bb99e..202140922 100644 --- a/masonry/src/widget/split.rs +++ b/masonry/src/widget/split.rs @@ -519,8 +519,8 @@ impl Widget for Split { } }; - let child1_size = self.child1.layout(ctx, &child1_bc); - let child2_size = self.child2.layout(ctx, &child2_bc); + let child1_size = ctx.run_layout(&mut self.child1, &child1_bc); + let child2_size = ctx.run_layout(&mut self.child2, &child2_bc); // Top-left align for both children, out of laziness. // Reduce our unsplit direction to the larger of the two widgets diff --git a/masonry/src/widget/tests/lifecycle_disable.rs b/masonry/src/widget/tests/lifecycle_disable.rs index e42dba6f2..9724eb981 100644 --- a/masonry/src/widget/tests/lifecycle_disable.rs +++ b/masonry/src/widget/tests/lifecycle_disable.rs @@ -94,7 +94,7 @@ fn disable_tree() { child.on_event(ctx, event); }) .layout_fn(|child, ctx, my_bc| { - let size = child.layout(ctx, my_bc); + let size = ctx.run_layout(&mut child, my_bc); ctx.place_child(child, Point::ZERO); size }) diff --git a/masonry/src/widget/tests/lifecycle_focus.rs b/masonry/src/widget/tests/lifecycle_focus.rs index f572dc3a5..133276702 100644 --- a/masonry/src/widget/tests/lifecycle_focus.rs +++ b/masonry/src/widget/tests/lifecycle_focus.rs @@ -178,7 +178,7 @@ fn resign_focus_on_disable() { child.lifecycle(ctx, event); }) .layout_fn(|child, ctx, bc| { - let layout = child.layout(ctx, bc); + let layout = ctx.run_layout(&mut child, bc); ctx.place_child(child, Point::ZERO); layout }) diff --git a/masonry/src/widget/tests/safety_rails.rs b/masonry/src/widget/tests/safety_rails.rs index e0439bd06..17034def4 100644 --- a/masonry/src/widget/tests/safety_rails.rs +++ b/masonry/src/widget/tests/safety_rails.rs @@ -12,7 +12,7 @@ fn make_parent_widget(child: W) -> ModularWidget> { ModularWidget::new(child) .lifecycle_fn(move |child, ctx, event| child.lifecycle(ctx, event)) .layout_fn(move |child, ctx, bc| { - let size = child.layout(ctx, bc); + let size = ctx.run_layout(child, bc); ctx.place_child(child, Point::ZERO); size }) @@ -69,7 +69,7 @@ fn check_forget_to_recurse_widget_added() { )] fn check_forget_to_recurse_layout() { let widget = make_parent_widget(Flex::row()).layout_fn(|_child, _ctx, _| { - // We forget to call child.layout(); + // We forget to call ctx.run_layout(); Size::ZERO }); @@ -84,8 +84,8 @@ fn check_forget_to_recurse_layout() { )] fn check_forget_to_call_place_child() { let widget = make_parent_widget(Flex::row()).layout_fn(|child, ctx, bc| { - // We call child.layout(), but forget place_child - child.layout(ctx, bc) + // We call ctx.run_layout(), but forget place_child + ctx.run_layout(child, bc) }); let _harness = TestHarness::create(widget); @@ -122,7 +122,7 @@ fn allow_non_recurse_cursor_oob() { } }) .layout_fn(|child, ctx, bc| { - let _size = child.layout(ctx, bc); + let _size = ctx.run_layout(child, bc); ctx.place_child(child, Point::ZERO); Size::new(6000.0, 6000.0) }); @@ -139,7 +139,7 @@ fn allow_non_recurse_oob_paint() { // We forget to call child.paint(); }) .layout_fn(|child, ctx, bc| { - let _size = child.layout(ctx, bc); + let _size = ctx.run_layout(child, bc); ctx.place_child(child, Point::new(500.0, 500.0)); Size::new(600.0, 600.0) }); @@ -217,7 +217,7 @@ fn check_forget_children_changed() { }) .layout_fn(|child, ctx, bc| { if let Some(child) = child { - let size = child.layout(ctx, bc); + let size = ctx.run_layout(child, bc); ctx.place_child(child, Point::ZERO); size } else { @@ -273,8 +273,8 @@ fn check_recurse_lifecycle_twice() { #[test] fn check_recurse_layout_twice() { let widget = make_parent_widget(Flex::row()).layout_fn(|child, ctx, bc| { - let size = child.layout(ctx, bc); - let _ = child.layout(ctx, bc); + let size = ctx.run_layout(child, bc); + let _ = ctx.run_layout(child, bc); ctx.place_child(child, Point::ZERO); size }); @@ -311,7 +311,7 @@ fn check_layout_stashed() { } }) .layout_fn(|child, ctx, bc| { - let size = child.layout(ctx, bc); + let size = ctx.run_layout(child, bc); ctx.place_child(child, Point::ZERO); size }); @@ -329,7 +329,7 @@ fn check_layout_stashed() { fn check_paint_rect_includes_children() { use crate::widget::Label; let widget = make_parent_widget(Label::new("Hello world")).layout_fn(|child, ctx, bc| { - let _size = child.layout(ctx, bc); + let _size = ctx.run_layout(child, bc); ctx.place_child(child, Point::ZERO); Size::ZERO }); diff --git a/masonry/src/widget/widget.rs b/masonry/src/widget/widget.rs index 52065f7ec..414bfcda3 100644 --- a/masonry/src/widget/widget.rs +++ b/masonry/src/widget/widget.rs @@ -91,9 +91,9 @@ pub trait Widget: AsAny { /// A leaf widget should determine its size (subject to the provided /// constraints) and return it. /// - /// A container widget will recursively call [`WidgetPod::layout`](crate::WidgetPod::layout) on its + /// A container widget will recursively call [`LayoutCtx::run_layout`] on its /// child widgets, providing each of them an appropriate box constraint, - /// compute layout, then call [`LayoutCtx::place_child`] on each of its children. + /// run some layout logic, then call [`LayoutCtx::place_child`] on each of its children. /// Finally, it should return the size of the container. The container /// can recurse in any order, which can be helpful to, for example, compute /// the size of non-flex widgets first, to determine the amount of space diff --git a/masonry/src/widget/widget_pod.rs b/masonry/src/widget/widget_pod.rs index 4cd05cab5..e98eb7045 100644 --- a/masonry/src/widget/widget_pod.rs +++ b/masonry/src/widget/widget_pod.rs @@ -2,15 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 use smallvec::SmallVec; -use tracing::{info_span, trace, warn}; -use vello::kurbo::{Rect, Size}; +use tracing::{info_span, trace}; use crate::tree_arena::ArenaRefChildren; use crate::widget::WidgetState; -use crate::{ - BoxConstraints, InternalLifeCycle, LayoutCtx, LifeCycle, LifeCycleCtx, StatusChange, Widget, - WidgetId, -}; +use crate::{InternalLifeCycle, LifeCycle, LifeCycleCtx, StatusChange, Widget, WidgetId}; // TODO - rewrite links in doc @@ -108,7 +104,7 @@ impl WidgetPod { // TODO - This method should take a 'can_skip: Fn(WidgetRef) -> bool' // predicate and only panic if can_skip returns false. #[inline(always)] - fn call_widget_method_with_checks( + pub(crate) fn call_widget_method_with_checks( &mut self, method_name: &str, ctx: &mut Ctx, @@ -423,177 +419,4 @@ impl WidgetPod { call_widget || extra_event.is_some() } - - // --- MARK: LAYOUT --- - - /// Compute layout of a widget. - /// - /// Generally called by container widgets as part of their [`layout`] - /// method. - /// - /// [`layout`]: Widget::layout - pub fn layout(&mut self, parent_ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { - self.call_widget_method_with_checks( - "layout", - parent_ctx, - |ctx| { - ( - ctx.widget_state_children.reborrow(), - ctx.widget_children.reborrow(), - ) - }, - |self2, parent_ctx| self2.layout_inner(parent_ctx, bc), - ); - - let id = self.id().to_raw(); - let state_mut = parent_ctx - .widget_state_children - .get_child_mut(id) - .expect("WidgetPod: inner widget not found in widget tree"); - state_mut.item.size - } - - fn layout_inner(&mut self, parent_ctx: &mut LayoutCtx, bc: &BoxConstraints) -> bool { - let id = self.id().to_raw(); - let widget_mut = parent_ctx - .widget_children - .get_child_mut(id) - .expect("WidgetPod: inner widget not found in widget tree"); - let mut state_mut = parent_ctx - .widget_state_children - .get_child_mut(id) - .expect("WidgetPod: inner widget not found in widget tree"); - let widget = widget_mut.item; - let state = state_mut.item; - - if state.is_stashed { - debug_panic!( - "Error in '{}' #{}: trying to compute layout of stashed widget.", - widget.short_type_name(), - id, - ); - state.size = Size::ZERO; - return false; - } - - bc.debug_check(widget.short_type_name()); - trace!("Computing layout with constraints {:?}", bc); - - state.local_paint_rect = Rect::ZERO; - - state.request_layout = false; - let new_size = { - let mut inner_ctx = LayoutCtx { - widget_state: state, - widget_state_children: state_mut.children.reborrow_mut(), - widget_children: widget_mut.children, - global_state: parent_ctx.global_state, - mouse_pos: parent_ctx.mouse_pos, - }; - - widget.layout(&mut inner_ctx, bc) - }; - - if state.request_layout { - debug_panic!( - "Error in '{}' #{}: layout pass has requested layout.", - widget.short_type_name(), - id, - ); - } - - // We reset `needs_layout` after the layout call, which means if - // the widget sets needs_layout to true, that will be overridden. - // This shouldn't happen in practice, except in one case: if we access - // a child using `get_raw_mut` before the child's layout is run. In that - // case the child's needs_layout is still true, and propagates up to - // this widget. The line below resets it to false. - state.needs_layout = false; - - state.needs_compose = true; - state.is_expecting_place_child_call = true; - // TODO - Not everything that has been re-laid out needs to be repainted. - state.request_paint = true; - state.needs_paint = true; - state.request_accessibility = true; - state.needs_accessibility = true; - - state.local_paint_rect = state - .local_paint_rect - .union(new_size.to_rect() + state.paint_insets); - - #[cfg(debug_assertions)] - { - for child_id in widget.children_ids() { - let child_id = child_id.to_raw(); - let child_state_mut = state_mut - .children - .get_child_mut(child_id) - .unwrap_or_else(|| panic!("widget #{child_id} not found")); - let child_state = child_state_mut.item; - if child_state.is_expecting_place_child_call { - debug_panic!( - "Error in '{}' #{}: missing call to place_child method for child widget '{}' #{}. During layout pass, if a widget calls WidgetPod::layout() on its child, it then needs to call LayoutCtx::place_child() on the same child.", - widget.short_type_name(), - id, - child_state.widget_name, - child_state.id.to_raw(), - ); - } - - // TODO - This check might be redundant with the code updating local_paint_rect - let child_rect = child_state.paint_rect(); - if !rect_contains(&state.local_paint_rect, &child_rect) && !state.is_portal { - debug_panic!( - "Error in '{}' #{}: paint_rect {:?} doesn't contain paint_rect {:?} of child widget '{}' #{}", - widget.short_type_name(), - id, - state.local_paint_rect, - child_rect, - child_state.widget_name, - child_state.id.to_raw(), - ); - } - } - } - - // TODO - Figure out how to deal with the overflow problem, eg: - // What happens if a widget returns a size larger than the allowed constraints? - // Some possibilities are: - // - Always clip: might be expensive - // - Display it anyway: might lead to graphical bugs - // - Panic: too harsh? - // Also, we need to avoid spurious crashes when we initialize the app and the - // size is (0,0) - // See https://github.com/linebender/xilem/issues/377 - - let state_mut = parent_ctx - .widget_state_children - .get_child_mut(id) - .expect("WidgetPod: inner widget not found in widget tree"); - parent_ctx.widget_state.merge_up(state_mut.item); - state_mut.item.size = new_size; - - self.log_layout_issues(widget.short_type_name(), new_size); - - true - } - - fn log_layout_issues(&self, type_name: &str, size: Size) { - if size.width.is_infinite() { - warn!("Widget `{type_name}` has an infinite width."); - } - if size.height.is_infinite() { - warn!("Widget `{type_name}` has an infinite height."); - } - } -} - -// TODO - negative rects? -/// Return `true` if all of `smaller` is within `larger`. -fn rect_contains(larger: &Rect, smaller: &Rect) -> bool { - smaller.x0 >= larger.x0 - && smaller.x1 <= larger.x1 - && smaller.y0 >= larger.y0 - && smaller.y1 <= larger.y1 } diff --git a/xilem/src/any_view.rs b/xilem/src/any_view.rs index c6bada541..8431cb9dd 100644 --- a/xilem/src/any_view.rs +++ b/xilem/src/any_view.rs @@ -87,7 +87,7 @@ impl Widget for DynWidget { } fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { - let size = self.inner.layout(ctx, bc); + let size = ctx.run_layout(&mut self.inner, bc); ctx.place_child(&mut self.inner, Point::ORIGIN); size } diff --git a/xilem/src/one_of.rs b/xilem/src/one_of.rs index c6536c79c..ed797c701 100644 --- a/xilem/src/one_of.rs +++ b/xilem/src/one_of.rs @@ -241,47 +241,47 @@ impl< fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size { match self { OneOfWidget::A(w) => { - let size = w.layout(ctx, bc); + let size = ctx.run_layout(w, bc); ctx.place_child(w, Point::ORIGIN); size } OneOfWidget::B(w) => { - let size = w.layout(ctx, bc); + let size = ctx.run_layout(w, bc); ctx.place_child(w, Point::ORIGIN); size } OneOfWidget::C(w) => { - let size = w.layout(ctx, bc); + let size = ctx.run_layout(w, bc); ctx.place_child(w, Point::ORIGIN); size } OneOfWidget::D(w) => { - let size = w.layout(ctx, bc); + let size = ctx.run_layout(w, bc); ctx.place_child(w, Point::ORIGIN); size } OneOfWidget::E(w) => { - let size = w.layout(ctx, bc); + let size = ctx.run_layout(w, bc); ctx.place_child(w, Point::ORIGIN); size } OneOfWidget::F(w) => { - let size = w.layout(ctx, bc); + let size = ctx.run_layout(w, bc); ctx.place_child(w, Point::ORIGIN); size } OneOfWidget::G(w) => { - let size = w.layout(ctx, bc); + let size = ctx.run_layout(w, bc); ctx.place_child(w, Point::ORIGIN); size } OneOfWidget::H(w) => { - let size = w.layout(ctx, bc); + let size = ctx.run_layout(w, bc); ctx.place_child(w, Point::ORIGIN); size } OneOfWidget::I(w) => { - let size = w.layout(ctx, bc); + let size = ctx.run_layout(w, bc); ctx.place_child(w, Point::ORIGIN); size }