Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions arcade/gui/widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import warnings
import weakref
from abc import ABC
from collections.abc import Iterable
Expand Down Expand Up @@ -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.
Expand Down
36 changes: 20 additions & 16 deletions arcade/gui/widgets/layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand Down Expand Up @@ -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(),
Expand All @@ -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,
Expand Down
111 changes: 111 additions & 0 deletions tests/unit/gui/test_layout_size_hint_warning.py
Original file line number Diff line number Diff line change
@@ -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)
20 changes: 10 additions & 10 deletions tests/unit/gui/test_layouting_anchorlayout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
)
Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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))
)
Expand All @@ -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()
Expand All @@ -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)))

Expand All @@ -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))
)
Expand Down
Loading
Loading