From 96d26e28b4d3613c6fe297708d3755d8bf101e84 Mon Sep 17 00:00:00 2001 From: Paul V Craven Date: Thu, 7 May 2026 12:50:37 -0500 Subject: [PATCH] Add get_child_data() and get_child_entry() to UIWidget Provides public access to a child's layout data (anchor_x, align_x, etc.) without accessing the private _children list. Modifying the returned dict updates the layout on the next do_layout() call. Co-Authored-By: Claude Opus 4.6 --- arcade/gui/widgets/__init__.py | 41 ++++++++ tests/unit/gui/test_child_data_access.py | 116 +++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 tests/unit/gui/test_child_data_access.py diff --git a/arcade/gui/widgets/__init__.py b/arcade/gui/widgets/__init__.py index 7f1c9a803..088d2d92a 100644 --- a/arcade/gui/widgets/__init__.py +++ b/arcade/gui/widgets/__init__.py @@ -257,6 +257,47 @@ def remove(self, child: UIWidget) -> dict | None: return c.data return None + def get_child_data(self, child: UIWidget) -> dict | None: + """Get the layout data for a child widget. + + Returns the kwargs dict that was passed when the child was + added (e.g., ``anchor_x``, ``align_x`` for + :py:class:`UIAnchorLayout` children). + + The returned dict is the *live* internal data. Modifying it + will affect the child's layout on the next + :py:meth:`do_layout` call. + + Args: + child: The child widget to look up. + + Returns: + The layout data dict, or ``None`` if *child* is not a + direct child of this widget. + """ + for entry in self._children: + if entry.child == child: + return entry.data + return None + + def get_child_entry(self, index: int) -> tuple[UIWidget, dict]: + """Get the child widget and its layout data by index. + + Supports negative indices (e.g., ``-1`` for the last child), + following standard Python sequence semantics. + + Args: + index: Position of the child (0-based, in add order). + + Returns: + A ``(child_widget, layout_data_dict)`` tuple. + + Raises: + IndexError: If *index* is out of range. + """ + entry = self._children[index] + return entry.child, entry.data + def clear(self): """Removes all children""" for child in self.children: diff --git a/tests/unit/gui/test_child_data_access.py b/tests/unit/gui/test_child_data_access.py new file mode 100644 index 000000000..823e335b0 --- /dev/null +++ b/tests/unit/gui/test_child_data_access.py @@ -0,0 +1,116 @@ +import pytest + +from arcade.gui import UIDummy +from arcade.gui.widgets.layout import UIAnchorLayout, UIBoxLayout, UIGridLayout + + +def test_get_child_data_returns_correct_dict(window): + """get_child_data() should return the kwargs dict passed to add().""" + layout = UIAnchorLayout(width=500, height=500, size_hint=None) + child = UIDummy(width=100, height=100) + layout.add(child, anchor_x="left", align_x=50, anchor_y="top", align_y=-10) + + data = layout.get_child_data(child) + + assert data is not None + assert data["anchor_x"] == "left" + assert data["align_x"] == 50 + assert data["anchor_y"] == "top" + assert data["align_y"] == -10 + + +def test_get_child_data_unknown_widget_returns_none(window): + """get_child_data() should return None for a widget not in the layout.""" + layout = UIAnchorLayout(width=500, height=500, size_hint=None) + stranger = UIDummy(width=50, height=50) + + assert layout.get_child_data(stranger) is None + + +def test_get_child_data_modification_affects_layout(window): + """Modifying the returned dict should change positioning on next do_layout().""" + layout = UIAnchorLayout(x=0, y=0, width=500, height=500, size_hint=None) + child = UIDummy(width=100, height=100) + layout.add(child, anchor_x="left", align_x=0, anchor_y="bottom", align_y=0) + + layout.do_layout() + original_left = child.left + + data = layout.get_child_data(child) + data["align_x"] = 50 + layout.do_layout() + + assert child.left == original_left + 50 + + +def test_get_child_entry_returns_first_child(window): + """get_child_entry(0) should return the first added child and its data.""" + layout = UIAnchorLayout(width=500, height=500, size_hint=None) + first = UIDummy(width=100, height=100) + second = UIDummy(width=100, height=100) + layout.add(first, anchor_x="left") + layout.add(second, anchor_x="right") + + child, data = layout.get_child_entry(0) + + assert child is first + assert data["anchor_x"] == "left" + + +def test_get_child_entry_negative_index(window): + """get_child_entry(-1) should return the last child.""" + layout = UIAnchorLayout(width=500, height=500, size_hint=None) + first = UIDummy(width=100, height=100) + last = UIDummy(width=100, height=100) + layout.add(first, anchor_x="left") + layout.add(last, anchor_x="right") + + child, data = layout.get_child_entry(-1) + + assert child is last + assert data["anchor_x"] == "right" + + +def test_get_child_entry_out_of_range_raises(window): + """get_child_entry() should raise IndexError for out-of-range indices.""" + layout = UIAnchorLayout(width=500, height=500, size_hint=None) + layout.add(UIDummy(width=50, height=50)) + + with pytest.raises(IndexError): + layout.get_child_entry(5) + + +def test_works_with_box_layout(window): + """get_child_data() should work with UIBoxLayout children.""" + layout = UIBoxLayout(width=500, height=500, size_hint=None) + child = UIDummy(width=100, height=100) + layout.add(child) + + data = layout.get_child_data(child) + assert data is not None + + +def test_works_with_grid_layout(window): + """get_child_data() should work with UIGridLayout children.""" + layout = UIGridLayout(column_count=2, row_count=2, size_hint=None) + child = UIDummy(width=100, height=100) + layout.add(child, column=0, row=0) + + data = layout.get_child_data(child) + assert data is not None + assert data["column"] == 0 + assert data["row"] == 0 + + +def test_after_remove_and_readd_returns_new_data(window): + """After remove() + add(), get_child_data() should return the new data.""" + layout = UIAnchorLayout(width=500, height=500, size_hint=None) + child = UIDummy(width=100, height=100) + + layout.add(child, anchor_x="left", align_x=10) + layout.remove(child) + layout.add(child, anchor_x="right", align_x=99) + + data = layout.get_child_data(child) + assert data["anchor_x"] == "right" + assert data["align_x"] == 99