diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index f275509e2..7f1c9a803 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import warnings import weakref from abc import ABC from collections.abc import Iterable @@ -1026,6 +1027,38 @@ def do_layout(self): frame, this will happen automatically if the position or size of this widget changed. """ + def _warn_if_size_hint_overrides_fixed_size(self, width, height, size_hint) -> None: + """Warn when a fixed width/height is given but the size_hint will override it. + + Layouts have non-None size_hint by default, which causes the parent layout to + resize them, overriding any fixed width/height given by the developer. + + Args: + width: The width argument passed to __init__, or ``...`` if + width was not explicitly provided. + height: The height argument passed to __init__, or ``...`` if + height was not explicitly provided. + size_hint: The size_hint argument passed to __init__. + """ + class_name = type(self).__name__ + sh_w = size_hint[0] if size_hint is not None else None + sh_h = size_hint[1] if size_hint is not None else None + + if width is not ... and sh_w is not None: + warnings.warn( + f"{class_name} was given a fixed width, but size_hint_x is {sh_w!r}. " + f"The size_hint will override the fixed width. " + f"Set size_hint=(None, ...) to use a fixed width.", + stacklevel=3, + ) + if height is not ... and sh_h is not None: + warnings.warn( + f"{class_name} was given a fixed height, but size_hint_y is {sh_h!r}. " + f"The size_hint will override the fixed height. " + f"Set size_hint=(..., None) to use a fixed height.", + stacklevel=3, + ) + class UISpace(UIWidget): """Widget reserving space, can also have a background color. diff --git a/arcade/gui/widgets/layout.py b/arcade/gui/widgets/layout.py index bdf251217..ffd010689 100644 --- a/arcade/gui/widgets/layout.py +++ b/arcade/gui/widgets/layout.py @@ -4,6 +4,7 @@ from collections.abc import Iterable from dataclasses import dataclass from typing import Literal, TypeVar +from types import EllipsisType from typing_extensions import override @@ -73,19 +74,20 @@ def __init__( *, x: float = 0, y: float = 0, - width: float = 1, - height: float = 1, + width: float | EllipsisType = ..., + height: float | EllipsisType = ..., children: Iterable[UIWidget] = tuple(), size_hint=(1, 1), size_hint_min=None, size_hint_max=None, **kwargs, ): + self._warn_if_size_hint_overrides_fixed_size(width, height, size_hint) super().__init__( x=x, y=y, - width=width, - height=height, + width=1 if width is ... else width, + height=1 if height is ... else height, children=children, size_hint=size_hint, size_hint_min=size_hint_min, @@ -239,10 +241,10 @@ class UIBoxLayout(UILayout): def __init__( self, *, - x=0, - y=0, - width=1, - height=1, + x: float = 0, + y: float = 0, + width: float | EllipsisType = ..., + height: float | EllipsisType = ..., vertical=True, align="center", children: Iterable[UIWidget] = tuple(), @@ -252,11 +254,12 @@ def __init__( style=None, **kwargs, ): + self._warn_if_size_hint_overrides_fixed_size(width, height, size_hint) super().__init__( x=x, y=y, - width=width, - height=height, + width=1 if width is ... else width, + height=1 if height is ... else height, children=children, size_hint=size_hint, size_hint_max=size_hint_max, @@ -485,10 +488,10 @@ class UIGridLayout(UILayout): def __init__( self, *, - x=0, - y=0, - width=1, - height=1, + x: float = 0, + y: float = 0, + width: float | EllipsisType = ..., + height: float | EllipsisType = ..., align_horizontal="center", align_vertical="center", children: Iterable[UIWidget] = tuple(), @@ -500,11 +503,12 @@ def __init__( row_count: int = 1, **kwargs, ): + self._warn_if_size_hint_overrides_fixed_size(width, height, size_hint) super().__init__( x=x, y=y, - width=width, - height=height, + width=1 if width is ... else width, + height=1 if height is ... else height, children=children, size_hint=size_hint, size_hint_max=size_hint_max, diff --git a/tests/unit/gui/test_layout_size_hint_warning.py b/tests/unit/gui/test_layout_size_hint_warning.py new file mode 100644 index 000000000..a0e5c2910 --- /dev/null +++ b/tests/unit/gui/test_layout_size_hint_warning.py @@ -0,0 +1,111 @@ +"""Tests that layouts warn when explicit width/height conflicts with active size_hint.""" +import warnings + +import pytest + +from arcade.gui import UIBoxLayout +from arcade.gui.widgets.layout import UIAnchorLayout, UIGridLayout + + +def test_anchor_layout_warns_when_width_given_with_default_size_hint(window): + """UIAnchorLayout should warn when width is given but size_hint_x is active.""" + with pytest.warns(UserWarning, match="size_hint_x"): + UIAnchorLayout(width=500) + + +def test_anchor_layout_warns_when_height_given_with_default_size_hint(window): + """UIAnchorLayout should warn when height is given but size_hint_y is active.""" + with pytest.warns(UserWarning, match="size_hint_y"): + UIAnchorLayout(height=500) + + +def test_anchor_layout_no_warning_when_size_hint_none(window): + """UIAnchorLayout should not warn when size_hint=None is explicitly set.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") + UIAnchorLayout(width=500, height=500, size_hint=None) + + +def test_anchor_layout_no_warning_when_no_explicit_size(window): + """UIAnchorLayout should not warn when width/height are not explicitly given.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") + UIAnchorLayout(size_hint=(1, 1)) + + +def test_anchor_layout_no_warning_when_size_hint_x_none(window): + """UIAnchorLayout should not warn for width when size_hint_x is None.""" + # No width warning expected (only height warning) + with pytest.warns(UserWarning, match="size_hint_y"): + UIAnchorLayout(width=500, height=500, size_hint=(None, 1)) + + +def test_anchor_layout_no_warning_when_size_hint_y_none(window): + """UIAnchorLayout should not warn for height when size_hint_y is None.""" + # No height warning expected (only width warning) + with pytest.warns(UserWarning, match="size_hint_x"): + UIAnchorLayout(width=500, height=500, size_hint=(1, None)) + + +def test_box_layout_warns_when_width_given_with_default_size_hint(window): + """UIBoxLayout should warn when width is given but size_hint_x is active.""" + with pytest.warns(UserWarning, match="size_hint_x"): + UIBoxLayout(width=200) + + +def test_box_layout_warns_when_height_given_with_default_size_hint(window): + """UIBoxLayout should warn when height is given but size_hint_y is active.""" + with pytest.warns(UserWarning, match="size_hint_y"): + UIBoxLayout(height=200) + + +def test_box_layout_no_warning_when_size_hint_none(window): + """UIBoxLayout should not warn when size_hint=None is explicitly set.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") + UIBoxLayout(width=200, height=200, size_hint=None) + + +def test_box_layout_no_warning_when_no_explicit_size(window): + """UIBoxLayout should not warn when width/height are not explicitly given.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") + UIBoxLayout(size_hint=(0, 0)) + + +def test_grid_layout_warns_when_width_given_with_default_size_hint(window): + """UIGridLayout should warn when width is given but size_hint_x is active.""" + with pytest.warns(UserWarning, match="size_hint_x"): + UIGridLayout(width=200) + + +def test_grid_layout_warns_when_height_given_with_default_size_hint(window): + """UIGridLayout should warn when height is given but size_hint_y is active.""" + with pytest.warns(UserWarning, match="size_hint_y"): + UIGridLayout(height=200) + + +def test_grid_layout_no_warning_when_size_hint_none(window): + """UIGridLayout should not warn when size_hint=None is explicitly set.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") + UIGridLayout(width=200, height=200, size_hint=None) + + +def test_grid_layout_no_warning_when_no_explicit_size(window): + """UIGridLayout should not warn when width/height are not explicitly given.""" + with warnings.catch_warnings(): + warnings.simplefilter("error") + UIGridLayout(size_hint=(0, 0)) + + +def test_warning_message_includes_class_name(window): + """Warning should include the layout class name for clear identification.""" + with pytest.warns(UserWarning, match="UIBoxLayout"): + UIBoxLayout(width=200) + + with pytest.warns(UserWarning, match="UIAnchorLayout"): + UIAnchorLayout(width=200) + + with pytest.warns(UserWarning, match="UIGridLayout"): + UIGridLayout(width=200) diff --git a/tests/unit/gui/test_layouting_anchorlayout.py b/tests/unit/gui/test_layouting_anchorlayout.py index 0948dab83..b6fbc5b8d 100644 --- a/tests/unit/gui/test_layouting_anchorlayout.py +++ b/tests/unit/gui/test_layouting_anchorlayout.py @@ -8,7 +8,7 @@ def test_place_widget(window): dummy = UIDummy(width=100, height=200) - subject = UIAnchorLayout(x=0, y=0, width=500, height=500) + subject = UIAnchorLayout(x=0, y=0, width=500, height=500, size_hint=None) subject.add( dummy, @@ -30,7 +30,7 @@ def test_place_widget_relative_to_own_content_rect(window): dummy = UIDummy(width=100, height=200) subject = ( - UIAnchorLayout(x=0, y=0, width=500, height=500) + UIAnchorLayout(x=0, y=0, width=500, height=500, size_hint=None) .with_border(width=2) .with_padding(left=50, top=100) ) @@ -68,7 +68,7 @@ def test_place_box_layout(window, ui): def test_grow_child_half(window): - subject = UIAnchorLayout(width=400, height=400) + subject = UIAnchorLayout(width=400, height=400, size_hint=None) dummy = subject.add(UIDummy(width=100, height=100, size_hint=(0.5, 0.5))) subject._do_layout() @@ -78,7 +78,7 @@ def test_grow_child_half(window): def test_grow_child_full_width(window): - subject = UIAnchorLayout(width=400, height=400) + subject = UIAnchorLayout(width=400, height=400, size_hint=None) dummy = subject.add(UIDummy(width=100, height=100, size_hint=(1, 0.5))) subject._do_layout() @@ -88,7 +88,7 @@ def test_grow_child_full_width(window): def test_grow_child_full_height(window): - subject = UIAnchorLayout(width=400, height=400) + subject = UIAnchorLayout(width=400, height=400, size_hint=None) dummy = subject.add(UIDummy(width=100, height=100, size_hint=(0.5, 1))) subject._do_layout() @@ -98,7 +98,7 @@ def test_grow_child_full_height(window): def test_grow_child_to_max_size(window): - subject = UIAnchorLayout(width=400, height=400) + subject = UIAnchorLayout(width=400, height=400, size_hint=None) dummy = subject.add(UIDummy(width=100, height=100, size_hint=(1, 1), size_hint_max=(200, 150))) subject._do_layout() @@ -108,7 +108,7 @@ def test_grow_child_to_max_size(window): def test_shrink_child_to_min_size(window): - subject = UIAnchorLayout(width=400, height=400) + subject = UIAnchorLayout(width=400, height=400, size_hint=None) dummy = subject.add( UIDummy(width=100, height=100, size_hint=(0.1, 0.1), size_hint_min=(200, 150)) ) @@ -121,7 +121,7 @@ def test_shrink_child_to_min_size(window): def test_children_can_grow_out_of_bounce(window): """This tests behavior, which is used for scrolling.""" - subject = UIAnchorLayout(width=400, height=400) + subject = UIAnchorLayout(width=400, height=400, size_hint=None) dummy = subject.add(UIDummy(width=100, height=100, size_hint=(2, 2))) subject._do_layout() @@ -132,7 +132,7 @@ def test_children_can_grow_out_of_bounce(window): def test_children_limited_to_layout_size_when_enforced(window): """This tests behavior, which is used for scrolling.""" - subject = UIAnchorLayout(width=400, height=400) + subject = UIAnchorLayout(width=400, height=400, size_hint=None) subject._restrict_child_size = True dummy = subject.add(UIDummy(width=100, height=100, size_hint=(2, 2))) @@ -143,7 +143,7 @@ def test_children_limited_to_layout_size_when_enforced(window): def test_only_adjust_size_if_size_hint_is_given_for_dimension(window): - subject = UIAnchorLayout(width=400, height=400) + subject = UIAnchorLayout(width=400, height=400, size_hint=None) dummy = subject.add( UIDummy(width=100, height=100, size_hint=(2, None), size_hint_min=(None, 200)) ) diff --git a/tests/unit/gui/test_layouting_boxlayout.py b/tests/unit/gui/test_layouting_boxlayout.py index 894d24155..7a366bf5c 100644 --- a/tests/unit/gui/test_layouting_boxlayout.py +++ b/tests/unit/gui/test_layouting_boxlayout.py @@ -324,7 +324,7 @@ def test_size_hint_contains_border_and_padding(window, ui): def test_vertical_resize_child_according_size_hint_full(window): - box = UIBoxLayout(width=200, height=200, vertical=True) + box = UIBoxLayout(width=200, height=200, vertical=True, size_hint=None) dummy_1 = box.add(UIDummy(width=100, height=100, size_hint=(1, 1))) box._do_layout() @@ -334,7 +334,7 @@ def test_vertical_resize_child_according_size_hint_full(window): def test_vertical_resize_child_according_size_hint_half(window): - box = UIBoxLayout(width=200, height=200, vertical=True) + box = UIBoxLayout(width=200, height=200, vertical=True, size_hint=None) dummy_1 = box.add(UIDummy(width=100, height=100, size_hint=(0.5, 0.5))) box._do_layout() @@ -344,7 +344,7 @@ def test_vertical_resize_child_according_size_hint_half(window): def test_vertical_resize_children_according_size_hint(window): - box = UIBoxLayout(width=300, height=400, vertical=True) + box = UIBoxLayout(width=300, height=400, vertical=True, size_hint=None) dummy_1 = box.add(UIDummy(size_hint_min=(100, 100), size_hint=(1, 1))) dummy_2 = box.add(UIDummy(size_hint_min=(100, 100), size_hint=(0.5, 0.5))) @@ -356,7 +356,7 @@ def test_vertical_resize_children_according_size_hint(window): def test_vertical_ignores_size_hint_none(window): - box = UIBoxLayout(width=300, height=400, vertical=True) + box = UIBoxLayout(width=300, height=400, vertical=True, size_hint=None) dummy_1 = box.add(UIDummy(width=100, height=100, size_hint=(1, None))) dummy_2 = box.add(UIDummy(width=100, height=100, size_hint=(None, 1))) @@ -368,7 +368,7 @@ def test_vertical_ignores_size_hint_none(window): def test_vertical_fit_content(window): - box = UIBoxLayout(width=100, height=100, vertical=True) + box = UIBoxLayout(width=100, height=100, vertical=True, size_hint=None) _ = box.add(UIDummy(width=100, height=50)) _ = box.add(UIDummy(width=20, height=100)) @@ -378,7 +378,7 @@ def test_vertical_fit_content(window): def test_horizontal_resize_child_according_size_hint_full(window): - box = UIBoxLayout(width=200, height=200, vertical=False) + box = UIBoxLayout(width=200, height=200, vertical=False, size_hint=None) dummy_1 = box.add(UIDummy(width=100, height=100, size_hint=(1, 1))) box._do_layout() @@ -388,7 +388,7 @@ def test_horizontal_resize_child_according_size_hint_full(window): def test_horizontal_resize_child_according_size_hint_half(window): - box = UIBoxLayout(width=200, height=200, vertical=False) + box = UIBoxLayout(width=200, height=200, vertical=False, size_hint=None) dummy_1 = box.add(UIDummy(width=100, height=100, size_hint=(0.5, 0.5))) box._do_layout() @@ -398,7 +398,7 @@ def test_horizontal_resize_child_according_size_hint_half(window): def test_horizontal_resize_children_according_size_hint(window): - box = UIBoxLayout(width=300, height=400, vertical=False) + box = UIBoxLayout(width=300, height=400, vertical=False, size_hint=None) dummy_1 = box.add(UIDummy(size_hint_min=(100, 100), size_hint=(1, 1))) dummy_2 = box.add(UIDummy(size_hint_min=(100, 100), size_hint=(0.5, 0.5))) @@ -410,7 +410,7 @@ def test_horizontal_resize_children_according_size_hint(window): def test_horizontal_ignores_size_hint_none(window): - box = UIBoxLayout(width=300, height=400, vertical=False) + box = UIBoxLayout(width=300, height=400, vertical=False, size_hint=None) dummy_1 = box.add(UIDummy(width=100, height=100, size_hint=(1, None))) dummy_2 = box.add(UIDummy(width=100, height=100, size_hint=(None, 1))) @@ -422,7 +422,7 @@ def test_horizontal_ignores_size_hint_none(window): def test_horizontal_fit_content(window): - box = UIBoxLayout(width=100, height=100, vertical=False) + box = UIBoxLayout(width=100, height=100, vertical=False, size_hint=None) _ = box.add(UIDummy(width=100, height=50)) _ = box.add(UIDummy(width=20, height=100))