Skip to content

Commit edf9800

Browse files
authored
FlexboxLayout: detect binding loops when components have height-for-width children (slint-ui#11381)
1 parent ccec446 commit edf9800

2 files changed

Lines changed: 94 additions & 4 deletions

File tree

internal/compiler/passes/binding_analysis.rs

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -556,21 +556,37 @@ fn recurse_expression(
556556
// Visit layout geometry dependencies
557557
if matches!(expr, Expression::SolveFlexboxLayout(..)) {
558558
// The solve needs the main-axis dimension (width for row,
559-
// height for column). For column direction, it also needs
560-
// the cross-axis width for height-for-width items, but that
561-
// dependency is safe (set by parent before solve) and doesn't
562-
// need to be declared here.
559+
// height for column). On the cross axis, *builtin* items
560+
// receive the perpendicular size through the item VTable's
561+
// `cross_axis_constraint` parameter and never read `self.width`
562+
// at runtime, so no edge is declared for them here. *Component*
563+
// items don't have that shortcut: their compiled
564+
// `layoutinfo-<cross>` binding is evaluated as an ordinary
565+
// property and really does depend on the cross-axis dimension
566+
// (typically via inner height-for-width items). That edge is
567+
// added by `visit_layout_items_layoutinfo_cross_axis_dependencies`
568+
// so the binding-loop pass can detect the cycle.
563569
use crate::layout::FlexboxAxisRelation;
564570
match layout.axis_relation(Orientation::Horizontal) {
565571
FlexboxAxisRelation::MainAxis => {
566572
if let Some(nr) = layout.geometry.rect.width_reference.as_ref() {
567573
vis(&nr.clone().into(), P);
568574
}
575+
visit_layout_items_layoutinfo_cross_axis_dependencies(
576+
layout.elems.iter().map(|fi| &fi.item),
577+
Orientation::Vertical,
578+
vis,
579+
);
569580
}
570581
FlexboxAxisRelation::CrossAxis => {
571582
if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
572583
vis(&nr.clone().into(), P);
573584
}
585+
visit_layout_items_layoutinfo_cross_axis_dependencies(
586+
layout.elems.iter().map(|fi| &fi.item),
587+
Orientation::Horizontal,
588+
vis,
589+
);
574590
}
575591
FlexboxAxisRelation::Unknown => {
576592
// Runtime direction: conservatively depend on both
@@ -580,6 +596,16 @@ fn recurse_expression(
580596
if let Some(nr) = layout.geometry.rect.height_reference.as_ref() {
581597
vis(&nr.clone().into(), P);
582598
}
599+
visit_layout_items_layoutinfo_cross_axis_dependencies(
600+
layout.elems.iter().map(|fi| &fi.item),
601+
Orientation::Horizontal,
602+
vis,
603+
);
604+
visit_layout_items_layoutinfo_cross_axis_dependencies(
605+
layout.elems.iter().map(|fi| &fi.item),
606+
Orientation::Vertical,
607+
vis,
608+
);
583609
}
584610
}
585611
} else if let Expression::ComputeFlexboxLayoutInfo(_, orientation) = expr {
@@ -767,6 +793,39 @@ fn visit_layout_items_dependencies<'a>(
767793
}
768794
}
769795

796+
/// Visit cross-axis `layoutinfo-<cross>` dependencies for child elements that
797+
/// have a compiled `layoutinfo-<cross>` binding (i.e. an inlined component
798+
/// root, or a nested layout).
799+
///
800+
/// Pure builtins (`Image`, `Text`, `Rectangle`, …) do not set `layout_info_prop`
801+
/// — their cross-axis size is computed through the item VTable, which accepts a
802+
/// `cross_axis_constraint` argument, so they never read `self.width` at
803+
/// runtime and the parent's `SolveFlexboxLayout` has no real dependency on
804+
/// them. Elements that *do* set `layout_info_prop` run an ordinary property
805+
/// binding that may transitively depend on the cross-axis dimension (e.g. an
806+
/// inner word-wrapping `Text` or aspect-ratio `Image`). Declaring that edge
807+
/// lets `binding_analysis` detect cycles through component boundaries instead
808+
/// of letting them surface as a runtime recursion panic.
809+
fn visit_layout_items_layoutinfo_cross_axis_dependencies<'a>(
810+
items: impl Iterator<Item = &'a LayoutItem>,
811+
cross_axis: Orientation,
812+
vis: &mut impl FnMut(&PropertyPath, ReadType),
813+
) {
814+
for it in items {
815+
let element = it.element.clone();
816+
if let Some(nr) = element.borrow().layout_info_prop(cross_axis) {
817+
vis(&nr.clone().into(), ReadType::PropertyRead);
818+
} else if let ElementType::Component(base) = &element.borrow().base_type
819+
&& let Some(nr) = base.root_element.borrow().layout_info_prop(cross_axis)
820+
{
821+
vis(
822+
&PropertyPath { elements: vec![ByAddress(element.clone())], prop: nr.clone() },
823+
ReadType::PropertyRead,
824+
);
825+
}
826+
}
827+
}
828+
770829
/// The builtin function can call native code, and we need to visit the properties that are accessed by it
771830
fn visit_implicit_layout_info_dependencies(
772831
orientation: crate::layout::Orientation,
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
// Copyright © SixtyFPS GmbH <info@slint.dev>
2+
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3+
4+
// A component whose cross-axis `layoutinfo-v` binding transitively reads its
5+
// own width (here, through a word-wrapping `Text` inside an inner layout)
6+
// forms a cycle when placed inside a FlexboxLayout: `SolveFlexboxLayout` needs
7+
// the child's `layoutinfo-v`, but the child's compiled `layoutinfo-v` binding
8+
// reads widths that depend back on `SolveFlexboxLayout`. Builtins avoid this
9+
// because they receive the cross-axis constraint through the item VTable
10+
// (`cross_axis_constraint`), but components don't have that shortcut — so the
11+
// dependency must be reported by the binding-loop pass.
12+
13+
component Inner {
14+
// >error{The binding for the property 'layoutinfo-v' is part of a binding loop (layoutinfo-v -> layout-cache -> width -> width -> layout-cache -> width -> layoutinfo-v -> layoutinfo-v)}
15+
HorizontalLayout {
16+
// > <error{The binding for the property 'width' is part of a binding loop (layoutinfo-v -> layout-cache -> width -> width -> layout-cache -> width -> layoutinfo-v -> layoutinfo-v)}
17+
// > <^error{The binding for the property 'layout-cache' is part of a binding loop (layoutinfo-v -> layout-cache -> width -> width -> layout-cache -> width -> layoutinfo-v -> layoutinfo-v)}
18+
// > <^^error{The binding for the property 'width' is part of a binding loop (layoutinfo-v -> layout-cache -> width -> width -> layout-cache -> width -> layoutinfo-v -> layoutinfo-v)}
19+
// > <^^^error{The binding for the property 'layoutinfo-v' is part of a binding loop (layoutinfo-v -> layout-cache -> width -> width -> layout-cache -> width -> layoutinfo-v -> layoutinfo-v)}
20+
Text { text: "hello"; wrap: word-wrap; }
21+
}
22+
}
23+
//<<<error{The binding for the property 'layoutinfo-v' is part of a binding loop (layoutinfo-v -> layout-cache -> width -> width -> layout-cache -> width -> layoutinfo-v -> layoutinfo-v)}
24+
25+
export component Demo inherits Window {
26+
FlexboxLayout {
27+
// > <error{The binding for the property 'layout-cache' is part of a binding loop (layoutinfo-v -> layout-cache -> width -> width -> layout-cache -> width -> layoutinfo-v -> layoutinfo-v)}
28+
// > <^error{The binding for the property 'width' is part of a binding loop (layoutinfo-v -> layout-cache -> width -> width -> layout-cache -> width -> layoutinfo-v -> layoutinfo-v)}
29+
Inner {}
30+
}
31+
}

0 commit comments

Comments
 (0)