mirror of https://github.com/linebender/xilem
227 lines
7.9 KiB
Rust
227 lines
7.9 KiB
Rust
// 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 dpi::LogicalSize;
|
|
use smallvec::SmallVec;
|
|
use tracing::{info_span, trace};
|
|
use vello::kurbo::{Point, Rect, Size};
|
|
|
|
use crate::app::{RenderRoot, RenderRootSignal, WindowSizePolicy};
|
|
use crate::core::{BoxConstraints, LayoutCtx, Widget, WidgetPod, WidgetState};
|
|
use crate::passes::{enter_span_if, recurse_on_children};
|
|
|
|
// --- MARK: RUN LAYOUT ---
|
|
/// 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<W: Widget + ?Sized>(
|
|
parent_ctx: &mut LayoutCtx<'_>,
|
|
pod: &mut WidgetPod<W>,
|
|
bc: &BoxConstraints,
|
|
) -> Size {
|
|
let id = pod.id();
|
|
let mut widget = parent_ctx.widget_children.item_mut(id).unwrap();
|
|
let mut state = parent_ctx.widget_state_children.item_mut(id).unwrap();
|
|
|
|
let trace = parent_ctx.global_state.trace.layout;
|
|
let _span = enter_span_if(
|
|
trace,
|
|
parent_ctx.global_state,
|
|
widget.reborrow(),
|
|
state.reborrow(),
|
|
);
|
|
|
|
let mut children_ids = SmallVec::new();
|
|
if cfg!(debug_assertions) {
|
|
children_ids = widget.item.children_ids();
|
|
|
|
// We forcefully set request_layout to true for all children.
|
|
// This is used below to check that widget.layout(..) visited all of them.
|
|
for child_id in widget.item.children_ids() {
|
|
let child_state = state.children.item_mut(child_id).unwrap().item;
|
|
if !child_state.is_stashed {
|
|
child_state.request_layout = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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
|
|
// 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.item.is_explicitly_stashed {
|
|
debug_panic!(
|
|
"Error in '{}' {}: trying to compute layout of stashed widget.",
|
|
widget.item.short_type_name(),
|
|
pod.id(),
|
|
);
|
|
state.item.size = Size::ZERO;
|
|
return Size::ZERO;
|
|
}
|
|
|
|
// TODO - Not everything that has been re-laid out needs to be repainted.
|
|
state.item.needs_paint = true;
|
|
state.item.needs_compose = true;
|
|
state.item.needs_accessibility = true;
|
|
state.item.request_paint = true;
|
|
state.item.request_compose = true;
|
|
state.item.request_accessibility = true;
|
|
|
|
bc.debug_check(widget.item.short_type_name());
|
|
if trace {
|
|
trace!("Computing layout with constraints {:?}", bc);
|
|
}
|
|
|
|
state.item.local_paint_rect = Rect::ZERO;
|
|
|
|
// TODO - Handle more elegantly
|
|
// We suppress need_layout and request_layout for stashed children
|
|
// to avoid unnecessary relayouts in corner cases.
|
|
recurse_on_children(
|
|
pod.id(),
|
|
widget.reborrow_mut(),
|
|
state.children.reborrow_mut(),
|
|
|_, state| {
|
|
if state.item.is_stashed {
|
|
state.item.needs_layout = false;
|
|
state.item.request_layout = false;
|
|
}
|
|
},
|
|
);
|
|
|
|
let new_size = {
|
|
let mut inner_ctx = LayoutCtx {
|
|
widget_state: state.item,
|
|
widget_state_children: state.children.reborrow_mut(),
|
|
widget_children: widget.children,
|
|
global_state: parent_ctx.global_state,
|
|
};
|
|
|
|
// TODO - If constraints are the same and request_layout isn't set,
|
|
// skip calling layout
|
|
inner_ctx.widget_state.request_layout = false;
|
|
widget.item.layout(&mut inner_ctx, bc)
|
|
};
|
|
if state.item.request_layout {
|
|
debug_panic!(
|
|
"Error in '{}' {}: layout request flag was set during layout pass",
|
|
widget.item.short_type_name(),
|
|
pod.id(),
|
|
);
|
|
}
|
|
if trace {
|
|
trace!(
|
|
"Computed layout: size={}, baseline={}, insets={:?}",
|
|
new_size, state.item.baseline_offset, state.item.paint_insets,
|
|
);
|
|
}
|
|
|
|
state.item.needs_layout = false;
|
|
state.item.is_expecting_place_child_call = true;
|
|
|
|
state.item.local_paint_rect = state
|
|
.item
|
|
.local_paint_rect
|
|
.union(new_size.to_rect() + state.item.paint_insets);
|
|
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
let name = widget.item.short_type_name();
|
|
for child_id in widget.item.children_ids() {
|
|
let child_state = state.children.item_mut(child_id).unwrap().item;
|
|
|
|
if child_state.is_stashed {
|
|
continue;
|
|
}
|
|
|
|
if child_state.request_layout {
|
|
debug_panic!(
|
|
"Error in '{}' {}: LayoutCtx::run_layout() was not called with child widget '{}' {}.",
|
|
name,
|
|
pod.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 '{}' {}.",
|
|
name,
|
|
pod.id(),
|
|
child_state.widget_name,
|
|
child_state.id,
|
|
);
|
|
}
|
|
}
|
|
|
|
let new_children_ids = widget.item.children_ids();
|
|
if children_ids != new_children_ids && !state.item.children_changed {
|
|
debug_panic!(
|
|
"Error in '{}' {}: children changed during layout pass",
|
|
name,
|
|
pod.id(),
|
|
);
|
|
}
|
|
|
|
if !new_size.width.is_finite() || !new_size.height.is_finite() {
|
|
debug_panic!(
|
|
"Error in '{}' {}: invalid size {}",
|
|
name,
|
|
pod.id(),
|
|
new_size
|
|
);
|
|
}
|
|
}
|
|
|
|
let state_mut = parent_ctx.widget_state_children.item_mut(id).unwrap();
|
|
parent_ctx.widget_state.merge_up(state_mut.item);
|
|
state_mut.item.size = new_size;
|
|
new_size
|
|
}
|
|
|
|
// --- MARK: ROOT ---
|
|
/// See the [passes documentation](../doc/05_pass_system.md#layout-pass).
|
|
pub(crate) fn run_layout_pass(root: &mut RenderRoot) {
|
|
if !root.root_state().needs_layout {
|
|
return;
|
|
}
|
|
|
|
let _span = info_span!("layout").entered();
|
|
root.global_state.needs_pointer_pass = true;
|
|
|
|
let window_size = root.get_kurbo_size();
|
|
let bc = match root.size_policy {
|
|
WindowSizePolicy::User => BoxConstraints::tight(window_size),
|
|
WindowSizePolicy::Content => BoxConstraints::UNBOUNDED,
|
|
};
|
|
|
|
let mut dummy_state = WidgetState::synthetic(root.root.id(), root.get_kurbo_size());
|
|
let root_state_token = root.widget_arena.states.roots_mut();
|
|
let root_widget_token = root.widget_arena.widgets.roots_mut();
|
|
let mut ctx = LayoutCtx {
|
|
global_state: &mut root.global_state,
|
|
widget_state: &mut dummy_state,
|
|
widget_state_children: root_state_token,
|
|
widget_children: root_widget_token,
|
|
};
|
|
|
|
let size = run_layout_on(&mut ctx, &mut root.root, &bc);
|
|
ctx.place_child(&mut root.root, Point::ORIGIN);
|
|
|
|
if let WindowSizePolicy::Content = root.size_policy {
|
|
let new_size =
|
|
LogicalSize::new(size.width, size.height).to_physical(root.global_state.scale_factor);
|
|
if root.size != new_size {
|
|
root.size = new_size;
|
|
root.global_state
|
|
.emit_signal(RenderRootSignal::SetSize(new_size));
|
|
}
|
|
}
|
|
}
|