Skip to content

Commit f6ef5ba

Browse files
Exclude collapsed children from box layout sizing and gaps (#2990)
AI-assisted implementation and testing with OpenAI Codex.
1 parent b2ee367 commit f6ef5ba

2 files changed

Lines changed: 120 additions & 6 deletions

File tree

‎arcade/gui/widgets/layout.py‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ def add(self, child: W, **kwargs) -> W:
296296
bind(child, "size_hint", self._trigger_size_hint_update, weak=True)
297297
bind(child, "size_hint_min", self._trigger_size_hint_update, weak=True)
298298
bind(child, "size_hint_max", self._trigger_size_hint_update, weak=True)
299+
bind(child, "visible", self._trigger_size_hint_update, weak=True)
299300

300301
return super().add(child, **kwargs)
301302

@@ -308,6 +309,7 @@ def remove(self, child: UIWidget):
308309
unbind(child, "size_hint", self._trigger_size_hint_update)
309310
unbind(child, "size_hint_min", self._trigger_size_hint_update)
310311
unbind(child, "size_hint_max", self._trigger_size_hint_update)
312+
unbind(child, "visible", self._trigger_size_hint_update)
311313

312314
return super().remove(child)
313315

@@ -317,12 +319,12 @@ def _trigger_size_hint_update(self):
317319
def _update_size_hints(self):
318320
self._size_hint_requires_update = False
319321

320-
required_space_between = max(0, len(self.children) - 1) * self._space_between
321322
min_child_sizes = [
322323
UILayout.min_size_of(child) for child in self.children if child.visible is not None
323324
]
325+
required_space_between = max(0, len(min_child_sizes) - 1) * self._space_between
324326

325-
if len(self.children) == 0:
327+
if not min_child_sizes:
326328
width = 0
327329
height = 0
328330
elif self.vertical:
@@ -362,12 +364,11 @@ def do_layout(self):
362364
start_y = self.content_rect.top
363365
start_x = self.content_rect.left
364366

365-
if not self.children:
366-
return
367-
368367
children_to_render = [
369368
(child, data) for child, data in self._children if child.visible is not None
370369
]
370+
if not children_to_render:
371+
return
371372

372373
# main axis
373374
constraints = [
@@ -377,7 +378,7 @@ def do_layout(self):
377378

378379
available_space = (
379380
self.content_height if self.vertical else self.content_width
380-
) - self._space_between * (len(self.children) - 1)
381+
) - self._space_between * (len(children_to_render) - 1)
381382
main_sizes = _box_axis_algorithm(constraints, available_space)
382383

383384
# orthogonal axis
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import pytest
2+
3+
from arcade.gui import UIBoxLayout, UIWidget
4+
from arcade.types import LBWH
5+
6+
7+
@pytest.mark.parametrize("vertical", [False, True])
8+
@pytest.mark.parametrize("count", [0, 1, 3])
9+
def test_all_collapsed_children_preserve_only_padding_and_border(vertical, count):
10+
children = [UIWidget(width=100, height=50) for _ in range(count)]
11+
for child in children:
12+
child.visible = None
13+
original_rects = [child.rect for child in children]
14+
box = UIBoxLayout(vertical=vertical, children=children, space_between=10)
15+
box.with_padding(left=2, right=3, top=5, bottom=7).with_border(width=4)
16+
17+
box.fit_content()
18+
box.do_layout()
19+
20+
assert box.size_hint_min == (13, 20)
21+
assert box.size == (13, 20)
22+
assert [child.rect for child in children] == original_rects
23+
24+
25+
@pytest.mark.parametrize("vertical", [False, True])
26+
@pytest.mark.parametrize("collapsed_index", [0, 1, 2])
27+
def test_collapsed_children_do_not_reserve_gaps(vertical, collapsed_index):
28+
first = UIWidget(size_hint=(1, 1), size_hint_min=(20, 30))
29+
second = UIWidget(size_hint=(1, 1), size_hint_min=(20, 30))
30+
collapsed = UIWidget(width=100, height=100)
31+
collapsed.visible = None
32+
original_rect = collapsed.rect
33+
children = [first, second]
34+
children.insert(collapsed_index, collapsed)
35+
box = UIBoxLayout(vertical=vertical, children=children, space_between=10)
36+
37+
assert box.size_hint_min == ((20, 70) if vertical else (50, 30))
38+
box.rect = LBWH(0, 0, 120, 120)
39+
box.do_layout()
40+
41+
if vertical:
42+
assert first.rect == LBWH(0, 65, 120, 55)
43+
assert second.rect == LBWH(0, 0, 120, 55)
44+
else:
45+
assert first.rect == LBWH(0, 0, 55, 120)
46+
assert second.rect == LBWH(65, 0, 55, 120)
47+
assert collapsed.rect == original_rect
48+
49+
50+
@pytest.mark.parametrize("vertical", [False, True])
51+
@pytest.mark.parametrize("visible", [False, True])
52+
def test_hidden_children_still_participate_in_layout(vertical, visible):
53+
first = UIWidget(width=20, height=30)
54+
first.visible = visible
55+
second = UIWidget(width=40, height=50)
56+
box = UIBoxLayout(vertical=vertical, children=[first, second], space_between=10)
57+
58+
box.fit_content()
59+
box.do_layout()
60+
61+
assert box.size_hint_min == ((40, 90) if vertical else (70, 50))
62+
if vertical:
63+
assert first.top == box.top
64+
assert first.bottom - second.top == 10
65+
assert second.bottom == box.bottom
66+
else:
67+
assert first.left == box.left
68+
assert second.left - first.right == 10
69+
assert second.right == box.right
70+
71+
72+
@pytest.mark.parametrize("vertical", [False, True])
73+
def test_visibility_changes_update_the_minimum_size(vertical):
74+
first = UIWidget(width=20, height=30)
75+
second = UIWidget(width=40, height=50)
76+
box = UIBoxLayout(vertical=vertical, children=[first, second], space_between=10)
77+
78+
first.visible = None
79+
box._prepare_layout()
80+
assert box.size_hint_min == (40, 50)
81+
82+
second.visible = None
83+
box._prepare_layout()
84+
assert box.size_hint_min == (0, 0)
85+
86+
first.visible = False
87+
box._prepare_layout()
88+
assert box.size_hint_min == (20, 30)
89+
90+
second.visible = True
91+
box._prepare_layout()
92+
assert box.size_hint_min == ((40, 90) if vertical else (70, 50))
93+
94+
95+
def test_collapse_and_restore_through_ui_manager(window, ui):
96+
first = UIWidget(width=20, height=30)
97+
second = UIWidget(width=40, height=50)
98+
box = UIBoxLayout(children=[first, second], space_between=10)
99+
ui.add(box)
100+
ui.execute_layout()
101+
assert box.size == (40, 90)
102+
103+
first.visible = None
104+
ui.execute_layout()
105+
assert box.size == (40, 50)
106+
107+
second.visible = None
108+
ui.execute_layout()
109+
assert box.size == (0, 0)
110+
111+
first.visible = False
112+
ui.execute_layout()
113+
assert box.size == (20, 30)

0 commit comments

Comments
 (0)