11from __future__ import annotations
22
3+ import weakref
34from abc import ABC
45from collections .abc import Iterable
56from enum import IntEnum
67from types import EllipsisType
7- from typing import TYPE_CHECKING , NamedTuple , TypeVar
8+ from typing import Any , Generic , TYPE_CHECKING , NamedTuple , TypeVar , overload
9+ from weakref import WeakKeyDictionary
810
911from pyglet .event import EVENT_HANDLED , EVENT_UNHANDLED , EventDispatcher
1012from pyglet .math import Vec2
3133 from arcade .gui .ui_manager import UIManager
3234
3335W = TypeVar ("W" , bound = "UIWidget" )
36+ P = TypeVar ("P" )
3437
3538
3639class FocusMode (IntEnum ):
@@ -51,6 +54,51 @@ class _ChildEntry(NamedTuple):
5154 data : dict
5255
5356
57+ class WeakRef (Generic [P ]):
58+ """A weak reference to a UIWidget parent, which is used to prevent memory leaks."""
59+
60+ __slots__ = ("name" , "obs" )
61+ name : str
62+ """Attribute name of the property"""
63+ obs : WeakKeyDictionary [Any , weakref .ref [P ]]
64+ """Weak dictionary to hold the values"""
65+
66+ def __init__ (self ):
67+ self .obs = WeakKeyDictionary ()
68+
69+ def get (self , instance : Any ) -> P | None :
70+ """Get value for owner instance"""
71+ # If the value is not set, return None
72+ value = self .obs .get (instance )
73+ return value () if value else None
74+
75+ def set (self , instance , value : P | None ):
76+ """Set value for owner instance"""
77+ # Store a weak reference to the value
78+ if value is None :
79+ self .obs .pop (instance , None )
80+ else :
81+ self .obs [instance ] = weakref .ref (value )
82+
83+ def __set_name__ (self , owner , name ):
84+ self .name = name
85+
86+ @overload
87+ def __get__ (self , instance : None , instance_type ) -> Self : ...
88+
89+ @overload
90+ def __get__ (self , instance : Any , instance_type ) -> P | None : ...
91+
92+ def __get__ (self , instance : Any | None , instance_type ) -> Self | P | None :
93+ """Get the value for the owner instance, or None if not set."""
94+ if instance is None :
95+ return self
96+ return self .get (instance )
97+
98+ def __set__ (self , instance , value : P | None ):
99+ self .set (instance , value )
100+
101+
54102@copy_dunders_unimplemented
55103class UIWidget (EventDispatcher , ABC ):
56104 """The :class:`UIWidget` class is the base class required for creating widgets.
@@ -71,6 +119,9 @@ class UIWidget(EventDispatcher, ABC):
71119 size_hint_max: max width and height in pixel
72120 """
73121
122+ parent : WeakRef [UIManager | UIWidget | None ] = WeakRef ()
123+ """A weak reference to the parent UIManager or UIWidget,
124+ which does not prevent garbage collection of the parent."""
74125 rect = Property (LBWH (0 , 0 , 1 , 1 ))
75126 visible = Property (True )
76127 focused = Property (False )
@@ -113,7 +164,6 @@ def __init__(
113164 ):
114165 self ._requires_render = True
115166 self .rect = LBWH (x , y , width , height )
116- self .parent : UIManager | UIWidget | None = None
117167
118168 # Size hints are properties that can be used by layouts
119169 self .size_hint = size_hint
@@ -126,21 +176,21 @@ def __init__(
126176 for child in children :
127177 self .add (child )
128178
129- bind (self , "rect" , self .trigger_full_render )
130- bind (self , "focused" , self .trigger_full_render )
179+ bind (self , "rect" , UIWidget .trigger_full_render )
180+ bind (self , "focused" , UIWidget .trigger_full_render )
131181 bind (
132- self , "visible" , self .trigger_full_render
182+ self , "visible" , UIWidget .trigger_full_render
133183 ) # TODO maybe trigger_parent_render would be enough
134- bind (self , "_children" , self .trigger_render )
135- bind (self , "_border_width" , self .trigger_render )
136- bind (self , "_border_color" , self .trigger_render )
137- bind (self , "_bg_color" , self .trigger_render )
138- bind (self , "_bg_tex" , self .trigger_render )
139- bind (self , "_padding_top" , self .trigger_render )
140- bind (self , "_padding_right" , self .trigger_render )
141- bind (self , "_padding_bottom" , self .trigger_render )
142- bind (self , "_padding_left" , self .trigger_render )
143- bind (self , "_strong_background" , self .trigger_render )
184+ bind (self , "_children" , UIWidget .trigger_render )
185+ bind (self , "_border_width" , UIWidget .trigger_render )
186+ bind (self , "_border_color" , UIWidget .trigger_render )
187+ bind (self , "_bg_color" , UIWidget .trigger_render )
188+ bind (self , "_bg_tex" , UIWidget .trigger_render )
189+ bind (self , "_padding_top" , UIWidget .trigger_render )
190+ bind (self , "_padding_right" , UIWidget .trigger_render )
191+ bind (self , "_padding_bottom" , UIWidget .trigger_render )
192+ bind (self , "_padding_left" , UIWidget .trigger_render )
193+ bind (self , "_strong_background" , UIWidget .trigger_render )
144194
145195 def add (self , child : W , ** kwargs ) -> W :
146196 """Add a widget as a child.
@@ -692,9 +742,9 @@ def __init__(
692742
693743 self .interaction_buttons = interaction_buttons
694744
695- bind (self , "pressed" , self .trigger_render )
696- bind (self , "hovered" , self .trigger_render )
697- bind (self , "disabled" , self .trigger_render )
745+ bind (self , "pressed" , UIInteractiveWidget .trigger_render )
746+ bind (self , "hovered" , UIInteractiveWidget .trigger_render )
747+ bind (self , "disabled" , UIInteractiveWidget .trigger_render )
698748
699749 def on_event (self , event : UIEvent ) -> bool | None :
700750 """Handles mouse events and triggers on_click event if the widget is clicked.
0 commit comments