|
| 1 | +# Arcade Enhancement: Public Access to Child Layout Data |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +When a widget is added to a layout (e.g., `UIAnchorLayout.add(child, anchor_x="left", align_x=50)`), the layout kwargs (`anchor_x`, `align_x`, `anchor_y`, `align_y`) are stored internally in a `_ChildEntry` named tuple, accessible only through the private `_children` list property. |
| 6 | + |
| 7 | +The public `children` property strips this data: |
| 8 | + |
| 9 | +```python |
| 10 | +# arcade/gui/widgets/__init__.py line 529 |
| 11 | +@property |
| 12 | +def children(self) -> list[UIWidget]: |
| 13 | + """Provides all child widgets.""" |
| 14 | + return [child for child, data in self._children] |
| 15 | +``` |
| 16 | + |
| 17 | +This means there's **no public way** to read or modify a child's layout parameters after it's been added, short of removing and re-adding it, or accessing the private `_children` attribute. |
| 18 | + |
| 19 | +## Real-World Use Case |
| 20 | + |
| 21 | +A game UI with buttons positioned in an `UIAnchorLayout`. On window resize, the button position (`align_x`, `align_y`) needs to update without rebuilding the entire widget tree: |
| 22 | + |
| 23 | +```python |
| 24 | +# Current workaround — accesses private API |
| 25 | +entry = self._btn_anchor._children[0] # _ChildEntry(child, data) |
| 26 | +entry.data["align_x"] = new_x |
| 27 | +entry.data["align_y"] = new_y |
| 28 | +``` |
| 29 | + |
| 30 | +Without this, the only alternative is to call `remove()` + `add()` each frame, which is wasteful and causes flicker. |
| 31 | + |
| 32 | +## Current Internal Structure |
| 33 | + |
| 34 | +```python |
| 35 | +# arcade/gui/widgets/__init__.py |
| 36 | + |
| 37 | +class _ChildEntry(NamedTuple): |
| 38 | + child: UIWidget |
| 39 | + data: dict |
| 40 | + |
| 41 | +class UIWidget: |
| 42 | + _children = ListProperty[_ChildEntry]() |
| 43 | + |
| 44 | + def add(self, child, **kwargs): |
| 45 | + child.parent = self |
| 46 | + self._children.append(_ChildEntry(child, kwargs)) |
| 47 | + return child |
| 48 | + |
| 49 | + def remove(self, child): |
| 50 | + child.parent = None |
| 51 | + for c in self._children: |
| 52 | + if c.child == child: |
| 53 | + self._children.remove(c) |
| 54 | + return c.data # Note: remove() already returns the data |
| 55 | + return None |
| 56 | + |
| 57 | + @property |
| 58 | + def children(self) -> list[UIWidget]: |
| 59 | + return [child for child, data in self._children] |
| 60 | +``` |
| 61 | + |
| 62 | +Note that `remove()` already returns the layout data dict, which suggests the framework considers this data useful — it just doesn't provide a way to access it without removing the child. |
| 63 | + |
| 64 | +## Proposed API |
| 65 | + |
| 66 | +Add two methods to `UIWidget`: |
| 67 | + |
| 68 | +```python |
| 69 | +def get_child_data(self, child: UIWidget) -> dict | None: |
| 70 | + """Get the layout data for a child widget. |
| 71 | +
|
| 72 | + Returns the kwargs dict that was passed when the child was added |
| 73 | + (e.g., anchor_x, align_x for UIAnchorLayout children). |
| 74 | + Modifying the returned dict will affect the child's layout |
| 75 | + on the next do_layout() call. |
| 76 | +
|
| 77 | + Args: |
| 78 | + child: The child widget to look up. |
| 79 | +
|
| 80 | + Returns: |
| 81 | + The layout data dict, or None if the child is not found. |
| 82 | + """ |
| 83 | + for entry in self._children: |
| 84 | + if entry.child == child: |
| 85 | + return entry.data |
| 86 | + return None |
| 87 | + |
| 88 | +def get_child_entry(self, index: int) -> tuple[UIWidget, dict]: |
| 89 | + """Get the child widget and its layout data by index. |
| 90 | +
|
| 91 | + Args: |
| 92 | + index: The index of the child (0-based, in add order). |
| 93 | +
|
| 94 | + Returns: |
| 95 | + A tuple of (child_widget, layout_data_dict). |
| 96 | +
|
| 97 | + Raises: |
| 98 | + IndexError: If the index is out of range. |
| 99 | + """ |
| 100 | + entry = self._children[index] |
| 101 | + return entry.child, entry.data |
| 102 | +``` |
| 103 | + |
| 104 | +### Usage After Change |
| 105 | + |
| 106 | +```python |
| 107 | +# Look up by child reference |
| 108 | +data = layout.get_child_data(my_button_row) |
| 109 | +if data: |
| 110 | + data["align_x"] = new_x |
| 111 | + data["align_y"] = new_y |
| 112 | + |
| 113 | +# Look up by index |
| 114 | +child, data = layout.get_child_entry(0) |
| 115 | +data["align_x"] = new_x |
| 116 | +``` |
| 117 | + |
| 118 | +## Design Decisions |
| 119 | + |
| 120 | +### Why return a mutable dict? |
| 121 | + |
| 122 | +The layout data dict is already mutable internally — `do_layout()` reads from it each time. Returning it directly lets callers modify positioning without remove/re-add cycles, which is the primary use case. This matches the existing pattern where `remove()` returns the same dict. |
| 123 | + |
| 124 | +### Why two methods? |
| 125 | + |
| 126 | +- `get_child_data(child)` — when you have a reference to the child widget (common case) |
| 127 | +- `get_child_entry(index)` — when you know the position but not the widget (useful for single-child layouts) |
| 128 | + |
| 129 | +### Why not make `_ChildEntry` public? |
| 130 | + |
| 131 | +`_ChildEntry` is a `NamedTuple` with `child` and `data` fields. Making it public is an option, but the proposed methods are simpler — callers don't need to learn about a new type. The internal storage structure can evolve independently. |
| 132 | + |
| 133 | +### Why not a `children_with_data` property? |
| 134 | + |
| 135 | +A property returning `list[tuple[UIWidget, dict]]` would work but encourages iterating the full list. The lookup methods are more intentional and match the typical use case of updating a specific child. |
| 136 | + |
| 137 | +## Files to Change |
| 138 | + |
| 139 | +| File | Change | |
| 140 | +|------|--------| |
| 141 | +| `arcade/gui/widgets/__init__.py` | Add `get_child_data()` and `get_child_entry()` to `UIWidget` | |
| 142 | +| `tests/unit/gui/` | Tests for both methods | |
| 143 | +| `doc/` | Document the new methods, add example | |
| 144 | + |
| 145 | +## Test Plan |
| 146 | + |
| 147 | +1. `get_child_data(child)` returns the correct dict for an added child |
| 148 | +2. `get_child_data(unknown_widget)` returns `None` |
| 149 | +3. Modifying the returned dict affects layout on next `do_layout()` call |
| 150 | +4. `get_child_entry(0)` returns first child and its data |
| 151 | +5. `get_child_entry(-1)` returns last child (Python index semantics) |
| 152 | +6. `get_child_entry(out_of_range)` raises `IndexError` |
| 153 | +7. Works with `UIAnchorLayout`, `UIBoxLayout`, `UIGridLayout` |
| 154 | +8. Returned dict matches what was passed to `add()` |
| 155 | +9. After `remove()` + `add()`, `get_child_data()` returns the new data |
0 commit comments