Skip to content

Commit a557053

Browse files
Copiloteruvanos
andauthored
Warn when layout width/height is given but size_hint will override it (#2834)
* Initial plan * Add size_hint warnings for GUI layouts when fixed width/height conflicts with active size_hint * Address review feedback: use Ellipsis sentinel, add type hints, use catch_warnings in tests Co-authored-by: eruvanos <9437863+eruvanos@users.noreply.github.com> * Inline Ellipsis sentinel and move warning to UILayout method Co-authored-by: eruvanos <9437863+eruvanos@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: eruvanos <9437863+eruvanos@users.noreply.github.com>
1 parent a8630e5 commit a557053

File tree

5 files changed

+184
-36
lines changed

5 files changed

+184
-36
lines changed

arcade/gui/widgets/__init__.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import warnings
34
import weakref
45
from abc import ABC
56
from collections.abc import Iterable
@@ -1026,6 +1027,38 @@ def do_layout(self):
10261027
frame, this will happen automatically if the position or size of this widget changed.
10271028
"""
10281029

1030+
def _warn_if_size_hint_overrides_fixed_size(self, width, height, size_hint) -> None:
1031+
"""Warn when a fixed width/height is given but the size_hint will override it.
1032+
1033+
Layouts have non-None size_hint by default, which causes the parent layout to
1034+
resize them, overriding any fixed width/height given by the developer.
1035+
1036+
Args:
1037+
width: The width argument passed to __init__, or ``...`` if
1038+
width was not explicitly provided.
1039+
height: The height argument passed to __init__, or ``...`` if
1040+
height was not explicitly provided.
1041+
size_hint: The size_hint argument passed to __init__.
1042+
"""
1043+
class_name = type(self).__name__
1044+
sh_w = size_hint[0] if size_hint is not None else None
1045+
sh_h = size_hint[1] if size_hint is not None else None
1046+
1047+
if width is not ... and sh_w is not None:
1048+
warnings.warn(
1049+
f"{class_name} was given a fixed width, but size_hint_x is {sh_w!r}. "
1050+
f"The size_hint will override the fixed width. "
1051+
f"Set size_hint=(None, ...) to use a fixed width.",
1052+
stacklevel=3,
1053+
)
1054+
if height is not ... and sh_h is not None:
1055+
warnings.warn(
1056+
f"{class_name} was given a fixed height, but size_hint_y is {sh_h!r}. "
1057+
f"The size_hint will override the fixed height. "
1058+
f"Set size_hint=(..., None) to use a fixed height.",
1059+
stacklevel=3,
1060+
)
1061+
10291062

10301063
class UISpace(UIWidget):
10311064
"""Widget reserving space, can also have a background color.

arcade/gui/widgets/layout.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from collections.abc import Iterable
55
from dataclasses import dataclass
66
from typing import Literal, TypeVar
7+
from types import EllipsisType
78

89
from typing_extensions import override
910

@@ -73,19 +74,20 @@ def __init__(
7374
*,
7475
x: float = 0,
7576
y: float = 0,
76-
width: float = 1,
77-
height: float = 1,
77+
width: float | EllipsisType = ...,
78+
height: float | EllipsisType = ...,
7879
children: Iterable[UIWidget] = tuple(),
7980
size_hint=(1, 1),
8081
size_hint_min=None,
8182
size_hint_max=None,
8283
**kwargs,
8384
):
85+
self._warn_if_size_hint_overrides_fixed_size(width, height, size_hint)
8486
super().__init__(
8587
x=x,
8688
y=y,
87-
width=width,
88-
height=height,
89+
width=1 if width is ... else width,
90+
height=1 if height is ... else height,
8991
children=children,
9092
size_hint=size_hint,
9193
size_hint_min=size_hint_min,
@@ -239,10 +241,10 @@ class UIBoxLayout(UILayout):
239241
def __init__(
240242
self,
241243
*,
242-
x=0,
243-
y=0,
244-
width=1,
245-
height=1,
244+
x: float = 0,
245+
y: float = 0,
246+
width: float | EllipsisType = ...,
247+
height: float | EllipsisType = ...,
246248
vertical=True,
247249
align="center",
248250
children: Iterable[UIWidget] = tuple(),
@@ -252,11 +254,12 @@ def __init__(
252254
style=None,
253255
**kwargs,
254256
):
257+
self._warn_if_size_hint_overrides_fixed_size(width, height, size_hint)
255258
super().__init__(
256259
x=x,
257260
y=y,
258-
width=width,
259-
height=height,
261+
width=1 if width is ... else width,
262+
height=1 if height is ... else height,
260263
children=children,
261264
size_hint=size_hint,
262265
size_hint_max=size_hint_max,
@@ -485,10 +488,10 @@ class UIGridLayout(UILayout):
485488
def __init__(
486489
self,
487490
*,
488-
x=0,
489-
y=0,
490-
width=1,
491-
height=1,
491+
x: float = 0,
492+
y: float = 0,
493+
width: float | EllipsisType = ...,
494+
height: float | EllipsisType = ...,
492495
align_horizontal="center",
493496
align_vertical="center",
494497
children: Iterable[UIWidget] = tuple(),
@@ -500,11 +503,12 @@ def __init__(
500503
row_count: int = 1,
501504
**kwargs,
502505
):
506+
self._warn_if_size_hint_overrides_fixed_size(width, height, size_hint)
503507
super().__init__(
504508
x=x,
505509
y=y,
506-
width=width,
507-
height=height,
510+
width=1 if width is ... else width,
511+
height=1 if height is ... else height,
508512
children=children,
509513
size_hint=size_hint,
510514
size_hint_max=size_hint_max,
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""Tests that layouts warn when explicit width/height conflicts with active size_hint."""
2+
import warnings
3+
4+
import pytest
5+
6+
from arcade.gui import UIBoxLayout
7+
from arcade.gui.widgets.layout import UIAnchorLayout, UIGridLayout
8+
9+
10+
def test_anchor_layout_warns_when_width_given_with_default_size_hint(window):
11+
"""UIAnchorLayout should warn when width is given but size_hint_x is active."""
12+
with pytest.warns(UserWarning, match="size_hint_x"):
13+
UIAnchorLayout(width=500)
14+
15+
16+
def test_anchor_layout_warns_when_height_given_with_default_size_hint(window):
17+
"""UIAnchorLayout should warn when height is given but size_hint_y is active."""
18+
with pytest.warns(UserWarning, match="size_hint_y"):
19+
UIAnchorLayout(height=500)
20+
21+
22+
def test_anchor_layout_no_warning_when_size_hint_none(window):
23+
"""UIAnchorLayout should not warn when size_hint=None is explicitly set."""
24+
with warnings.catch_warnings():
25+
warnings.simplefilter("error")
26+
UIAnchorLayout(width=500, height=500, size_hint=None)
27+
28+
29+
def test_anchor_layout_no_warning_when_no_explicit_size(window):
30+
"""UIAnchorLayout should not warn when width/height are not explicitly given."""
31+
with warnings.catch_warnings():
32+
warnings.simplefilter("error")
33+
UIAnchorLayout(size_hint=(1, 1))
34+
35+
36+
def test_anchor_layout_no_warning_when_size_hint_x_none(window):
37+
"""UIAnchorLayout should not warn for width when size_hint_x is None."""
38+
# No width warning expected (only height warning)
39+
with pytest.warns(UserWarning, match="size_hint_y"):
40+
UIAnchorLayout(width=500, height=500, size_hint=(None, 1))
41+
42+
43+
def test_anchor_layout_no_warning_when_size_hint_y_none(window):
44+
"""UIAnchorLayout should not warn for height when size_hint_y is None."""
45+
# No height warning expected (only width warning)
46+
with pytest.warns(UserWarning, match="size_hint_x"):
47+
UIAnchorLayout(width=500, height=500, size_hint=(1, None))
48+
49+
50+
def test_box_layout_warns_when_width_given_with_default_size_hint(window):
51+
"""UIBoxLayout should warn when width is given but size_hint_x is active."""
52+
with pytest.warns(UserWarning, match="size_hint_x"):
53+
UIBoxLayout(width=200)
54+
55+
56+
def test_box_layout_warns_when_height_given_with_default_size_hint(window):
57+
"""UIBoxLayout should warn when height is given but size_hint_y is active."""
58+
with pytest.warns(UserWarning, match="size_hint_y"):
59+
UIBoxLayout(height=200)
60+
61+
62+
def test_box_layout_no_warning_when_size_hint_none(window):
63+
"""UIBoxLayout should not warn when size_hint=None is explicitly set."""
64+
with warnings.catch_warnings():
65+
warnings.simplefilter("error")
66+
UIBoxLayout(width=200, height=200, size_hint=None)
67+
68+
69+
def test_box_layout_no_warning_when_no_explicit_size(window):
70+
"""UIBoxLayout should not warn when width/height are not explicitly given."""
71+
with warnings.catch_warnings():
72+
warnings.simplefilter("error")
73+
UIBoxLayout(size_hint=(0, 0))
74+
75+
76+
def test_grid_layout_warns_when_width_given_with_default_size_hint(window):
77+
"""UIGridLayout should warn when width is given but size_hint_x is active."""
78+
with pytest.warns(UserWarning, match="size_hint_x"):
79+
UIGridLayout(width=200)
80+
81+
82+
def test_grid_layout_warns_when_height_given_with_default_size_hint(window):
83+
"""UIGridLayout should warn when height is given but size_hint_y is active."""
84+
with pytest.warns(UserWarning, match="size_hint_y"):
85+
UIGridLayout(height=200)
86+
87+
88+
def test_grid_layout_no_warning_when_size_hint_none(window):
89+
"""UIGridLayout should not warn when size_hint=None is explicitly set."""
90+
with warnings.catch_warnings():
91+
warnings.simplefilter("error")
92+
UIGridLayout(width=200, height=200, size_hint=None)
93+
94+
95+
def test_grid_layout_no_warning_when_no_explicit_size(window):
96+
"""UIGridLayout should not warn when width/height are not explicitly given."""
97+
with warnings.catch_warnings():
98+
warnings.simplefilter("error")
99+
UIGridLayout(size_hint=(0, 0))
100+
101+
102+
def test_warning_message_includes_class_name(window):
103+
"""Warning should include the layout class name for clear identification."""
104+
with pytest.warns(UserWarning, match="UIBoxLayout"):
105+
UIBoxLayout(width=200)
106+
107+
with pytest.warns(UserWarning, match="UIAnchorLayout"):
108+
UIAnchorLayout(width=200)
109+
110+
with pytest.warns(UserWarning, match="UIGridLayout"):
111+
UIGridLayout(width=200)

tests/unit/gui/test_layouting_anchorlayout.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
def test_place_widget(window):
99
dummy = UIDummy(width=100, height=200)
1010

11-
subject = UIAnchorLayout(x=0, y=0, width=500, height=500)
11+
subject = UIAnchorLayout(x=0, y=0, width=500, height=500, size_hint=None)
1212

1313
subject.add(
1414
dummy,
@@ -30,7 +30,7 @@ def test_place_widget_relative_to_own_content_rect(window):
3030
dummy = UIDummy(width=100, height=200)
3131

3232
subject = (
33-
UIAnchorLayout(x=0, y=0, width=500, height=500)
33+
UIAnchorLayout(x=0, y=0, width=500, height=500, size_hint=None)
3434
.with_border(width=2)
3535
.with_padding(left=50, top=100)
3636
)
@@ -68,7 +68,7 @@ def test_place_box_layout(window, ui):
6868

6969

7070
def test_grow_child_half(window):
71-
subject = UIAnchorLayout(width=400, height=400)
71+
subject = UIAnchorLayout(width=400, height=400, size_hint=None)
7272
dummy = subject.add(UIDummy(width=100, height=100, size_hint=(0.5, 0.5)))
7373

7474
subject._do_layout()
@@ -78,7 +78,7 @@ def test_grow_child_half(window):
7878

7979

8080
def test_grow_child_full_width(window):
81-
subject = UIAnchorLayout(width=400, height=400)
81+
subject = UIAnchorLayout(width=400, height=400, size_hint=None)
8282
dummy = subject.add(UIDummy(width=100, height=100, size_hint=(1, 0.5)))
8383

8484
subject._do_layout()
@@ -88,7 +88,7 @@ def test_grow_child_full_width(window):
8888

8989

9090
def test_grow_child_full_height(window):
91-
subject = UIAnchorLayout(width=400, height=400)
91+
subject = UIAnchorLayout(width=400, height=400, size_hint=None)
9292
dummy = subject.add(UIDummy(width=100, height=100, size_hint=(0.5, 1)))
9393

9494
subject._do_layout()
@@ -98,7 +98,7 @@ def test_grow_child_full_height(window):
9898

9999

100100
def test_grow_child_to_max_size(window):
101-
subject = UIAnchorLayout(width=400, height=400)
101+
subject = UIAnchorLayout(width=400, height=400, size_hint=None)
102102
dummy = subject.add(UIDummy(width=100, height=100, size_hint=(1, 1), size_hint_max=(200, 150)))
103103

104104
subject._do_layout()
@@ -108,7 +108,7 @@ def test_grow_child_to_max_size(window):
108108

109109

110110
def test_shrink_child_to_min_size(window):
111-
subject = UIAnchorLayout(width=400, height=400)
111+
subject = UIAnchorLayout(width=400, height=400, size_hint=None)
112112
dummy = subject.add(
113113
UIDummy(width=100, height=100, size_hint=(0.1, 0.1), size_hint_min=(200, 150))
114114
)
@@ -121,7 +121,7 @@ def test_shrink_child_to_min_size(window):
121121

122122
def test_children_can_grow_out_of_bounce(window):
123123
"""This tests behavior, which is used for scrolling."""
124-
subject = UIAnchorLayout(width=400, height=400)
124+
subject = UIAnchorLayout(width=400, height=400, size_hint=None)
125125
dummy = subject.add(UIDummy(width=100, height=100, size_hint=(2, 2)))
126126

127127
subject._do_layout()
@@ -132,7 +132,7 @@ def test_children_can_grow_out_of_bounce(window):
132132

133133
def test_children_limited_to_layout_size_when_enforced(window):
134134
"""This tests behavior, which is used for scrolling."""
135-
subject = UIAnchorLayout(width=400, height=400)
135+
subject = UIAnchorLayout(width=400, height=400, size_hint=None)
136136
subject._restrict_child_size = True
137137
dummy = subject.add(UIDummy(width=100, height=100, size_hint=(2, 2)))
138138

@@ -143,7 +143,7 @@ def test_children_limited_to_layout_size_when_enforced(window):
143143

144144

145145
def test_only_adjust_size_if_size_hint_is_given_for_dimension(window):
146-
subject = UIAnchorLayout(width=400, height=400)
146+
subject = UIAnchorLayout(width=400, height=400, size_hint=None)
147147
dummy = subject.add(
148148
UIDummy(width=100, height=100, size_hint=(2, None), size_hint_min=(None, 200))
149149
)

0 commit comments

Comments
 (0)